Compare commits

..
17 Commits
Author SHA1 Message Date
JingMatrix 4e67371193 Release TEESimulator v2.1 2025-11-28 20:00:07 +01:00
JingMatrixandGitHub 2ef89f15c6 Fix date format of vendor patch level (#24)
This was a mistake during the refactoring of TrickyStoreOSS.
After correcting it, we can obtain STRONG integrity (instead of DEVICE) with a valid keybox.

The correct format can be easily found using the `Key Attestation` app.
2025-11-28 19:45:53 +01:00
JingMatrixandGitHub 4f608247fe Set boot digest via resetprop (#22)
The stub method `SystemProperties.set` has wrong signature and is unable to set read-only system properties.
2025-11-28 13:11:54 +01:00
QingandJingMatrix 22cbe5a9a7 Clear generated key cache on keybox updates for Android 12+ (#16)
Ensures that the cache of generated keys is invalidated and cleared whenever a keybox file is updated. This prevents the system from using stale certificates after a keybox change.

Co-authored-by: JingMatrix <jingmatrix@gmail.com>
2025-11-27 23:29:43 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a6fa137e32 Bump org.bouncycastle:bcpkix-jdk18on from 1.82 to 1.83 (#13)
Bumps [org.bouncycastle:bcpkix-jdk18on](https://github.com/bcgit/bc-java) from 1.82 to 1.83.
- [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html)
- [Commits](https://github.com/bcgit/bc-java/commits)

---
updated-dependencies:
- dependency-name: org.bouncycastle:bcpkix-jdk18on
  dependency-version: '1.83'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-27 22:40:33 +01:00
JingMatrixandGitHub 5afefba7bd Clean up cached keys on successful import (#18)
Generated and attestation keys are cached, and if a key is imported with the same name, the cached key would be returned instead of the newly imported one.

This change invalidates the cached key when a key is successfully imported with the same alias.
Close #17 as fixed.

The logging has also been improved to be more consistent across the different interceptors.
2025-11-27 15:43:56 +01:00
JingMatrix ba9578c59b Prepare to release TEESimulator 2.0
The following two bugs are fixed:
1. `zygisk.json` is renamed to `update.json`, which is indicated in `module.prop`.
2. To avoid over optimization of R8, we must keep certains packages, which are found after many experiments.
2025-11-26 18:34:08 +01:00
JingMatrixandGitHub 733e64c3cb Support key generation with attestation keys (#15)
This commit enhances the interception logic to correctly handle key
generation requests that specify an `attestationKey` (via
`setAttestKeyAlias`).

When an attestation key is used, the system signs the newly generated
key with it. A simple leaf certificate patch after the fact is
insufficient, as it breaks this cryptographic chain. To create a valid,
verifiable chain, we must now intercept these `generateKey` operations
and perform a full software-based key and certificate generation, even
when in patch mode.

This ensures that keys attested by other simulated keys are correctly
signed and chained together, bypassing more sophisticated detection
methods.

Fixes:
- Correctly use the `android.hardware.security.keymint.Tag` constants for
  building authorization lists, resolving a bug where internal ASN.1
  sequence indices were being used improperly.
2025-11-26 16:50:30 +01:00
JingMatrixandGitHub 7f94ba4b5b Improve logging to understand detection methods (#14)
Via extensive and detailed logging, we can inspect various detection techniques of target packages.
2025-11-26 11:43:53 +01:00
JingMatrixandGitHub fa1d9ecc56 Bypass detection by skipping imported keys (#12)
In patch mode, a key's origin provides a robust way to avoid modifying
user-imported keys, which is a well-known detection vector. This commit
implements a new strategy to check the `KeyOrigin` tag from the key's
metadata. If a key is marked as `IMPORTED` or `SECURELY_IMPORTED`, the
patching process is now skipped entirely.

This new origin-based check is more reliable and cleaner than the
previous fingerprinting implementation, which has been removed.

Additionally, this commit acknowledges a remaining detection vector in
patch mode: when an `attestationKey` is used, a key must be generated.
Purely software-generated keys are detectable. To address this in the
future, the full software "generate mode" must be implemented even for
devices without a broken TEE. The old key generation logic has been
stubbed with a TODO in preparation for this redesign.
2025-11-26 02:54:43 +01:00
JingMatrix eec9e77631 Add GitHub CI build config 2025-11-26 00:19:05 +01:00
JingMatrix d18692fbef Add module template files
Current AOSP keybox can be found at:
https://cs.android.com/android/platform/superproject/main/+/main:device/generic/trusty/keymaster_soft_wrapped_attestation_keys.xml

However, the support of parsing private keys in iecs format is not implemented yet.
2025-11-26 00:19:05 +01:00
JingMatrix 13b4786cd9 Restructure and overhaul entire Kotlin codebase
This commit introduces a complete architectural refactoring of the
Kotlin-based interception logic, based on the source of
1. https://github.com/5ec1cff/TrickyStore
2. https://github.com/beakthoven/TrickyStoreOSS

The primary purpose of this code is to intercept binder transactions to
the Android Keystore and KeyMint services. The overall workflow operates
in conjunction with a native library (injected via ptrace). The native
library hooks the binder's `transact` function and forwards pre- and
post-transaction events to the Kotlin side. This Kotlin code contains
all the high-level logic for parsing parameters, patching certificates,
and generating simulated keys.

The codebase is now organized into a clear, package-based architecture:

- attestation: Manages the creation and patching of ASN.1 attestation
  data structures.
- config: Handles loading and observing configuration files from disk.
- interception: Contains the core binder interception framework and its
  specific implementations for legacy Keystore (Android Q/R) and modern
  KeyMint/Keystore2 (Android S+).
- logging: Provides a centralized and consistent logging utility.
- pki: Manages Public Key Infrastructure, including certificate
  generation, parsing of key store XML files, and cryptographic helpers.
- util: Contains Android-specific utility functions for device properties.

This refactoring focuses on establishing a robust and extensible
architecture. The fine-tuning of the interception logic itself,
especially for corner cases in key generation and patching, is currently
under redesign and will be further refined in subsequent commits.
2025-11-26 00:19:01 +01:00
JingMatrix 612de6cdf2 Add binder transaction interception framework
This commit introduces a comprehensive framework for intercepting and manipulating binder transactions on Android at the `ioctl` level. It provides a man-in-the-middle layer between the binder driver and user-space `libbinder`, enabling detailed analysis and control over IPC.

The core mechanism works by hooking the `ioctl` system call within the context of a target process. It specifically intercepts the `BINDER_WRITE_READ` command's return buffer from the kernel.

Key components of the framework:

- IOCTL Hook: Intercepts `BR_TRANSACTION` commands delivered by the binder driver to the process.
- Transaction Rewriting: If a transaction is intended for a monitored service, its destination is rewritten in-memory to a local `BinderStub`. The original transaction details are saved in a thread-local context.
- BinderStub: A fake binder service that receives the hijacked transaction. It retrieves the original context and delegates processing to the `BinderInterceptor`.
- BinderInterceptor: The central management class. It maintains a registry of monitored binders and their associated callback interfaces. It orchestrates the pre-transact and post-transact hooks.
- Callback Protocol: Defines a clear protocol for a remote tool to:
    - Register and unregister binders for interception.
    - Receive pre-transaction notifications and choose to: continue, modify data, skip the transaction, or provide an immediate fake reply.
    - Receive post-transaction notifications with the final result and modify the reply.
2025-11-25 19:21:05 +01:00
JingMatrix 020a930a31 Add stub for AOSP Binder and utility components
The primary function of these stubs is to provide necessary interface definitions and that can be utilized by `binder_interceptor.cpp` during compilation (and runtime).

Crucially, `libTEESimulator.so` (which encapsulates these stubs) is dynamically loaded into the target process via `ptrace` after the system's official libraries, such as `/system/lib64/libbinder.so` and `/system/lib64/libutils.so`, have already been loaded and their symbols resolved by the dynamic linker.

Consequently, the dynamic linker will have already established bindings to the robust, canonical implementations within the system libraries for existing code paths. The dynamic linker does not automatically re-resolve or update these established symbol bindings when a new library with conflicting definitions is loaded later.

The AOSP files are downloaded via links:
1. https://android.googlesource.com/platform/frameworks/native/+/refs/heads/main/libs/binder/include/binder
2. https://android.googlesource.com/platform/system/core/+/refs/heads/main/libutils/binder/include/utils

The link for binder header in Android kernel is:
https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/include/uapi/linux/android/binder.h
2025-11-25 19:21:05 +01:00
JingMatrix 0c1937bd3e Implement shared library injection via ptrace
There are still many functions in the header `utils.hpp` not implemented yet, which are however not needed for our purpose.
2025-11-25 19:20:59 +01:00
JingMatrix 95262d4b58 Feat: Add 'app' subproject and integrate LSPlt submodule
This commit introduces the main application subproject, 'app', and sets up the necessary infrastructure for the TEESimulator.

Key changes:
*   'app' Subproject Setup: Added the new :app module with its initial structure, including build files, manifest, and Kotlin main entry point.
*   LSPlt Integration: Added the LSPlt hooking framework as a Git submodule in app/src/main/cpp/external/ and configured its use in CMake.
*   Native Build Configuration: Configured the C++ build to use LSPlt statically and compile two essential native libraries: libinject.so (for injection) and libTEESimulator.so (for interception/logic).
*   Module Packaging: Implemented complex Gradle logic within app/build.gradle.kts to automate the creation of a flashable zip module (supporting Magisk, Ksu, and Apatch) with versioning based on Git information.
*   Initial Module Files: Added the template files (module.prop, update-binary, updater-script) for the flashable module structure.
2025-11-22 16:22:27 +01:00
94 changed files with 1045 additions and 14307 deletions
+73 -111
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,106 +60,72 @@ 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
./gradlew --parallel zipRelease zipDebug --stacktrace
- 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 }}
- name: Bump update.json
run: |
COUNT=$(git rev-list HEAD --count)
RELEASE_NAME=$(basename zips/*Release*.zip)
ZIP_URL="https://github.com/${{ github.repository }}/releases/download/${VER}/${RELEASE_NAME}"
jq ".versionCode = $COUNT | .zipUrl = \"$ZIP_URL\"" module/update.json > /tmp/update.json
mv /tmp/update.json module/update.json
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add module/update.json
git diff --cached --quiet || {
git commit -m "chore(release): bump update.json to $VER [skip ci]"
git push origin HEAD:main
}
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
+1
View File
@@ -0,0 +1 @@
out
+55 -128
View File
@@ -1,160 +1,87 @@
<p align="center">
<h1 align="center">TEESimulator-RS</h1>
<p align="center"><b>Pass hardware security checks on a rooted Android phone</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]
> This is a fork of [JingMatrix/TEESimulator](https://github.com/JingMatrix/TEESimulator). It adds certificate generation written in Rust, generated keys that survive reboots, and attestation behavior that matches stock Android. See the upstream repo for the original project.
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
Some Android apps refuse to run on a rooted phone. They ask the phone to prove it still has a genuine security chip, a check called hardware attestation. A rooted phone normally fails that check.
* **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.
TEESimulator makes it pass. Android runs a system process named `keystore2` that answers these proof requests. TEESimulator sits in front of `keystore2`, watches for the requests apps make to create keys and read their certificates, and builds the proof itself: a full chain of certificates signed by your `keybox.xml`. To the app, the phone looks genuine.
## 📱 Requirements
- Android 10 or above
It replaces TrickyStore and its forks completely. It reads config from the same files, so you can switch without moving anything, but the internals are rewritten: certificates are generated in Rust, keys are saved across reboots, and each app gets its own limit on how fast it can request hardware-backed keys.
## 📦 Installation & Configuration
## Requirements
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!
> [!IMPORTANT]
> You need a valid `keybox.xml`. This is the file used to sign the proof. Without it, TEESimulator can only produce software-only certificates, which strict apps reject.
**All configuration files are monitored and will take effect immediately upon saving.**
1. Android 10 or newer
2. A root manager: KernelSU, Magisk, or APatch
3. A `keybox.xml` file at `/data/adb/tricky_store/keybox.xml`
### The `keybox.xml` Root of Trust
## Quick start
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.
1. Download the latest ZIP from [Releases](https://github.com/Enginex0/TEESimulator-RS/releases).
2. Install it with your root manager, then reboot.
3. Put your `keybox.xml` at `/data/adb/tricky_store/keybox.xml`.
4. List the apps you want to cover in `/data/adb/tricky_store/target.txt`.
5. Check that it works with Play Integrity or the Key Attestation Demo app.
## How it works
```
App
| asks the phone to prove it has real security hardware
v
+----------------------------------------------------+
| keystore2 (the Android process that answers) |
| |
| ioctl <- TEESimulator hooks the call here |
| | |
| v |
| builds a certificate chain and signs it |
| with your keybox.xml |
+----------------------------------------------------+
| the signed chain goes back to the app
v
App -> sees a genuine, hardware-backed device
```xml
<?xml version="1.0"?>
<AndroidAttestation>
<Keybox DeviceID="...">
<Key algorithm="ecdsa|rsa">
<PrivateKey format="pem">...</PrivateKey>
<CertificateChain>...</CertificateChain>
</Key>
</Keybox>
</AndroidAttestation>
```
**Certificate generation in Rust.** A native library, `libcertgen.so`, builds the X.509 certificate chains in Rust with the `ring` crypto library, encoding the bytes by hand in DER, the standard certificate format. Three key types fall outside `ring`'s support (the P-224, P-521, and Curve25519 curves); for those it falls back to Java's BouncyCastle.
### Mode and Keybox Configuration (`target.txt`)
**Hooking keystore2.** Inside the `keystore2` process, TEESimulator redirects `ioctl`, the low-level system call Android uses to pass messages between processes. It does this with `lsplt`, a hooking library. From there it can read and answer three kinds of request: creating a key, importing a key, and fetching a key's certificate.
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.
**Matching stock Android.** The output matches what a real device produces. Keys that are not attested get self-signed certificates. The fields inside the attestation record keep the same order. Fields that only exist on certain Android versions appear only on those versions. The same usage checks run before a key is used.
#### Mode Suffixes
**Keys that survive reboots.** Generated keys are written to disk and stay valid after a restart. File locking stops two writers from corrupting the store.
* **`!` → 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.
**Per-app rate limit.** Each app may request at most 2 hardware-backed keys per 30 seconds, and only 2 at a time. Past that, it receives a software-only certificate.
#### Multi-Keybox Configuration
## 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 config files live in `/data/adb/tricky_store/`. TEESimulator reloads them the moment you save, so a reboot is not needed.
### target.txt
Lists the apps TEESimulator handles, one package name per line. A suffix sets how each app is handled.
| Suffix | What it does |
|--------|--------------|
| `!` | Always make a software key |
| `?` | Keep the real hardware key, patch only its certificate |
| none | Decide automatically |
To use more than one keybox, add a `[filename.xml]` header above the apps that should use that file:
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`)
Sets the security patch dates reported in the attestation certificates. Global defaults go at the top. Override them for one app with a `[package.name]` header.
| Key | What it sets |
|-----|--------------|
| `system` | OS patch level |
| `vendor` | Vendor patch level |
| `boot` | Boot and kernel patch level |
| `all` | All three at once |
Accepted values: `today`, a `YYYY-MM-DD` template, `no` to omit the field, `device_default`, or `prop` to read the value from a system property.
This allows you to configure the security patch level that the simulator will report in its forged attestation certificates.
```
system=YYYY-MM-05
vendor=device_default
boot=no
[com.google.android.gms]
system=2025-10-01
# Advanced Configuration
system=2025-11
boot=no # Do not report a boot patch level
vendor=20251101 # Report a specific vendor patch level
```
### boot_props_mode
Controls global `ro.boot.*` property spoofing. Values: `auto` (default), `force`, or `disable`.
In `auto`, Oplus-family devices (OnePlus/OPPO/realme/Oplus) skip boot-state prop spoofing to avoid conflicts with vendor TEE services such as ultrasonic fingerprint calibration. Create `/data/adb/tricky_store/boot_props_mode` with `force` to restore the old behavior, or `disable` to turn it off on any device.
## Building from source
You need JDK 21, the Android SDK and NDK 29, Rust (stable) with the `aarch64-linux-android` target, and `cargo-ndk`.
```bash
git clone --recursive https://github.com/Enginex0/TEESimulator-RS.git
cd TEESimulator-RS
./gradlew zipRelease zipDebug
```
The ZIPs land in `out/`. Gradle runs `cargo ndk` for you to cross-compile `libcertgen.so`. To build on CI instead, push to `main` or run Actions > Build > Run workflow.
## Compatibility
| Root manager | Status |
|---|---|
| KernelSU | Tested, including the Action button and 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) for the original TEESimulator and its interception design
- [ring](https://github.com/briansmith/ring) for the Rust cryptography
- [fatalcoder524](https://github.com/fatalcoder524) for contributions and collaboration
- [huguangares](https://github.com/huguangares) for collaboration and testing
## License
[GNU General Public License v3.0](LICENSE)
**Note:** This only affects the Key Attestation data generated by the simulator. It does not change system properties.
+16 -120
View File
@@ -2,7 +2,6 @@ import com.android.build.api.artifact.SingleArtifact
import java.io.ByteArrayOutputStream
import javax.inject.Inject
import org.gradle.process.ExecOperations
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.application)
@@ -28,16 +27,9 @@ abstract class GitExecutor @Inject constructor(private val execOperations: ExecO
// Instantiate the helper class using Gradle's object factory
val gitExecutor = objects.newInstance(GitExecutor::class.java)
// versionCode = git commit count + floor offset. The 2026-07-08 public-release
// history scrub (0f1143a) rewrote history and dropped the raw commit count below
// the build number already shipped to testers (298), so post-scrub counts read as
// downgrades. The floor offset lifts versionCode back above that peak and keeps it
// monotonic across the rewrite; each later commit still bumps it by one.
val versionCodeFloorOffset = 5
val gitCommitCount =
gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt() + versionCodeFloorOffset
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.1"
val verName = "v2.1"
android {
namespace = "org.matrix.TEESimulator"
@@ -64,7 +56,6 @@ android {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
buildFeatures { buildConfig = true }
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
@@ -73,81 +64,12 @@ android {
}
}
kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } }
dependencies {
compileOnly(project(":stub"))
compileOnly(libs.annotation)
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)
environment(
"PATH",
"${System.getProperty("user.home")}/.cargo/bin:${System.getenv("PATH") ?: ""}",
)
}
// AGP auto-detects jniLibs/ as an input to mergeJniLibFolders — wire the dependency
tasks.configureEach {
if (name.endsWith("JniLibFolders") && name.startsWith("merge")) {
dependsOn(buildRustCertgen)
}
}
// Auto-rewrite module/update.json on every packaging build so versionCode and
// zipUrl track gitCommitCount automatically, matching module.prop.
val refreshUpdateJson by
tasks.registering {
group = "TEESimulator-RS Module Packaging"
description = "Rewrite module/update.json to match current verName and gitCommitCount."
val updateJsonFile = rootProject.projectDir.resolve("module/update.json")
val capturedVerName = verName
val capturedCount = gitCommitCount
inputs.property("verName", capturedVerName)
inputs.property("gitCommitCount", capturedCount)
outputs.file(updateJsonFile)
doLast {
val fullVer = "$capturedVerName-$capturedCount"
updateJsonFile.writeText(
"""{
"version": "$fullVer",
"versionCode": $capturedCount,
"zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/download/$fullVer/TEESimulator-RS-$fullVer-Release.zip",
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/changelog.md"
}
"""
)
}
}
androidComponents {
onVariants(selector().all()) { variant ->
val capitalized = variant.name.replaceFirstChar { it.uppercase() }
@@ -156,23 +78,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(refreshUpdateJson)
dependsOn("strip${capitalized}DebugSymbols")
if (isDebug) {
from(variant.artifacts.get(SingleArtifact.APK)) {
@@ -189,27 +109,19 @@ androidComponents {
}
}
val nativeLibsDir =
if (isDebug) {
"intermediates/merged_native_libs/${variant.name}/merge${capitalized}NativeLibs/out/lib"
} else {
from(
project.layout.buildDirectory.dir(
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
}
from(project.layout.buildDirectory.dir(nativeLibsDir)) {
into("lib")
include(
"**/libinject.so",
"**/libTEESimulator.so",
"**/libsupervisor.so",
"**/libcertgen.so",
)
) {
into("lib") // Place them in the 'lib' subfolder of the staging directory.
include("**/libinject.so", "**/libTEESimulator.so")
}
// Now, copy and process the files from 'module' directory.
val sourceModuleDir = rootProject.projectDir.resolve("module")
from(sourceModuleDir) {
exclude("module.prop") // Exclude the template file.
exclude("diag.sh") // Debug-only diagnostic plane; included for debug below.
}
// Copy and filter the module.prop template separately.
@@ -218,35 +130,19 @@ androidComponents {
// Use expand() for simple key-value replacement.
expand(
"REPLACEMEVERCODE" to gitCommitCount.toString(),
"REPLACEMEVER" to "$verName-$gitCommitCount",
"REPLACEMEVER" to
"$verName ($gitCommitCount-$gitCommitHash-${variant.name})",
)
}
if (isDebug) {
from(sourceModuleDir) { include("diag.sh") }
}
// The destination for all the above 'from' operations.
into(tempModuleDir)
if (isDebug) {
doLast {
// Debug-only: grant the keystore + soterserver (platform_app) domains
// external-storage access for the per-UID NDJSON sink. diag.sh (shipped
// only in debug) carries the shell side of the diagnostic plane.
tempModuleDir.get().asFile.resolve("sepolicy.rule")
.appendText(
"\nallow keystore media_rw_data_file { dir file } *" +
"\nallow platform_app media_rw_data_file { dir file } *\n",
)
}
}
}
// 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)
@@ -259,7 +155,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)
@@ -273,7 +169,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(
@@ -286,7 +182,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 { *; }
-4
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)
@@ -23,9 +22,6 @@ add_executable(libinject.so inject/main.cpp inject/utils.cpp)
target_include_directories(libinject.so PUBLIC include)
target_link_libraries(libinject.so PRIVATE lsplt_static)
add_executable(libsupervisor.so supervisor.cpp)
target_link_libraries(libsupervisor.so PRIVATE log)
add_library(${CMAKE_PROJECT_NAME} SHARED binder_interceptor.cpp)
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC external/linux-kernel/include include)
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE binder lsplt_static utils)
+52 -77
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
@@ -278,12 +276,6 @@ static sp<BinderInterceptor> g_interceptor_instance = nullptr;
// =============================================================================================
class BinderStub : public BBinder {
public:
const String16& getInterfaceDescriptor() const override {
static const String16 kDescriptor("org.matrix.TEESimulator.BinderStub");
return kDescriptor;
}
protected:
status_t onTransact(uint32_t code, const Parcel &data, Parcel *reply, uint32_t flags) override {
if (code != intercept::kBackdoorCode) {
@@ -350,16 +342,15 @@ static sp<BinderStub> g_stub_instance = nullptr;
namespace {
/**
* @brief Analyses a binder transaction. If the target is monitored,
* hijacks the transaction by rewriting its destination to our BinderStub.
* @param txn_data Pointer to the transaction data within the ioctl buffer.
*/
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)
return;
bool hijack = false;
ThreadTransactionInfo info;
@@ -368,15 +359,9 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
info.transaction_code = intercept::kBackdoorCode;
info.target_binder = nullptr;
hijack = true;
// Check 2: Spoof uid of KeyStore requests from the daemon to bypass permission check
} else if (txn_data->sender_euid == 0) {
// The kernel driver fills sender_euid.
// libbinder.so trusts this value to populate IPCThreadState.
txn_data->sender_euid = 1000;
LOGV("[Hook] Spoofing UID for transaction: 0 -> %d", txn_data->sender_euid);
hijack = false; // Never hijack to avoid recursion
// Check 3: Normal interception based on registry of monitored binders
} else {
}
// Check 2: Normal interception based on registry of monitored binders
else {
// Safe casting based on Binder driver ABI
RefBase::weakref_type *weak_ref = reinterpret_cast<RefBase::weakref_type *>(txn_data->target.ptr);
@@ -385,17 +370,18 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
// The raw pointer to the binder object itself is stored in the cookie
BBinder *target_binder_ptr = reinterpret_cast<BBinder *>(txn_data->cookie);
// Create a weak pointer for the lookup and to store in our context map.
// This is safe because we are holding a strong reference.
wp<BBinder> wp_target = target_binder_ptr;
// This is safe ONLY because we successfully called attemptIncStrong().
// The sp<> constructor will not increment the ref count again, it just adopts the one we have.
// When sp_target goes out of scope, it will call decStrong(), releasing our temporary reference.
sp<BBinder> sp_target = sp<BBinder>::fromExisting(target_binder_ptr);
if (g_interceptor_instance->shouldIntercept(wp_target, txn_data->code)) {
// Now we can safely use sp_target (which implicitly converts to a wp) for the lookup.
if (g_interceptor_instance->isBinderIntercepted(sp_target)) {
info.transaction_code = txn_data->code;
info.target_binder = wp_target; // Assign the valid weak pointer
info.target_binder = sp_target; // Assign the valid weak pointer
hijack = true;
}
// Manually release the temporary strong reference we acquired at the start.
target_binder_ptr->decStrong(nullptr);
// No need to manually call decStrong(); the sp destructor handles it.
}
}
@@ -403,10 +389,7 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
uint64_t tx_id = ++g_transaction_id_counter;
info.transaction_id = tx_id;
// tx_id is the same counter handed to the Kotlin interceptor, and sender_euid is the
// calling app; together they correlate this native hijack with that UID's per-UID file.
LOGV("[Hook] Hijacking Transaction %" PRIu64 " (Code: %u, uid=%u)", tx_id, txn_data->code,
txn_data->sender_euid);
LOGV("[Hook] Hijacking Transaction %" PRIu64 " (Code: %u)", tx_id, txn_data->code);
// Rewrite the destination to our Stub
txn_data->target.ptr = reinterpret_cast<uintptr_t>(g_stub_instance->getWeakRefs());
@@ -431,29 +414,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;
}
}
@@ -473,17 +471,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);
}
}
@@ -526,29 +520,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;
}
@@ -598,16 +581,9 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
Parcel pre_req, pre_resp;
writeTransactionData(pre_req, tx_id, target, code, flags, request);
status_t pre_status = callback->transact(intercept::kPreTransact, pre_req, &pre_resp);
if (pre_status != OK) {
// Block when interceptor is dead to prevent privacy leak to third-party apps
if (callback->pingBinder() != OK) {
LOGE("[TX_ID: %" PRIu64 "] Interceptor DEAD. Blocking to prevent attestation leak.", tx_id);
result = DEAD_OBJECT;
return true;
}
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed (not dead). Forwarding.", tx_id);
return false;
if (callback->transact(intercept::kPreTransact, pre_req, &pre_resp) != OK) {
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed. Forwarding original call.", tx_id);
return false; // Callback failed, proceed as if not intercepted
}
int32_t action = pre_resp.readInt32();
@@ -660,8 +636,7 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
VALIDATE_STATUS(tx_id, post_req.appendFrom(reply, 0, reply_size));
}
status_t post_status = callback->transact(intercept::kPostTransact, post_req, &post_resp);
if (post_status == OK) {
if (callback->transact(intercept::kPostTransact, post_req, &post_resp) == OK) {
int32_t post_action = post_resp.readInt32();
if (post_action == intercept::kActionOverrideReply && reply) {
result = post_resp.readInt32(); // Read new status
+62 -203
View File
@@ -7,7 +7,6 @@
#include <sys/mman.h>
#include <sys/ptrace.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/system_properties.h>
#include <sys/uio.h>
#include <sys/un.h>
@@ -18,7 +17,6 @@
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <optional>
#include <string>
#include <vector>
@@ -97,6 +95,10 @@ constexpr size_t kMagicLength = 16;
constexpr size_t kMaxPathLength = PATH_MAX;
// Maximum length for file paths.
constexpr const char *kSystemFileContext = "u:object_r:system_file:s0";
// SELinux context for system files,
// used for socket creation and library file context.
constexpr const char *kLibcModule = "libc.so";
// Name of the C standard library.
@@ -213,8 +215,8 @@ private:
* @brief Transfers a file descriptor from the injector process to the remote process.
*
* This function uses Unix domain sockets with SCM_RIGHTS to send a file descriptor.
* It involves creating local and remote sockets, binding, and then coordinating
* sendmsg/recvmsg calls using ptrace.
* It involves setting SELinux contexts, creating local and remote sockets, binding,
* and then coordinating sendmsg/recvmsg calls using ptrace.
*
* @param pid The target process ID.
* @param lib_path The path to the library file being transferred.
@@ -231,14 +233,29 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
uintptr_t libc_return_addr) {
LOGD("Attempting to transfer file descriptor for library: %s", lib_path);
// Create a local Unix domain socket for FD transfer.
// 1. Set SELinux context for socket creation in the injector process.
// This is crucial for Android where SELinux might prevent socket operations.
if (!set_sockcreate_con(constants::kSystemFileContext)) {
LOGE("Failed to set socket creation context.");
return std::nullopt;
}
// 2. Create a local Unix domain socket for FD transfer.
UniqueFd local_socket = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
if (local_socket == -1) {
PLOGE("Failed to create local Unix domain socket.");
return std::nullopt;
}
// Open the local library file to get a file descriptor.
// 3. Set SELinux context for the library file if possible.
// This might be required for the target process to open/access it later if directly opening by path.
// For FD transfer, this is less critical as the FD's context is inherited, but good practice.
if (setfilecon(lib_path, constants::kSystemFileContext) == -1) {
// Log a warning, but don't fail, as FD transfer might still work.
PLOGE("Failed to set context of library file: %s. This might cause issues.", lib_path);
}
// 4. Open the local library file to get a file descriptor.
UniqueFd local_lib_fd = open(lib_path, O_RDONLY | O_CLOEXEC);
if (local_lib_fd == -1) {
PLOGE("Failed to open library file: %s", lib_path);
@@ -254,7 +271,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
void *errno_addr; // Address of __errno for getting remote errno.
} funcs{};
// Resolve required libc functions in the remote process.
// 5. Resolve required libc functions in the remote process.
funcs.socket_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "socket");
funcs.bind_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "bind");
funcs.recvmsg_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "recvmsg");
@@ -289,28 +306,25 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
}
};
// Create a Unix domain socket in the remote process.
// 6. Create a Unix domain socket in the remote process.
std::vector<uintptr_t> args = {AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0};
int remote_fd = static_cast<int>(
remote_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.socket_addr), libc_return_addr, args));
if (remote_fd <= 0) {
// remote_call returns 0 on failure.
// socket() returning 0 is technically possible (if stdin closed),
// but highly unlikely for a daemon. We treat 0 as failure here to catch the injection error.
if (remote_fd == -1) {
errno = get_remote_errno(); // Set local errno for PLOGE.
PLOGE("Failed to create remote socket (returned %d).", remote_fd);
PLOGE("Failed to create remote socket.");
return std::nullopt;
}
LOGD("Successfully created remote socket with FD: %d", remote_fd);
// Generate a unique magic string for the abstract Unix domain socket path.
// 7. Generate a unique magic string for the abstract Unix domain socket path.
auto magic = generateMagic(constants::kMagicLength);
struct sockaddr_un sock_addr{.sun_family = AF_UNIX, .sun_path = {0}};
// Abstract Unix domain sockets have sun_path[0] as null, and the name starts from sun_path[1].
memcpy(sock_addr.sun_path + 1, magic.c_str(), magic.size());
socklen_t addr_len = sizeof(sock_addr.sun_family) + 1 + magic.size(); // Length includes null byte and magic.
// Push the sockaddr_un structure to the remote process's stack.
// 8. Push the sockaddr_un structure to the remote process's stack.
auto remote_addr = push_memory(pid, regs, &sock_addr, sizeof(sock_addr));
if (remote_addr == 0) {
LOGE("Failed to push socket address to remote memory.");
@@ -318,7 +332,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
return std::nullopt;
}
// Bind the remote socket to the abstract Unix domain socket path.
// 9. Bind the remote socket to the abstract Unix domain socket path.
args = {static_cast<uintptr_t>(remote_fd), remote_addr, static_cast<uintptr_t>(addr_len)};
auto bind_result = remote_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.bind_addr), libc_return_addr, args);
if (bind_result == static_cast<uintptr_t>(-1)) {
@@ -332,7 +346,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
// Prepare control message buffer for SCM_RIGHTS (file descriptor passing).
char cmsgbuf[CMSG_SPACE(sizeof(int))] = {0};
// Push the control message buffer to the remote process's stack.
// 10. Push the control message buffer to the remote process's stack.
auto remote_cmsgbuf = push_memory(pid, regs, &cmsgbuf, sizeof(cmsgbuf));
if (remote_cmsgbuf == 0) {
LOGE("Failed to push control message buffer to remote memory.");
@@ -345,7 +359,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
msg_hdr.msg_control = reinterpret_cast<void *>(remote_cmsgbuf);
msg_hdr.msg_controllen = sizeof(cmsgbuf);
// Push the msghdr structure to the remote process's stack.
// 11. Push the msghdr structure to the remote process's stack.
auto remote_hdr = push_memory(pid, regs, &msg_hdr, sizeof(msg_hdr));
if (remote_hdr == 0) {
LOGE("Failed to push message header to remote memory.");
@@ -353,16 +367,16 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
return std::nullopt;
}
// Initiate the remote recvmsg call. This will block the remote process.
// 12. Initiate the remote recvmsg call. This will block the remote process.
args = {static_cast<uintptr_t>(remote_fd), remote_hdr, MSG_WAITALL};
if (!remote_pre_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.recvmsg_addr), libc_return_addr, args)) {
if (!remote_pre_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.recvmsg_addr), 0, args)) {
LOGE("Failed to initiate remote recvmsg call.");
close_remote(remote_fd);
return std::nullopt;
}
LOGD("Remote recvmsg initiated, waiting for FD transfer...");
// Prepare the local msghdr for sending the file descriptor.
// 13. Prepare the local msghdr for sending the file descriptor.
// The msg_control and msg_name fields of the local msghdr are set up.
msg_hdr.msg_control = &cmsgbuf; // Use local cmsgbuf for sending.
msg_hdr.msg_name = &sock_addr;
@@ -382,7 +396,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
*reinterpret_cast<int *>(CMSG_DATA(cmsg)) = local_lib_fd; // The FD to send.
}
// Send the file descriptor from the injector to the remote process.
// 14. Send the file descriptor from the injector to the remote process.
if (sendmsg(local_socket, &msg_hdr, 0) == -1) {
PLOGE("Failed to send file descriptor to remote process.");
// We do not close local_lib_fd here as it might be transferred even if
@@ -393,9 +407,9 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
}
LOGD("Local FD %d sent to remote process.", local_lib_fd.operator const int &());
// Complete the remote recvmsg call. This will retrieve the return value.
// 15. Complete the remote recvmsg call. This will retrieve the return value.
auto recvmsg_result =
static_cast<ssize_t>(remote_post_call(pid, regs, libc_return_addr));
static_cast<ssize_t>(remote_post_call(pid, regs, 0)); // No specific expected return address for recvmsg
if (recvmsg_result == -1) {
errno = get_remote_errno();
PLOGE("Remote recvmsg call failed.");
@@ -404,7 +418,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
}
LOGD("Remote recvmsg completed with result: %zd", recvmsg_result);
// Read the control message buffer back from the remote process to extract the FD.
// 16. Read the control message buffer back from the remote process to extract the FD.
if (read_proc(pid, remote_cmsgbuf, &cmsgbuf, sizeof(cmsgbuf)) != sizeof(cmsgbuf)) {
LOGE("Failed to read control message buffer from remote process.");
close_remote(remote_fd);
@@ -425,7 +439,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
LOGI("Successfully transferred FD %d to remote process, new remote FD: %d", local_lib_fd.operator const int &(),
transferred_fd);
// Close the remote socket.
// 17. Close the remote socket.
close_remote(remote_fd);
return transferred_fd;
@@ -628,130 +642,6 @@ static bool remote_call_entry(int pid, struct user_regs_struct &regs, uintptr_t
return true; // Return true if the call itself completed, regardless of its return value.
}
/**
* @brief RAII wrapper to ensure a temporary file is deleted (unlinked)
* when the object goes out of scope.
*
* This is crucial for stealth: we want the library to exist on the filesystem
* for the shortest time possible.
*/
class ScopedFileDeleter {
public:
explicit ScopedFileDeleter(std::string path) : path_(std::move(path)) {}
~ScopedFileDeleter() {
if (!path_.empty()) {
LOGD("Cleaning up staged file: %s", path_.c_str());
unlink(path_.c_str());
}
}
// Disable copy to prevent double-deletion issues
ScopedFileDeleter(const ScopedFileDeleter&) = delete;
ScopedFileDeleter& operator=(const ScopedFileDeleter&) = delete;
private:
std::string path_;
};
/**
* @brief Copies a file from source to destination.
*
* @param src Absolute path to source file.
* @param dst Absolute path to destination file.
* @return True on success, false on failure.
*/
static bool copy_file(const char* src, const char* dst) {
std::ifstream src_file(src, std::ios::binary);
std::ofstream dst_file(dst, std::ios::binary);
if (!src_file) {
PLOGE("Failed to open source file for copying: %s", src);
return false;
}
if (!dst_file) {
PLOGE("Failed to open destination file for copying: %s", dst);
return false;
}
dst_file << src_file.rdbuf();
return src_file.good() && dst_file.good();
}
/**
* @brief Performs injection via the "Staging" method.
*
* This strategy is used when direct FD passing fails (e.g., due to Seccomp filters).
* 1. Copies the library to a world-readable location (/data/local/tmp).
* 2. Loads it via standard dlopen().
* 3. Immediately deletes the file to hide tracks.
*
* @param pid The target process ID.
* @param regs The target process registers (must be Red-Zone adjusted if x86_64).
* @param local_map Local memory map.
* @param remote_map Remote memory map.
* @param lib_path The path to the original library.
* @param libc_return_addr Return address for remote calls.
* @return The handle of the loaded library, or std::nullopt on failure.
*/
static std::optional<uintptr_t> inject_via_staging(int pid, struct user_regs_struct &regs,
const std::vector<lsplt::MapInfo> &local_map,
const std::vector<lsplt::MapInfo> &remote_map,
const char *lib_path, uintptr_t libc_return_addr) {
LOGI("Initiating Staging Fallback mechanism...");
// Generate a random path in /data/local/tmp
// /data/local/tmp is chosen because it is traversable by most contexts.
std::string staged_path = "/data/local/tmp/lib" + generateMagic(8) + ".so";
// Ensure the file is deleted when this function exits (Success or Failure).
// The kernel keeps the inode alive for the mapped process even after unlink.
ScopedFileDeleter file_guard(staged_path);
LOGD("Staging library to: %s", staged_path.c_str());
// Copy the library
if (!copy_file(lib_path, staged_path.c_str())) {
LOGE("Failed to copy library during staging.");
return std::nullopt;
}
// Set Permissions to 644 (RW-R--R--)
// This allows the target process (likely running as a specific UID) to read the file.
if (chmod(staged_path.c_str(), 0644) != 0) {
PLOGE("Failed to chmod staged file.");
return std::nullopt;
}
// Resolve 'dlopen' in the remote process
auto dlopen_addr = find_func_addr(local_map, remote_map, constants::kLibdlModule, "dlopen");
if (!dlopen_addr) {
LOGE("Failed to find 'dlopen' in remote process.");
return std::nullopt;
}
// Push the staged path to remote memory
uintptr_t remote_path_addr = push_string(pid, regs, staged_path.c_str());
if (remote_path_addr == 0) {
LOGE("Failed to push staged path string to remote memory.");
return std::nullopt;
}
// Call dlopen(path, RTLD_NOW)
std::vector<uintptr_t> args = {remote_path_addr, RTLD_NOW};
uintptr_t handle = remote_call(pid, regs, reinterpret_cast<uintptr_t>(dlopen_addr),
libc_return_addr, args);
if (handle == 0) {
std::string error_msg = get_remote_dlerror(pid, regs, local_map, remote_map, libc_return_addr);
LOGE("Staged dlopen failed. dlerror: %s", error_msg.c_str());
return std::nullopt;
}
LOGI("Successfully loaded staged library. Handle: %p", reinterpret_cast<void*>(handle));
return handle;
}
/**
* @brief RAII wrapper for ptrace attachment and detachment.
*
@@ -804,31 +694,12 @@ private:
bool attached_; // Flag indicating current attachment status.
};
// RAII Class to ensure registers are always restored
class RegisterRestorer {
public:
RegisterRestorer(int pid, const struct user_regs_struct& original_regs)
: pid_(pid), regs_(original_regs) {}
~RegisterRestorer() {
// Always restore registers when this object goes out of scope
if (set_regs(pid_, regs_)) {
LOGD("Original registers for process %d restored.", pid_);
} else {
PLOGE("Failed to restore original registers for process %d.", pid_);
}
}
private:
int pid_;
struct user_regs_struct regs_;
};
/**
* @brief Injects a shared library into a target process using ptrace.
*
* This is the main orchestration function for the library injection.
* It handles attachment, remote memory/register manipulation, FD transfer,
* staging fallback, remote dlopen/dlsym, and remote entry point execution.
* remote dlopen/dlsym, and remote entry point execution.
*
* @param pid The target process ID.
* @param lib_path The absolute path to the shared library to inject.
@@ -871,14 +742,6 @@ bool inject_library(int pid, const char *lib_path, const char *entry_name) {
backup_regs = current_regs; // Store a copy for restoration.
LOGD("Process %d registers backed up.", pid);
// Skip the Red Zone (128 bytes) on x86_64 to prevent stack corruption
#if defined(__x86_64__)
current_regs.rsp -= 128;
#endif
// Ensures original state is restored even if injection fails/crashes mid-way.
RegisterRestorer reg_guard(pid, backup_regs);
// Create a scope to ensure RAII objects are destroyed BEFORE register restoration
{
// 4. Scan local and remote memory maps to resolve function addresses.
@@ -897,57 +760,53 @@ bool inject_library(int pid, const char *lib_path, const char *entry_name) {
}
LOGD("Found libc return address: %p", reinterpret_cast<void *>(libc_return_addr));
// 6. Attempt to transfer the library's file descriptor to the remote process.
int remote_fd = -1;
// 6. Transfer the library's file descriptor to the remote process.
auto lib_fd_opt = transfer_fd_to_remote(pid, lib_path, current_regs, local_map, remote_map,
reinterpret_cast<uintptr_t>(libc_return_addr));
std::optional<RemoteLibraryHandle> remote_lib_guard;
std::optional<uintptr_t> handle_opt;
if (lib_fd_opt) {
remote_fd = *lib_fd_opt;
remote_lib_guard.emplace(pid, remote_fd);
remote_lib_guard->set_libc_return_addr(reinterpret_cast<uintptr_t>(libc_return_addr));
LOGD("FD Transfer successful (FD: %d). Attempting android_dlopen_ext...", remote_fd);
handle_opt = remote_dlopen(pid, current_regs, local_map, remote_map, remote_fd, lib_path,
reinterpret_cast<uintptr_t>(libc_return_addr));
} else {
LOGW("Failed to transfer library file descriptor for '%s' to target process %d.", lib_path, pid);
if (!lib_fd_opt) {
LOGE("Failed to transfer library file descriptor for '%s' to target process %d.", lib_path, pid);
return false;
}
RemoteLibraryHandle remote_lib_guard(pid, *lib_fd_opt);
LOGD("Library FD %d transferred to remote process %d.", remote_lib_guard.fd(), pid);
remote_lib_guard.set_libc_return_addr(reinterpret_cast<uintptr_t>(libc_return_addr));
// 7. Staging Fallback (Copy-Inject-Delete) if FD transfer failed.
// 7. Remotely load the library using the transferred file descriptor.
auto handle_opt = remote_dlopen(pid, current_regs, local_map, remote_map, remote_lib_guard.fd(), lib_path,
reinterpret_cast<uintptr_t>(libc_return_addr));
if (!handle_opt) {
handle_opt = inject_via_staging(pid, current_regs, local_map, remote_map,
lib_path, reinterpret_cast<uintptr_t>(libc_return_addr));
}
if (!handle_opt || *handle_opt == 0) {
LOGE("Failed to load library '%s' in remote process %d.", lib_path, pid);
// If dlopen fails, the remote_lib_guard.fd() is still valid in the target process and needs to be closed.
// The RemoteLibraryHandle constructor takes care of this.
return false;
}
uintptr_t handle = *handle_opt;
if (remote_lib_guard) remote_lib_guard->set_handle(handle);
remote_lib_guard.set_handle(*handle_opt);
// 8. Find the entry point symbol in the remotely loaded library.
auto entry_opt = remote_find_entry(pid, current_regs, entry_name, local_map, remote_map,
handle, reinterpret_cast<uintptr_t>(libc_return_addr));
remote_lib_guard.handle(), reinterpret_cast<uintptr_t>(libc_return_addr));
if (!entry_opt) {
LOGE("Failed to find entry point '%s' in remote library (handle %p).", entry_name,
reinterpret_cast<void *>(handle));
reinterpret_cast<void *>(remote_lib_guard.handle()));
return false;
}
uintptr_t entry_addr = *entry_opt;
// 9. Call the remote entry point function.
if (!remote_call_entry(pid, current_regs, entry_addr, handle,
if (!remote_call_entry(pid, current_regs, entry_addr, remote_lib_guard.handle(),
reinterpret_cast<uintptr_t>(libc_return_addr))) {
LOGE("Failed to call remote entry point '%s'.", entry_name);
return false;
}
}
// 10. Restore original registers of the target process.
if (!set_regs(pid, backup_regs)) {
LOGE("Failed to restore original registers for process %d.", pid);
return false;
}
LOGD("Original registers for process %d restored.", pid);
LOGI("Library injection completed successfully for process %d.", pid);
return true;
}
+11 -18
View File
@@ -263,14 +263,7 @@ bool get_regs(int pid, struct user_regs_struct &regs) {
struct iovec reg_iov = {.iov_base = &regs, .iov_len = sizeof(struct user_regs_struct)};
if (ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &reg_iov) == -1) {
PLOGE("Failed to get register set for PID %d.", pid);
#if defined(__arm__)
if (ptrace(PTRACE_GETREGS, pid, 0, &regs) == -1) {
PLOGE("Fallback to PTRACE_GETREGS failed.");
return false;
}
#else
return false;
#endif
}
#else
# error "Unsupported architecture for register access in get_regs."
@@ -303,14 +296,7 @@ bool set_regs(int pid, struct user_regs_struct &regs) {
struct iovec reg_iov = {.iov_base = &regs, .iov_len = sizeof(struct user_regs_struct)};
if (ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, &reg_iov) == -1) {
PLOGE("Failed to set register set for PID %d.", pid);
#if defined(__arm__)
if (ptrace(PTRACE_SETREGS, pid, 0, &regs) == -1) {
PLOGE("Fallback to PTRACE_SETREGS failed.");
return false;
}
#else
return false;
#endif
}
#else
# error "Unsupported architecture for register access in set_regs."
@@ -602,10 +588,17 @@ bool remote_pre_call(int pid, struct user_regs_struct &regs, uintptr_t func_addr
size_t stack_args_size = args.size() * sizeof(uintptr_t);
align_stack(regs, stack_args_size);
// i386 cdecl expects arguments pushed Right-to-Left (stack grows down).
// Since `write_proc` writes to increasing addresses (up), a linear write
// starting at the new SP places the first argument at the lowest address.
// This matches the ABI memory layout without needing to reverse the vector.
// Push all arguments onto the stack (order is important if ABI is right-to-left push).
// The current implementation writes args.data() directly,
// assuming it's already in the correct order for push.
// For cdecl, arguments are pushed right-to-left.
// A vector `args = {A, B, C}` means A is arg1, B is arg2 etc.
// So, `C` should be pushed first, then `B`, then `A`.
// `write_proc` copies linearly.
// This implies `args` should be pre-reversed for cdecl.
// For simplicity, we assume the remote function is compatible with how it's pushed,
// or that it's variadic where order doesn't matter for first args.
// A robust i386 implementation would need to push args in reverse order.
if (write_proc(pid, static_cast<uintptr_t>(regs.REG_SP), args.data(), stack_args_size) !=
static_cast<ssize_t>(stack_args_size)) {
LOGE("Failed to push arguments for i386 remote call.");
-76
View File
@@ -1,76 +0,0 @@
// Fork-based supervisor for instant daemon restart
#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;
static void signal_handler(int sig) {
should_exit = 1;
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <daemon> [args...]\n", argv[0]);
return 1;
}
// Forward termination signals to exit cleanly
signal(SIGTERM, signal_handler);
signal(SIGINT, signal_handler);
const char *daemon_path = argv[1];
char **daemon_argv = &argv[1];
int backoff_ms = 500;
while (!should_exit) {
struct timespec child_start;
clock_gettime(CLOCK_MONOTONIC, &child_start);
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
usleep(100000); // 100ms backoff on fork failure
continue;
}
if (pid == 0) {
// Child: become the daemon
prctl(PR_SET_PDEATHSIG, SIGKILL); // Die if parent dies
setpriority(PRIO_PROCESS, 0, 10); // lower CPU priority than foreground
execv(daemon_path, daemon_argv);
perror("execv failed");
_exit(127);
}
// Parent: wait for child to exit
int status;
waitpid(pid, &status, 0);
if (should_exit) break;
// Exponential backoff on rapid crashes, reset if child was stable
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
long lived_ms = (now.tv_sec - child_start.tv_sec) * 1000 +
(now.tv_nsec - child_start.tv_nsec) / 1000000;
if (lived_ms > 30000) {
backoff_ms = 500;
} else {
usleep(backoff_ms * 1000);
if (backoff_ms < 30000) backoff_ms *= 2;
}
}
return 0;
}
@@ -1,21 +1,11 @@
package org.matrix.TEESimulator
import android.app.ActivityThread
import android.app.Application
import android.content.Context
import android.content.ContextWrapper
import android.os.Build
import android.os.Looper
import java.security.Security
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.matrix.TEESimulator.config.BootStateManager
import org.matrix.TEESimulator.config.ConfigurationManager
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.interception.soter.SoterProcessSupervisor
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.NativeCertGen
import org.matrix.TEESimulator.util.AndroidDeviceUtils
/**
@@ -25,6 +15,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.
@@ -35,79 +27,19 @@ object App {
fun main(args: Array<String>) {
SystemLogger.info("Welcome to TEESimulator!")
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
SystemLogger.error("Uncaught exception on ${thread.name}", throwable)
}
try {
val systemContext = prepareEnvironment()
// Spoof boot-state props before any hook attaches, so keystore2's
// cached snapshot reflects the spoofed values.
BootStateManager.apply()
// Load the package configuration.
ConfigurationManager.initialize()
// Set up the device's boot hash, which is crucial for attestation.
AndroidDeviceUtils.setupBootHash()
// Initialize and start the appropriate keystore interceptors.
initializeInterceptors()
// Set up the device's boot key and hash, which are crucial for attestation.
AndroidDeviceUtils.setupBootKeyAndHash()
// Android ships with a stripped-down Bouncy Castle provider under the name "BC".
// We must remove the system provider first to ensure the full Bouncy Castle library
// (packaged with the app) is used.
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
NativeCertGen.initialize("/data/adb/modules/tricky_store/libcertgen.so")
// Mount the SOTER forge on the on-demand soterserver process. The supervisor
// binds and (re)injects on its own thread, returning at once so it never blocks the loop.
SoterProcessSupervisor.start(systemContext)
// This starts the message queue processing. It blocks here indefinitely
// processing messages until Looper.myLooper().quit() is called.
Looper.loop()
// Enter an infinite loop to keep the service running.
maintainService()
} catch (e: Exception) {
SystemLogger.error("A fatal error occurred in the main application thread.", e)
throw e
}
}
/** Initializes the necessary Android framework internals to satisfy KeyStore requirements. */
private fun prepareEnvironment(): Context {
// 1. Prepare Main Looper
if (Looper.getMainLooper() == null) {
@Suppress("deprecation") Looper.prepareMainLooper()
}
// 2. Initialize ActivityThread for the current process
val activityThread = ActivityThread.systemMain()
// 3. Get the system context. The stub declares getSystemContext(): ContextImpl
// (a bare class), so cast to the Context it really is at runtime for the wiring.
@Suppress("CAST_NEVER_SUCCEEDS")
val systemContext = activityThread.getSystemContext() as Context
// 4. Create a dummy Application object and attach the context
val app = Application()
val attachMethod =
ContextWrapper::class.java.getDeclaredMethod("attachBaseContext", Context::class.java)
attachMethod.isAccessible = true
attachMethod.invoke(app, systemContext)
// 5. Inject this application object into ActivityThread's mInitialApplication field.
// This is what KeyStore.getApplicationContext() looks for.
val mInitialApplicationField =
ActivityThread::class.java.getDeclaredField("mInitialApplication")
mInitialApplicationField.isAccessible = true
mInitialApplicationField.set(activityThread, app)
return systemContext
}
/**
* Selects and initializes the correct keystore interceptor based on the Android SDK version. It
* retries initialization until it succeeds.
@@ -121,7 +53,9 @@ object App {
Thread.sleep(RETRY_DELAY_MS)
}
SystemLogger.info("Interceptors initialized successfully.")
// Load the package configuration after interceptors are ready.
ConfigurationManager.initialize()
SystemLogger.info("Interceptors and configuration initialized successfully.")
}
/**
@@ -136,7 +70,6 @@ object App {
SystemLogger.info(
"Using KeystoreInterceptor for Android Q/R (SDK ${Build.VERSION.SDK_INT})"
)
android.security.keystore.AndroidKeyStoreProvider.install()
KeystoreInterceptor
}
// For Android S (12) and newer, use the Keystore2Interceptor.
@@ -144,8 +77,18 @@ object App {
SystemLogger.info(
"Using Keystore2Interceptor for Android S and later (SDK ${Build.VERSION.SDK_INT})"
)
android.security.keystore2.AndroidKeyStoreProvider.install()
Keystore2Interceptor
}
}
/**
* Puts the main thread into a long-running sleep loop. This is a common pattern to keep a
* background service process alive indefinitely.
*/
private fun maintainService() {
SystemLogger.info("Service started successfully. Entering maintenance mode.")
while (true) {
Thread.sleep(SERVICE_SLEEP_MS)
}
}
}
@@ -1,16 +1,10 @@
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
import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.ASN1OctetString
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.DERNull
import org.bouncycastle.asn1.DEROctetString
@@ -18,10 +12,7 @@ import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.DERSet
import org.bouncycastle.asn1.DERTaggedObject
import org.bouncycastle.asn1.x509.Extension
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.util.AndroidDeviceUtils
import org.matrix.TEESimulator.util.AndroidDeviceUtils.DO_NOT_REPORT
/**
* A builder object responsible for constructing the ASN.1 DER-encoded Android Key Attestation
@@ -33,23 +24,11 @@ object AttestationBuilder {
* Builds the complete X.509 attestation extension.
*
* @param params The parsed key generation parameters.
* @param uid The UID of the application requesting attestation.
* @param securityLevel The security level (e.g., TEE, StrongBox) to report.
* @return A Bouncy Castle [Extension] object ready to be added to a certificate.
*/
fun buildAttestationExtension(
params: KeyMintAttestation,
uid: Int,
securityLevel: Int,
): Extension {
val keyDescription = buildKeyDescription(params, uid, securityLevel)
SystemLogger.verbose {
val formattedString =
keyDescription.joinToString(separator = ", ") {
AttestationPatcher.formatAsn1Primitive(it)
}
"Forged attestation data: $formattedString"
}
fun buildAttestationExtension(params: KeyMintAttestation, securityLevel: Int): Extension {
val keyDescription = buildKeyDescription(params, securityLevel)
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
}
@@ -60,139 +39,81 @@ object AttestationBuilder {
* @return The constructed [DERSequence] for the Root of Trust.
*/
internal fun buildRootOfTrust(originalRootOfTrust: ASN1Encodable?): DERSequence {
val verifiedBootKey = AndroidDeviceUtils.bootKey
val verifiedBootHash =
(originalRootOfTrust as? ASN1Sequence)?.let {
// Try to preserve the original boot hash if it exists.
(it.getObjectAt(AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX)
as? ASN1OctetString)
?.octets
} ?: AndroidDeviceUtils.getBootHashFromProperty()
val rootOfTrustElements = arrayOfNulls<ASN1Encodable>(4)
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX] =
DEROctetString(AndroidDeviceUtils.bootKey)
DEROctetString(verifiedBootKey)
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_DEVICE_LOCKED_INDEX] =
ASN1Boolean.TRUE // deviceLocked: true, for security
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_STATE_INDEX] =
ASN1Enumerated(0) // verifiedBootState: Verified
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX] =
DEROctetString(AndroidDeviceUtils.bootHash)
DEROctetString(verifiedBootHash)
return DERSequence(rootOfTrustElements)
}
/**
* Assembles a map representing the desired state of simulated hardware-enforced properties. A
* null value for a given tag indicates that it should be removed from the attestation.
*
* @param uid The UID of the calling application.
* @return A map where keys are attestation tag numbers and values are the desired
* [DERTaggedObject] or null to signify removal.
*/
fun getSimulatedHardwareProperties(uid: Int): Map<Int, DERTaggedObject?> {
val properties = mutableMapOf<Int, DERTaggedObject?>()
// OS Version is always present.
properties[AttestationConstants.TAG_OS_VERSION] =
/** Assembles a list of simulated hardware-enforced properties. */
internal fun addSimulatedHardwareProperties(vector: org.bouncycastle.asn1.ASN1EncodableVector) {
vector.add(
DERTaggedObject(
true,
AttestationConstants.TAG_OS_VERSION,
ASN1Integer(AndroidDeviceUtils.osVersion.toLong()),
)
val osPatch = AndroidDeviceUtils.getPatchLevel(uid)
properties[AttestationConstants.TAG_OS_PATCHLEVEL] =
if (osPatch != DO_NOT_REPORT) {
DERTaggedObject(
true,
AttestationConstants.TAG_OS_PATCHLEVEL,
ASN1Integer(osPatch.toLong()),
)
} else {
null // Signal for removal
}
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(uid)
properties[AttestationConstants.TAG_VENDOR_PATCHLEVEL] =
if (vendorPatch != DO_NOT_REPORT) {
DERTaggedObject(
true,
AttestationConstants.TAG_VENDOR_PATCHLEVEL,
ASN1Integer(vendorPatch.toLong()),
)
} else {
null // Signal for removal
}
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
SystemLogger.info(
"Attestation patch levels for uid=$uid: os=$osPatch, vendor=$vendorPatch, boot=$bootPatch"
)
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
if (bootPatch != DO_NOT_REPORT) {
DERTaggedObject(
true,
AttestationConstants.TAG_BOOT_PATCHLEVEL,
ASN1Integer(bootPatch.toLong()),
)
} else {
null // Signal for removal
}
return properties
vector.add(
DERTaggedObject(
true,
AttestationConstants.TAG_OS_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.patchLevel.toLong()),
)
)
vector.add(
DERTaggedObject(
true,
AttestationConstants.TAG_VENDOR_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.vendorPatchLevelLong.toLong()),
)
)
vector.add(
DERTaggedObject(
true,
AttestationConstants.TAG_BOOT_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.bootPatchLevelLong.toLong()),
)
)
}
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)
}
/** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */
private fun buildKeyDescription(params: KeyMintAttestation, securityLevel: Int): ASN1Sequence {
val teeEnforced = buildTeeEnforcedList(params)
val softwareEnforced = buildSoftwareEnforcedList()
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.attestVersion.toLong()), // attestationVersion
ASN1Enumerated(securityLevel), // attestationSecurityLevel
ASN1Integer(AndroidDeviceUtils.keymasterVersion.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,
uid: Int,
securityLevel: Int,
): DERSequence {
private fun buildTeeEnforcedList(params: KeyMintAttestation): DERSequence {
val list =
mutableListOf<ASN1Encodable>(
DERTaggedObject(
@@ -215,130 +136,43 @@ 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),
),
DERTaggedObject(
true,
AttestationConstants.TAG_OS_VERSION,
ASN1Integer(AndroidDeviceUtils.osVersion.toLong()),
),
DERTaggedObject(
true,
AttestationConstants.TAG_OS_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.patchLevel.toLong()),
),
DERTaggedObject(
true,
AttestationConstants.TAG_VENDOR_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.vendorPatchLevelLong.toLong()),
),
DERTaggedObject(
true,
AttestationConstants.TAG_BOOT_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.bootPatchLevelLong.toLong()),
),
)
)
// Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels.
val simulatedProperties = getSimulatedHardwareProperties(uid)
simulatedProperties.values.filterNotNull().forEach { list.add(it) }
// Add optional device identifiers if they were provided.
params.brand?.let {
@@ -368,33 +202,6 @@ object AttestationBuilder {
)
)
}
params.serial?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_SERIAL,
DEROctetString(it),
)
)
}
params.imei?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_IMEI,
DEROctetString(it),
)
)
}
params.meid?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_MEID,
DEROctetString(it),
)
)
}
params.manufacturer?.let {
list.add(
DERTaggedObject(
@@ -413,51 +220,35 @@ object AttestationBuilder {
)
)
}
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 300) {
params.secondImei?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_SECOND_IMEI,
DEROctetString(it),
)
)
}
}
return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray())
}
/**
* 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) {
params.imei?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_APPLICATION_ID,
createApplicationId(uid),
AttestationConstants.TAG_ATTESTATION_ID_IMEI,
DEROctetString(it),
)
)
}
params.secondImei?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_SECOND_IMEI,
DEROctetString(it),
)
)
}
params.meid?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_MEID,
DEROctetString(it),
)
)
}
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) {
if (AndroidDeviceUtils.attestVersion >= 400) {
list.add(
DERTaggedObject(
true,
@@ -467,142 +258,25 @@ 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())
}
/**
* A wrapper for a byte array that provides content-based equality. This is necessary for using
* signature digests in a Set.
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
* Keystore.
*/
private data class Digest(val digest: ByteArray) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return digest.contentEquals((other as Digest).digest)
}
override fun hashCode(): Int = digest.contentHashCode()
}
/**
* Creates the AttestationApplicationId structure. This structure contains information about the
* package(s) and their signing certificates.
*
* @param uid The UID of the application.
* @return A DER-encoded octet string containing the application ID information.
* @throws IllegalStateException If the PackageManager or package information cannot be
* retrieved.
*/
@Throws(Throwable::class)
internal fun createApplicationId(uid: Int): DEROctetString {
val appUid = uid % 100000
if (appUid == 0 || appUid == 1000) {
return buildApplicationIdDer(listOf("AndroidSystem" to 1L), emptySet())
}
val pm =
ConfigurationManager.getPackageManager()
?: throw IllegalStateException("PackageManager not found!")
val packages =
pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid")
val sha256 = MessageDigest.getInstance("SHA-256")
val packageInfoList = mutableListOf<Pair<String, Long>>()
val signatureDigests = mutableSetOf<Digest>()
val userId = uid / 100000
packages.forEach { packageName ->
val packageInfo =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.getPackageInfo(
packageName,
PackageManager.GET_SIGNING_CERTIFICATES.toLong(),
userId,
)
} else {
@Suppress("DEPRECATION")
pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId)
}
packageInfoList.add(packageInfo.packageName to packageInfo.longVersionCode)
packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature ->
signatureDigests.add(Digest(sha256.digest(signature.toByteArray())))
}
}
return buildApplicationIdDer(packageInfoList, signatureDigests)
}
private fun buildApplicationIdDer(
packages: List<Pair<String, Long>>,
digests: Set<Digest>,
): DEROctetString {
val packageInfoList =
packages.map { (name, version) ->
DERSequence(
arrayOf(
DEROctetString(name.toByteArray(StandardCharsets.UTF_8)),
ASN1Integer(version),
)
)
}
val applicationIdSequence =
DERSequence(
arrayOf(
DERSet(packageInfoList.toTypedArray()),
DERSet(digests.map { DEROctetString(it.digest) }.toTypedArray()),
private fun buildSoftwareEnforcedList(): DERSequence {
val list =
arrayOf<ASN1Encodable>(
DERTaggedObject(
true,
AttestationConstants.TAG_CREATION_DATETIME,
ASN1Integer(System.currentTimeMillis()),
)
// The ATTESTATION_APPLICATION_ID is technically software-enforced, but we are
// omitting it
// for this simulation as it is complex to generate correctly for arbitrary UIDs.
)
return DEROctetString(applicationIdSequence.encoded)
return DERSequence(list)
}
}
@@ -1,8 +1,10 @@
package org.matrix.TEESimulator.attestation
/**
* Defines constants for KeyMint attestation, mainly the tags of properties and authorizations of a
* cryptographic key, as specified in the Android hardware security HAL.
* Defines constants for KeyMint attestation tags, as specified in the Android hardware security
* HAL.
*
* These tags identify specific properties and authorizations of a cryptographic key.
*/
object AttestationConstants {
// https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/KeyCreationResult.aidl
@@ -44,11 +46,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 +58,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
@@ -92,8 +88,4 @@ object AttestationConstants {
const val TAG_CERTIFICATE_SUBJECT = 1007
const val TAG_CERTIFICATE_NOT_BEFORE = 1008
const val TAG_CERTIFICATE_NOT_AFTER = 1009
// --- Other Constants ---
// https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp
const val CHALLENGE_LENGTH_LIMIT = 128
}
@@ -1,28 +1,23 @@
package org.matrix.TEESimulator.attestation
import android.security.keystore.KeyProperties
import java.nio.charset.StandardCharsets
import java.security.PrivateKey
import java.security.PublicKey
import java.security.cert.Certificate
import java.security.cert.X509Certificate
import java.security.interfaces.ECPrivateKey
import java.security.interfaces.ECPublicKey
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.util.Date
import org.bouncycastle.asn1.*
import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.ASN1TaggedObject
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.DERTaggedObject
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.KeyBox
import org.matrix.TEESimulator.pki.KeyBoxManager
import org.matrix.TEESimulator.util.toHex
/**
* Handles the modification (patching) of Android Key Attestation extensions within certificates.
@@ -43,12 +38,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()
@@ -65,7 +55,8 @@ object AttestationPatcher {
// 2. Get the appropriate keybox for the given algorithm to sign the new
// certificate.
val keybox = getKeyboxForUidAndAlgorithm(uid, originalLeaf.sigAlgName)
val algorithm = originalLeaf.publicKey.algorithm
val keybox = getKeyboxForUidAndAlgorithm(uid, algorithm)
// 3. Create the new, patched leaf certificate.
val patchedLeaf =
@@ -73,9 +64,7 @@ object AttestationPatcher {
originalLeafHolder,
parsedAttestation,
keybox,
uid,
notBefore,
notAfter,
originalLeaf.sigAlgName,
)
// 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's
@@ -102,396 +91,97 @@ object AttestationPatcher {
* @param originalLeafHolder A Bouncy Castle holder for the original leaf certificate.
* @param parsedAttestation The parsed components of the original attestation.
* @param keybox The KeyBox containing the new issuer certificate and signing key.
* @param uid The UID of the application requesting the certificate.
* @param sigAlgName The signature algorithm name (e.g., "SHA256withECDSA") from the original
* certificate. This is required to ensure the new certificate is signed using a compatible
* algorithm.
* @return A new [Certificate] object.
*/
private fun createPatchedLeafCertificate(
originalLeafHolder: X509CertificateHolder,
parsedAttestation: ParsedAttestation,
keybox: KeyBox,
uid: Int,
notBefore: Date? = null,
notAfter: Date? = null,
sigAlgName: String,
): 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,
)
// Create the new, patched attestation extension.
val patchedExtension = createPatchedAttestationExtension(parsedAttestation, uid)
val patchedExtension = createPatchedAttestationExtension(parsedAttestation)
builder.addExtension(patchedExtension)
// Copy all other extensions from the original certificate, except for the attestation.
originalLeafHolder.extensions.extensionOIDs.forEach {
builder.addExtension(
if (it == ATTESTATION_OID) patchedExtension else originalLeafHolder.getExtension(it)
)
}
originalLeafHolder.extensions.extensionOIDs
.filter { it != ATTESTATION_OID }
.forEach { builder.addExtension(originalLeafHolder.getExtension(it)) }
// Sign the new leaf with the keybox key. The signature algorithm must match THAT key, not
// the original leaf's: when an RSA leaf is re-rooted under an EC-only keybox, this signs
// with ECDSA. The RSA subject public key is untouched and the chain still verifies to the
// keybox root.
val signer =
JcaContentSignerBuilder(signatureAlgorithmFor(keybox.keyPair.private))
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(keybox.keyPair.private)
val newCertificate = JcaX509CertificateConverter().getCertificate(builder.build(signer))
// Sign the newly built certificate with the private key from our keybox.
val signer = JcaContentSignerBuilder(sigAlgName).build(keybox.keyPair.private)
// Log the signature of the newly created certificate to observe its non-deterministic
// nature.
val signatureBytes = (newCertificate as X509Certificate).signature
SystemLogger.verbose { "Signature of patched leaf cert: ${signatureBytes.toHex()}" }
return newCertificate
return JcaX509CertificateConverter().getCertificate(builder.build(signer))
}
/**
* Retrieves the appropriate signing KeyBox (KeyPair and certificate chain) for a given UID
* based on a specified algorithm identifier.
*
* @param uid The UID of the application for which the signing is being performed.
* @param algorithm A string representing the desired algorithm. This can be either:
* 1. A simple key type like "RSA" or "EC".
* 2. A full JCA signature algorithm name like "SHA256withRSA".
*
* @return The algorithm-matching [KeyBox] when present, otherwise any available key (fail-safe).
* @throws IllegalArgumentException only if the keybox file contains no usable signing key.
*/
private fun getKeyboxForUidAndAlgorithm(uid: Int, algorithm: String): KeyBox {
val keyboxFile = ConfigurationManager.getKeyboxFileForUid(uid)
// Normalize the algorithm name. The input might be a full signature algorithm
// (e.g., "SHA256withRSA") or just the key type (e.g., "RSA").
val keyType =
when {
algorithm.contains("RSA", ignoreCase = true) -> KeyProperties.KEY_ALGORITHM_RSA
algorithm.contains("EC", ignoreCase = true) ->
KeyProperties.KEY_ALGORITHM_EC // This also covers "ECDSA"
else -> algorithm // If no match, assume it's already a simple key type string.
}
val matching = KeyBoxManager.getAttestationKey(keyboxFile, keyType)
if (matching != null) return matching
// Fail-safe: no algorithm-matching key (e.g. an EC-only Google keybox asked to re-root an
// RSA leaf). Fall back to any available key instead of throwing -- a throw here aborts the
// patch and the caller hands back the device's REAL, unlocked attestation. Re-signing under
// the available key keeps the chain rooted at the keybox with our forged, locked Root of
// Trust; a leaf's signature algorithm is independent of its subject key, so an RSA subject
// key signs validly under an EC keybox key.
return KeyBoxManager.getAnyAttestationKey(keyboxFile)?.also {
SystemLogger.debug(
"No '$keyType' attestation key in $keyboxFile for UID $uid; re-signing under the " +
"available keybox key to avoid leaking the device's real attestation."
)
}
return KeyBoxManager.getAttestationKey(keyboxFile, algorithm)
?: throw IllegalArgumentException(
"No usable attestation key for UID $uid in file $keyboxFile (requested '$keyType')"
"No keybox found for UID $uid and algorithm $algorithm in file $keyboxFile"
)
}
/** SHA-256 signature algorithm name matching the keybox signing key's type. */
private fun signatureAlgorithmFor(signingKey: PrivateKey): String =
when (signingKey) {
is ECPrivateKey -> "SHA256withECDSA"
is RSAPrivateKey -> "SHA256withRSA"
else ->
throw IllegalArgumentException(
"Unsupported keybox signing key type: ${signingKey.algorithm}"
)
}
/** Recursively formats an ASN1Primitive into a concise, readable string. */
fun formatAsn1Primitive(obj: ASN1Encodable?): String {
val primitive = obj?.toASN1Primitive()
return when (primitive) {
null -> "NULL"
is ASN1Integer -> primitive.value.toString()
is ASN1Enumerated -> primitive.value.toString()
is ASN1Boolean -> primitive.isTrue.toString()
is ASN1Null -> "NULL"
is ASN1OctetString -> {
val bytes = primitive.octets
// Attempt to decode as a printable string, otherwise show hex
if (bytes.all { it >= 32 && it < 127 }) {
"\"${String(bytes, StandardCharsets.UTF_8)}\""
} else if (bytes.isEmpty()) {
"\"\""
} else {
"#" + bytes.toHex()
}
}
is ASN1TaggedObject ->
"[TAG ${primitive.tagNo}]${formatAsn1Primitive(primitive.baseObject)}"
is ASN1Sequence ->
primitive
.map { formatAsn1Primitive(it) }
.joinToString(prefix = "[", postfix = "]", separator = ", ")
is ASN1Set ->
primitive
.map { formatAsn1Primitive(it) }
.joinToString(prefix = "{", postfix = "}", separator = ", ")
else -> primitive.toString() // Fallback for other types
}
}
/** Reverse map of attestation tag number to its symbolic name, e.g. 704 -> "ROOT_OF_TRUST". */
private val attestTagNames: Map<Int, String> by lazy {
AttestationConstants::class
.java
.fields
.filter { it.name.startsWith("TAG_") && it.type == Int::class.java }
.associate { (it.get(null) as Int) to it.name.removePrefix("TAG_") }
}
/**
* Renders the full key-attestation extension of [cert] as a single structured line for the
* diagnostic dossier, or null when the certificate carries no attestation extension. This is the
* ground-truth view of what we actually emitted, so any divergence from a genuine TEE surfaces
* directly as a differing field rather than having to be guessed.
*/
fun formatAttestationExtension(cert: X509Certificate): String? {
val rawExtension = cert.getExtensionValue(ATTESTATION_OID.id) ?: return null
return runCatching {
val keyDescriptionDer = ASN1OctetString.getInstance(rawExtension).octets
formatKeyDescription(ASN1Sequence.getInstance(keyDescriptionDer))
}
.getOrElse { "<unparseable attestation extension: ${it.message}>" }
}
/** Renders the identity fields of every certificate in a returned chain for the dossier. */
fun formatCertChain(chain: List<Certificate>): String =
chain
.mapIndexed { index, cert ->
val x509 = cert as? X509Certificate ?: return@mapIndexed "[$index] <non-X509>"
"[$index] subject=${x509.subjectX500Principal.name} " +
"issuer=${x509.issuerX500Principal.name} " +
"serial=${x509.serialNumber.toString(16)} " +
"notBefore=${x509.notBefore} notAfter=${x509.notAfter}"
}
.joinToString(separator = " ; ")
/**
* Verifies every certificate in [chain] against its issuer and renders the outcome for the
* dossier. The forged chain is [leaf] + keybox certs, so edge 0<-1 proves the leaf was signed by
* the key matching the issuer cert and later edges test the keybox's own chain. For an RSA issuer
* it also reports signature-bytes vs modulus-bytes: a signature longer than the modulus is the
* exact DATA_TOO_LARGE_FOR_KEY_SIZE the app's verifier throws, so the offending edge is
* identifiable from the log alone.
*/
fun formatChainVerification(chain: List<Certificate>): String {
if (chain.size < 2) return "<single cert; nothing to chain-verify>"
return (0 until chain.size - 1).joinToString(separator = " ; ") { i ->
val child = chain[i] as? X509Certificate ?: return@joinToString "[$i]<non-X509>"
val parent =
chain[i + 1] as? X509Certificate ?: return@joinToString "[$i]<parent non-X509>"
val outcome =
runCatching {
child.verify(parent.publicKey)
"OK"
}
.getOrElse { "FAIL(${it.javaClass.simpleName}: ${it.message?.take(80)})" }
val rsaSizes =
(parent.publicKey as? RSAPublicKey)?.let {
val sigBytes = child.signature.size
val modBytes = (it.modulus.bitLength() + 7) / 8
" sig=${sigBytes}B mod=${modBytes}B" + if (sigBytes > modBytes) " OVERSIZE" else ""
} ?: ""
"[$i]${describeKey(child.publicKey)}<-[${i + 1}]${describeKey(parent.publicKey)}:" +
"$outcome$rsaSizes"
}
}
/**
* Per-cert key type/size, subject, issuer, and signature length, for reconstructing the chain a
* caller verifies. The signature length reveals the signer's key size, so a 4096-bit signature
* landing on a 2048-bit issuer (DATA_TOO_LARGE) is visible without the certificate bytes.
*/
fun formatChainKeys(chain: List<Certificate>): String =
chain
.mapIndexed { index, cert ->
val x509 = cert as? X509Certificate ?: return@mapIndexed "[$index]<non-X509>"
"[$index]${describeKey(x509.publicKey)} " +
"subj=${x509.subjectX500Principal.name} " +
"iss=${x509.issuerX500Principal.name} " +
"sigLen=${x509.signature.size}B"
}
.joinToString(separator = " ; ")
private fun describeKey(key: PublicKey): String =
when (key) {
is RSAPublicKey -> "RSA${key.modulus.bitLength()}"
is ECPublicKey -> "EC${key.params.curve.field.fieldSize}"
else -> key.algorithm
}
private fun formatKeyDescription(seq: ASN1Sequence): String {
val fields = seq.toArray()
return "attestVer=${formatAsn1Primitive(fields[AttestationConstants.KEY_DESCRIPTION_ATTESTATION_VERSION_INDEX])} " +
"attestSecLvl=${formatSecurityLevel(fields[AttestationConstants.KEY_DESCRIPTION_ATTESTATION_SECURITY_LEVEL_INDEX])} " +
"kmVer=${formatAsn1Primitive(fields[AttestationConstants.KEY_DESCRIPTION_KEYMINT_VERSION_INDEX])} " +
"kmSecLvl=${formatSecurityLevel(fields[AttestationConstants.KEY_DESCRIPTION_KEYMINT_SECURITY_LEVEL_INDEX])} " +
"challenge=${formatAsn1Primitive(fields[AttestationConstants.KEY_DESCRIPTION_ATTESTATION_CHALLENGE_INDEX])} " +
"uniqueId=${formatAsn1Primitive(fields[AttestationConstants.KEY_DESCRIPTION_UNIQUE_ID_INDEX])} " +
"sw=${formatAuthorizationList(fields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX])} " +
"tee=${formatAuthorizationList(fields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX])}"
}
private fun formatSecurityLevel(obj: ASN1Encodable): String {
val level = (obj.toASN1Primitive() as? ASN1Enumerated)?.value?.toInt()
val name =
when (level) {
0 -> "Software"
1 -> "TEE"
2 -> "StrongBox"
else -> "?"
}
return "$level($name)"
}
private fun formatAuthorizationList(obj: ASN1Encodable): String {
val seq = obj.toASN1Primitive() as? ASN1Sequence ?: return formatAsn1Primitive(obj)
return seq
.map { element ->
val tagged = element as? ASN1TaggedObject ?: return@map formatAsn1Primitive(element)
val name = attestTagNames[tagged.tagNo] ?: "TAG"
val value =
if (tagged.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST)
formatRootOfTrust(tagged.baseObject)
else formatAsn1Primitive(tagged.baseObject)
"${tagged.tagNo}($name)=$value"
}
.joinToString(prefix = "[", postfix = "]", separator = ", ")
}
/**
* Decodes the Root of Trust sub-sequence explicitly — it is the field a detector most often uses
* to unmask a simulated TEE (a random verifiedBootKey, an unexpected verifiedBootState, or a
* deviceLocked that disagrees with the bootloader all live here).
*/
private fun formatRootOfTrust(obj: ASN1Encodable): String {
val fields = (obj.toASN1Primitive() as? ASN1Sequence)?.toArray() ?: return formatAsn1Primitive(obj)
val state = fields.getOrNull(AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_STATE_INDEX)
val stateName =
when ((state?.toASN1Primitive() as? ASN1Enumerated)?.value?.toInt()) {
0 -> "Verified"
1 -> "SelfSigned"
2 -> "Unverified"
3 -> "Failed"
else -> "?"
}
return "[bootKey=${formatAsn1Primitive(fields.getOrNull(AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX))}, " +
"deviceLocked=${formatAsn1Primitive(fields.getOrNull(AttestationConstants.ROOT_OF_TRUST_DEVICE_LOCKED_INDEX))}, " +
"verifiedBootState=${formatAsn1Primitive(state)}($stateName), " +
"bootHash=${formatAsn1Primitive(fields.getOrNull(AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX))}]"
}
// Function to check if a given ASN1Sequence contains the Root of Trust tag.
private fun sequenceContainsRootOfTrust(seq: ASN1Encodable): Boolean {
if (seq !is ASN1Sequence) return false
return seq.any { element ->
(element as? ASN1TaggedObject)?.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST
}
}
/** Parses the critical components from an existing attestation extension. */
private fun parseAttestationExtension(certHolder: X509CertificateHolder): ParsedAttestation? {
val extension = certHolder.getExtension(ATTESTATION_OID) ?: return null
val sequence = ASN1Sequence.getInstance(extension.extnValue.octets)
val allFields = sequence.toArray()
// Check if the fields are in the wrong order and swap them if necessary.
val softwareEnforcedCandidate =
allFields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX]
val teeEnforcedCandidate =
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX]
// The signature of a swapped order: the RoT is in the software list's position.
if (
sequenceContainsRootOfTrust(softwareEnforcedCandidate) &&
!sequenceContainsRootOfTrust(teeEnforcedCandidate)
) {
// Swap the elements in the array to restore the standard order.
allFields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX] =
teeEnforcedCandidate
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] =
softwareEnforcedCandidate
}
val teeEnforced =
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] as ASN1Sequence
val teeEnforcedVector = ASN1EncodableVector()
var originalRootOfTrust: ASN1Encodable? = null
val teeEnforcedMap = mutableMapOf<Int, ASN1TaggedObject>()
teeEnforced.forEach { element ->
val taggedObject = element as ASN1TaggedObject
if (taggedObject.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST) {
originalRootOfTrust = taggedObject.baseObject.toASN1Primitive()
} else {
teeEnforcedMap[taggedObject.tagNo] = taggedObject
teeEnforcedVector.add(taggedObject)
}
}
return ParsedAttestation(allFields, teeEnforcedMap, originalRootOfTrust)
return ParsedAttestation(allFields, teeEnforcedVector, originalRootOfTrust)
}
/** Constructs a new, patched attestation extension using simulated device properties. */
private fun createPatchedAttestationExtension(parsed: ParsedAttestation, uid: Int): Extension {
val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed
private fun createPatchedAttestationExtension(parsed: ParsedAttestation): Extension {
val (allFields, teeEnforcedVector, originalRootOfTrust) = parsed
SystemLogger.verbose {
val formattedString =
allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
"Original attestation data: $formattedString"
}
// Build the new Root of Trust and add/replace it in the map.
// Build the new Root of Trust with our simulated values.
val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust)
teeEnforcedMap[AttestationConstants.TAG_ROOT_OF_TRUST] =
teeEnforcedVector.add(
DERTaggedObject(true, AttestationConstants.TAG_ROOT_OF_TRUST, newRootOfTrust)
)
// Get the desired state for simulated properties.
val simulatedProperties = AttestationBuilder.getSimulatedHardwareProperties(uid)
// Add other simulated hardware properties.
AttestationBuilder.addSimulatedHardwareProperties(teeEnforcedVector)
// Apply the desired state: update, add, or remove properties from the original map.
simulatedProperties.forEach { (tag, value) ->
if (value != null) {
// If the value is not null, add or update it.
teeEnforcedMap[tag] = value
} else {
// If the value is null, remove the tag from the map.
teeEnforcedMap.remove(tag)
}
}
// Re-assemble the TEE enforced list from the map's values, sorting for DER compliance.
val sortedElements = teeEnforcedMap.values.sortedBy { it.tagNo }
// Re-assemble the ASN.1 sequences.
// The list MUST be sorted by tag number for DER compliance.
// Manually convert the vector to a List, then sort it.
val elementList = (0 until teeEnforcedVector.size()).map { teeEnforcedVector.get(it) }
val sortedElements = elementList.sortedBy { (it as ASN1TaggedObject).tagNo }
val sortedTeeEnforced = DERSequence(sortedElements.toTypedArray())
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"
}
val patchedOctets = DEROctetString(patchedSequence)
return Extension(ATTESTATION_OID, false, patchedOctets)
@@ -500,7 +190,7 @@ object AttestationPatcher {
/** Helper data class to hold the parsed components of an attestation extension. */
private data class ParsedAttestation(
val allFields: Array<ASN1Encodable>,
val teeEnforcedMap: MutableMap<Int, ASN1TaggedObject>,
val teeEnforcedVector: ASN1EncodableVector,
val rootOfTrust: ASN1Encodable?,
)
}
@@ -1,7 +1,8 @@
package org.matrix.TEESimulator.attestation
import android.annotation.SuppressLint
import android.security.KeyStoreException
import android.app.ActivityThread
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyPairGenerator
@@ -9,9 +10,6 @@ import java.security.KeyStore
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.security.spec.ECGenParameterSpec
import java.security.spec.RSAKeyGenParameterSpec
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.ASN1OctetString
@@ -20,7 +18,6 @@ import org.bouncycastle.asn1.ASN1TaggedObject
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.cert.X509CertificateHolder
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.util.AndroidDeviceUtils
import org.matrix.TEESimulator.util.toHex
/**
@@ -41,25 +38,16 @@ object DeviceAttestationService {
* Holds key data extracted from a genuine device attestation. This data can be used as a
* baseline for creating simulated attestations.
*
* @property verifiedBootKey The verified boot public key digest from the root of trust.
* @property verifiedBootHash The verified boot hash from the root of trust.
* @property attestVersion The attestation version (e.g., 400 for KeyMint 4.0).
* @property keymasterVersion The Keymaster or KeyMint HAL version.
* @property osVersion The Android OS version integer.
* @property osPatchLevel The Android security patch level (e.g., 202511).
* @property vendorPatchLevel The vendor-specific security patch level.
* @property bootPatchLevel The bootloader's security patch level.
*/
data class AttestationData(
val moduleHash: ByteArray?,
val verifiedBootKey: ByteArray?,
val verifiedBootHash: ByteArray?,
val attestVersion: Int?,
val keymasterVersion: Int?,
val osVersion: Int?,
val osPatchLevel: Int?,
val vendorPatchLevel: Int?,
val bootPatchLevel: Int?,
)
// A unique alias for the key used to perform the TEE functionality check.
@@ -71,58 +59,6 @@ object DeviceAttestationService {
*/
val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() }
// Per (algorithm, security-level) attestation-capability verdicts, keyed by probe-key alias.
// A device may attest one algorithm or security level yet lack a provisioned attestation key
// for another (e.g. a TEE that attests RSA over a StrongBox that cannot), so each pair is
// probed and cached on its own.
private data class ProbeSpec(
val algorithm: String,
val strongBox: Boolean,
val keyAlias: String,
)
private val rsaTeeProbe =
ProbeSpec(KeyProperties.KEY_ALGORITHM_RSA, false, "TEESimulator_RsaAttestCheck")
private val rsaStrongBoxProbe =
ProbeSpec(KeyProperties.KEY_ALGORITHM_RSA, true, "TEESimulator_RsaAttestCheckSb")
private val ecTeeProbe =
ProbeSpec(KeyProperties.KEY_ALGORITHM_EC, false, "TEESimulator_EcAttestCheck")
private val ecStrongBoxProbe =
ProbeSpec(KeyProperties.KEY_ALGORITHM_EC, true, "TEESimulator_EcAttestCheckSb")
private val attestableVerdicts = ConcurrentHashMap<String, Boolean>()
private val attestProbesInFlight = ConcurrentHashMap<String, AtomicBoolean>()
/**
* Whether the real hardware can attest an RSA key at the requested security level. AUTO dispatch
* reads this to forge RSA attestation only where the hardware genuinely cannot serve it.
*
* Only a definitive verdict is cached: a successful probe, or a permanent keystore failure. A
* transient or unrecognized failure leaves the verdict unset and reports attestable, so dispatch
* PATCHes the genuine chain and re-probes next read — a one-off keystore hiccup can never freeze
* the device into forging an attestation it could serve.
*/
fun isRsaAttestable(strongBox: Boolean): Boolean =
isHardwareAttestable(if (strongBox) rsaStrongBoxProbe else rsaTeeProbe)
/** Whether the real hardware can attest an EC key at the requested security level. */
fun isEcAttestable(strongBox: Boolean): Boolean =
isHardwareAttestable(if (strongBox) ecStrongBoxProbe else ecTeeProbe)
private fun isHardwareAttestable(probe: ProbeSpec): Boolean {
attestableVerdicts[probe.keyAlias]?.let { return it }
val probeInFlight =
attestProbesInFlight.computeIfAbsent(probe.keyAlias) { AtomicBoolean(false) }
if (probeInFlight.compareAndSet(false, true)) {
try {
probeAttestability(probe)?.let { attestableVerdicts[probe.keyAlias] = it }
} finally {
probeInFlight.set(false)
}
}
return attestableVerdicts[probe.keyAlias] ?: true
}
/**
* Lazily fetches and parses attestation data from a genuinely generated certificate. The result
* is cached. Returns null if the TEE is not functional or parsing fails.
@@ -138,6 +74,16 @@ object DeviceAttestationService {
private fun checkTeeFunctionality(): Boolean {
SystemLogger.info("Performing TEE functionality check...")
return try {
// Ensure mainline modules and the correct Keystore provider are initialized.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
android.app.ActivityThread.initializeMainlineModules()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
android.security.keystore2.AndroidKeyStoreProvider.install()
} else {
android.security.keystore.AndroidKeyStoreProvider.install()
}
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val keyPairGenerator =
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
@@ -163,84 +109,6 @@ object DeviceAttestationService {
}
}
/**
* Probes whether the real hardware can attest a key matching [probe] by generating one with an
* attestation challenge at the probe's algorithm and security level. Mirrors
* [checkTeeFunctionality]; the request runs as the module UID, so it is skipped by interception
* and reaches genuine hardware rather than the forge path.
*
* @return `true` if attestation succeeded, `false` only on a confirmed attestation-keys-
* unavailable failure, or `null` on a transient or unrecognized failure where the caller
* fails open and re-probes.
*/
private fun probeAttestability(probe: ProbeSpec): Boolean? {
val label = "${probe.algorithm} attestation (strongBox=${probe.strongBox})"
SystemLogger.info("Performing $label capability check...")
return try {
val keyPairGenerator =
KeyPairGenerator.getInstance(probe.algorithm, "AndroidKeyStore")
val challenge = ByteArray(16).apply { SecureRandom().nextBytes(this) }
val builder =
KeyGenParameterSpec.Builder(probe.keyAlias, KeyProperties.PURPOSE_SIGN)
.setDigests(KeyProperties.DIGEST_SHA256)
.setAttestationChallenge(challenge)
.setIsStrongBoxBacked(probe.strongBox)
if (probe.algorithm == KeyProperties.KEY_ALGORITHM_RSA) {
builder
.setAlgorithmParameterSpec(RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4))
.setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
} else {
builder.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
}
keyPairGenerator.initialize(builder.build())
keyPairGenerator.generateKeyPair()
SystemLogger.info("$label capability check successful.")
true
} catch (e: Exception) {
if (isAttestationUnavailable(e)) {
SystemLogger.info("$label unsupported by hardware; AUTO will forge attestation.")
false
} else {
SystemLogger.warning(
"$label capability check failed transiently; treating as capable.",
e,
)
null
}
} finally {
deleteProbeKey(probe.keyAlias)
}
}
/**
* Whether [error] definitively means the hardware cannot attest the probed key: a permanent
* [KeyStoreException] from the keystore. Transient failures and non-keystore errors return
* `false`, so the caller fails open and re-probes rather than caching a guess. The probe runs a
* fixed, valid spec as root, so its only permanent keystore failure mode is missing attestation
* support; [KeyStoreException.isTransientFailure] draws the transient/permanent line.
*/
private fun isAttestationUnavailable(error: Throwable): Boolean {
var cause: Throwable? = error
while (cause != null) {
val keyStoreError = cause as? KeyStoreException
if (keyStoreError != null) return !keyStoreError.isTransientFailure
cause = cause.cause
}
return false
}
private fun deleteProbeKey(keyAlias: String) {
try {
KeyStore.getInstance("AndroidKeyStore").apply { load(null) }.deleteEntry(keyAlias)
} catch (e: Exception) {
SystemLogger.warning("Failed to delete attestation probe key.", e)
}
}
/**
* Retrieves the attestation certificate generated during the TEE check. The key entry is
* deleted after retrieval to clean up.
@@ -283,24 +151,14 @@ 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 = ", ") {
AttestationPatcher.formatAsn1Primitive(it)
}
"Cached attestation data: $formattedString"
}
val fields = keyDescriptionSeq.toArray()
val deviceAttestVersion =
val attestVersion =
ASN1Integer.getInstance(
fields[AttestationConstants.KEY_DESCRIPTION_ATTESTATION_VERSION_INDEX]
)
.positiveValue
.toInt()
// The device KeyMint HAL can report a version below its OS's AOSP value (100 on an A16
// where BAKLAVA mandates 400); cache the AOSP value so the forge matches an updated device.
val attestVersion = AndroidDeviceUtils.aospAttestVersion ?: deviceAttestVersion
val keymasterVersion =
ASN1Integer.getInstance(
fields[AttestationConstants.KEY_DESCRIPTION_KEYMINT_VERSION_INDEX]
@@ -308,27 +166,8 @@ object DeviceAttestationService {
.positiveValue
.toInt()
var moduleHash: ByteArray? = null
var verifiedBootKey: ByteArray? = null
var verifiedBootHash: ByteArray? = null
var osVersion: Int? = null
var osPatchLevel: Int? = null
var vendorPatchLevel: Int? = null
var bootPatchLevel: Int? = null
val softwareEnforced =
ASN1Sequence.getInstance(
fields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX]
)
moduleHash =
softwareEnforced
.toArray()
.firstOrNull {
(it as? ASN1TaggedObject)?.tagNo == AttestationConstants.TAG_MODULE_HASH
}
?.let {
ASN1OctetString.getInstance((it as ASN1TaggedObject).baseObject).octets
}
val teeEnforced =
ASN1Sequence.getInstance(
@@ -340,14 +179,6 @@ object DeviceAttestationService {
AttestationConstants.TAG_ROOT_OF_TRUST -> {
val rotSeq = ASN1Sequence.getInstance(tagged.baseObject.toASN1Primitive())
if (rotSeq.size() >= 4) {
verifiedBootKey =
ASN1OctetString.getInstance(
rotSeq.getObjectAt(
AttestationConstants
.ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX
)
)
.octets
verifiedBootHash =
ASN1OctetString.getInstance(
rotSeq.getObjectAt(
@@ -358,55 +189,19 @@ object DeviceAttestationService {
.octets
}
}
AttestationConstants.TAG_OS_VERSION -> {
AttestationConstants.TAG_OS_VERSION -> { // OS Version (TAG_OS_VERSION)
osVersion =
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
.positiveValue
.toInt()
}
AttestationConstants.TAG_OS_PATCHLEVEL -> {
osPatchLevel =
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
.positiveValue
.toInt()
}
AttestationConstants.TAG_VENDOR_PATCHLEVEL -> {
vendorPatchLevel =
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
.positiveValue
.toInt()
}
AttestationConstants.TAG_BOOT_PATCHLEVEL -> {
bootPatchLevel =
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
.positiveValue
.toInt()
}
}
}
if (verifiedBootKey?.all { it == 0.toByte() } == true) {
verifiedBootKey = null
}
if (verifiedBootHash?.all { it == 0.toByte() } == true) {
verifiedBootHash = null
}
SystemLogger.info(
"Successfully extracted attestation data: version=$deviceAttestVersion, osVersion=$osVersion, osPatch=$osPatchLevel, vendorPatch=$vendorPatchLevel, bootPatch=$bootPatchLevel, moduleHash=${moduleHash?.toHex()}, bootKey=${verifiedBootKey?.toHex()}, bootHash=${verifiedBootHash?.toHex()}"
)
return AttestationData(
moduleHash,
verifiedBootKey,
verifiedBootHash,
attestVersion,
keymasterVersion,
osVersion,
osPatchLevel,
vendorPatchLevel,
bootPatchLevel,
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, bootHash=${verifiedBootHash?.toHex()}"
)
return AttestationData(verifiedBootHash, attestVersion, keymasterVersion, osVersion)
} catch (e: Exception) {
SystemLogger.error("Failed to parse attestation data from certificate.", e)
return null
@@ -1,7 +1,8 @@
package org.matrix.TEESimulator.attestation
import android.hardware.security.keymint.*
import android.hardware.security.keymint.KeyOrigin
import android.hardware.security.keymint.EcCurve
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.Tag
import java.math.BigInteger
import java.util.Date
import javax.security.auth.x500.X500Principal
@@ -19,11 +20,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>,
val digest: List<Int>,
val rsaPublicExponent: BigInteger?,
@@ -35,54 +33,26 @@ data class KeyMintAttestation(
val brand: ByteArray?,
val device: ByteArray?,
val product: ByteArray?,
val serial: ByteArray?,
val imei: ByteArray?,
val meid: ByteArray?,
val manufacturer: ByteArray?,
val model: ByteArray?,
val imei: 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 macLength: Int? = null,
val rsaOaepMgfDigest: List<Int>,
val meid: ByteArray?,
) {
/** 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),
// AOSP: [key_param(tag = PADDING, field = PaddingMode)]
padding = params.findAllPaddingMode(Tag.PADDING),
// AOSP: [key_param(tag = PURPOSE, field = KeyPurpose)]
purpose = params.findAllKeyPurpose(Tag.PURPOSE),
@@ -112,40 +82,15 @@ data class KeyMintAttestation(
brand = params.findBlob(Tag.ATTESTATION_ID_BRAND),
device = params.findBlob(Tag.ATTESTATION_ID_DEVICE),
product = params.findBlob(Tag.ATTESTATION_ID_PRODUCT),
serial = params.findBlob(Tag.ATTESTATION_ID_SERIAL),
imei = params.findBlob(Tag.ATTESTATION_ID_IMEI),
meid = params.findBlob(Tag.ATTESTATION_ID_MEID),
manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER),
model = params.findBlob(Tag.ATTESTATION_ID_MODEL),
imei = params.findBlob(Tag.ATTESTATION_ID_IMEI),
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),
macLength = params.findInteger(Tag.MAC_LENGTH),
rsaOaepMgfDigest = params.findAllDigests(Tag.RSA_OAEP_MGF_DIGEST),
meid = params.findBlob(Tag.ATTESTATION_ID_MEID),
) {
// 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 ---
@@ -162,10 +107,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()
@@ -178,14 +119,6 @@ private fun Array<KeyParameter>.findDate(tag: Int): Date? =
private fun Array<KeyParameter>.findBlob(tag: Int): ByteArray? =
this.find { it.tag == tag }?.value?.blob
/** Maps to AOSP field = BlockMode (Repeated) */
private fun Array<KeyParameter>.findAllBlockMode(tag: Int): List<Int> =
this.filter { it.tag == tag }.map { it.value.blockMode }
/** Maps to AOSP field = BlockMode (Repeated) */
private fun Array<KeyParameter>.findAllPaddingMode(tag: Int): List<Int> =
this.filter { it.tag == tag }.map { it.value.paddingMode }
/** Maps to AOSP field = KeyPurpose (Repeated) */
private fun Array<KeyParameter>.findAllKeyPurpose(tag: Int): List<Int> =
this.filter { it.tag == tag }.map { it.value.keyPurpose }
@@ -194,21 +127,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).
@@ -1,123 +0,0 @@
package org.matrix.TEESimulator.config
import android.os.SystemProperties
import java.io.File
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.util.AndroidDeviceUtils
object BootStateManager {
private const val CONFIG_PATH = "/data/adb/tricky_store"
private const val BOOT_PROPS_MODE_FILE = "boot_props_mode"
private enum class BootPropsMode {
AUTO,
FORCE,
DISABLE,
}
private val targets =
linkedMapOf(
"ro.boot.verifiedbootstate" to "green",
"ro.boot.flash.locked" to "1",
"ro.boot.veritymode" to "enforcing",
"ro.boot.vbmeta.device_state" to "locked",
)
private val fillIfAbsent =
linkedMapOf(
"ro.boot.vbmeta.invalidate_on_error" to "yes",
"ro.boot.vbmeta.avb_version" to "1.2",
"ro.boot.vbmeta.hash_alg" to "sha256",
"ro.boot.vbmeta.size" to "11904",
)
fun apply() {
val mode = readBootPropsMode()
when (mode) {
BootPropsMode.DISABLE -> {
SystemLogger.info("BootStateManager: disabled by $BOOT_PROPS_MODE_FILE")
return
}
BootPropsMode.AUTO -> {
if (isOplusFamilyDevice()) {
SystemLogger.warning(
"BootStateManager: skipping boot-state prop spoofing on Oplus-family device in auto mode"
)
return
}
}
BootPropsMode.FORCE -> {
SystemLogger.info("BootStateManager: force-enabled by $BOOT_PROPS_MODE_FILE")
}
}
for ((name, target) in targets) {
val current = SystemProperties.get(name, "")
if (current.isEmpty()) {
SystemLogger.debug("BootStateManager: $name absent on this device, skip")
continue
}
if (current == target) {
SystemLogger.debug("BootStateManager: $name already $target, skip")
continue
}
SystemLogger.info("BootStateManager: setting $name=$target (was: '$current')")
AndroidDeviceUtils.setProperty(name, target)
}
for ((name, value) in fillIfAbsent) {
val current = SystemProperties.get(name, "")
if (current.isNotEmpty()) {
SystemLogger.debug("BootStateManager: $name already '$current', skip")
continue
}
SystemLogger.info("BootStateManager: filling absent $name=$value")
AndroidDeviceUtils.setProperty(name, value)
}
}
fun shouldSpoofBootProps(): Boolean =
when (readBootPropsMode()) {
BootPropsMode.DISABLE -> false
BootPropsMode.AUTO -> !isOplusFamilyDevice()
BootPropsMode.FORCE -> true
}
private fun readBootPropsMode(): BootPropsMode {
val file = File(CONFIG_PATH, BOOT_PROPS_MODE_FILE)
if (!file.exists()) return BootPropsMode.AUTO
val raw =
runCatching { file.readText().trim().lowercase() }
.getOrElse {
SystemLogger.warning("BootStateManager: failed to read ${file.absolutePath}", it)
return BootPropsMode.AUTO
}
return when (raw) {
"1", "true", "on", "enable", "enabled", "force" -> BootPropsMode.FORCE
"0", "false", "off", "disable", "disabled", "none" -> BootPropsMode.DISABLE
else -> BootPropsMode.AUTO
}
}
private fun isOplusFamilyDevice(): Boolean {
val props =
listOf(
"ro.product.manufacturer",
"ro.product.brand",
"ro.product.vendor.manufacturer",
"ro.product.vendor.brand",
"ro.product.odm.manufacturer",
"ro.product.odm.brand",
"ro.boot.hardware.sku",
"ro.boot.project_name",
)
val joined =
props.joinToString(separator = " ") { name ->
SystemProperties.get(name, "")
}.lowercase()
return listOf("oneplus", "oplus", "oppo", "realme").any { joined.contains(it) }
}
}
@@ -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,8 +39,8 @@ object ConfigurationManager {
// --- In-Memory Configuration State ---
@Volatile private var packageModes = mapOf<String, Mode>()
@Volatile private var packageKeyboxes = mapOf<String, String>()
@Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null
@Volatile private var packagePatchLevels = mapOf<String, CustomPatchLevel>()
@Volatile private var isTeeBroken: Boolean? = null
@Volatile var customPatchLevelOverride: CustomPatchLevel? = null
// Cache for UID to package name resolution.
private val uidToPackagesCache = ConcurrentHashMap<Int, Array<String>>()
@@ -52,20 +53,11 @@ object ConfigurationManager {
configRoot.mkdirs()
SystemLogger.info("Configuration root is: ${configRoot.absolutePath}")
// First, ensure the package manager service is running, as the TEE check depends on it.
// This prevents a race condition on startup.
SystemLogger.info("Waiting for PackageManagerService to be ready...")
if (getPackageManager() == null) {
SystemLogger.error(
"PackageManagerService is not available. TEE check will likely fail."
)
} else {
SystemLogger.info("PackageManagerService is ready.")
}
// Initial load of all configuration files.
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,58 +75,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 {
for (pkg in getPackagesForUid(uid)) {
when (packageModes[pkg]) {
Mode.GENERATE,
Mode.PATCH -> return false
Mode.AUTO -> return true
null -> continue
}
}
return false
}
/** 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
}
/**
* Retrieves the custom patch level configuration for a given UID. It first checks for a
* package-specific override and falls back to the global configuration.
*
* @param uid The UID of the calling application.
* @return The applicable [CustomPatchLevel], or null if no custom configuration exists.
*/
fun getPatchLevelForUid(uid: Int): CustomPatchLevel? {
val packages = getPackagesForUid(uid)
// Find the first package-specific configuration for this UID.
val packageSpecificPatchLevel =
packages.firstNotNullOfOrNull { pkg -> packagePatchLevels[pkg] }
return packageSpecificPatchLevel ?: globalCustomPatchLevel
return null // No configuration found for this UID.
}
/**
@@ -177,6 +144,7 @@ object ConfigurationManager {
newModes[pkg] = Mode.PATCH
newKeyboxes[pkg] = currentKeybox
}
// No suffix means AUTO mode.
else -> {
newModes[trimmedLine] = Mode.AUTO
newKeyboxes[trimmedLine] = currentKeybox
@@ -194,48 +162,26 @@ object ConfigurationManager {
}
}
/**
* Loads and parses the `security_patch.txt` file, which can define both global and per-package
* security patch levels.
*/
/** Loads the security patch level override configuration from `security_patch.txt`. */
private fun loadPatchLevelConfig(file: File) {
if (!file.exists()) {
globalCustomPatchLevel = null
packagePatchLevels = emptyMap()
return
}
try {
val newPackageLevels = mutableMapOf<String, CustomPatchLevel>()
var currentContext = "" // Empty string for global context
val contextLines = mutableMapOf<String, MutableList<String>>()
val contextRegex = Regex("^\\[([a-zA-Z0-9_.-]+)]$")
// First pass: group lines by context (global or package-specific).
file.readLines().forEach { line ->
val trimmedLine = line.trim()
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) return@forEach
contextRegex.find(trimmedLine)?.let { currentContext = it.groupValues[1] }
?: run {
contextLines
.computeIfAbsent(currentContext) { mutableListOf() }
.add(trimmedLine)
if (file.exists()) {
try {
val lines =
file.readLines().mapNotNull { line ->
val trimmed = line.trim()
if (trimmed.isNotEmpty() && !trimmed.startsWith("#")) trimmed else null
}
}
// Helper function to parse a set of lines into a CustomPatchLevel object.
fun parseLines(lines: List<String>?): CustomPatchLevel? {
if (lines.isNullOrEmpty()) return null
if (lines.isEmpty()) {
customPatchLevelOverride = null
return
}
// Handle simple case: one line sets the patch level for all components.
if (lines.size == 1 && '=' !in lines[0]) {
return CustomPatchLevel(
system = null,
vendor = null,
boot = null,
all = lines[0],
)
customPatchLevelOverride =
CustomPatchLevel(system = null, vendor = null, boot = null, all = lines[0])
return
}
// Handle key-value pair configuration.
@@ -249,44 +195,45 @@ object ConfigurationManager {
.toMap()
val all = map["all"]
return CustomPatchLevel(
system = map["system"] ?: all,
vendor = map["vendor"] ?: all,
boot = map["boot"] ?: all,
all = all,
)
customPatchLevelOverride =
CustomPatchLevel(
system = map["system"] ?: all,
vendor = map["vendor"] ?: all,
boot = map["boot"] ?: all,
all = all,
)
SystemLogger.info("Loaded custom security patch levels.")
} catch (e: Exception) {
SystemLogger.error("Failed to load or parse ${file.name}", e)
}
// Parse global and per-package configurations.
var newGlobalLevel = parseLines(contextLines[""])
// TrickyAddon writes Pixel bulletin dates for boot/vendor but system=prop
// resolves to the real device prop — force boot/vendor through the same path
// to prevent cross-component date mismatches on non-Pixel devices.
if (newGlobalLevel?.system.equals("prop", ignoreCase = true)) {
SystemLogger.info(
"system=prop: forcing boot/vendor to derive from device props (were: boot=${newGlobalLevel?.boot}, vendor=${newGlobalLevel?.vendor})"
)
newGlobalLevel = newGlobalLevel?.copy(boot = "prop", vendor = "prop")
}
contextLines.remove("") // Remove global context to iterate over packages next
for ((pkg, lines) in contextLines) {
parseLines(lines)?.let { newPackageLevels[pkg] = it }
}
// Atomically update the configuration state.
globalCustomPatchLevel = newGlobalLevel
packagePatchLevels = newPackageLevels
SystemLogger.info(
"Loaded custom security patch levels: global config exists=${newGlobalLevel != null}, " +
"${newPackageLevels.size} package-specific configs."
)
} catch (e: Exception) {
SystemLogger.error("Failed to load or parse ${file.name}", e)
} else {
customPatchLevelOverride = null
}
}
/** Checks the device's TEE status and writes the result to a file for persistence. */
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.
@@ -298,12 +245,8 @@ object ConfigurationManager {
val file = if (event != DELETE) File(configRoot, path) else null
when (path) {
TARGET_PACKAGES_FILE ->
file?.let { loadTargetPackages(it) }
?: SystemLogger.warning("$TARGET_PACKAGES_FILE was deleted.")
PATCH_LEVEL_FILE ->
file?.let { loadPatchLevelConfig(it) }
?: SystemLogger.warning("$PATCH_LEVEL_FILE was deleted.")
TARGET_PACKAGES_FILE -> loadTargetPackages(file!!)
PATCH_LEVEL_FILE -> loadPatchLevelConfig(file!!)
// Any change to an XML file is assumed to be a keybox.
// The cache in KeyBoxManager will handle reloading it on its next use.
else ->
@@ -313,15 +256,10 @@ object ConfigurationManager {
)
KeyBoxManager.invalidateCache(path)
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
// Drop only the patched cert chains so the next
// attestation request re-signs with the new keybox.
// Do NOT drop generatedKeys — that would destroy
// every alias/private key in memory and on disk,
// logging users out of any app that pinned a
// persisted keystore alias.
// Clear cached keys possibly containing old certificates
org.matrix.TEESimulator.interception.keystore.shim
.KeyMintSecurityLevelInterceptor
.invalidatePatchedChains("updating $file")
.clearAllGeneratedKeys("updating $file")
}
}
}
@@ -351,29 +289,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 {
@@ -387,7 +303,7 @@ object ConfigurationManager {
/** Waits for a system service to become available, with retries. */
private fun waitForSystemService(name: String): IBinder? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return ServiceManager.waitForService(name)
}
// Fallback for older Android versions.
@@ -41,7 +41,7 @@ abstract class BinderInterceptor : Binder() {
* Skips the original call and immediately returns a custom reply parcel to the caller. The
* provided parcel will be recycled after use.
*/
data class OverrideReply(val reply: Parcel, val code: Int = 0) : TransactionResult()
data class OverrideReply(val code: Int = 0, val reply: Parcel) : TransactionResult()
/**
* Modifies the transaction's input data before forwarding it to the original binder method.
@@ -109,21 +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 {
when (code) {
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
when (code) {
// These codes are defined in the native layer to distinguish hook types.
PRE_TRANSACT_CODE -> handlePreTransact(txId, data)
POST_TRANSACT_CODE -> handlePostTransact(txId, data)
else -> return super.onTransact(code, data, reply, flags)
}
// The reply parcel is guaranteed to be non-null for our custom transactions.
writeResultToReply(result, reply!!)
return true
}
@@ -224,27 +220,19 @@ abstract class BinderInterceptor : Binder() {
}
}
/**
* Logs an intercepted transaction. For a targeted UID every transaction — whether we intercept
* or merely observe it — is recorded on that UID's own diagnostic plane, so its keystore
* timeline reads cleanly end to end. Untargeted UIDs get a single terse, rate-limited line.
*/
/** Helper function for consistent logging of intercepted transactions. */
protected fun logTransaction(
txId: Long,
methodName: String,
callingUid: Int,
callingPid: Int,
skipPost: Boolean = false,
isIntercepting: Boolean = true,
) {
if (SystemLogger.isUidLogged(callingUid)) {
val action = if (skipPost) "observe" else "intercept"
SystemLogger.uidLog(callingUid, txId, "tx", "$methodName action=$action pid=$callingPid")
return
}
SystemLogger.verbose {
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
"[TX_ID: $txId] Observe $methodName for packages=[$packages] (uid=$callingUid, pid=$callingPid)"
}
val action = if (isIntercepting) "Intercept" else "Observe"
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
SystemLogger.debug(
"[TX_ID: $txId] $action $methodName for packages=[$packages] (uid=$callingUid, pid=$callingPid)"
)
}
companion object {
@@ -255,8 +243,6 @@ abstract class BinderInterceptor : Binder() {
private const val BACKDOOR_TRANSACTION_CODE = 0xdeadbeef.toInt()
// Code used by the backdoor binder to register a new interceptor.
private const val REGISTER_INTERCEPTOR_CODE = 1
// Code used by the backdoor binder to unregister an interceptor.
private const val UNREGISTER_INTERCEPTOR_CODE = 2
// --- Hook Type Codes ---
// Indicates that the call is for a pre-transaction hook.
@@ -300,47 +286,17 @@ abstract class BinderInterceptor : Binder() {
}
}
fun register(
backdoor: IBinder,
target: IBinder,
interceptor: BinderInterceptor,
filteredCodes: IntArray = intArrayOf(),
): Boolean {
val data = Parcel.obtain()
val reply = Parcel.obtain()
return try {
data.writeStrongBinder(target)
data.writeStrongBinder(interceptor)
data.writeInt(filteredCodes.size)
for (code in filteredCodes) data.writeInt(code)
val ok = backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
if (ok) {
SystemLogger.info(
"Registered interceptor for target: $target (${filteredCodes.size} filtered codes)"
)
} else {
SystemLogger.error("Register transact returned false for target: $target")
}
ok
} catch (e: Exception) {
SystemLogger.error("Failed to register binder interceptor.", e)
false
} finally {
data.recycle()
reply.recycle()
}
}
/** Uses the backdoor binder to unregister an interceptor for a specific target service. */
fun unregister(backdoor: IBinder, target: IBinder) {
/** 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)
backdoor.transact(UNREGISTER_INTERCEPTOR_CODE, data, reply, 0)
SystemLogger.info("Unregistered interceptor for target: $target")
data.writeStrongBinder(interceptor)
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
SystemLogger.info("Registered interceptor for target: $target")
} catch (e: Exception) {
SystemLogger.error("Failed to unregister binder interceptor.", e)
SystemLogger.error("Failed to register binder interceptor.", e)
} finally {
data.recycle()
reply.recycle()
@@ -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,59 +1,16 @@
package org.matrix.TEESimulator.interception.keystore
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyParameterValue
import android.hardware.security.keymint.Tag
import android.os.Parcel
import android.os.Parcelable
import android.security.KeyStore
import android.security.keystore.KeystoreResponse
import android.system.keystore2.Authorization
import java.nio.ByteBuffer
import java.nio.ByteOrder
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
private const val FLAT_STRIDE_HEADER = 12
private const val MAX_AUTH_COUNT = 256
private const val SENTINEL_MODTIME = 4_294_967_297L
private const val HIGH_MODTIME = 4_999_999_999L
private fun synthesizeSseMessage(errorCode: Int): String =
when (errorCode) {
2 -> "Error::Rc(SYSTEM_ERROR)"
4 -> "Error::Rc(PERMISSION_DENIED)"
6 -> "Error::Rc(VALUE_CORRUPTED)"
7 -> "Error::Rc(KEY_NOT_FOUND)"
10 -> "Error::Rc(BACKEND_BUSY)"
-3 -> "Error::Km(UNSUPPORTED_KEY_SIZE)"
-6 -> "Error::Km(INCOMPATIBLE_PURPOSE)"
-7 -> "Error::Km(INCOMPATIBLE_ALGORITHM)"
-29 -> "Error::Km(TOO_MANY_OPERATIONS)"
-49 -> "Error::Km(UNSUPPORTED_TAG)"
-75 -> "Error::Km(INVALID_INPUT_LENGTH)"
-76 -> "Error::Km(INVALID_TAG)"
else -> if (errorCode > 0) "Error::Rc($errorCode)" else "Error::Km($errorCode)"
}
fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply {
val parcel =
Parcel.obtain().apply {
writeInt(EX_SERVICE_SPECIFIC)
writeString(synthesizeSseMessage(errorCode))
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.
@@ -70,31 +27,14 @@ object InterceptorUtils {
}
}
/** Creates an `KeystoreResponse` parcel that indicates success with no data. */
fun createSuccessKeystoreResponse(): KeystoreResponse {
val parcel = Parcel.obtain()
try {
parcel.writeInt(KeyStore.NO_ERROR)
parcel.writeString("")
parcel.setDataPosition(0)
return KeystoreResponse.CREATOR.createFromParcel(parcel)
} finally {
parcel.recycle()
}
}
/** Creates an `OverrideReply` parcel that indicates success with no data. */
fun createSuccessReply(
writeResultCode: Boolean = true
): BinderInterceptor.TransactionResult.OverrideReply {
fun createSuccessReply(): BinderInterceptor.TransactionResult.OverrideReply {
val parcel =
Parcel.obtain().apply {
writeNoException()
if (writeResultCode) {
writeInt(KeyStore.NO_ERROR)
}
writeInt(KeyStore.NO_ERROR)
}
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
return BinderInterceptor.TransactionResult.OverrideReply(0, parcel)
}
/** Creates an `OverrideReply` parcel containing a raw byte array. */
@@ -104,222 +44,38 @@ object InterceptorUtils {
writeNoException()
writeByteArray(data)
}
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
return BinderInterceptor.TransactionResult.OverrideReply(KeyStore.NO_ERROR, parcel)
}
/** Creates an `OverrideReply` parcel containing a typed array. */
fun <T : Parcelable> createTypedArrayReply(
array: Array<T>,
flags: Int = 0,
): BinderInterceptor.TransactionResult.OverrideReply {
val parcel =
Parcel.obtain().apply {
writeNoException()
writeTypedArray(array, flags)
}
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
/** Correlates a captured reply parcel to the app that triggered it, for [createTypedObjectReply]. */
data class ReplyDiagnostic(val uid: Int, val txId: Long?, val event: String)
/** Creates an `OverrideReply` parcel containing a Parcelable object. */
fun <T : Parcelable?> createTypedObjectReply(
obj: T,
flags: Int = 0,
diagnostic: ReplyDiagnostic? = null,
): BinderInterceptor.TransactionResult.OverrideReply {
val parcel =
Parcel.obtain().apply {
writeNoException()
writeTypedObject(obj, flags)
}
if (diagnostic != null && SystemLogger.isUidLogged(diagnostic.uid)) {
val savedPos = parcel.dataPosition()
val wire = parcel.marshall()
parcel.setDataPosition(savedPos)
SystemLogger.uidLogRaw(diagnostic.uid, diagnostic.txId, diagnostic.event, "len=${wire.size}", wire)
}
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
return BinderInterceptor.TransactionResult.OverrideReply(0, parcel)
}
/**
* Extracts the base alias from a potentially prefixed alias string. For example, it converts
* "USRCERT_my_key" to "my_key".
* Extracts the true key alias from the keystore-prefixed string (e.g., "user_cert_my-alias" ->
* "my-alias").
*/
fun extractAlias(prefixedAlias: String): String {
val underscoreIndex = prefixedAlias.indexOf('_')
return if (underscoreIndex != -1) {
// Return the part of the string after the first underscore.
prefixedAlias.substring(underscoreIndex + 1)
val secondUnderscoreIndex = prefixedAlias.indexOf('_', underscoreIndex + 1)
return if (secondUnderscoreIndex != -1) {
prefixedAlias.substring(secondUnderscoreIndex + 1)
} else {
// If there's no underscore, return the original string.
prefixedAlias
}
}
/** 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
return runCatching { reply.readException() }.exceptionOrNull() != null
}
fun createServiceSpecificErrorReply(
errorCode: Int
): BinderInterceptor.TransactionResult.OverrideReply = createErrorReply(errorCode)
fun normalizeServiceSpecificReply(reply: Parcel): Parcel? {
reply.setDataPosition(0)
if (reply.readInt() != EX_SERVICE_SPECIFIC) {
reply.setDataPosition(0)
return null
}
// Advance position past message and stack header to reach errorCode.
reply.readString()
reply.readInt()
val errorCode = reply.readInt()
reply.setDataPosition(0)
return Parcel.obtain().apply {
writeInt(EX_SERVICE_SPECIFIC)
writeString(synthesizeSseMessage(errorCode))
writeInt(0)
writeInt(errorCode)
}
}
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)
val patched =
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 normalizeAuthorizationLayout(patched)
}
/**
* Reorders a generateKey reply's authorizations only when the marshalled reply would read, to a
* flat 12-byte-stride parcel fingerprint, as the TEE-simulator sentinel: a last slot of
* securityLevel 4 or 256, tag 1, union 32 with the 0x1_0000_0001 pseudo-timestamp, or any
* timestamp past [HIGH_MODTIME]. Real Android 16 hardware emits a 13-authorization layout for
* single-purpose EC keys that lands on that sentinel, so mirroring hardware byte-for-byte is
* itself flagged. Clients resolve authorizations by tag and the certificate chain is a separate
* field, so reordering is the minimal capability-preserving way to clear the false positive for
* any caller. Already-clean replies are returned unchanged.
*/
fun normalizeAuthorizationLayout(authorizations: Array<Authorization>): Array<Authorization> {
if (authorizations.size < 2) return authorizations
if (!flatStrideFingerprintMatches(marshalTypedArray(authorizations))) return authorizations
val n = authorizations.size
for (src in 1 until n) {
val candidate = moveAuthorization(authorizations, src, 0)
if (!flatStrideFingerprintMatches(marshalTypedArray(candidate))) return candidate
}
for (src in 0 until n) {
for (dst in 0 until n) {
if (src == dst) continue
val candidate = moveAuthorization(authorizations, src, dst)
if (!flatStrideFingerprintMatches(marshalTypedArray(candidate))) return candidate
}
}
return authorizations
}
private fun moveAuthorization(
authorizations: Array<Authorization>,
src: Int,
dst: Int,
): Array<Authorization> {
val reordered = authorizations.toMutableList()
reordered.add(dst, reordered.removeAt(src))
return reordered.toTypedArray()
}
private fun marshalTypedArray(authorizations: Array<Authorization>): ByteArray {
val parcel = Parcel.obtain()
return try {
// keystore2 AIDL compile stubs omit the Parcelable supertype these types carry at runtime.
parcel.writeTypedArray(authorizations.map { it as Parcelable }.toTypedArray(), 0)
parcel.marshall()
} finally {
parcel.recycle()
}
}
private fun flatStrideFingerprintMatches(marshalled: ByteArray): Boolean =
runCatching {
val parcel = ByteBuffer.wrap(marshalled).order(ByteOrder.LITTLE_ENDIAN)
val count = parcel.getInt(0)
if (count !in 1..MAX_AUTH_COUNT) return@runCatching false
var off = 4
var lastSec = 0L
var lastTag = 0L
var lastUnion = 0L
repeat(count) {
lastSec = u32(parcel, off)
lastTag = u32(parcel, off + 4)
lastUnion = u32(parcel, off + 8)
off += FLAT_STRIDE_HEADER
off = alignWord(off + flatPayloadSize(parcel, off, lastUnion))
}
off = skipDriftedByteArray(parcel, off)
off = skipDriftedByteArray(parcel, off)
val modtime = parcel.getLong(alignWord(off))
val unknownUnion = lastUnion !in 0..14
modtime > HIGH_MODTIME ||
(modtime == SENTINEL_MODTIME &&
(lastSec == 4L || lastSec == 256L) &&
lastTag == 1L &&
lastUnion == 32L &&
unknownUnion)
}.getOrDefault(false)
private fun flatPayloadSize(parcel: ByteBuffer, off: Int, union: Long): Int =
when {
union in 1..11 -> 4
union == 12L || union == 13L -> 8
union == 14L -> alignWord(off + 4 + parcel.getInt(off)) - off
else -> 0
}
private fun skipDriftedByteArray(parcel: ByteBuffer, off: Int): Int {
if (parcel.getInt(off) == 0) return off + 4
val lengthPos = off + 4
return alignWord(lengthPos + 4 + parcel.getInt(lengthPos))
}
private fun u32(parcel: ByteBuffer, off: Int): Long = parcel.getInt(off).toLong() and 0xFFFFFFFFL
private fun alignWord(off: Int): Int = (off + 3) and 3.inv()
}
@@ -1,27 +1,19 @@
package org.matrix.TEESimulator.interception.keystore
import android.annotation.SuppressLint
import android.hardware.security.keymint.KeyOrigin
import android.hardware.security.keymint.SecurityLevel
import android.os.Build
import android.hardware.security.keymint.Tag
import android.os.IBinder
import android.os.Parcel
import android.os.ServiceManager
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.AttestationDossier
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertificateGenerator
import org.matrix.TEESimulator.pki.CertificateHelper
/**
@@ -33,28 +25,16 @@ import org.matrix.TEESimulator.pki.CertificateHelper
*/
@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")
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "getKeyEntry")
private val DELETE_KEY_TRANSACTION =
InterceptorUtils.getTransactCode(stubBinderClass, "deleteKey")
private val UPDATE_SUBCOMPONENT_TRANSACTION =
InterceptorUtils.getTransactCode(stubBinderClass, "updateSubcomponent")
private val LIST_ENTRIES_TRANSACTION =
InterceptorUtils.getTransactCode(stubBinderClass, "listEntries")
private val LIST_ENTRIES_BATCHED_TRANSACTION =
if (Build.VERSION.SDK_INT >= 34)
InterceptorUtils.getTransactCode(stubBinderClass, "listEntriesBatched")
else null
private val GET_NUMBER_OF_ENTRIES_TRANSACTION =
InterceptorUtils.getTransactCode(stubBinderClass, "getNumberOfEntries")
private val GRANT_TRANSACTION = InterceptorUtils.getTransactCode(stubBinderClass, "grant")
private val UNGRANT_TRANSACTION = InterceptorUtils.getTransactCode(stubBinderClass, "ungrant")
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "deleteKey")
private val transactionNames: Map<Int, String> by lazy {
stubBinderClass.declaredFields
IKeystoreService.Stub::class
.java
.declaredFields
.filter {
it.isAccessible = true
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
@@ -62,42 +42,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
}
private const val RESPONSE_KEY_NOT_FOUND = 7
private const val RESPONSE_PERMISSION_DENIED = 6
private const val KEY_PERMISSION_GET_INFO = 0x4
private const val KEY_PERMISSION_UPDATE = 0x80
// KeyStoreManager.grantKeyAccess() became a public app API in Android 16 (API 36). Before that,
// grant was a hidden API and SELinux denied untrusted_app, so a synthetic-key grant must answer
// PERMISSION_DENIED pre-36 and a coherent virtualized grant on 36+.
private const val GRANT_PUBLIC_API_SDK = 36
private val deletedSoftwareKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
fun forgetDeletedKey(keyId: KeyIdentifier) {
if (deletedSoftwareKeys.remove(keyId)) {
SystemLogger.debug("Cleared deletion marker for ${keyId.alias}")
}
}
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,
GRANT_TRANSACTION,
UNGRANT_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).
@@ -105,30 +53,6 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
override fun onInterceptorReady(service: IBinder, backdoor: IBinder) {
val keystoreInterface = IKeystoreService.Stub.asInterface(service)
setupSecurityLevelInterceptors(keystoreInterface, backdoor)
setupMaintenanceInterceptor(backdoor)
}
/**
* Hooks the keystore2 daemon's `android.security.maintenance` binder, which is hosted by the
* same process, so synthetic key state follows real key-lifecycle events. Best-effort: if the
* service is absent the synthetic plane simply forgoes lifecycle parity.
*/
private fun setupMaintenanceInterceptor(backdoor: IBinder) {
runCatching {
ServiceManager.getService("android.security.maintenance")?.let { maintenance ->
SystemLogger.info("Found maintenance binder. Registering interceptor...")
register(
backdoor,
maintenance,
Keystore2MaintenanceInterceptor,
Keystore2MaintenanceInterceptor.interceptedCodes,
)
}
?: SystemLogger.warning(
"Maintenance binder not found; skipping lifecycle parity."
)
}
.onFailure { SystemLogger.error("Failed to intercept maintenance binder.", it) }
}
private fun setupSecurityLevelInterceptors(service: IKeystoreService, backdoor: IBinder) {
@@ -138,13 +62,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...")
val interceptor =
KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
register(
backdoor,
tee.asBinder(),
interceptor,
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
)
interceptor.loadPersistedKeys()
register(backdoor, tee.asBinder(), interceptor)
}
}
.onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) }
@@ -155,13 +73,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
val interceptor =
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
register(
backdoor,
strongbox.asBinder(),
interceptor,
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
)
interceptor.loadPersistedKeys()
register(backdoor, strongbox.asBinder(), interceptor)
}
}
.onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) }
@@ -176,253 +88,44 @@ 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)
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
val isGMS = packages.contains("com.google.android.gms")
if (isGMS || ConfigurationManager.shouldSkipUid(callingUid)) {
return TransactionResult.ContinueAndSkipPost
}
return runCatching {
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
if (ListEntriesHandler.cacheParameters(txId, data, isBatchMode)) {
TransactionResult.Continue
} else {
TransactionResult.ContinueAndSkipPost
}
}
.getOrElse {
SystemLogger.error(
"[TX_ID: $txId] Failed to parse parameters for ${transactionNames[code]!!}",
it,
)
TransactionResult.ContinueAndSkipPost
}
} else if (
code == GET_KEY_ENTRY_TRANSACTION ||
code == DELETE_KEY_TRANSACTION ||
code == UPDATE_SUBCOMPONENT_TRANSACTION
) {
if (code == GET_KEY_ENTRY_TRANSACTION || code == DELETE_KEY_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (code == UPDATE_SUBCOMPONENT_TRANSACTION) {
if (ConfigurationManager.shouldSkipUid(callingUid))
return TransactionResult.ContinueAndSkipPost
return handleUpdateSubcomponent(callingUid, data)
}
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val descriptor =
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.ContinueAndSkipPost
?: return TransactionResult.SkipTransaction
// Domain.GRANT read (Android 16+ KeyStoreManager grant). Served for ANY grantee uid —
// including isolated services (bindIsolatedService) with no package mapping — so
// resolve
// it before the package-scoped skip; caller-binding in resolveGrant() is the real
// access
// gate. On Android <= 15 no grants are ever issued (grant() denies), so softwareGrants
// is
// empty and this falls through to the real keystore2.
if (code == GET_KEY_ENTRY_TRANSACTION && descriptor.domain == Domain.GRANT) {
val grant =
KeyMintSecurityLevelInterceptor.resolveGrant(descriptor.nspace, callingUid)
if (grant == null) {
// Ours but wrong caller -> KEY_NOT_FOUND (caller-binding); not ours -> real
// keystore2.
return if (
KeyMintSecurityLevelInterceptor.softwareGrants.containsKey(
descriptor.nspace
)
)
InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
else TransactionResult.ContinueAndSkipPost
}
if ((grant.accessVector and KEY_PERMISSION_GET_INFO) == 0) {
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
}
val response =
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(grant.ownerKeyId)
?: return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
// Same object the owner read returns -> coherent chain across planes.
return InterceptorUtils.createTypedObjectReply(response)
}
// generateKey force-forges attest/device-id keys even for skipped UIDs; getKeyEntry
// must serve them back or the framework's attestKeyAlias lookup gets KEY_NOT_FOUND.
if (code != GET_KEY_ENTRY_TRANSACTION && ConfigurationManager.shouldSkipUid(callingUid))
if (ConfigurationManager.shouldSkipUid(callingUid))
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) {
if (descriptor.domain == Domain.KEY_ID) {
// The probe pipeline (and some AOSP callers) switch follow-up
// operations to KEY_ID semantics after generateKey returns a
// KEY_ID descriptor. Without this branch, our software keys
// are invisible to KEY_ID-based getKeyEntry calls and the
// request falls through to the real keystore2 daemon, which
// legitimately responds with KEY_NOT_FOUND. Duck Detector's
// TimingSideChannelProbe captures that exception during its
// warmup phase and surfaces it as
// "Captured private binder exception during timing skip".
// Resolving by KEY_ID and returning the cached response keeps
// the call on the happy path, eliminating the warmup signal.
val info =
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
callingUid,
descriptor.nspace,
)
if (info?.response != null) {
SystemLogger.info(
"[TX_ID: $txId] Found generated response via KEY_ID nspace=${descriptor.nspace}"
)
logServedChain(callingUid, txId, "keyid:${descriptor.nspace}", info.response)
return InterceptorUtils.createTypedObjectReply(info.response)
}
val teeResp =
KeyMintSecurityLevelInterceptor.findTeeResponseByKeyId(
callingUid,
descriptor.nspace,
)
if (teeResp != null) {
SystemLogger.info(
"[TX_ID: $txId] Found TEE response via KEY_ID nspace=${descriptor.nspace}"
)
logServedChain(callingUid, txId, "keyid:${descriptor.nspace}", teeResp)
return InterceptorUtils.createTypedObjectReply(teeResp)
}
}
// Domain.GRANT is handled earlier (before the package-scoped skip); an alias-less
// read reaching here is KEY_ID or unknown, so it falls through to the real
// keystore2.
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)
}
// Owned keys were served above; for a skipped UID a non-owned key must still skip
// post-processing so we never patch an un-targeted app's real key.
return if (ConfigurationManager.shouldSkipUid(callingUid))
TransactionResult.ContinueAndSkipPost
else TransactionResult.Continue
if (code == DELETE_KEY_TRANSACTION) {
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
return TransactionResult.ContinueAndSkipPost
}
val response =
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
?: return TransactionResult.Continue
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
SystemLogger.info("${descriptor.alias} was an attestation key")
SystemLogger.info("[TX_ID: $txId] Found generated response for ${descriptor.alias}:")
response.metadata?.authorizations?.forEach {
KeyMintParameterLogger.logParameter(callingUid, txId, it.keyParameter)
KeyMintParameterLogger.logParameter(it.keyParameter)
}
logServedChain(callingUid, txId, descriptor.alias, response)
return InterceptorUtils.createTypedObjectReply(response)
} else if (code == GRANT_TRANSACTION) {
logTransaction(txId, transactionNames[code] ?: "grant", callingUid, callingPid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val key =
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.ContinueAndSkipPost
val granteeUid = data.readInt()
val accessVector = data.readInt()
// Synthetic (generatedKeys) AND patch-mode (teeResponses) keys are ours; both must
// grant
// coherently so the Domain.GRANT readback returns the same chain the owner read
// returns.
// Real hardware keys fall through to the real keystore2, which applies the same SELinux
// gate the platform would.
val ownerKeyId =
resolveOwnerKeyId(key, callingUid)?.takeIf {
KeyMintSecurityLevelInterceptor.ownsKeyResponse(it)
} ?: return TransactionResult.ContinueAndSkipPost
// Version-gated to mirror the real TEE 1:1. Pre-Android-16, grant was a hidden API and
// SELinux denied untrusted_app, so keystore2 returns PERMISSION_DENIED. Android 16
// (API 36) exposes KeyStoreManager.grantKeyAccess(), so an app grants its own key:
// issue a coherent, caller-bound, access-vector-carrying grant whose Domain.GRANT read
// returns the owner's chain.
if (Build.VERSION.SDK_INT < GRANT_PUBLIC_API_SDK) {
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
}
val grantId =
KeyMintSecurityLevelInterceptor.issueGrant(ownerKeyId, granteeUid, accessVector)
val reply =
KeyDescriptor().apply {
domain = Domain.GRANT
nspace = grantId
alias = null
blob = null
}
return InterceptorUtils.createTypedObjectReply(reply)
} else if (code == UNGRANT_TRANSACTION) {
logTransaction(txId, transactionNames[code] ?: "ungrant", callingUid, callingPid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val key =
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.ContinueAndSkipPost
val granteeUid = data.readInt()
val ownerKeyId =
resolveOwnerKeyId(key, callingUid)?.takeIf {
KeyMintSecurityLevelInterceptor.ownsKeyResponse(it)
} ?: return TransactionResult.ContinueAndSkipPost
// Same version gate as grant(): denied pre-36, revoke the virtualized grant on 36+.
if (Build.VERSION.SDK_INT < GRANT_PUBLIC_API_SDK) {
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
}
KeyMintSecurityLevelInterceptor.revokeGrant(ownerKeyId, granteeUid)
return InterceptorUtils.createSuccessReply(writeResultCode = false)
} else {
logTransaction(
txId,
transactionNames[code] ?: "unknown code=$code",
callingUid,
callingPid,
true,
false,
)
}
@@ -441,398 +144,56 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
reply: Parcel?,
resultCode: Int,
): TransactionResult {
if (target != keystoreService || reply == null) return TransactionResult.SkipTransaction
if (InterceptorUtils.hasException(reply)) {
val normalized = InterceptorUtils.normalizeServiceSpecificReply(reply)
return if (normalized != null) TransactionResult.OverrideReply(normalized)
else TransactionResult.SkipTransaction
}
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 == GET_KEY_ENTRY_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
return runCatching {
val updatedKeyDescriptors =
ListEntriesHandler.injectGeneratedKeys(txId, callingUid, reply)
InterceptorUtils.createTypedArrayReply(updatedKeyDescriptors)
}
.getOrElse {
SystemLogger.error(
"[TX_ID: $txId] Failed to update the result of ${transactionNames[code]!!}.",
it,
)
TransactionResult.SkipTransaction
}
} else if (code == GET_KEY_ENTRY_TRANSACTION) {
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)!!
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
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.
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."
)
return TransactionResult.SkipTransaction
}
val originalChain = CertificateHelper.getCertificateChain(response)
val authorizations = response.metadata?.authorizations
val origin =
authorizations
?.find { it.keyParameter.tag == Tag.ORIGIN }
?.let { it.keyParameter.value.origin }
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)
// Snapshot metadata bytes for the same reason as the
// primary doSoftwareKeyGen path — loss-less restore
// after reboot.
val metadataBytesForPersist =
response.metadata?.let { md ->
runCatching {
val parcel = android.os.Parcel.obtain()
try {
md.writeToParcel(parcel, 0)
parcel.marshall()
} finally {
parcel.recycle()
}
}
.getOrNull()
}
GeneratedKeyPersistence.save(
keyId = keyId,
keyPair = keyData.first,
secretKey = null,
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,
metadataBytes = metadataBytesForPersist,
)
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,
)
// PATCH decode point: the patched chain actually served back to the app on
// getKeyEntry — the ground truth a patch-mode detector reads.
AttestationDossier.log(callingUid, txId, "PATCH", finalChain.asList())
return InterceptorUtils.createTypedObjectReply(response)
if (origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED) {
SystemLogger.info("[TX_ID: $txId] Skip patching for imported keys.")
return TransactionResult.SkipTransaction
}
.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
}
// Perform the attestation patch.
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
CertificateHelper.updateCertificateChain(response.metadata, newChain).getOrThrow()
InterceptorUtils.createTypedObjectReply(response)
} catch (e: Exception) {
SystemLogger.error("[TX_ID: $txId] Failed to patch certificate chain.", e)
TransactionResult.SkipTransaction
}
}
return TransactionResult.SkipTransaction
}
/**
* Resolves the owner [KeyIdentifier] a grant/ungrant call targets. APP/alias keys map directly;
* KEY_ID keys are looked up by nspace (mirrors the deleteKey resolver). Returns null for
* anything not addressable, so callers fall through to the real keystore2.
*/
private fun resolveOwnerKeyId(descriptor: KeyDescriptor, callingUid: Int): KeyIdentifier? =
when {
descriptor.alias != null -> KeyIdentifier(callingUid, descriptor.alias)
descriptor.domain == Domain.KEY_ID ->
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
callingUid,
descriptor.nspace,
)
?.let { info ->
KeyMintSecurityLevelInterceptor.generatedKeys.entries
.firstOrNull {
it.value.nspace == info.nspace && it.key.uid == callingUid
}
?.key
}
else -> null
}
/**
* Records the certificate chain actually served back to [uid] on a getKeyEntry, keyed by
* [alias]. The app reassembles its final chain from these served chains (the leaf alias plus
* the attest-key alias), so logging each one with key sizes and a per-edge verification makes a
* verification failure in the app's combined chain reproducible from the log, not inferred.
*/
private fun logServedChain(uid: Int, txId: Long, alias: String, response: KeyEntryResponse?) {
if (response == null || !SystemLogger.isUidLogged(uid)) return
val chain = CertificateHelper.getCertificateChain(response)?.asList() ?: return
SystemLogger.uidLog(uid, txId, "served", "alias=$alias depth=${chain.size}")
SystemLogger.uidLog(uid, txId, "served-keys", AttestationPatcher.formatChainKeys(chain))
SystemLogger.uidLog(uid, txId, "served-verify", AttestationPatcher.formatChainVerification(chain))
}
private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult {
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val descriptor =
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.ContinueAndSkipPost
if (descriptor.domain == Domain.GRANT) {
val grant =
KeyMintSecurityLevelInterceptor.resolveGrant(descriptor.nspace, callingUid)
if (grant == null) {
return if (
KeyMintSecurityLevelInterceptor.softwareGrants.containsKey(descriptor.nspace)
)
InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
else TransactionResult.ContinueAndSkipPost
}
if ((grant.accessVector and KEY_PERMISSION_UPDATE) == 0) {
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
}
val generatedKeyInfo =
KeyMintSecurityLevelInterceptor.generatedKeys[grant.ownerKeyId]
val response =
generatedKeyInfo?.response
?: KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(grant.ownerKeyId)
?: return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
return updateResponseSubcomponent(
response = response,
publicCert = data.createByteArray(),
certificateChain = data.createByteArray(),
persist = {
if (generatedKeyInfo != null) {
GeneratedKeyPersistence.rePersistIfNeeded(
grant.ownerKeyId.uid,
generatedKeyInfo,
)
}
},
label = "grant[${descriptor.nspace}] -> ${grant.ownerKeyId}",
)
}
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) {
// Patch-mode key (cached in teeResponses, not generatedKeys): the real keystore2
// applies
// the update, so drop our stale cached chain. Otherwise getKeyEntry replays the
// pre-update generated attestation (duck STALE_TEE_RESPONSE_AFTER_KEY_ID_UPDATE).
when (descriptor.domain) {
Domain.KEY_ID ->
KeyMintSecurityLevelInterceptor.evictTeeResponseByKeyId(
callingUid,
descriptor.nspace,
)
Domain.APP ->
descriptor.alias?.let {
KeyMintSecurityLevelInterceptor.evictTeeResponse(
KeyIdentifier(callingUid, it)
)
}
else -> {}
}
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
}
return updateResponseSubcomponent(
response = generatedKeyInfo.response,
publicCert = data.createByteArray(),
certificateChain = data.createByteArray(),
persist = {
GeneratedKeyPersistence.rePersistIfNeeded(callingUid, generatedKeyInfo)
},
label = "key[${generatedKeyInfo.nspace}]",
)
}
private fun updateResponseSubcomponent(
response: KeyEntryResponse,
publicCert: ByteArray?,
certificateChain: ByteArray?,
persist: () -> Unit,
label: String,
): TransactionResult {
SystemLogger.info("Updating sub-component with $label")
val metadata = response.metadata
metadata.certificate = publicCert
metadata.certificateChain = certificateChain
persist()
SystemLogger.verbose(
"Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]"
)
return InterceptorUtils.createSuccessReply(writeResultCode = false)
}
}
@@ -1,113 +0,0 @@
package org.matrix.TEESimulator.interception.keystore
import android.os.IBinder
import android.os.Parcel
import android.security.maintenance.IKeystoreMaintenance
import android.system.keystore2.Domain
import android.system.keystore2.KeyDescriptor
import org.matrix.TEESimulator.interception.core.BinderInterceptor
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
/**
* Intercepts the keystore2 daemon's `android.security.maintenance` binder so our synthetic key
* state follows the same lifecycle events the platform applies to real keys.
*
* This is a pure side-effect hook: every handled transaction mutates only our own synthetic state
* and then returns [TransactionResult.ContinueAndSkipPost], so the real keystore2 still performs
* the real operation. We never fabricate a maintenance reply, so real key lifecycle is never
* disturbed.
*
* Mounted via `register()` from [Keystore2Interceptor.onInterceptorReady]; the maintenance binder
* is hosted by the same keystore2 process, so the already-injected native hook reaches it too.
*/
object Keystore2MaintenanceInterceptor : BinderInterceptor() {
private val stubClass = IKeystoreMaintenance.Stub::class.java
private val CLEAR_NAMESPACE_TRANSACTION =
InterceptorUtils.getTransactCode(stubClass, "clearNamespace")
private val DELETE_ALL_KEYS_TRANSACTION =
InterceptorUtils.getTransactCode(stubClass, "deleteAllKeys")
private val MIGRATE_KEY_NAMESPACE_TRANSACTION =
InterceptorUtils.getTransactCode(stubClass, "migrateKeyNamespace")
/** Only the lifecycle transactions we mirror; unresolved codes (-1) are dropped. */
val interceptedCodes: IntArray by lazy {
listOf(
CLEAR_NAMESPACE_TRANSACTION,
DELETE_ALL_KEYS_TRANSACTION,
MIGRATE_KEY_NAMESPACE_TRANSACTION,
)
.filter { it != -1 }
.toIntArray()
}
override fun onPreTransact(
txId: Long,
target: IBinder,
code: Int,
flags: Int,
callingUid: Int,
callingPid: Int,
data: Parcel,
): TransactionResult {
when (code) {
CLEAR_NAMESPACE_TRANSACTION -> handleClearNamespace(data)
DELETE_ALL_KEYS_TRANSACTION ->
KeyMintSecurityLevelInterceptor.clearAllGeneratedKeys("maintenance.deleteAllKeys")
MIGRATE_KEY_NAMESPACE_TRANSACTION -> handleMigrateKeyNamespace(data, callingUid)
}
// Always let the real keystore2 perform the real lifecycle operation.
return TransactionResult.ContinueAndSkipPost
}
private fun handleClearNamespace(data: Parcel) {
data.enforceInterface(IKeystoreMaintenance.DESCRIPTOR)
val domain = data.readInt()
val nspace = data.readLong()
// Only Domain.APP namespaces map to our per-uid synthetic keys; nspace is the app uid.
if (domain == Domain.APP) {
KeyMintSecurityLevelInterceptor.clearNamespaceKeys(nspace.toInt())
}
}
private fun handleMigrateKeyNamespace(data: Parcel, callingUid: Int) {
data.enforceInterface(IKeystoreMaintenance.DESCRIPTOR)
val source = data.readTypedObject(KeyDescriptor.CREATOR) ?: return
val destination = data.readTypedObject(KeyDescriptor.CREATOR) ?: return
val srcId = resolveSyntheticKeyId(source, callingUid) ?: return
if (!KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(srcId)) return // not ours
val dstId = resolveDestinationKeyId(destination, callingUid)
if (dstId == null) {
// Migrated out of our trackable (Domain.APP/alias) space -> drop our shadow so reads
// fall through to the real keystore2, which now owns it at the new namespace.
KeyMintSecurityLevelInterceptor.cleanupKeyData(srcId)
} else {
KeyMintSecurityLevelInterceptor.migrateGeneratedKey(srcId, dstId)
}
}
/** Resolves a synthetic owner key from a source descriptor (Domain.APP alias or KEY_ID). */
private fun resolveSyntheticKeyId(descriptor: KeyDescriptor, callingUid: Int): KeyIdentifier? =
when {
descriptor.alias != null -> KeyIdentifier(callingUid, descriptor.alias)
descriptor.domain == Domain.KEY_ID ->
KeyMintSecurityLevelInterceptor.generatedKeys.entries
.firstOrNull {
it.key.uid == callingUid && it.value.nspace == descriptor.nspace
}
?.key
else -> null
}
/** Destination must be an addressable Domain.APP alias for us to keep tracking the key. */
private fun resolveDestinationKeyId(
descriptor: KeyDescriptor,
callingUid: Int,
): KeyIdentifier? {
val alias = descriptor.alias ?: return null
if (descriptor.domain != Domain.APP) return null
val uid = if (descriptor.nspace > 0) descriptor.nspace.toInt() else callingUid
return KeyIdentifier(uid, alias)
}
}
@@ -4,28 +4,12 @@ import android.annotation.SuppressLint
import android.os.IBinder
import android.os.Parcel
import android.security.Credentials
import android.security.KeyStore
import android.security.keymaster.ExportResult
import android.security.keymaster.KeyCharacteristics
import android.security.keymaster.KeymasterArguments
import android.security.keymaster.KeymasterCertificateChain
import android.security.keymaster.KeymasterDefs
import android.security.keystore.IKeystoreCertificateChainCallback
import android.security.keystore.IKeystoreExportKeyCallback
import android.security.keystore.IKeystoreKeyCharacteristicsCallback
import android.security.keystore.IKeystoreService
import java.math.BigInteger
import java.security.KeyPair
import java.security.cert.Certificate
import java.util.Date
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.attestation.AttestationBuilder
import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.keystore.InterceptorUtils.extractAlias
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertificateGenerator
import org.matrix.TEESimulator.pki.CertificateHelper
/**
@@ -55,35 +39,11 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "attestKey")
}
private val transactionNames: Map<Int, String> by lazy {
IKeystoreService.Stub::class
.java
.declaredFields
.filter {
it.isAccessible = true
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
}
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
}
// A map to dispatch transaction handling for software key generation.
private val generateKeyHandlers:
Map<Int, (Long, Int, Int, Parcel) -> TransactionResult> by lazy {
mapOf(
GENERATE_KEY_TRANSACTION to ::handleGenerateKey,
GET_KEY_CHARACTERISTICS_TRANSACTION to ::handleGetKeyCharacteristics,
EXPORT_KEY_TRANSACTION to ::handleExportKey,
ATTEST_KEY_TRANSACTION to ::handleAttestKey,
)
}
override val serviceName = "android.security.keystore"
override val processName = "keystore"
override val injectionCommand = "exec ./inject `pidof keystore` libTEESimulator.so entry"
// State management for the multi-step key generation process.
private val keygenParameters = ConcurrentHashMap<KeyIdentifier, LegacyKeygenParameters>()
private val generatedKeyPairs = ConcurrentHashMap<KeyIdentifier, KeyPair>()
private const val SERVICE_DESCRIPTOR = "android.security.keystore.IKeystoreService"
// Cache to store the fully patched chain after the leaf is requested.
private val patchedChainCache = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
@@ -98,187 +58,24 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
data: Parcel,
): TransactionResult {
// This interceptor only needs to act on pre-transaction for software key generation.
// Handle 'generate' mode interceptions using the handler map.
if (ConfigurationManager.shouldGenerate(callingUid)) {
generateKeyHandlers[code]?.let { handler ->
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
return handler(txId, callingUid, callingPid, data)
when (code) {
GENERATE_KEY_TRANSACTION,
GET_KEY_CHARACTERISTICS_TRANSACTION,
EXPORT_KEY_TRANSACTION,
ATTEST_KEY_TRANSACTION -> {
// TODO: Implement the full software simulation logic.
logTransaction(txId, "unimplemented-generate-flow", callingUid, callingPid)
return InterceptorUtils.createSuccessReply()
}
}
} else if (ConfigurationManager.shouldGenerate(callingUid)) {
if (code == GET_TRANSACTION) return TransactionResult.Continue
}
// Handle 'patch' mode interceptions for the 'get' transaction.
if (ConfigurationManager.shouldPatch(callingUid) && code == GET_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
return TransactionResult.Continue
}
// Default behavior for all other transactions.
logTransaction(
txId,
transactionNames[code] ?: "unknown code=$code",
callingUid,
callingPid,
true,
)
return TransactionResult.ContinueAndSkipPost
}
private fun handleGenerateKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
return runCatching {
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val callback =
IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
val alias = InterceptorUtils.extractAlias(data.readString()!!)
val keyId = KeyIdentifier(uid, alias)
// Read and parse the key generation arguments.
val keymasterArgs = KeymasterArguments()
if (data.readInt() == 1) {
keymasterArgs.readFromParcel(data)
}
keygenParameters[keyId] =
LegacyKeygenParameters.fromKeymasterArguments(keymasterArgs)
// Create a fake successful response for the callback.
val characteristics = KeyCharacteristics()
characteristics.swEnforced = KeymasterArguments()
characteristics.hwEnforced = keymasterArgs
val keystoreResponse = InterceptorUtils.createSuccessKeystoreResponse()
callback.onFinished(keystoreResponse, characteristics)
InterceptorUtils.createSuccessReply()
}
.getOrElse {
SystemLogger.error("[TX_ID: $txId] Failed during handleGenerateKey.", it)
TransactionResult.ContinueAndSkipPost
}
}
private fun handleGetKeyCharacteristics(
txId: Long,
uid: Int,
pid: Int,
data: Parcel,
): TransactionResult {
return runCatching {
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val callback =
IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
val alias = InterceptorUtils.extractAlias(data.readString()!!)
val keyId = KeyIdentifier(uid, alias)
val params =
keygenParameters[keyId]
?: throw IllegalStateException("No params found for $keyId")
val characteristics =
KeyCharacteristics().apply {
swEnforced = KeymasterArguments()
hwEnforced =
KeymasterArguments().apply {
addEnum(KeymasterDefs.KM_TAG_ALGORITHM, params.algorithm)
}
}
callback.onFinished(
InterceptorUtils.createSuccessKeystoreResponse(),
characteristics,
)
InterceptorUtils.createSuccessReply()
}
.getOrElse {
SystemLogger.error("[TX_ID: $txId] Failed during handleGetKeyCharacteristics.", it)
TransactionResult.ContinueAndSkipPost
}
}
private fun handleExportKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
return runCatching {
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val callback = IKeystoreExportKeyCallback.Stub.asInterface(data.readStrongBinder())
val alias = InterceptorUtils.extractAlias(data.readString()!!)
val keyId = KeyIdentifier(uid, alias)
val params =
keygenParameters[keyId]
?: throw IllegalStateException("No params found for $keyId")
// Generate a software key pair using the new generator.
val keyPair =
CertificateGenerator.generateSoftwareKeyPair(params.toKeyMintAttestation())
?: throw Exception("Failed to generate software key pair.")
generatedKeyPairs[keyId] = keyPair
// Create a successful ExportResult containing the public key.
val exportResultParcel =
Parcel.obtain().apply {
writeInt(KeyStore.NO_ERROR)
writeByteArray(keyPair.public.encoded)
setDataPosition(0)
}
val exportResult = ExportResult.CREATOR.createFromParcel(exportResultParcel)
exportResultParcel.recycle()
callback.onFinished(exportResult)
InterceptorUtils.createSuccessReply()
}
.getOrElse {
SystemLogger.error("[TX_ID: $txId] Failed during handleExportKey.", it)
TransactionResult.ContinueAndSkipPost
}
}
private fun handleAttestKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
return runCatching {
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val callback =
IKeystoreCertificateChainCallback.Stub.asInterface(data.readStrongBinder())
val alias = InterceptorUtils.extractAlias(data.readString()!!)
val keyId = KeyIdentifier(uid, alias)
// Get the attestation challenge from the arguments.
val params =
keygenParameters[keyId]
?: throw IllegalStateException("No params found for $keyId")
val keyPair =
generatedKeyPairs[keyId]
?: throw IllegalStateException("No keypair found for $keyId")
val attestationArgs = KeymasterArguments()
if (data.readInt() == 1) {
attestationArgs.readFromParcel(data)
val challenge =
attestationArgs.getBytes(
KeymasterDefs.KM_TAG_ATTESTATION_CHALLENGE,
ByteArray(0),
)
params.attestationChallenge = challenge
}
val certificateChain =
CertificateGenerator.generateCertificateChain(
uid,
keyPair,
null, // No attestKeyAlias in legacy flow
params.toKeyMintAttestation(), // Convert to modern format
1, // SecurityLevel.TRUSTED_ENVIRONMENT
) ?: throw Exception("CertificateGenerator failed to create attested key pair.")
val chainAsByteList = certificateChain.map { it.encoded }
val certChain = KeymasterCertificateChain(chainAsByteList)
callback.onFinished(InterceptorUtils.createSuccessKeystoreResponse(), certChain)
InterceptorUtils.createSuccessReply()
}
.getOrElse {
SystemLogger.error("[TX_ID: $txId] Failed during handleAttestKey.", it)
TransactionResult.ContinueAndSkipPost
}
}
override fun onPostTransact(
txId: Long,
target: IBinder,
@@ -296,22 +93,16 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
reply == null ||
InterceptorUtils.hasException(reply)
) {
SystemLogger.debug(
"[TX_ID: $txId] Skip parsing post-transaction for [target, code, reply]: [$target, $code, $reply]"
)
return TransactionResult.SkipTransaction
}
if (!ConfigurationManager.shouldPatch(callingUid)) return TransactionResult.SkipTransaction
return try {
data.enforceInterface(IKeystoreService.DESCRIPTOR)
data.enforceInterface(SERVICE_DESCRIPTOR)
val alias = data.readString() ?: ""
val extractedAlias = InterceptorUtils.extractAlias(alias)
val keyId = KeyIdentifier(callingUid, extractedAlias)
SystemLogger.debug(
"[TX_ID: $txId] Parsed $keyId during post-transaction of ${transactionNames[code]}"
)
when {
// Case 1: The app is requesting the leaf certificate.
@@ -320,11 +111,13 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
val originalLeafBytes =
reply.createByteArray() ?: return TransactionResult.SkipTransaction
val originalLeafCertResult = CertificateHelper.toCertificate(originalLeafBytes)
if (originalLeafCertResult !is CertificateHelper.OperationResult.Success) {
return TransactionResult.SkipTransaction
}
val originalLeafCert = originalLeafCertResult.data
// The original chain is not available,
// so we must pass a temporary one to the patcher.
// The patcher only needs the original leaf to extract details.
val originalLeafCert =
(CertificateHelper.toCertificate(originalLeafBytes)
as CertificateHelper.OperationResult.Success)
.data
val tempChain = arrayOf<Certificate>(originalLeafCert)
// Perform the COMPLETE patch and rebuild operation.
@@ -364,6 +157,11 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
)
InterceptorUtils.createByteArrayReply(caCertsBytes!!)
} else {
// We have no cached chain.
// This could mean the app requested the CA without requesting the leaf
// first, or patching failed.
// In this case, we cannot safely intervene.
// Let the original reply pass through.
SystemLogger.warning(
"[TX_ID: $txId] No cached chain found for CA request on alias '$extractedAlias'. Skipping."
)
@@ -379,136 +177,3 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
}
}
}
/**
* A data class to hold key generation parameters parsed from the legacy IKeystoreService's
* KeymasterArguments. It is used exclusively by the KeystoreInterceptor to manage state during the
* software key generation flow.
*/
private data class LegacyKeygenParameters(
val algorithm: Int,
val keySize: Int,
val purpose: List<Int>,
val digest: List<Int>,
val certificateNotBefore: Date?,
val rsaPublicExponent: BigInteger?,
val ecCurveName: String?, // Derived from keySize
) {
// The challenge is provided in a separate transaction (attestKey), so it must be mutable.
var attestationChallenge: ByteArray? = null
/**
* Converts the legacy parameters into the modern [KeyMintAttestation] data structure, which is
* required by the refactored [AttestationBuilder] and [CertificateGenerator].
*/
fun toKeyMintAttestation(): KeyMintAttestation {
// This conversion acts as a bridge, allowing our new generic components
// to be used by the legacy interceptor.
return KeyMintAttestation(
keySize = this.keySize,
algorithm = this.algorithm,
ecCurve = 0,
ecCurveName = this.ecCurveName ?: "",
origin = null,
blockMode = listOf<Int>(),
padding = listOf<Int>(),
purpose = this.purpose,
digest = this.digest,
rsaPublicExponent = this.rsaPublicExponent,
certificateSerial = null, // Not provided in legacy generateKey
certificateSubject = null, // Not provided in legacy generateKey
certificateNotBefore = this.certificateNotBefore,
certificateNotAfter = null, // Not provided in legacy generateKey
attestationChallenge = this.attestationChallenge,
// Device identifiers are not passed in legacy args;
// AttestationBuilder will fetch them from system properties.
brand = null,
device = null,
product = null,
serial = null,
imei = null,
meid = null,
manufacturer = null,
model = null,
secondImei = null,
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(),
)
}
companion object {
/** Factory method to create an instance from a [KeymasterArguments] object. */
fun fromKeymasterArguments(args: KeymasterArguments): LegacyKeygenParameters {
val algorithm = args.getEnum(KeymasterDefs.KM_TAG_ALGORITHM, 0)
val keySize = args.getUnsignedInt(KeymasterDefs.KM_TAG_KEY_SIZE, 0).toInt()
return LegacyKeygenParameters(
algorithm = algorithm,
keySize = keySize,
purpose = args.getEnums(KeymasterDefs.KM_TAG_PURPOSE),
digest = args.getEnums(KeymasterDefs.KM_TAG_DIGEST),
certificateNotBefore = args.getDate(KeymasterDefs.KM_TAG_ACTIVE_DATETIME, Date()),
rsaPublicExponent =
if (algorithm == KeymasterDefs.KM_ALGORITHM_RSA) getRsaExponent(args) else null,
ecCurveName =
if (algorithm == KeymasterDefs.KM_ALGORITHM_EC) deriveEcCurveName(keySize)
else null,
)
}
private fun deriveEcCurveName(keySize: Int): String =
when (keySize) {
224 -> "secp224r1"
256 -> "secp256r1"
384 -> "secp384r1"
521 -> "secp521r1"
else -> "secp256r1" // Default fallback
}
/**
* The RSA public exponent is not accessible via a public API in KeymasterArguments, so we
* must use reflection to extract it.
*/
private fun getRsaExponent(args: KeymasterArguments): BigInteger? {
return runCatching {
val getArgumentByTag =
KeymasterArguments::class
.java
.getDeclaredMethod("getArgumentByTag", Int::class.java)
getArgumentByTag.isAccessible = true
val rsaArgument =
getArgumentByTag.invoke(args, KeymasterDefs.KM_TAG_RSA_PUBLIC_EXPONENT)
val getLongTagValue =
KeymasterArguments::class
.java
.getDeclaredMethod(
"getLongTagValue",
Class.forName("android.security.keymaster.KeymasterArgument"),
)
getLongTagValue.isAccessible = true
getLongTagValue.invoke(args, rsaArgument) as BigInteger
}
.onFailure {
SystemLogger.error("Failed to read rsaPublicExponent via reflection.", it)
}
.getOrNull()
}
}
}
@@ -1,142 +0,0 @@
package org.matrix.TEESimulator.interception.keystore
import android.os.Parcel
import android.system.keystore2.Domain
import android.system.keystore2.IKeystoreService
import android.system.keystore2.KeyDescriptor
import java.util.TreeMap
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
import org.matrix.TEESimulator.logging.SystemLogger
/**
* Handler to intercept listEntries and listEntriesBatched transactions.
*
* References for all mentioned functions in AOSP:
* https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/database.rs
* https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/service.rs
* https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/utils.rs
*/
object ListEntriesHandler {
// Estimate for maximum size of a Binder response in bytes.
private const val RESPONSE_SIZE_LIMIT = 358400
// Parameters of AOSP function `list_key_entries` in utils.rs.
private data class ListEntriesParams(
val domain: Int,
val namespace: Long,
val startPastAlias: String?,
)
private val pendingParams = ConcurrentHashMap<Long, ListEntriesParams>()
// Based on AOSP function `estimate_safe_amount_to_return` in utils.rs.
private fun estimateSafeAmountToReturn(
keyDescriptors: Array<KeyDescriptor>,
responseSizeLimit: Int,
): Int {
var itemsToReturn = 0
var returnedBytes = 0
for (kd in keyDescriptors) {
// 4 bytes for the Domain enum
// 8 bytes for the Namespace long
returnedBytes += 4 + 8
kd.alias?.let { returnedBytes += 4 + it.toByteArray(Charsets.UTF_8).size }
kd.blob?.let { returnedBytes += 4 + it.size }
if (returnedBytes > responseSizeLimit) {
SystemLogger.warning(
"Key descriptors list (${keyDescriptors.size} items) may exceed binder size limit, returning $itemsToReturn items with estimated size: $returnedBytes bytes."
)
break
}
itemsToReturn++
}
return itemsToReturn
}
// Parse and store parameters for later use (in post-transaction).
fun cacheParameters(txId: Long, data: Parcel, isBatchMode: Boolean): Boolean {
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val domain = data.readInt()
val namespace = data.readLong()
val startPastAlias = if (isBatchMode) data.readString() else null
// List entries is only supported for Domain::APP and Domain::SELINUX.
// See AOSP function `get_key_descriptor_for_lookup` in service.rs.
// Note that all generated keys belong to Domain::APP.
if (domain == Domain.APP) {
pendingParams[txId] = ListEntriesParams(domain, namespace, startPastAlias)
SystemLogger.debug("[TX_ID: $txId] Cached ${pendingParams[txId]}.")
return true
}
return false
}
// Merge software-backed keys with hardware-backed keys in the reply parcel.
fun injectGeneratedKeys(txId: Long, callingUid: Int, reply: Parcel): Array<KeyDescriptor> {
val params =
pendingParams.remove(txId)
?: throw IllegalStateException("No params found for listing entries")
// By default we use the calling uid as namespace if domain is Domain::APP.
// The namespace parameter is thus ignored for non-privileged applications.
// See AOSP function `get_key_descriptor_for_lookup` in service.rs.
val keysToInject =
extractGeneratedKeyDescriptors(callingUid, callingUid.toLong(), params.startPastAlias)
val originalList = reply.createTypedArray(KeyDescriptor.CREATOR)!!
val mergedArray = mergeKeyDescriptors(originalList, keysToInject)
// Limit response size to avoid binder buffer overflow.
// See AOSP function `list_key_entries` in utils.rs.
val safeAmountToReturn = estimateSafeAmountToReturn(mergedArray, RESPONSE_SIZE_LIMIT)
return if (safeAmountToReturn < mergedArray.size) {
SystemLogger.debug(
"[TX_ID: $txId] Listing entries are truncated [${mergedArray.size} -> $safeAmountToReturn] to avoid transaction overflow."
)
mergedArray.copyOfRange(0, safeAmountToReturn)
} else {
SystemLogger.debug(
"[TX_ID: $txId] Listing entries returns ${mergedArray.size} [injected: ${keysToInject.size}] keys."
)
mergedArray
}
}
// Merge hardware and software key descriptors into a single sorted array.
private fun mergeKeyDescriptors(
hardwareKeys: Array<KeyDescriptor>,
keysToInject: List<KeyDescriptor>,
): Array<KeyDescriptor> {
// Uses TreeMap to ensure alphabetical ordering and uniqueness (prefer injected keys).
val combinedMap = TreeMap<String, KeyDescriptor>()
hardwareKeys.forEach { key -> key.alias?.let { combinedMap[it] = key } }
keysToInject.forEach { key -> key.alias?.let { combinedMap[it] = key } }
return combinedMap.values.toTypedArray()
}
// Based on AOSP function `list_past_alias` in database.rs
private fun extractGeneratedKeyDescriptors(
uid: Int,
namespace: Long,
startPastAlias: String?,
): List<KeyDescriptor> {
return KeyMintSecurityLevelInterceptor.generatedKeys.keys
.filter { it.uid == uid && (startPastAlias == null || it.alias > startPastAlias) }
.map { keyId ->
KeyDescriptor().apply {
this.domain = Domain.APP
this.nspace = namespace
this.alias = keyId.alias
this.blob = null
}
}
}
}
@@ -1,120 +0,0 @@
package org.matrix.TEESimulator.interception.keystore.shim
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.BlockMode
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.PaddingMode
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)
?: checkOperationAuthorizations(keyParams, opParams)
?: 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 checkOperationAuthorizations(
keyParams: KeyMintAttestation,
opParams: KeyMintAttestation,
): Int? {
if (opParams.blockMode.any { it !in keyParams.blockMode }) {
return KeystoreErrorCodes.incompatibleBlockMode
}
if (opParams.padding.any { it !in keyParams.padding }) {
return KeystoreErrorCodes.incompatiblePaddingMode
}
if (opParams.digest.any { it !in keyParams.digest }) {
return KeystoreErrorCodes.incompatibleDigest
}
if (opParams.rsaOaepMgfDigest.any { it !in keyParams.rsaOaepMgfDigest }) {
return KeystoreErrorCodes.incompatibleDigest
}
if (keyParams.algorithm == Algorithm.AES && opParams.blockMode.contains(BlockMode.GCM)) {
val requestedMacLength = opParams.minMacLength
val keyMinMacLength = keyParams.minMacLength
if (
requestedMacLength != null &&
keyMinMacLength != null &&
requestedMacLength < keyMinMacLength
) {
return KeystoreErrorCodes.invalidMacLength
}
}
if (
keyParams.algorithm == Algorithm.RSA &&
opParams.padding.contains(PaddingMode.RSA_OAEP) &&
opParams.digest.isEmpty()
) {
return KeystoreErrorCodes.incompatibleDigest
}
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
}
}
@@ -1,493 +0,0 @@
package org.matrix.TEESimulator.interception.keystore.shim
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.security.KeyPair
import java.security.MessageDigest
import java.security.cert.Certificate
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.ReentrantLock
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertificateHelper
data class PersistedKeyData(
val uid: Int,
val alias: String,
val nspace: Long,
val securityLevel: Int,
val isAttestationKey: Boolean,
val algorithm: Int,
val keySize: Int,
val ecCurve: Int,
val purposes: List<Int>,
val digests: List<Int>,
/** PKCS#8-encoded private key for asymmetric records, empty for symmetric. */
val privateKeyBytes: ByteArray,
val certChainBytes: List<ByteArray>,
/**
* Byte-identical KeyMetadata parcel snapshot. Restoring authorizations directly from these
* bytes preserves tag count, order, and exact security-level annotations across reboots — the
* kind of structural details apps fingerprint to decide whether the alias is still "the same
* key".
*/
val metadataBytes: ByteArray,
/**
* Raw secret material for symmetric records (AES, HMAC, 3DES). Empty for asymmetric. Critical
* for AndroidX security crypto MasterKey (AES-GCM-256) — without this every reboot regenerates
* a fresh AES key and EncryptedSharedPreferences becomes undecryptable, which is what banking
* apps interpret as session expiry and force a relogin.
*/
val symmetricKeyBytes: ByteArray,
val symmetricAlgorithm: String,
)
object GeneratedKeyPersistence {
/**
* Single source of truth for the on-disk format. Bump this every time the layout changes; older
* numbers are silently skipped on read so stale dev artifacts and pre-fix upstream files can't
* be partially rehydrated into broken in-memory state.
*
* History: 1 — original upstream layout (no metadata snapshot, no symmetric block; restored
* keys lose authorization tags and AES master keys altogether — apps relying on persisted
* keystore state across reboots get logged out) 2 — transitional dev-only format that added
* metadata but still missed the symmetric block; never shipped 3 — current: byte-identical
* KeyMetadata snapshot + raw symmetric key material so AES/HMAC keys survive reboots
*/
private const val FORMAT_VERSION = 3
private val PERSISTENCE_DIR = File(CONFIG_PATH, "persistent_keys")
// Per-filename locks to prevent concurrent writes to the same key file
private val fileLocks = ConcurrentHashMap<String, ReentrantLock>()
private fun getLockForKey(filename: String): ReentrantLock {
return fileLocks.computeIfAbsent(filename) { ReentrantLock() }
}
fun save(
keyId: KeyIdentifier,
keyPair: KeyPair?,
secretKey: javax.crypto.SecretKey?,
nspace: Long,
securityLevel: Int,
certChain: List<Certificate>,
algorithm: Int,
keySize: Int,
ecCurve: Int,
purposes: List<Int>,
digests: List<Int>,
isAttestationKey: Boolean,
metadataBytes: ByteArray? = null,
) {
require(keyPair != null || secretKey != null) {
"Either keyPair or secretKey must be provided"
}
val filename = keyFileName(keyId.uid, keyId.alias)
val lock = getLockForKey(filename)
SystemLogger.debug("[Persistence] Acquiring lock for $filename")
lock.lock()
try {
SystemLogger.debug("[Persistence] Lock acquired for $filename")
runCatching {
PERSISTENCE_DIR.mkdirs()
val finalFile = File(PERSISTENCE_DIR, filename)
val tmpFile = File(PERSISTENCE_DIR, "$filename.tmp")
try {
DataOutputStream(BufferedOutputStream(FileOutputStream(tmpFile))).use { out
->
out.writeInt(FORMAT_VERSION)
out.writeInt(securityLevel)
out.writeInt(keyId.uid)
out.writeUTF(keyId.alias)
out.writeLong(nspace)
out.writeBoolean(isAttestationKey)
out.writeInt(algorithm)
out.writeInt(keySize)
out.writeInt(ecCurve)
out.writeInt(purposes.size)
purposes.forEach { out.writeInt(it) }
out.writeInt(digests.size)
digests.forEach { out.writeInt(it) }
// Asymmetric key block (empty for symmetric-only).
val pkBytes = keyPair?.private?.encoded ?: ByteArray(0)
out.writeInt(pkBytes.size)
out.write(pkBytes)
out.writeInt(certChain.size)
certChain.forEach { cert ->
val encoded = cert.encoded
out.writeInt(encoded.size)
out.write(encoded)
}
// Metadata snapshot (always present, may be empty
// if the live KeyMetadata could not be marshalled).
val mdBytes = metadataBytes ?: ByteArray(0)
out.writeInt(mdBytes.size)
if (mdBytes.isNotEmpty()) out.write(mdBytes)
// Symmetric key block (empty for asymmetric keys).
if (secretKey != null) {
val skBytes = secretKey.encoded
out.writeUTF(secretKey.algorithm)
out.writeInt(skBytes.size)
out.write(skBytes)
} else {
out.writeUTF("")
out.writeInt(0)
}
}
} catch (e: Exception) {
tmpFile.delete()
throw e
}
// Atomic rename — if this fails the tmp is left behind and cleaned on next
// deleteAll
if (!tmpFile.renameTo(finalFile)) {
tmpFile.delete()
throw IllegalStateException(
"Failed to atomically rename $tmpFile -> $finalFile"
)
}
// Verify write succeeded - catches disk-full or filesystem errors
if (!finalFile.exists() || finalFile.length() < 20) {
throw IOException("File write verification failed - possible disk full")
}
SystemLogger.debug("Persisted key: $keyId")
}
.onFailure { e -> SystemLogger.error("Failed to persist key $keyId", e) }
} finally {
lock.unlock()
SystemLogger.debug("[Persistence] Lock released for $filename")
}
}
fun delete(keyId: KeyIdentifier) {
runCatching {
val file = File(PERSISTENCE_DIR, keyFileName(keyId.uid, keyId.alias))
if (file.exists()) {
if (file.delete()) {
fileLocks.remove(keyFileName(keyId.uid, keyId.alias))
SystemLogger.debug("Deleted persisted key: $keyId")
} else {
SystemLogger.warning("Failed to delete persisted key file: ${file.name}")
}
} else {
SystemLogger.debug("No persisted file to delete for: $keyId")
}
}
.onFailure { e -> SystemLogger.error("Failed to delete persisted key $keyId", e) }
}
fun deleteAll() {
runCatching {
if (!PERSISTENCE_DIR.exists()) {
SystemLogger.debug("No persistent_keys directory, nothing to delete")
return
}
val files = PERSISTENCE_DIR.listFiles()
if (files == null) {
SystemLogger.warning("Cannot list persistent_keys directory")
return
}
var count = 0
files.forEach { file ->
if (file.name.endsWith(".bin") || file.name.endsWith(".tmp")) {
if (file.delete()) count++
}
}
fileLocks.clear()
SystemLogger.info("Deleted $count persisted key files")
}
.onFailure { e -> SystemLogger.error("Failed to delete all persisted keys", e) }
}
fun loadAll(securityLevel: Int): List<PersistedKeyData> {
if (!PERSISTENCE_DIR.exists()) {
SystemLogger.debug("No persistent_keys directory, nothing to load")
return emptyList()
}
val files = PERSISTENCE_DIR.listFiles { _, name -> name.endsWith(".bin") }
if (files == null) {
SystemLogger.warning("Cannot read persistent_keys directory")
return emptyList()
}
if (files.isEmpty()) {
SystemLogger.debug("No persisted key files found")
return emptyList()
}
SystemLogger.info("Found ${files.size} persisted key files to process")
val result = mutableListOf<PersistedKeyData>()
for (file in files) {
runCatching {
DataInputStream(BufferedInputStream(FileInputStream(file))).use { input ->
val version = input.readInt()
if (version != FORMAT_VERSION) {
// Old upstream files (v1) and dev-only intermediate
// files (v2) are missing the metadata snapshot
// and/or symmetric key block — restoring them
// would put broken state in memory (apps relying
// on those records get logged out). Skip and let
// the next generateKey re-create cleanly with the
// new format. Affected apps re-login once after
// upgrade, then never again.
SystemLogger.info(
"Skipping ${file.name}: legacy format version $version. " +
"It will be replaced on next generateKey for this alias."
)
return@runCatching
}
val storedSecLevel = input.readInt()
val uid = input.readInt()
val alias = input.readUTF()
val nspace = input.readLong()
val isAttestKey = input.readBoolean()
val algo = input.readInt()
val kSize = input.readInt()
val curve = input.readInt()
val purposeCount = requireBounds(input.readInt(), 64, "purposeCount")
val purposes = (0 until purposeCount).map { input.readInt() }
val digestCount = requireBounds(input.readInt(), 64, "digestCount")
val digests = (0 until digestCount).map { input.readInt() }
val pkLen = requireBounds(input.readInt(), 8192, "pkLen")
val pkBytes = ByteArray(pkLen)
if (pkLen > 0) input.readFully(pkBytes)
val certCount = requireBounds(input.readInt(), 10, "certCount")
val certChainBytes =
(0 until certCount).map {
val certLen = requireBounds(input.readInt(), 65536, "certLen")
val certBytes = ByteArray(certLen)
input.readFully(certBytes)
certBytes
}
val metaLen = requireBounds(input.readInt(), 256 * 1024, "metaLen")
val metadataBytes =
ByteArray(metaLen).also { if (metaLen > 0) input.readFully(it) }
val skAlgo = input.readUTF()
val skLen = requireBounds(input.readInt(), 8192, "skLen")
val skBytes = ByteArray(skLen).also { if (skLen > 0) input.readFully(it) }
if (storedSecLevel == securityLevel) {
result.add(
PersistedKeyData(
uid = uid,
alias = alias,
nspace = nspace,
securityLevel = storedSecLevel,
isAttestationKey = isAttestKey,
algorithm = algo,
keySize = kSize,
ecCurve = curve,
purposes = purposes,
digests = digests,
privateKeyBytes = pkBytes,
certChainBytes = certChainBytes,
metadataBytes = metadataBytes,
symmetricKeyBytes = skBytes,
symmetricAlgorithm = skAlgo,
)
)
}
}
}
.onFailure { e ->
SystemLogger.warning("Skipping corrupted persisted key file: ${file.name}", e)
}
}
SystemLogger.info("Loaded ${result.size} persisted keys for security level $securityLevel")
return result
}
// Re-persist updates the cert chain for an already-persisted key without
// reconstructing authorization parameters from the response. This avoids
// pulling keymint Tag dependencies into this file and is correct because
// the only field that changes post-generation is the patched cert chain.
fun rePersistIfNeeded(
callingUid: Int,
generatedKeyInfo: KeyMintSecurityLevelInterceptor.GeneratedKeyInfo,
) {
val metadata = generatedKeyInfo.response.metadata
if (metadata == null) {
SystemLogger.debug("rePersist: no metadata, skipping")
return
}
val secLevel = metadata.keySecurityLevel
val entry =
KeyMintSecurityLevelInterceptor.generatedKeys.entries.find { (id, info) ->
id.uid == callingUid && info.nspace == generatedKeyInfo.nspace
}
if (entry == null) {
SystemLogger.debug(
"rePersist: key not found in map for uid=$callingUid nspace=${generatedKeyInfo.nspace}"
)
return
}
val keyId = entry.key
val filename = keyFileName(keyId.uid, keyId.alias)
val existing = File(PERSISTENCE_DIR, filename)
if (!existing.exists()) {
SystemLogger.debug("rePersist: no existing file for $keyId, skipping")
return
}
val newChain = CertificateHelper.getCertificateChain(metadata)
if (newChain == null) {
SystemLogger.warning("rePersist: could not extract cert chain for $keyId")
return
}
val persisted =
runCatching {
DataInputStream(BufferedInputStream(FileInputStream(existing))).use { input ->
val version = input.readInt()
if (version != FORMAT_VERSION) {
SystemLogger.warning(
"rePersist: legacy format version $version for $keyId, will not re-persist (next generateKey replaces it)"
)
return
}
readPersistedKeyData(input)
}
}
.getOrNull()
if (persisted == null) {
SystemLogger.warning("rePersist: failed to read existing data for $keyId")
return
}
val keyPair = generatedKeyInfo.keyPair
val secretKey = generatedKeyInfo.secretKey
if (keyPair == null && secretKey == null) {
SystemLogger.warning("rePersist: no key material for $keyId")
return
}
// Serialize the live KeyMetadata (now contains the user-installed cert
// chain via updateSubcomponent) so the next boot restores byte-identical
// metadata. KeyMetadata is binder-free, so marshall() is safe here.
val metadataBytes =
runCatching {
android.os.Parcel.obtain().let { parcel ->
try {
metadata.writeToParcel(parcel, 0)
parcel.marshall()
} finally {
parcel.recycle()
}
}
}
.getOrNull()
save(
keyId = keyId,
keyPair = keyPair,
secretKey = secretKey,
nspace = generatedKeyInfo.nspace,
securityLevel = secLevel,
certChain = newChain.toList(),
algorithm = persisted.algorithm,
keySize = persisted.keySize,
ecCurve = persisted.ecCurve,
purposes = persisted.purposes,
digests = persisted.digests,
isAttestationKey = persisted.isAttestationKey,
metadataBytes = metadataBytes,
)
SystemLogger.debug("Re-persisted key $keyId with updated cert chain")
}
// Corrupted binary files can have arbitrary length fields — cap allocations
private fun requireBounds(value: Int, max: Int, name: String): Int {
require(value in 0..max) { "$name out of bounds: $value (max $max)" }
return value
}
private fun keyFileName(uid: Int, alias: String): String {
val digest =
MessageDigest.getInstance("SHA-256").digest("$uid:$alias".toByteArray(Charsets.UTF_8))
return digest.joinToString("") { "%02x".format(it) } + ".bin"
}
// Reads all fields after the version int has already been consumed
// and validated by the caller.
private fun readPersistedKeyData(input: DataInputStream): PersistedKeyData {
val secLevel = input.readInt()
val uid = input.readInt()
val alias = input.readUTF()
val nspace = input.readLong()
val isAttestKey = input.readBoolean()
val algo = input.readInt()
val kSize = input.readInt()
val curve = input.readInt()
val purposeCount = requireBounds(input.readInt(), 64, "purposeCount")
val purposes = (0 until purposeCount).map { input.readInt() }
val digestCount = requireBounds(input.readInt(), 64, "digestCount")
val digests = (0 until digestCount).map { input.readInt() }
val pkLen = requireBounds(input.readInt(), 8192, "pkLen")
val pkBytes = ByteArray(pkLen)
if (pkLen > 0) input.readFully(pkBytes)
val certCount = requireBounds(input.readInt(), 10, "certCount")
val certChainBytes =
(0 until certCount).map {
val certLen = requireBounds(input.readInt(), 65536, "certLen")
val certBytes = ByteArray(certLen)
input.readFully(certBytes)
certBytes
}
val metaLen = requireBounds(input.readInt(), 256 * 1024, "metaLen")
val metadataBytes = ByteArray(metaLen).also { if (metaLen > 0) input.readFully(it) }
val skAlgo = input.readUTF()
val skLen = requireBounds(input.readInt(), 8192, "skLen")
val skBytes = ByteArray(skLen).also { if (skLen > 0) input.readFully(it) }
return PersistedKeyData(
uid = uid,
alias = alias,
nspace = nspace,
securityLevel = secLevel,
isAttestationKey = isAttestKey,
algorithm = algo,
keySize = kSize,
ecCurve = curve,
purposes = purposes,
digests = digests,
privateKeyBytes = pkBytes,
certChainBytes = certChainBytes,
metadataBytes = metadataBytes,
symmetricKeyBytes = skBytes,
symmetricAlgorithm = skAlgo,
)
}
}
@@ -1,73 +0,0 @@
package org.matrix.TEESimulator.interception.keystore.shim
import android.os.IBinder
import android.os.Parcel
import android.system.keystore2.IKeystoreOperation
import org.matrix.TEESimulator.interception.core.BinderInterceptor
import org.matrix.TEESimulator.interception.keystore.InterceptorUtils
/**
* Intercepts calls to an `IKeystoreOperation` service. This is used to log the data manipulation
* methods of a cryptographic operation.
*/
class OperationInterceptor(
private val original: IKeystoreOperation,
private val backdoor: IBinder,
private val isAead: Boolean,
) : BinderInterceptor() {
override fun onPreTransact(
txId: Long,
target: IBinder,
code: Int,
flags: Int,
callingUid: Int,
callingPid: Int,
data: Parcel,
): TransactionResult {
val methodName = transactionNames[code] ?: "unknown code=$code"
logTransaction(txId, methodName, callingUid, callingPid, true)
// Mirror SoftwareOperation's vendor gate: a real-key op must answer non-AEAD updateAad
// exactly as the forged-key path does. Samsung and Xiaomi-MTK TEEs accept it; rejecting
// here while the forged path accepts diverges the two and fingerprints the injection.
if (code == UPDATE_AAD_TRANSACTION && !isAead) {
return if (VendorQuirks.nonAeadUpdateAadSucceeds()) {
InterceptorUtils.createSuccessReply(writeResultCode = false)
} else {
InterceptorUtils.createServiceSpecificErrorReply(KeystoreErrorCodes.invalidTag)
}
}
if (code == FINISH_TRANSACTION || code == ABORT_TRANSACTION) {
KeyMintSecurityLevelInterceptor.removeOperationInterceptor(target, backdoor)
}
return TransactionResult.ContinueAndSkipPost
}
companion object {
private val UPDATE_AAD_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "updateAad")
private val UPDATE_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "update")
private val FINISH_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "finish")
private val ABORT_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort")
val INTERCEPTED_CODES =
intArrayOf(UPDATE_AAD_TRANSACTION, FINISH_TRANSACTION, ABORT_TRANSACTION)
private val transactionNames: Map<Int, String> by lazy {
IKeystoreOperation.Stub::class
.java
.declaredFields
.filter {
it.isAccessible = true
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
}
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
}
}
}
@@ -1,664 +0,0 @@
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.Build
import android.os.ServiceSpecificException
import android.os.SystemProperties
import android.system.keystore2.IKeystoreOperation
import android.system.keystore2.KeyParameters
import java.security.KeyPair
import java.security.Signature
import java.security.SignatureException
import java.util.concurrent.locks.LockSupport
import javax.crypto.Cipher
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
import org.matrix.TEESimulator.logging.SystemLogger
/**
* Mirrors the per-vendor TEE quirk that Duck Detector's OperationErrorPathProbe checks: real
* Samsung and Xiaomi-MTK TrustZone return success for updateAad on a non-AEAD operation, while
* every other vendor rejects it with a service-specific INVALID_TAG. The module reads the same
* device-identity fields the probe reads, so a forged software operation answers exactly as that
* vendor's real TEE would.
*/
internal object VendorQuirks {
private val UPDATE_AAD_ALLOWS_SUCCESS = setOf("samsung")
private val XIAOMI_BRANDS = setOf("xiaomi", "redmi", "poco")
fun nonAeadUpdateAadSucceeds(): Boolean {
val manufacturer = Build.MANUFACTURER.lowercase()
val brand = Build.BRAND.lowercase()
if (manufacturer in UPDATE_AAD_ALLOWS_SUCCESS || brand in UPDATE_AAD_ALLOWS_SUCCESS) {
return true
}
if (manufacturer != "xiaomi" && brand !in XIAOMI_BRANDS) return false
return isMediaTek()
}
private fun isMediaTek(): Boolean {
val roHardware = SystemProperties.get("ro.hardware", "")
return roHardware.startsWith("mt") || Build.HARDWARE.startsWith("mt", ignoreCase = true)
}
}
private sealed interface CryptoPrimitive {
fun updateAad(aadInput: ByteArray?) {
// Real Samsung / Xiaomi-MTK TEEs accept updateAad on non-AEAD ops; others reject it.
if (!VendorQuirks.nonAeadUpdateAadSucceeds()) {
throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
}
}
fun update(data: ByteArray?): ByteArray?
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
fun abort()
fun getBeginParameters(): Array<KeyParameter>? = null
}
private object JcaAlgorithmMapper {
fun mapSignatureAlgorithm(params: KeyMintAttestation): String {
val digest =
when (params.digest.firstOrNull()) {
Digest.SHA_2_256 -> "SHA256"
Digest.SHA_2_384 -> "SHA384"
Digest.SHA_2_512 -> "SHA512"
else -> "NONE"
}
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"
}
else ->
throw ServiceSpecificException(
KeystoreErrorCodes.incompatibleAlgorithm,
"Unsupported signature algorithm: ${params.algorithm}",
)
}
}
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
val keyAlgo =
when (params.algorithm) {
Algorithm.RSA -> "RSA"
Algorithm.AES -> "AES"
else ->
throw ServiceSpecificException(
KeystoreErrorCodes.incompatibleAlgorithm,
"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"
}
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"
}
return "$keyAlgo/$blockMode/$padding"
}
fun mapOaepDigest(digest: Int?): String =
when (digest) {
Digest.SHA1 -> "SHA-1"
Digest.SHA_2_224 -> "SHA-224"
Digest.SHA_2_256 -> "SHA-256"
Digest.SHA_2_384 -> "SHA-384"
Digest.SHA_2_512 -> "SHA-512"
else -> "SHA-256"
}
fun mapMacAlgorithm(params: KeyMintAttestation): String =
when (params.digest.firstOrNull()) {
Digest.SHA_2_256 -> "HmacSHA256"
Digest.SHA_2_384 -> "HmacSHA384"
Digest.SHA_2_512 -> "HmacSHA512"
else -> "HmacSHA256"
}
}
private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
private val signature: Signature =
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
initSign(keyPair.private)
}
override fun update(data: ByteArray?): ByteArray? {
if (data != null) signature.update(data)
return null
}
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray {
if (data != null) update(data)
return this.signature.sign()
}
override fun abort() {}
}
private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
private val signature: Signature =
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
initVerify(keyPair.public)
}
override fun update(data: ByteArray?): ByteArray? {
if (data != null) signature.update(data)
return null
}
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
if (data != null) update(data)
if (signature == null) {
throw ServiceSpecificException(
KeystoreErrorCodes.verificationFailed,
"Signature to verify is null",
)
}
if (!this.signature.verify(signature)) {
throw ServiceSpecificException(
KeystoreErrorCodes.verificationFailed,
"Signature verification failed",
)
}
return null
}
override fun abort() {}
}
private class CipherPrimitive(
cryptoKey: java.security.Key,
params: KeyMintAttestation,
private val opMode: Int,
txId: Long,
) : 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 if (params.padding.firstOrNull() == PaddingMode.RSA_OAEP) {
val mainDigest = JcaAlgorithmMapper.mapOaepDigest(params.digest.firstOrNull())
val mgfDigest =
params.rsaOaepMgfDigest.firstOrNull()?.let {
JcaAlgorithmMapper.mapOaepDigest(it)
} ?: mainDigest
init(
opMode,
cryptoKey,
javax.crypto.spec.OAEPParameterSpec(
mainDigest,
"MGF1",
java.security.spec.MGF1ParameterSpec(mgfDigest),
javax.crypto.spec.PSource.PSpecified.DEFAULT,
),
)
SystemLogger.debug {
"[SoftwareOp TX_ID: $txId] oaep-op main=$mainDigest mgf=$mgfDigest " +
"mode=${if (opMode == Cipher.DECRYPT_MODE) "decrypt" else "encrypt"}"
}
} else {
init(opMode, cryptoKey)
}
}
override fun updateAad(aadInput: ByteArray?) {
if (!isAead) {
if (!VendorQuirks.nonAeadUpdateAadSucceeds()) {
throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
}
return
}
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() {}
}
private class MacPrimitive(
secretKey: javax.crypto.SecretKey,
private val params: KeyMintAttestation,
private val txId: Long,
) : CryptoPrimitive {
private val mac: javax.crypto.Mac =
javax.crypto.Mac.getInstance(JcaAlgorithmMapper.mapMacAlgorithm(params)).apply {
init(secretKey)
}
override fun update(data: ByteArray?): ByteArray? {
if (data != null) mac.update(data)
return null
}
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
if (data != null) mac.update(data)
val full = mac.doFinal()
// Tag.MAC_LENGTH is optional on the AndroidKeyStore Mac SPI; default to the
// full digest length so real Mac use keeps working when it is omitted.
val tagBytes = (params.macLength ?: (full.size * 8)) / 8
val tag = full.copyOf(tagBytes)
if (params.purpose.firstOrNull() == KeyPurpose.VERIFY) {
if (signature == null) {
throw ServiceSpecificException(
KeystoreErrorCodes.verificationFailed,
"MAC to verify is null",
)
}
if (!java.security.MessageDigest.isEqual(tag, signature)) {
throw ServiceSpecificException(
KeystoreErrorCodes.verificationFailed,
"MAC verification failed",
)
}
return null
}
SystemLogger.debug {
"[SoftwareOp TX_ID: $txId] hmac-op digest=${params.digest.firstOrNull()} " +
"macLen=${params.macLength} tag=${tag.size}B result=ok"
}
return tag
}
override fun abort() {}
}
class SoftwareOperation(
private val txId: Long,
keyPair: KeyPair?,
secretKey: javax.crypto.SecretKey?,
params: KeyMintAttestation,
private val latencyFloorMs: Long = 0L,
) {
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 {
val purpose = params.purpose.firstOrNull()
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
if (purpose == null) {
// Defensive: if params somehow restored without a PURPOSE tag
// (corrupt v2 metadata, mismatched authorizations array on load,
// or future format drift) the original code crashed with NPE
// because Signer/Verifier/Cipher all dereference keyPair!!
// before checking purpose. Surface a clean keystore error
// instead so callers see a normal-looking operation failure
// they can recover from rather than the process appearing to
// silently corrupt their session.
SystemLogger.warning(
"[SoftwareOp TX_ID: $txId] Purpose missing on restored key " +
"(authorizations=${params.purpose}, keyPair=${if (keyPair != null) "present" else "null"}, " +
"secretKey=${if (secretKey != null) "present" else "null"}). " +
"Returning unsupportedPurpose."
)
throw ServiceSpecificException(
KeystoreErrorCodes.unsupportedPurpose,
"Restored key has no PURPOSE authorization",
)
}
primitive =
if (params.algorithm == Algorithm.HMAC) {
// An HMAC key is symmetric (secretKey set, keyPair null), so it must
// not fall through to the purpose-keyed Signer/Verifier paths, which
// require a keyPair. secretKey is populated at HMAC keygen and restore,
// so the throw is a defensive floor, not a live path.
MacPrimitive(
secretKey
?: throw ServiceSpecificException(
KeystoreErrorCodes.invalidArgument,
"[SoftwareOp TX_ID: $txId] HMAC op but secretKey null",
),
params,
txId,
)
} else {
when (purpose) {
KeyPurpose.SIGN -> {
val kp =
keyPair
?: throw ServiceSpecificException(
KeystoreErrorCodes.invalidArgument,
"[SoftwareOp TX_ID: $txId] SIGN requested but keyPair is null",
)
Signer(kp, params)
}
KeyPurpose.VERIFY -> {
val kp =
keyPair
?: throw ServiceSpecificException(
KeystoreErrorCodes.invalidArgument,
"[SoftwareOp TX_ID: $txId] VERIFY requested but keyPair is null",
)
Verifier(kp, params)
}
KeyPurpose.ENCRYPT -> {
val key: java.security.Key =
secretKey
?: keyPair?.public
?: throw ServiceSpecificException(
KeystoreErrorCodes.unsupportedPurpose,
"[SoftwareOp TX_ID: $txId] ENCRYPT requires either secretKey or keyPair.public",
)
CipherPrimitive(key, params, Cipher.ENCRYPT_MODE, txId)
}
KeyPurpose.DECRYPT -> {
val key: java.security.Key =
secretKey
?: keyPair?.private
?: throw ServiceSpecificException(
KeystoreErrorCodes.unsupportedPurpose,
"[SoftwareOp TX_ID: $txId] DECRYPT requires either secretKey or keyPair.private",
)
CipherPrimitive(key, params, Cipher.DECRYPT_MODE, txId)
}
KeyPurpose.AGREE_KEY -> {
val kp =
keyPair
?: throw ServiceSpecificException(
KeystoreErrorCodes.invalidArgument,
"[SoftwareOp TX_ID: $txId] AGREE_KEY requested but keyPair is null",
)
KeyAgreementPrimitive(kp)
}
else ->
throw ServiceSpecificException(
KeystoreErrorCodes.unsupportedPurpose,
"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.info(
"[SoftwareOp TX_ID: $txId] updateAad() ENTRY inputSize=${aadInput?.size ?: 0} primitive=${primitive::class.simpleName}"
)
checkActive()
checkInputLength(aadInput)
try {
primitive.updateAad(aadInput)
SystemLogger.info(
"[SoftwareOp TX_ID: $txId] updateAad() RETURNED_NORMALLY (unexpected for non-AEAD)"
)
} catch (throwable: Throwable) {
val top = throwable.stackTrace.firstOrNull()?.toString() ?: "<no-frame>"
val code = (throwable as? ServiceSpecificException)?.errorCode
SystemLogger.info(
"[SoftwareOp TX_ID: $txId] updateAad() THREW class=${throwable::class.java.name} code=$code msg=${throwable.message} top=$top"
)
throw throwable
}
}
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)
}
}
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)
}
}
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)
}
val incompatibleBlockMode: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_BLOCK_MODE", -8)
}
val incompatiblePaddingMode: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_PADDING_MODE", -11)
}
val incompatibleDigest: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_DIGEST", -13)
}
val invalidMacLength: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_MAC_LENGTH", -57)
}
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
}
}
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
IKeystoreOperation.Stub() {
@Synchronized
override fun updateAad(aadInput: ByteArray?) {
SystemLogger.info(
"[SoftwareOpBinder] updateAad() ENTRY callingUid=${android.os.Binder.getCallingUid()} size=${aadInput?.size ?: 0}"
)
try {
operation.updateAad(aadInput)
SystemLogger.info("[SoftwareOpBinder] updateAad() RETURNED_NORMALLY")
} catch (throwable: Throwable) {
val code = (throwable as? ServiceSpecificException)?.errorCode
SystemLogger.info(
"[SoftwareOpBinder] updateAad() PROPAGATING class=${throwable::class.java.name} code=$code msg=${throwable.message}"
)
throw throwable
}
}
@Synchronized
override fun update(input: ByteArray?): ByteArray? {
return operation.update(input)
}
@Synchronized
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
return operation.finish(input, signature)
}
@Synchronized
override fun abort() {
operation.abort()
}
}
@@ -1,170 +0,0 @@
package org.matrix.TEESimulator.interception.soter
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Handler
import android.os.HandlerThread
import android.os.IBinder
import java.util.concurrent.Executor
import java.util.concurrent.atomic.AtomicBoolean
import org.matrix.TEESimulator.interception.core.BinderInterceptor
import org.matrix.TEESimulator.logging.SystemLogger
/**
* Keeps [SoterServiceInterceptor] mounted on the on-demand, restartable
* `com.tencent.soter.soterserver` process.
*
* `AbstractKeystoreInterceptor` injects `keystore2` exactly once: it is always alive and
* servicemanager-published, so the daemon gets its binder from `ServiceManager` and may
* `exitProcess` on failure. soterserver inverts both — it is Intent-bound (NOT in
* `ServiceManager`) and may die and respawn. This supervisor therefore *binds* the SOTER
* service, which both triggers its on-demand start AND yields the `ISoterService` binder
* (the target the native MITM registry keys on); injects `libTEESimulator.so` on every
* (re)start; confirms the landing with the `0xdeadbeef` backdoor handshake; then registers
* the forge. It re-binds — re-poking, re-injecting, re-registering — whenever the process
* dies, never exiting.
*
* The bind recipe (action = the interface descriptor, package, `BIND_AUTO_CREATE`) and the
* rebind-on-death lifecycle mirror the SOTER SDK's own `SoterCoreTreble`, so the daemon
* connects exactly as a real client would. Everything runs on a dedicated [HandlerThread]
* so it never stalls keystore init or `Looper.loop()` in [org.matrix.TEESimulator.App].
*
* Observability (the checkpoint's mandatory gate): every lifecycle event — bind, connect,
* inject ok/fail, handshake, respawn — is logged via [SystemLogger], debug-gated. It never
* gates the forge.
*/
object SoterProcessSupervisor {
/** soterserver hosts the package's own process (recon 2026-06-26: process == package). */
private const val SOTER_PACKAGE = "com.tencent.soter.soterserver"
/** Reuses the daemon's native injector + `entry`, PID-resolved by the target package. */
private const val INJECTION_COMMAND =
"exec ./inject `pidof $SOTER_PACKAGE` libTEESimulator.so entry"
private const val REBIND_DELAY_MS = 1000L
private const val REBIND_MAX_MS = 30_000L
private val started = AtomicBoolean(false)
/** Re-bind backoff; doubles each failed (re)bind up to [REBIND_MAX_MS], resets on a clean mount. Handler-thread-confined. */
private var rebindDelay = REBIND_DELAY_MS
private lateinit var context: Context
private lateinit var handler: Handler
/** Delivers bind callbacks onto the supervisor thread so nothing touches the main looper. */
private val executor = Executor { command -> handler.post(command) }
/**
* Starts supervising on a dedicated thread and returns immediately. Idempotent. [context]
* must be able to bind services (the daemon's system context); supplied by the App wiring.
*/
fun start(context: Context) {
if (!started.compareAndSet(false, true)) return
this.context = context
handler = Handler(HandlerThread("soter-supervisor").apply { start() }.looper)
handler.post { bind() }
}
private val connection =
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
SystemLogger.debug("SOTER service connected; mounting forge")
service?.let(::mount)
}
override fun onServiceDisconnected(name: ComponentName?) {
SystemLogger.debug("SOTER service disconnected (process died); rebinding")
scheduleRetry()
}
override fun onBindingDied(name: ComponentName?) {
SystemLogger.debug("SOTER binding died; rebinding")
scheduleRetry()
}
override fun onNullBinding(name: ComponentName?) {
SystemLogger.debug("SOTER onBind returned null; rebinding")
scheduleRetry()
}
}
private fun bind() {
val intent = Intent(SoterServiceInterceptor.DESCRIPTOR).setPackage(SOTER_PACKAGE)
val bound =
runCatching {
context.bindService(intent, Context.BIND_AUTO_CREATE, executor, connection)
}
.getOrElse {
SystemLogger.debug { "SOTER bindService threw: $it" }
false
}
if (bound) {
SystemLogger.debug("SOTER bind requested (on-demand poke)")
} else {
SystemLogger.debug("SOTER bindService returned false; retrying")
scheduleRetry()
}
}
private fun rebind() {
runCatching { context.unbindService(connection) }
bind()
}
/**
* Re-attempts the bind after the current backoff, then widens it (capped at [REBIND_MAX_MS]).
* Every path that fails to leave the forge mounted routes here, so a live-but-uninjected
* binding is re-attempted instead of stranding the forge. A clean [mount] resets the backoff.
*/
private fun scheduleRetry() {
val delay = rebindDelay
rebindDelay = (rebindDelay * 2).coerceAtMost(REBIND_MAX_MS)
handler.postDelayed({ rebind() }, delay)
}
/** Confirms injection via the `0xdeadbeef` handshake, injecting first if absent, then registers. */
private fun mount(soterBinder: IBinder) {
var backdoor = BinderInterceptor.getBackdoor(soterBinder)
if (backdoor == null) {
SystemLogger.debug("SOTER backdoor absent; injecting libTEESimulator.so")
if (!injectLibrary()) {
SystemLogger.debug("SOTER injection failed; scheduling re-bind")
scheduleRetry()
return
}
backdoor = BinderInterceptor.getBackdoor(soterBinder)
}
if (backdoor == null) {
SystemLogger.debug("SOTER backdoor handshake failed after injection; scheduling re-bind")
scheduleRetry()
return
}
val registered =
BinderInterceptor.register(
backdoor,
soterBinder,
SoterServiceInterceptor,
SoterServiceInterceptor.interceptedCodes,
)
if (!registered) {
SystemLogger.debug("SOTER register failed; scheduling re-bind")
scheduleRetry()
return
}
rebindDelay = REBIND_DELAY_MS
SystemLogger.debug("SOTER forge mounted; handshake ok")
}
private fun injectLibrary(): Boolean =
runCatching {
Runtime.getRuntime().exec(arrayOf("/system/bin/sh", "-c", INJECTION_COMMAND)).waitFor() == 0
}
.getOrElse {
SystemLogger.debug { "SOTER inject exec failed: $it" }
false
}
}
@@ -1,229 +0,0 @@
package org.matrix.TEESimulator.interception.soter
import android.os.IBinder
import android.os.Parcel
import android.util.Base64
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.KeyPairGenerator
import org.matrix.TEESimulator.interception.core.BinderInterceptor
import org.matrix.TEESimulator.logging.SystemLogger
/**
* Forges healthy `com.tencent.soter.soterserver.ISoterService` (Layer A: AIDL over
* `/dev/binder`) replies from inside the injected soterserver app process, so the SOTER
* capability probe (春秋 / DuckDetector `SoterCapabilityProbe`) reads `available = true`
* / `damaged = false` on a bootloader-unlocked device whose SOTER TA can no longer use
* its factory ATTK. Replaces the external SoterFixer loop + the Hail freeze.
*
* Unconditional by design: the forge decision never consults `ConfigurationManager` /
* `target.txt` (Phase 10 spec §Decision, gate G). It is mounted by the SOTER process
* supervisor (10.B/10.W) against the ISoterService binder, so `onPreTransact` only sees
* transactions on that binder — matching the raw transaction code is therefore enough.
*
* Diagnostics follow the module's standard three-layer capture (debug-gated, per-UID
* NDJSON via [SystemLogger]; see `logging/SystemLogger.kt`): a `tx` line for every
* transaction ([logTransaction]), the raw inbound request parcel, and the raw forged
* reply wire. Capture is scoped to targeted UIDs (`isUidLogged`) exactly like the
* keystore lane — it does NOT make the forge conditional; the forge still fires for all.
*
* Transaction codes are HARDCODED 1..13 in AIDL declaration order, NOT resolved via
* [org.matrix.TEESimulator.interception.keystore.InterceptorUtils.getTransactCode]: the
* shipped soterserver build is R8/ProGuard obfuscated — there is no `ISoterService$Stub`
* class and no `TRANSACTION_*` fields (recon 2026-06-26, `a$a.smali` packed-switch). The
* codes are fixed by Tencent's `ISoterService.aidl` and are obfuscation-independent.
*
* Scope boundary (10.A vs 10.M): the seven primitive-returning methods are fully forged
* here. The six parcelable-returning methods emit the correct AIDL envelope + the
* recon-verified `writeToParcel` field order; 10.M fills the payloads with
* detector-satisfying values — a framed SOTER pubkey envelope the SDK's
* `retrieveJsonFromExportedData` parses to a non-null `SoterPubKeyModel`, a non-zero sign
* session, and a 256-byte signature.
*/
object SoterServiceInterceptor : BinderInterceptor() {
/** The surviving, obfuscation-stable interface identifier (used by the 10.B/10.W mount). */
const val DESCRIPTOR = "com.tencent.soter.soterserver.ISoterService"
// AIDL transaction codes = FIRST_CALL_TRANSACTION (1) + declaration index, verified
// against the obfuscated `a$a.smali` packed-switch (recon 2026-06-26). NOTE the 5/6
// order: removeAuthKey precedes getAuthKey in the real .aidl (the spec prose had it
// reversed). Comments record each method's return shape.
private const val TX_GENERATE_APP_SECURE_KEY = 1 // int
private const val TX_GET_APP_SECURE_KEY = 2 // SoterExportResult
private const val TX_HAS_ASK_ALREADY = 3 // boolean
private const val TX_GENERATE_AUTH_KEY = 4 // int
private const val TX_REMOVE_AUTH_KEY = 5 // int (NOT getAuthKey)
private const val TX_GET_AUTH_KEY = 6 // SoterExportResult (NOT removeAuthKey)
private const val TX_REMOVE_ALL_AUTH_KEY = 7 // int
private const val TX_HAS_AUTH_KEY = 8 // boolean
private const val TX_INIT_SIGH = 9 // SoterSessionResult (sic: Tencent's spelling)
private const val TX_FINISH_SIGN = 10 // SoterSignResult
private const val TX_GET_DEVICE_ID = 11 // SoterDeviceResult
private const val TX_GET_VERSION = 12 // int (real service returns 1)
private const val TX_GET_EXTRA_PARAM = 13 // SoterExtraParam
/** SOTER success result code (`SoterCoreResult` ERR_OK). */
private const val SOTER_OK = 0
/** finishSign signature length the probe expects. */
private const val SIGNATURE_LEN = 256
/** `cpu_id` placeholder in the export envelope; the local probe never reads its value
* (the backend pins the real per-`cpu_id` ATTK, which the forge cannot satisfy). */
private const val CPU_ID = "0000000000000000"
/** Code -> Tencent method name, for the `tx` diagnostic line. Names from the recon decompile. */
private val methodNames =
mapOf(
TX_GENERATE_APP_SECURE_KEY to "generateAppSecureKey",
TX_GET_APP_SECURE_KEY to "getAppSecureKey",
TX_HAS_ASK_ALREADY to "hasAskAlready",
TX_GENERATE_AUTH_KEY to "generateAuthKey",
TX_REMOVE_AUTH_KEY to "removeAuthKey",
TX_GET_AUTH_KEY to "getAuthKey",
TX_REMOVE_ALL_AUTH_KEY to "removeAllAuthKey",
TX_HAS_AUTH_KEY to "hasAuthKey",
TX_INIT_SIGH to "initSigh",
TX_FINISH_SIGN to "finishSign",
TX_GET_DEVICE_ID to "getDeviceId",
TX_GET_VERSION to "getVersion",
TX_GET_EXTRA_PARAM to "getExtraParam",
)
/** The codes this interceptor forges; consumed by the supervisor's registration (10.B/10.W). */
val interceptedCodes: IntArray = methodNames.keys.toIntArray()
/**
* Payload of [SoterExportResult.exportData] for getAppSecureKey (txn 2) and getAuthKey
* (txn 6). The detector's capability probe gates `damaged=false` on
* `SoterCore.getApp/AuthKeyModel() != null`, and the SDK's `retrieveJsonFromExportedData`
* (`SoterCoreBase`) returns a non-null `SoterPubKeyModel` only when this exact framing
* parses: `[4-byte LITTLE-ENDIAN json length][UTF-8 json][signature bytes]`. A
* non-empty-but-unframed blob throws inside the SDK and is read as `damaged` silently.
* The JSON parser swallows every exception, so only the framing is load-bearing; the
* `pub_key` is a genuine RSA-2048 SubjectPublicKeyInfo so a probe that base64/X.509-parses
* the field locally still succeeds. Lazily built — keygen runs once, off the mount path.
*/
private val exportBlob: ByteArray by lazy { buildExportBlob() }
/** getDeviceId (txn 11) payload — well-formed, non-empty; the probe never parses it. */
private val deviceIdBlob = "TEESIM-SOTER-0001".toByteArray(Charsets.UTF_8)
/** finishSign (txn 10) signature payload — [SIGNATURE_LEN] bytes. */
private val signatureBlob = ByteArray(SIGNATURE_LEN)
private fun buildExportBlob(): ByteArray {
val pubKey =
runCatching {
val generator = KeyPairGenerator.getInstance("RSA").apply { initialize(2048) }
Base64.encodeToString(generator.generateKeyPair().public.encoded, Base64.NO_WRAP)
}
.getOrDefault("")
val json =
"""{"pub_key":"$pubKey","counter":0,"cpu_id":"$CPU_ID","uid":0}"""
.toByteArray(Charsets.UTF_8)
val lengthPrefix = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(json.size).array()
return lengthPrefix + json + signatureBlob
}
override fun onPreTransact(
txId: Long,
target: IBinder,
code: Int,
flags: Int,
callingUid: Int,
callingPid: Int,
data: Parcel,
): TransactionResult {
val method = methodNames[code]
if (method == null) {
// Not an ISoterService method we forge — record it as observed, then pass through.
logTransaction(txId, "code=$code", callingUid, callingPid, skipPost = true)
return TransactionResult.ContinueAndSkipPost
}
logTransaction(txId, method, callingUid, callingPid)
captureRequest(callingUid, txId, method, data)
return when (code) {
// Primitive returns — fully forged here.
TX_GENERATE_APP_SECURE_KEY,
TX_GENERATE_AUTH_KEY,
TX_REMOVE_AUTH_KEY,
TX_REMOVE_ALL_AUTH_KEY -> forgedReply(callingUid, txId, method) { writeInt(SOTER_OK) }
TX_GET_VERSION -> forgedReply(callingUid, txId, method) { writeInt(1) }
TX_HAS_ASK_ALREADY,
TX_HAS_AUTH_KEY -> forgedReply(callingUid, txId, method) { writeInt(1) } // boolean true
// Parcelable returns — correct envelope + recon field order, payloads filled (10.M).
TX_GET_APP_SECURE_KEY,
TX_GET_AUTH_KEY ->
forgedReply(callingUid, txId, method) {
writeInt(1) // non-null marker
writeInt(SOTER_OK) // resultCode
writeByteArray(exportBlob) // exportData — framed SOTER pubkey envelope
writeInt(exportBlob.size) // exportDataLength
}
TX_INIT_SIGH ->
forgedReply(callingUid, txId, method) {
writeInt(1)
writeLong(1L) // session — any non-zero satisfies the probe
writeInt(SOTER_OK) // resultCode — probe requires == 0 (SoterCapabilityProbe.kt:107)
}
TX_FINISH_SIGN ->
forgedReply(callingUid, txId, method) {
writeInt(1)
writeInt(SOTER_OK) // resultCode — finishSign throws on != 0
writeByteArray(signatureBlob) // exportData = signature
writeInt(signatureBlob.size) // exportDataLength
}
TX_GET_DEVICE_ID ->
forgedReply(callingUid, txId, method) {
writeInt(1)
writeInt(SOTER_OK) // resultCode
writeByteArray(deviceIdBlob) // exportData = device id
writeInt(deviceIdBlob.size) // exportDataLength
}
TX_GET_EXTRA_PARAM ->
forgedReply(callingUid, txId, method) {
writeInt(1)
writeValue("optical") // SoterExtraParam.result = fingerprint sensor type
}
// Unreachable: method != null means code is one of the 13 above.
else -> TransactionResult.ContinueAndSkipPost
}
}
/** Snapshots the inbound request parcel to the per-UID NDJSON plane (debug + targeted only). */
private fun captureRequest(uid: Int, txId: Long, method: String, data: Parcel) {
if (!SystemLogger.isUidLogged(uid)) return
runCatching { data.marshall() }
.onSuccess { raw ->
SystemLogger.uidLogRaw(uid, txId, "$method-request", "len=${raw.size}", raw)
}
}
/**
* Builds an AIDL reply (`writeNoException()` then [body]) and snapshots its wire bytes to the
* per-UID NDJSON plane before handing it to the native hook. Parcelable bodies write their own
* `writeInt(1)` non-null marker; the native hook recycles the parcel after use.
*/
private fun forgedReply(
uid: Int,
txId: Long,
method: String,
body: Parcel.() -> Unit,
): TransactionResult.OverrideReply {
val reply = Parcel.obtain()
reply.writeNoException()
reply.body()
if (SystemLogger.isUidLogged(uid)) {
runCatching { reply.marshall() }
.onSuccess { raw ->
SystemLogger.uidLogRaw(uid, txId, "$method-reply", "len=${raw.size}", raw)
}
}
return TransactionResult.OverrideReply(reply)
}
}
@@ -1,74 +0,0 @@
package org.matrix.TEESimulator.logging
import android.hardware.security.keymint.Tag
import android.system.keystore2.Authorization
import java.security.cert.Certificate
import java.security.cert.X509Certificate
import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.util.AndroidDeviceUtils
/**
* Assembles the per-UID "attestation dossier": for a targeted app, the full decoded attestation we
* actually hand it, the identity of every certificate in the returned chain, and the source of each
* device value that fed that attestation. Emitting all three where a chain is produced turns "the
* app rejects us" into a field-by-field record that can be diffed against a genuine TEE.
*/
object AttestationDossier {
/**
* Records the dossier for [chain] under [uid], tagged with the [path] that produced it
* (`FORGE-rust`, `FORGE-bouncycastle`, or `PATCH`). No-op for untargeted UIDs and release
* builds; the expensive decoding is skipped entirely when the UID is out of scope.
*/
fun log(uid: Int, txId: Long, path: String, chain: List<Certificate>) {
if (!SystemLogger.isUidLogged(uid)) return
val leaf = chain.firstOrNull() as? X509Certificate
val extension =
leaf?.let { AttestationPatcher.formatAttestationExtension(it) }
?: "<no attestation extension>"
SystemLogger.uidLog(uid, txId, "attest", "path=$path depth=${chain.size} $extension")
SystemLogger.uidLog(uid, txId, "keybox", "file=${ConfigurationManager.getKeyboxFileForUid(uid)}")
SystemLogger.uidLog(uid, txId, "chain", AttestationPatcher.formatCertChain(chain))
SystemLogger.uidLog(uid, txId, "chain-verify", AttestationPatcher.formatChainVerification(chain))
SystemLogger.uidLog(uid, txId, "props", AndroidDeviceUtils.describeSources(uid))
}
/**
* Records the *shape* of the emitted authorization list — count, ordered tags, and per-auth
* securityLevel. This is the exact surface the duck detector's generate-mode parcel fingerprint
* stride-walks, so logging it readably lets a "fingerprint" detection be compared against the
* known genuine-TEE shape without decoding the marshalled reply offline.
*/
fun logAuthShape(uid: Int, txId: Long, authorizations: Array<Authorization>?) {
if (!SystemLogger.isUidLogged(uid)) return
val auths = authorizations ?: return
val shape = auths.joinToString(",") { "${tagName(it.keyParameter.tag)}/${it.securityLevel}" }
SystemLogger.uidLog(uid, txId, "auth-shape", "n=${auths.size} [$shape]")
}
/** Names the authorization tags that occur in generate-mode replies; others render as numbers. */
private fun tagName(tag: Int): String =
when (tag) {
Tag.PURPOSE -> "PURPOSE"
Tag.ALGORITHM -> "ALGORITHM"
Tag.KEY_SIZE -> "KEY_SIZE"
Tag.DIGEST -> "DIGEST"
Tag.PADDING -> "PADDING"
Tag.EC_CURVE -> "EC_CURVE"
Tag.RSA_PUBLIC_EXPONENT -> "RSA_PUBLIC_EXPONENT"
Tag.NO_AUTH_REQUIRED -> "NO_AUTH_REQUIRED"
Tag.ORIGIN -> "ORIGIN"
Tag.OS_VERSION -> "OS_VERSION"
Tag.OS_PATCHLEVEL -> "OS_PATCHLEVEL"
Tag.VENDOR_PATCHLEVEL -> "VENDOR_PATCHLEVEL"
Tag.BOOT_PATCHLEVEL -> "BOOT_PATCHLEVEL"
Tag.CREATION_DATETIME -> "CREATION_DATETIME"
Tag.ROOT_OF_TRUST -> "ROOT_OF_TRUST"
Tag.USER_ID -> "USER_ID"
Tag.USAGE_COUNT_LIMIT -> "USAGE_COUNT_LIMIT"
Tag.UNLOCKED_DEVICE_REQUIRED -> "UNLOCKED_DEVICE_REQUIRED"
Tag.ACTIVE_DATETIME -> "ACTIVE_DATETIME"
else -> "tag${tag and 0x0FFFFFFF}"
}
}
@@ -1,6 +1,11 @@
package org.matrix.TEESimulator.logging
import android.hardware.security.keymint.*
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.Digest
import android.hardware.security.keymint.EcCurve
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.Tag
import java.math.BigInteger
import java.nio.charset.StandardCharsets
import java.util.Date
@@ -29,23 +34,7 @@ object KeyMintParameterLogger {
.associate { field -> (field.get(null) as Int) to field.name }
}
val blockModeNames: Map<Int, String> by lazy {
BlockMode::class
.java
.fields
.filter { it.type == Int::class.java }
.associate { field -> (field.get(null) as Int) to field.name }
}
val paddingNames: Map<Int, String> by lazy {
PaddingMode::class
.java
.fields
.filter { it.type == Int::class.java }
.associate { field -> (field.get(null) as Int) to field.name }
}
val purposeNames: Map<Int, String> by lazy {
private val purposeNames: Map<Int, String> by lazy {
KeyPurpose::class
.java
.fields
@@ -69,31 +58,18 @@ object KeyMintParameterLogger {
.associate { field -> (field.get(null) as Int) to field.name }
}
/** Logs a single KeyParameter to the shared debug stream (used for un-scoped param dumps). */
fun logParameter(param: KeyParameter) {
SystemLogger.debug("KeyParam: ${describe(param)}")
}
/** Logs a single KeyParameter onto a targeted UID's diagnostic plane as a `param` record. */
fun logParameter(uid: Int, txId: Long, param: KeyParameter) {
SystemLogger.uidLog(uid, txId, "param", describe(param))
}
/**
* Formats a single KeyParameter into a readable `tag | Value` string. Shared by both
* [logParameter] overloads so the two logging planes render parameters identically.
* Logs a single KeyParameter in a formatted, readable way.
*
* @param param The KeyParameter to format.
* @param param The KeyParameter to log.
*/
private fun describe(param: KeyParameter): String {
fun logParameter(param: KeyParameter) {
val tagName = tagNames[param.tag] ?: "UNKNOWN_TAG"
val value = param.value
val formattedValue: String =
when (param.tag) {
Tag.ALGORITHM -> algorithmNames[value.algorithm]
Tag.BLOCK_MODE -> blockModeNames[value.blockMode]
Tag.EC_CURVE -> ecCurveNames[value.ecCurve]
Tag.PADDING -> paddingNames[value.paddingMode]
Tag.PURPOSE -> purposeNames[value.keyPurpose]
Tag.DIGEST -> digestNames[value.digest]
Tag.AUTH_TIMEOUT,
@@ -121,7 +97,7 @@ object KeyMintParameterLogger {
else -> "<raw>"
} ?: "Unknown Value"
return "%-25s | Value: %s".format(tagName, formattedValue)
SystemLogger.debug("KeyParam: %-25s | Value: %s".format(tagName, formattedValue))
}
private fun ByteArray.toReadableString(): String {
@@ -1,95 +1,39 @@
package org.matrix.TEESimulator.logging
import android.util.Base64
import android.util.Log
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import org.json.JSONObject
import org.matrix.TEESimulator.BuildConfig
import org.matrix.TEESimulator.config.ConfigurationManager
/**
* 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"
@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)
// The tag used for all log messages from this application.
private const val TAG = "TEESimulator"
/**
* Returns true if this message should be emitted. Resets the window if expired and emits a
* suppression summary for the previous window.
* Logs a debug message. Use this for fine-grained information that is useful for debugging.
*
* @param message The message to log.
*/
@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
}
/** Logs a debug message. Use this for fine-grained information that is useful for debugging. */
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. */
/**
* 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) {
Log.w(TAG, message, throwable)
@@ -98,7 +42,13 @@ 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) {
Log.e(TAG, message, throwable)
@@ -110,163 +60,10 @@ 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())
}
// --- UID-keyed diagnostic plane (debug builds only) -------------------------------------
/**
* True when [uid] should receive deep, per-UID diagnostic logging: a debug build AND the UID is
* targeted in `target.txt`. This is the single scope gate for the diagnostic plane; it reuses
* the existing activation set, so no new configuration surface is introduced.
*/
fun isUidLogged(uid: Int): Boolean = isDebugBuild && !ConfigurationManager.shouldSkipUid(uid)
/** Resolves a UID to its primary package name for log labelling, falling back to `uid:N`. */
private fun label(uid: Int): String =
ConfigurationManager.getPackagesForUid(uid).firstOrNull() ?: "uid:$uid"
/**
* Emits one structured diagnostic record for a targeted [uid]. The human form
* `[<pkg> tx=<txId>] <event>: <detail>` goes to logcat; the file sink receives one NDJSON object
* per line under that UID's own file. In-scope records bypass the global rate limiter: a
* targeted app's traffic is already volume-bounded, and dropping a line mid-probe would corrupt
* the very trace we are trying to read. No-op for untargeted UIDs and in release builds.
*/
fun uidLog(uid: Int, txId: Long?, event: String, detail: String) {
if (!isUidLogged(uid)) return
val correlation = txId?.let { " tx=$it" } ?: ""
Log.d(TAG, "[${label(uid)}$correlation] $event: $detail")
runCatching { uidWriter(uid).append(jsonRecord(uid, txId, event, detail, null)) }
}
/** Lazy [uidLog]: [detail] is only built for targeted UIDs in debug builds. */
inline fun uidLog(uid: Int, txId: Long?, event: String, detail: () -> String) {
if (!isUidLogged(uid)) return
uidLog(uid, txId, event, detail())
}
/**
* [uidLog] plus the exact wire bytes that produced the event, base64 (NO_WRAP) in a `raw_b64`
* field. This is the structured replacement for the per-call `.bin` parcel dumps: one NDJSON
* line on the per-UID file instead of a fresh undecodable file per transaction, with the raw
* parcel still recoverable for offline parsers.
*/
fun uidLogRaw(uid: Int, txId: Long?, event: String, detail: String, raw: ByteArray) {
if (!isUidLogged(uid)) return
val correlation = txId?.let { " tx=$it" } ?: ""
Log.d(TAG, "[${label(uid)}$correlation] $event: $detail (raw ${raw.size}B)")
runCatching {
val encoded = Base64.encodeToString(raw, Base64.NO_WRAP)
uidWriter(uid).append(jsonRecord(uid, txId, event, detail, encoded))
}
}
/**
* External-storage root for every debug diagnostic. `/data/media/0/TEESimulator` is the
* in-namespace backing path the keystore domain can reach; a normal file manager sees the same
* files at `/sdcard/TEESimulator`. Release builds never write here and purge it on boot
* (App.purgeDebugDiagnostics). The domain reaches it via a debug-only media_rw_data_file
* sepolicy grant, and service.sh pre-creates the directory.
*/
const val DIAGNOSTIC_DIR = "/data/media/0/TEESimulator"
private val uidLogDir = File(DIAGNOSTIC_DIR)
private const val UID_LOG_MAX_BYTES = 4L * 1024 * 1024
private val uidWriters = ConcurrentHashMap<Int, UidLogFile>()
private val recordClock =
DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS").withZone(ZoneId.systemDefault())
private fun jsonRecord(
uid: Int,
txId: Long?,
event: String,
detail: String,
rawB64: String?,
): String =
JSONObject()
.apply {
put("ts", recordClock.format(Instant.now()))
put("uid", uid)
put("pkg", label(uid))
txId?.let { put("tx", it) }
put("event", event)
put("detail", detail)
rawB64?.let { put("raw_b64", it) }
}
.toString()
private fun uidWriter(uid: Int): UidLogFile =
uidWriters.computeIfAbsent(uid) { key ->
UidLogFile(key, uidLogDir).also { file ->
val packages =
ConfigurationManager.getPackagesForUid(key).joinToString().ifEmpty { "<unresolved>" }
runCatching {
file.append(jsonRecord(key, null, "session", "packages=[$packages]", null))
}
}
}
/**
* Append-only NDJSON sink for a single UID at `<logDir>/teesim-uid-<uid>.ndjson`, rotated once
* to `.ndjson.1` at [UID_LOG_MAX_BYTES]; one JSON object per line. Writes are synchronised
* because the keystore binder pool is multi-threaded, and every operation is wrapped so a
* logging fault can never propagate into the daemon. Created only on the debug-gated path.
*/
private class UidLogFile(uid: Int, private val logDir: File) {
private val primary = File(logDir, "teesim-uid-$uid.ndjson")
private val rotated = File(logDir, "teesim-uid-$uid.ndjson.1")
private var writer: BufferedWriter? = null
private var size = 0L
@Synchronized
fun append(jsonLine: String) {
runCatching {
val out = writer ?: open()
out.write(jsonLine)
out.write("\n")
out.flush()
size += jsonLine.length + 1
if (size >= UID_LOG_MAX_BYTES) rotate()
}
}
private fun open(): BufferedWriter {
logDir.mkdirs()
val out = BufferedWriter(FileWriter(primary, /* append = */ true))
writer = out
size = primary.length()
return out
}
private fun rotate() {
runCatching {
writer?.flush()
writer?.close()
}
writer = null
runCatching {
if (rotated.exists()) rotated.delete()
primary.renameTo(rotated)
}
size = 0L
}
}
}
@@ -1,13 +1,14 @@
package org.matrix.TEESimulator.pki
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.KeyPurpose
import android.os.Build
import android.util.Pair
import java.math.BigInteger
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.Security
import java.security.cert.Certificate
import java.security.cert.X509Certificate
import java.security.spec.ECGenParameterSpec
import java.security.spec.RSAKeyGenParameterSpec
import java.util.Date
@@ -20,7 +21,6 @@ import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import org.matrix.TEESimulator.attestation.AttestationBuilder
import org.matrix.TEESimulator.attestation.AttestationConstants
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
@@ -35,7 +35,13 @@ import org.matrix.TEESimulator.logging.SystemLogger
*/
object CertificateGenerator {
private const val UNDEFINED_NOT_AFTER = 253402300799000L
init {
// Android ships with a stripped-down Bouncy Castle provider under the name "BC".
// We must remove the system provider first to ensure the full Bouncy Castle library
// (packaged with the app) is used.
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
}
/**
* Generates a software-based cryptographic key pair.
@@ -50,10 +56,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}"
@@ -69,82 +72,16 @@ object CertificateGenerator {
}
/**
* Generates a certificate chain for a given key pair. This is the primary function for creating
* attested certificates.
* Generates a new key pair and a corresponding certificate chain containing a simulated
* attestation.
*
* @param uid The UID of the application requesting the key.
* @param subjectKeyPair The key pair for which the certificate will be generated.
* @param alias The alias for the new key.
* @param attestKeyAlias Optional alias of a key to use for attestation signing.
* @param params The parameters for the new key and its attestation.
* @param securityLevel The security level to embed in the attestation.
* @return A [List] of [Certificate] forming the new chain, or `null` on failure.
*/
fun generateCertificateChain(
uid: Int,
subjectKeyPair: KeyPair,
attestKeyAlias: String?,
params: KeyMintAttestation,
securityLevel: Int,
): List<Certificate>? {
val challenge = params.attestationChallenge
if (challenge != null && challenge.size > AttestationConstants.CHALLENGE_LENGTH_LIMIT)
throw IllegalArgumentException(
"Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})"
)
return 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))
}
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
val wantsAttestKey =
attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val attestKeyInfo =
if (wantsAttestKey) getAttestationKeyInfo(uid, attestKeyAlias) else null
// When the caller designates an attest key, the leaf MUST be signed by it and returned
// alone (the caller appends the attest key's own chain). Re-rooting under the keybox
// here instead yields a self-rooted leaf that, concatenated with the attest key chain,
// double-roots and fails verification (WRONG_PUBLIC_KEY_TYPE). Refuse rather than emit
// a
// broken chain.
if (wantsAttestKey && attestKeyInfo == null) {
SystemLogger.error(
"Designated attest key '$attestKeyAlias' not found for uid $uid; refusing to " +
"emit a keybox-rooted leaf that would break the caller's chain."
)
return null
}
val (signingKey, issuer) =
attestKeyInfo?.let { it.first to it.second }
?: (keybox.keyPair to getIssuerFromKeybox(keybox))
val leafCert =
buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel)
if (attestKeyInfo != 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
}
}
/**
* A convenience function that combines key pair generation and certificate chain generation.
* Primarily used by the modern Keystore2 interceptor where generation is a single step.
* @return A [Pair] containing the new [KeyPair] and its certificate chain, or `null` on
* failure.
*/
fun generateAttestedKeyPair(
uid: Int,
@@ -153,27 +90,51 @@ object CertificateGenerator {
params: KeyMintAttestation,
securityLevel: Int,
): Pair<KeyPair, List<Certificate>>? {
return try {
SystemLogger.info("Generating new attested key pair for alias: '$alias' (UID: $uid)")
val newKeyPair =
generateSoftwareKeyPair(params)
?: throw Exception("Failed to generate underlying software key pair.")
return runCatching {
SystemLogger.info(
"Generating new attested key pair for alias: '$alias' (UID: $uid)"
)
val newKeyPair =
generateSoftwareKeyPair(params)
?: throw Exception("Failed to generate underlying software key pair.")
val chain =
generateCertificateChain(uid, newKeyPair, attestKeyAlias, params, securityLevel)
?: throw Exception("Failed to generate certificate chain for new key pair.")
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
SystemLogger.info("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
}
// Determine the signing key and issuer. If an attestKey is provided, use it.
// Otherwise, fall back to the root key from the keybox.
val (signingKey, issuer) =
if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second }
?: (keybox.keyPair to getIssuerFromKeybox(keybox))
} else {
keybox.keyPair to getIssuerFromKeybox(keybox)
}
// Build the new leaf certificate with the simulated attestation.
val leafCert =
buildCertificate(newKeyPair, signingKey, issuer, params, securityLevel)
// If not self-attesting, the chain is just the leaf. Otherwise, append the keybox
// chain.
val chain =
if (attestKeyAlias != null) {
listOf(leafCert)
} else {
listOf(leafCert) + keybox.certificates
}
SystemLogger.info(
"Successfully generated new certificate chain for alias: '$alias'."
)
Pair(newKeyPair, chain)
}
.onFailure {
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", it)
}
.getOrNull()
}
fun getIssuerFromKeybox(keybox: KeyBox) =
private fun getIssuerFromKeybox(keybox: KeyBox) =
X509CertificateHolder(keybox.certificates[0].encoded).subject
private fun getKeyboxForAlgorithm(uid: Int, algorithm: Int): KeyBox {
@@ -184,28 +145,8 @@ object CertificateGenerator {
Algorithm.RSA -> "RSA"
else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm")
}
// Prefer the algorithm-matching keybox, but fall back to any usable key (EC preferred) when
// none exists. An EC attestation key validly ECDSA-signs a leaf carrying an RSA subject key,
// so an EC-only keybox can still root an RSA forge. Without this fallback an RSA ATTEST_KEY
// request on an EC-only keybox throws -75 and the caller's chain never roots ("unknown
// certificate"). Mirrors the patch path's fail-safe
// (AttestationPatcher.getKeyboxForUidAndAlgorithm) and the RSA-leaf-under-EC-keybox handling
// in commit e6d5e4d.
val matched = KeyBoxManager.getAttestationKey(keyboxFile, algorithmName)
val keybox =
matched
?: KeyBoxManager.getAnyAttestationKey(keyboxFile)
?: throw android.os.ServiceSpecificException(
-75, // ATTESTATION_KEYS_NOT_PROVISIONED
"No usable attestation key in $keyboxFile",
)
// Surface which keybox actually signs the forge, so an EC-only-keybox fallback (an RSA leaf
// rooted under the EC key) is visible on the per-UID plane instead of silent.
SystemLogger.uidLog(uid, null, "keybox-pick") {
"req=$algorithmName ${if (matched != null) "matched" else "fellback-to-any"} " +
"signer=${getIssuerFromKeybox(keybox)}"
}
return keybox
return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName)
?: 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. */
@@ -218,15 +159,6 @@ object CertificateGenerator {
val certChain = CertificateHelper.getCertificateChain(keyInfo.response)
if (!certChain.isNullOrEmpty()) {
val issuer = X509CertificateHolder(certChain[0].encoded).subject
// The leaf is signed by keyInfo.keyPair, but the caller verifies it against the
// public key of the chain getCertChain(attestKeyAlias) serves. A two-rooted EC chain
// (DATA_TOO_LARGE_FOR_MODULUS) is exactly those two disagreeing on algorithm; log
// both at the signing instant so an EC attest-key run pins the mismatched edge.
SystemLogger.uidLog(uid, null, "attest-sign") {
"alias=$attestKeyAlias signerKey=${keyInfo.keyPair?.public?.algorithm} " +
"servedLeafKey=${certChain[0].publicKey.algorithm} " +
"depth=${certChain.size} issuer=$issuer"
}
Pair(keyInfo.keyPair, issuer)
} else {
null
@@ -239,112 +171,41 @@ object CertificateGenerator {
}
}
/** Maps KeyPurpose values to X.509 KeyUsage bits per KeyCreationResult.aidl spec */
private fun buildKeyUsageFromPurposes(purposes: List<Int>): Int {
var bits = 0
for (purpose in purposes) {
bits =
bits or
when (purpose) {
KeyPurpose.SIGN -> KeyUsage.digitalSignature
KeyPurpose.DECRYPT -> KeyUsage.dataEncipherment
KeyPurpose.WRAP_KEY -> KeyUsage.keyEncipherment
KeyPurpose.AGREE_KEY -> KeyUsage.keyAgreement
KeyPurpose.ATTEST_KEY -> KeyUsage.keyCertSign
else -> 0
}
}
return bits
}
/** Constructs a new X.509 certificate with a simulated attestation extension. */
private fun buildCertificate(
subjectKeyPair: KeyPair,
signingKeyPair: KeyPair,
issuer: X500Name,
params: KeyMintAttestation,
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,
)
// Add KeyUsage extension only if purposes map to valid bits
val keyUsageBits = buildKeyUsageFromPurposes(params.purpose)
if (keyUsageBits != 0) {
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
}
if (params.attestationChallenge != null) {
builder.addExtension(
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
)
}
// Add standard extensions.
builder.addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.keyCertSign))
// Add our custom, simulated attestation extension.
builder.addExtension(AttestationBuilder.buildAttestationExtension(params, 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)
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(signingKeyPair.private)
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)
val contentSigner = JcaContentSignerBuilder(signerAlgorithm).build(signingKeyPair.private)
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
}
@@ -3,9 +3,6 @@ package org.matrix.TEESimulator.pki
import android.security.keystore.KeyProperties
import java.io.File
import java.io.StringReader
import java.security.cert.X509Certificate
import java.security.interfaces.ECPrivateKey
import java.security.interfaces.RSAPrivateKey
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
import org.matrix.TEESimulator.logging.SystemLogger
@@ -54,38 +51,7 @@ object KeyBoxManager {
// If it's not in the cache, the `getOrPut` block is executed to parse and store it.
val keyMap =
keyStoreCache.getOrPut(keyStoreFileName) { parseKeyStoreFile(keyStoreFileName) }
val keyBox = keyMap[algorithm]
if (keyBox != null) {
// Surface attestation cert serials on every fetch so a revoked/leaked keybox is
// obvious from logcat alone -- Google's CRL and Duck's "mass abuse" check both match
// by certificate serial (lowercase hex). Logged here rather than at parse time because
// the parse is cached and would emit at most once per boot.
val serials =
keyBox.certificates.joinToString(", ") { cert ->
(cert as? X509Certificate)?.serialNumber?.toString(16) ?: "?"
}
SystemLogger.info(
"Using $algorithm keybox $keyStoreFileName; attestation cert serials (hex): $serials"
)
}
return keyBox
}
/**
* Retrieves any usable attestation key from a key store file, preferring EC.
*
* EC is the modern device-attestation key type and validly signs a leaf carrying either an EC
* or an RSA subject key. This is the fail-safe used when no algorithm-matching key exists, so
* patching can still re-root the chain under the keybox instead of leaking the device's real
* attestation.
*
* @param keyStoreFileName The name of the XML file (e.g., "keybox.xml").
* @return The preferred [KeyBox], or `null` if the file contains no usable key.
*/
fun getAnyAttestationKey(keyStoreFileName: String): KeyBox? {
val keyMap =
keyStoreCache.getOrPut(keyStoreFileName) { parseKeyStoreFile(keyStoreFileName) }
return keyMap[KeyProperties.KEY_ALGORITHM_EC] ?: keyMap.values.firstOrNull()
return keyMap[algorithm]
}
/**
@@ -191,10 +157,10 @@ object KeyBoxManager {
// Use runCatching to ensure one malformed key doesn't stop the whole
// process.
runCatching {
val xmlAlgorithm = currentAlgorithm
val algorithm = currentAlgorithm
val keyPem = currentPrivateKeyPem
if (
xmlAlgorithm != null &&
algorithm != null &&
keyPem != null &&
currentCertificatePems.isNotEmpty()
) {
@@ -210,42 +176,21 @@ object KeyBoxManager {
.data
}
// Derive the TRUE algorithm from the key object itself.
// This is our source of truth.
val derivedAlgorithm =
when (keyPair.private) {
is RSAPrivateKey -> KeyProperties.KEY_ALGORITHM_RSA
is ECPrivateKey -> KeyProperties.KEY_ALGORITHM_EC
else ->
throw IllegalArgumentException(
"Unsupported key type found: ${keyPair.private.javaClass.name}"
)
// Normalize the algorithm name for consistent lookups.
val normalizedAlgorithm =
when (algorithm.lowercase()) {
"ecdsa" -> KeyProperties.KEY_ALGORITHM_EC
"rsa" -> KeyProperties.KEY_ALGORITHM_RSA
else -> algorithm
}
// Normalize the algorithm from the XML tag to compare it
// fairly with the derived algorithm.
val normalizedXmlAlgorithm =
when {
xmlAlgorithm.contains("RSA", ignoreCase = true) ==
true -> KeyProperties.KEY_ALGORITHM_RSA
xmlAlgorithm.contains("EC", ignoreCase = true) ==
true -> KeyProperties.KEY_ALGORITHM_EC
else -> xmlAlgorithm
}
// Warn the user if the XML tag was misleading.
if (normalizedXmlAlgorithm != derivedAlgorithm) {
if (foundKeys.containsKey(normalizedAlgorithm)) {
SystemLogger.warning(
"Key algorithm mismatch in XML file. Tag said '$xmlAlgorithm' but key is actually '$derivedAlgorithm'. Using the correct derived algorithm."
"Duplicate key found for algorithm '$normalizedAlgorithm'. The later one in the file will be used."
)
}
if (foundKeys.containsKey(derivedAlgorithm)) {
SystemLogger.warning(
"Duplicate key found for algorithm '$derivedAlgorithm'. The later one in the file will be used."
)
}
foundKeys[derivedAlgorithm] = KeyBox(keyPair, certificates)
foundKeys[normalizedAlgorithm] =
KeyBox(keyPair, certificates)
}
}
.onFailure {
@@ -1,129 +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,
// Diagnostic plane: the calling app UID keys the native log lines, and debugLogging mirrors the
// APK debug variant so the native extension dump is silent in release.
val uid: Int,
val debugLogging: Boolean,
)
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
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,23 +1,16 @@
package org.matrix.TEESimulator.util
import android.content.pm.PackageManager
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
import javax.xml.parsers.DocumentBuilderFactory
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DERSequence
import org.matrix.TEESimulator.attestation.DeviceAttestationService
import org.matrix.TEESimulator.config.BootStateManager
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger
import org.w3c.dom.Element
/**
* Provides utility functions for accessing Android system properties and device-specific
@@ -25,301 +18,117 @@ import org.w3c.dom.Element
*/
object AndroidDeviceUtils {
/**
* Internal constant to signify that a patch level should not be included in the attestation.
*/
internal const val DO_NOT_REPORT = -1
// --- Boot Key and Verified Boot Hash ---
/** A randomly generated boot key, used as a fallback for attestation. */
val bootKey: ByteArray by lazy { generateRandomBytes(32) }
/**
* Lazily initializes and retrieves the verified boot key digest. The value is sourced in the
* following order:
* 1. From the `ro.boot.vbmeta.public_key_digest` system property.
* 2. From a cached TEE attestation record.
* 3. As a randomly generated 32-byte value (fallback).
*/
val bootKey: ByteArray by lazy {
initializeBootProperty(
propertyName = "ro.boot.vbmeta.public_key_digest",
attestationValueProvider = {
DeviceAttestationService.CachedAttestationData?.verifiedBootKey
},
expectedSize = 32,
recordSource = { bootKeySource = it },
)
}
/**
* Lazily initializes and retrieves the verified boot hash (vbmeta digest). The value is sourced
* in the following order:
* 1. From the `ro.boot.vbmeta.digest` system property.
* 2. From a cached TEE attestation record.
* 3. As a randomly generated 32-byte value (fallback).
*/
val bootHash: ByteArray by lazy {
initializeBootProperty(
propertyName = "ro.boot.vbmeta.digest",
attestationValueProvider = {
DeviceAttestationService.CachedAttestationData?.verifiedBootHash
},
expectedSize = 32,
recordSource = { bootHashSource = it },
)
}
// Records which fallback tier supplied bootKey/bootHash so the diagnostic dossier can flag a
// random-fallback value — a real verifiedBootKey that resolves to random bytes is a textbook
// simulated-TEE tell. Populated by initializeBootProperty on first access.
@Volatile private var bootKeySource: String = "uninitialized"
@Volatile private var bootHashSource: String = "uninitialized"
/**
* Public function to explicitly trigger the initialization of the boot key and hash. Accessing
* these properties here ensures they are set up before they might be needed elsewhere.
*/
fun setupBootKeyAndHash() {
SystemLogger.debug("Triggering initialization of boot key and hash...")
// Accessing the properties will trigger their `lazy` initialization logic.
bootKey
bootHash
SystemLogger.debug("Boot key and hash initialization complete.")
}
/**
* Generic initializer for boot properties like the key and hash. It attempts to read from a
* system property first, then from a TEE attestation, and finally falls back to a random value
* Initializes the verified boot hash (`ro.boot.vbmeta.digest`). It attempts to read from system
* properties first, then from a real TEE attestation, and finally falls back to a random value
* if neither is available.
*
* @param propertyName The name of the system property (e.g., "ro.boot.vbmeta.digest").
* @param attestationValueProvider A function that supplies the value from a cached attestation.
* @param expectedSize The expected length of the byte array (e.g., 32 for a SHA-256 digest).
* @return The resulting byte array for the property.
*/
private fun initializeBootProperty(
propertyName: String,
attestationValueProvider: () -> ByteArray?,
expectedSize: Int,
recordSource: (String) -> Unit,
): ByteArray {
getProperty(propertyName, expectedSize)?.let {
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
recordSource("system-prop")
persistToFile(propertyName, it)
return it
fun setupBootHash() {
getBootHashFromProperty()?.also {
SystemLogger.debug("Using boot hash from system property: ${it.toHex()}")
}
try {
attestationValueProvider()?.let {
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
recordSource("tee-attestation")
setBootProperty(propertyName, it)
persistToFile(propertyName, it)
return it
?: getBootHashFromAttestation()?.also {
SystemLogger.debug("Using boot hash from TEE attestation: ${it.toHex()}")
setBootHashProperty(it)
}
?: generateRandomBytes(32).also {
SystemLogger.debug("Using randomly generated boot hash: ${it.toHex()}")
setBootHashProperty(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()}")
recordSource("persistent-file")
setBootProperty(propertyName, it)
return it
}
return generateRandomBytes(expectedSize).also {
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
recordSource("random-fallback")
setBootProperty(propertyName, it)
persistToFile(propertyName, it)
}
}
/**
* Retrieves a system property and validates its format.
* Retrieves the verified boot meta digest from system properties.
*
* @param name The name of the system property.
* @param expectedSize The expected byte length of the property (e.g., 32 for a 64-char hex
* string).
* @return The property value as a ByteArray, or null if not found or invalid.
* @return The boot hash as a ByteArray, or null if not found or invalid.
*/
@OptIn(ExperimentalStdlibApi::class)
private fun getProperty(name: String, expectedSize: Int): ByteArray? {
val value = SystemProperties.get(name, null)
if (value.isNullOrBlank()) {
fun getBootHashFromProperty(): ByteArray? {
val digest = SystemProperties.get("ro.boot.vbmeta.digest", null)
if (digest.isNullOrBlank()) {
return null
}
// A valid digest is (2 * size) hex characters.
return if (value.length == expectedSize * 2) value.hexToByteArray() else null
// A valid digest is 64 hex characters (32 bytes).
return if (digest.length == 64) digest.hexToByteArray() else null
}
/**
* Sets a system property using the `resetprop` command.
* Retrieves the verified boot hash from a cached TEE attestation record.
*
* @param name The name of the property to set.
* @param bytes The value to set, which will be converted to a hex string.
* @return The verified boot hash, or null if not available.
*/
private fun setProperty(name: String, bytes: ByteArray) {
val hex = bytes.toHex()
try {
SystemLogger.debug("Setting system property '$name' to: $hex")
val command = arrayOf("resetprop", name, hex)
val process = Runtime.getRuntime().exec(command)
val exitCode = process.waitFor()
if (exitCode != 0) {
val errorOutput = process.errorStream.bufferedReader().readText()
SystemLogger.error(
"resetprop for '$name' failed with exit code $exitCode: $errorOutput"
)
}
} catch (e: Exception) {
SystemLogger.error("Failed to set '$name' property via resetprop.", e)
}
}
private fun setBootProperty(name: String, bytes: ByteArray) {
if (!BootStateManager.shouldSpoofBootProps()) {
SystemLogger.info("Skipping system property '$name' because boot prop spoofing is disabled")
return
}
setProperty(name, bytes)
}
internal fun setProperty(name: String, value: String) {
try {
SystemLogger.debug("Setting system property '$name' to: $value")
val command = arrayOf("resetprop", name, value)
val process = Runtime.getRuntime().exec(command)
val exitCode = process.waitFor()
if (exitCode != 0) {
val errorOutput = process.errorStream.bufferedReader().readText()
SystemLogger.error(
"resetprop for '$name' failed with exit code $exitCode: $errorOutput"
)
}
} catch (e: Exception) {
SystemLogger.error("Failed to set '$name' property via resetprop.", e)
}
}
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? {
private fun getBootHashFromAttestation(): ByteArray? {
return try {
val file = fileForProperty(propertyName)
if (!file.exists()) return null
val bytes = file.readBytes()
if (bytes.size == expectedSize) bytes else null
DeviceAttestationService.CachedAttestationData?.verifiedBootHash
} catch (e: Exception) {
SystemLogger.error("Failed to read $propertyName from file.", e)
SystemLogger.error("Failed to get boot hash from attestation.", e)
null
}
}
/**
* Sets the `ro.boot.vbmeta.digest` system property using the `resetprop` command.
*
* @param bytes The 32-byte digest to set.
*/
private fun setBootHashProperty(bytes: ByteArray) {
val hex = bytes.toHex()
try {
SystemLogger.debug("Setting system property 'ro.boot.vbmeta.digest' to: $hex")
// Construct the command to be executed
val command = arrayOf("resetprop", "ro.boot.vbmeta.digest", hex)
// Execute the command
val process = Runtime.getRuntime().exec(command)
// Wait for the process to complete and check the exit code for errors
val exitCode = process.waitFor()
if (exitCode != 0) {
val errorOutput = process.errorStream.bufferedReader().readText()
SystemLogger.error(
"resetprop command failed with exit code $exitCode: $errorOutput"
)
}
} catch (e: Exception) {
SystemLogger.error("Failed to set vbmeta digest property by executing resetprop.", e)
}
}
/** Generates a cryptographically random byte array of a specified length. */
private fun generateRandomBytes(size: Int): ByteArray =
ByteArray(size).also { ThreadLocalRandom.current().nextBytes(it) }
// --- Patch Level Properties ---
fun getPatchLevel(uid: Int): Int {
val custom = getCustomPatchLevelFor(uid, "system", isLong = false)
return custom ?: getRealDevicePatchLevelInt("system", isLong = false)
}
val patchLevel: Int
get() =
getCustomPatchLevelFor("system", isLong = false)
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = false)
fun getVendorPatchLevelLong(uid: Int): Int {
val custom = getCustomPatchLevelFor(uid, "vendor", isLong = true)
return custom ?: getRealDevicePatchLevelInt("vendor", isLong = true)
}
val vendorPatchLevelLong: Int
get() =
getCustomPatchLevelFor("vendor", isLong = true)
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true)
fun getBootPatchLevelLong(uid: Int): Int {
val custom = getCustomPatchLevelFor(uid, "boot", isLong = true)
return custom ?: getRealDevicePatchLevelInt("boot", isLong = true)
}
val bootPatchLevelLong: Int
get() =
getCustomPatchLevelFor("boot", isLong = true)
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true)
/**
* Summarises, for a targeted [uid], the device values that feed attestation and where each came
* from. This is what exposes attested-versus-live mismatches: a random-fallback verifiedBootKey,
* a patch level overridden away from the live prop, or an OS version pulled from a stale cache.
*/
fun describeSources(uid: Int): String {
val customPatchLevel = ConfigurationManager.getPatchLevelForUid(uid) != null
val osVersionSource =
if (DeviceAttestationService.CachedAttestationData?.osVersion != null) "cache" else "map"
return "osVersion=$osVersion(src=$osVersionSource) " +
"osPatch=${getPatchLevel(uid)} vendorPatch=${getVendorPatchLevelLong(uid)} " +
"bootPatch=${getBootPatchLevelLong(uid)} customPatchLevel=$customPatchLevel " +
"bootKey=${bootKey.toHex()}(src=$bootKeySource) " +
"bootHash=${bootHash.toHex()}(src=$bootHashSource) " +
"teeCacheData=${DeviceAttestationService.CachedAttestationData != null}"
}
/**
* Retrieves the definitive device patch level integer for a given component. This function
* encapsulates the entire fallback chain and guarantees a non-null return.
* Retrieves a custom patch level from the configuration if available.
*
* Fallback Priority:
* 1. Cached TEE attestation data.
* 2. Specific system property (e.g., ro.vendor.build.security_patch).
* 3. Default system patch level from Build.VERSION.SECURITY_PATCH.
*
* @param component The component ("system", "vendor", "boot").
* @param isLong Whether the final integer should be in YYYYMMDD format.
* @return The patch level as a guaranteed non-null Integer.
*/
private fun getRealDevicePatchLevelInt(component: String, isLong: Boolean): Int {
// Get value from cached TEE attestation data
DeviceAttestationService.CachedAttestationData?.let { data ->
val value =
when (component) {
"system" -> data.osPatchLevel
"vendor" -> data.vendorPatchLevel
"boot" -> data.bootPatchLevel
else -> null
}
if (value != null) return value
}
// We only check the specific vendor property, as the boot one is non-existent.
if (component == "vendor") {
val propValue = SystemProperties.get("ro.vendor.build.security_patch", "")
if (!propValue.isNullOrBlank()) {
parsePatchLevelValue(propValue, isLong)?.let { parsedValue ->
return parsedValue
}
}
}
return Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong)
}
/**
* Retrieves a custom patch level from the configuration if available for a specific UID.
*
* @param uid The UID of the calling application.
* @param component The component to get the patch level for ("system", "vendor", "boot").
* @param isLong Whether to return the patch level in `YYYYMMDD` or `YYYYMM` format.
* @return The custom patch level, or null if not configured.
*/
private fun getCustomPatchLevelFor(uid: Int, component: String, isLong: Boolean): Int? {
val config = ConfigurationManager.getPatchLevelForUid(uid) ?: return null
private fun getCustomPatchLevelFor(component: String, isLong: Boolean): Int? {
val config = ConfigurationManager.customPatchLevelOverride ?: return null
val value =
when (component) {
"system" -> config.system ?: config.all
@@ -328,52 +137,11 @@ object AndroidDeviceUtils {
else -> config.all
} ?: return null
// First, resolve dynamic keywords and templates into a concrete date string.
val resolvedValue = resolveDateKeywords(value)
return when {
resolvedValue.equals("device_default", ignoreCase = true) -> null
// Resolve from live system prop — matches what detectors see via getprop,
// even when PIF has spoofed ro.build.version.security_patch via resetprop
resolvedValue.equals("prop", ignoreCase = true) ->
parsePatchLevelValue(
SystemProperties.get("ro.build.version.security_patch", ""),
isLong,
)
resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
else -> parsePatchLevelValue(resolvedValue, isLong)
// "prop" or "no" indicates falling back to the system default.
if (value.equals("no", ignoreCase = true) || value.equals("prop", ignoreCase = true)) {
return null
}
}
/**
* Resolves special date keywords and templates into a concrete "YYYY-MM-DD" date string.
*
* @param value The configuration value string (e.g., "today", "YYYY-MM-01").
* @return A concrete date string, or the original value if it's not a dynamic date keyword.
*/
private fun resolveDateKeywords(value: String): String {
// Handle the "today" keyword.
if (value.equals("today", ignoreCase = true)) {
return LocalDate.now().toString() // Returns "YYYY-MM-DD" format
}
// Handle date templates like "YYYY-MM-01" or "2025-MM-DD".
if (
value.contains("YYYY", ignoreCase = true) ||
value.contains("MM", ignoreCase = true) ||
value.contains("DD", ignoreCase = true)
) {
val now = LocalDate.now()
// Chain replacements for YYYY, MM, and DD placeholders.
return value
.replace("YYYY", now.year.toString(), ignoreCase = true)
.replace("MM", String.format("%02d", now.monthValue), ignoreCase = true)
.replace("DD", String.format("%02d", now.dayOfMonth), ignoreCase = true)
}
// If it's not a dynamic keyword or template, return the original value.
return value
return parsePatchLevelValue(value, isLong)
}
/** Parses a patch level string (e.g., "2025-11-01") into an integer format. */
@@ -390,9 +158,7 @@ object AndroidDeviceUtils {
6 -> { // YYYYMM
val year = normalized.substring(0, 4).toInt()
val month = normalized.substring(4, 6).toInt()
// Synthesizing day=01 from YYYY-MM disagrees with real device bulletins;
// propagate null so callers fall back to a YYYY-MM-DD source.
if (isLong) null else year * 100 + month
if (isLong) year * 10000 + month * 100 + 1 else year * 100 + month
}
else -> null
}
@@ -439,451 +205,54 @@ object AndroidDeviceUtils {
Build.VERSION_CODES.BAKLAVA to 400, // KeyMint 4.0
)
/** AOSP-mandated attestation version for the running OS, or null when the SDK is unmapped. */
internal val aospAttestVersion: Int?
get() = attestVersionMap[Build.VERSION.SDK_INT]
val attestVersion: Int
get() =
DeviceAttestationService.CachedAttestationData?.attestVersion
?: attestVersionMap[Build.VERSION.SDK_INT]
?: 400 // Default to a recent version
/**
* Retrieves the attestation version for the given security level. A readable KeyMint VINTF
* declaration wins first, so local probes that compare the attested version against the
* device's manifest see a coherent pair. Otherwise the legacy chain applies: cached attestation
* data, then attestVersionMap[SDK_INT], then 400 as last resort.
*
* @param securityLevel The security level of the attestation (1 for TEE, 2 for StrongBox).
* @return The appropriate attestation version number.
*/
fun getAttestVersion(securityLevel: Int): Int {
vintfKeyMintVersion?.let { version ->
SystemLogger.debug(
"attestVersion=${version.attestationVersion} source=vintf securityLevel=$securityLevel"
)
return version.attestationVersion
}
val cached = DeviceAttestationService.CachedAttestationData?.attestVersion
val version =
cached ?: 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")
SystemLogger.debug(
"vintf-version attest=$version keymaster=$version source=$source securityLevel=$securityLevel"
)
return version
}
/**
* Retrieves the Keymaster/KeyMint version. A readable KeyMint VINTF declaration is
* authoritative because recent local detectors compare the attested version directly against
* the manifest declaration.
*
* @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 {
vintfKeyMintVersion?.let { version ->
SystemLogger.debug(
"keymasterVersion=${version.keymasterVersion} source=vintf securityLevel=$securityLevel"
)
return version.keymasterVersion
}
return getAttestVersion(securityLevel)
}
/**
* KeyMint/Keymaster version pair resolved from a device VINTF manifest. [attestationVersion] is
* the value written into the attestation record; [keymasterVersion] is the HAL version field.
* They coincide for AIDL KeyMint and diverge only for legacy HIDL Keymaster.
*/
private data class VintfKeyMintVersion(
val attestationVersion: Int,
val keymasterVersion: Int,
val sourcePath: String,
)
/**
* KeyMint version derived from the device's VINTF manifests, or null when none is readable.
* Resolved lazily so the manifest scan happens once, off the attestation hot path.
*/
private val vintfKeyMintVersion: VintfKeyMintVersion? by lazy {
readVintfKeyMintVersion().also { version ->
if (version != null) {
SystemLogger.info(
"Using KeyMint version from VINTF: attestation=${version.attestationVersion}, " +
"keymaster=${version.keymasterVersion}, source=${version.sourcePath}"
)
} else {
SystemLogger.debug(
"No usable KeyMint VINTF declaration found; using attestation fallback"
)
}
}
}
private fun readVintfKeyMintVersion(): VintfKeyMintVersion? {
val files = linkedMapOf<String, File>()
VINTF_MANIFEST_DIRS.forEach { path ->
val dir = File(path)
if (!dir.exists() || !dir.isDirectory) return@forEach
val listed =
runCatching {
dir.listFiles { file ->
file.isFile && file.name.endsWith(".xml", ignoreCase = true)
}
}
.getOrElse { throwable ->
SystemLogger.debug("Unable to list VINTF dir $path: ${throwable.message}")
null
}
listed?.forEach { file -> files[file.absolutePath] = file }
}
VINTF_MANIFEST_FILES.forEach { path ->
val file = File(path)
if (file.exists() && file.isFile) {
files[file.absolutePath] = file
}
}
return files.values
.flatMap { file ->
runCatching { parseKeyMintVersions(file) }
.getOrElse { throwable ->
SystemLogger.debug(
"Unable to parse KeyMint VINTF ${file.absolutePath}: ${throwable.message}"
)
emptyList()
}
}
.maxByOrNull { it.attestationVersion }
}
private fun parseKeyMintVersions(file: File): List<VintfKeyMintVersion> {
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file)
val root = document.documentElement ?: return emptyList()
return directChildElements(root, "hal").flatMap { hal ->
val halName = directChildTexts(hal, "name").firstOrNull().orEmpty()
val versions = directChildTexts(hal, "version")
val fqnames = directChildTexts(hal, "fqname")
val interfaces =
directChildElements(hal, "interface").associate { interfaceElement ->
val name = directChildTexts(interfaceElement, "name").firstOrNull().orEmpty()
val instances = directChildTexts(interfaceElement, "instance").toSet()
name to instances
}
when (halName) {
KEYMINT_HAL_NAME ->
if (hasDefaultInstance(fqnames, interfaces, KEYMINT_INTERFACE_NAME)) {
versions.mapNotNull { version ->
version
.toIntOrNull()
?.takeIf { it > 0 }
?.let { aidlVersion ->
val attestationVersion = aidlVersion * 100
VintfKeyMintVersion(
attestationVersion = attestationVersion,
keymasterVersion = attestationVersion,
sourcePath = file.absolutePath,
)
}
}
} else {
emptyList()
}
KEYMASTER_HAL_NAME ->
if (hasDefaultInstance(fqnames, interfaces, KEYMASTER_INTERFACE_NAME)) {
(versions.flatMap(::expandHidlVersions) +
fqnames.mapNotNull(::versionFromFqname))
.distinct()
.mapNotNull { version ->
expectedLegacyVersions(version)?.let { expected ->
VintfKeyMintVersion(
attestationVersion = expected.second,
keymasterVersion = expected.first,
sourcePath = file.absolutePath,
)
}
}
} else {
emptyList()
}
else -> emptyList()
}
}
}
private fun directChildElements(parent: Element, tagName: String): List<Element> = buildList {
val children = parent.childNodes
for (index in 0 until children.length) {
val child = children.item(index)
if (child is Element && child.tagName == tagName) {
add(child)
}
}
}
private fun directChildTexts(parent: Element, tagName: String): List<String> =
directChildElements(parent, tagName)
.map { it.textContent.trim() }
.filter { it.isNotEmpty() }
private fun hasDefaultInstance(
fqnames: List<String>,
interfaces: Map<String, Set<String>>,
interfaceName: String,
): Boolean =
fqnames.any { fqname ->
fqname.substringAfter("::", fqname).substringBefore("/") == interfaceName &&
fqname.substringAfter("/", "") == DEFAULT_INSTANCE
} || interfaces[interfaceName]?.contains(DEFAULT_INSTANCE) == true
private fun versionFromFqname(fqname: String): String? =
FQNAME_VERSION_REGEX.find(fqname)?.groupValues?.getOrNull(1)
private fun expectedLegacyVersions(version: String): Pair<Int, Int>? =
when (version) {
"3.0" -> 3 to 2
"4.0" -> 4 to 3
"4.1" -> 41 to 4
else -> null
}
private fun expandHidlVersions(version: String): List<String> {
val range = HIDL_VERSION_RANGE_REGEX.matchEntire(version) ?: return listOf(version)
val major = range.groupValues[1]
val firstMinor = range.groupValues[2].toInt()
val lastMinor = range.groupValues[3].toInt()
return (firstMinor..lastMinor).map { minor -> "$major.$minor" }
}
private val VINTF_MANIFEST_DIRS =
listOf(
"/system/etc/vintf/manifest",
"/system_ext/etc/vintf/manifest",
"/product/etc/vintf/manifest",
"/vendor/etc/vintf/manifest",
"/odm/etc/vintf/manifest",
)
private val VINTF_MANIFEST_FILES =
listOf(
"/system/etc/vintf/manifest.xml",
"/system_ext/etc/vintf/manifest.xml",
"/product/etc/vintf/manifest.xml",
"/vendor/etc/vintf/manifest.xml",
"/odm/etc/vintf/manifest.xml",
)
private const val KEYMINT_HAL_NAME = "android.hardware.security.keymint"
private const val KEYMASTER_HAL_NAME = "android.hardware.keymaster"
private const val KEYMINT_INTERFACE_NAME = "IKeyMintDevice"
private const val KEYMASTER_INTERFACE_NAME = "IKeymasterDevice"
private const val DEFAULT_INSTANCE = "default"
private val FQNAME_VERSION_REGEX = Regex("^@([0-9]+(?:\\.[0-9]+)?)::")
private val HIDL_VERSION_RANGE_REGEX = Regex("^([0-9]+)\\.([0-9]+)-([0-9]+)$")
val keymasterVersion: Int
get() =
DeviceAttestationService.CachedAttestationData?.keymasterVersion
?: 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 }
.sortedBy { it.first }
}
.getOrElse {
SystemLogger.error("Failed to get APEX package information.", it)
emptyList()
}
}
results.distinctBy { it.first }
}
val moduleHash: ByteArray by lazy {
DeviceAttestationService.CachedAttestationData?.moduleHash
?.also { SystemLogger.debug { "module-hash source=cache hash=${it.toHex().take(8)}" } }
?: supplementaryModuleHash()?.also {
SystemLogger.debug { "module-hash source=framework-api hash=${it.toHex().take(8)}" }
}
?: runCatching {
data class ModuleEntry(val nameEncoded: ByteArray, val fullEncoded: ByteArray)
val modules =
apexInfos.map { (packageName, versionCode) ->
val nameOctet = DEROctetString(packageName.toByteArray(Charsets.UTF_8))
val versionInt = ASN1Integer(versionCode)
val vec = ASN1EncodableVector()
vec.add(nameOctet)
vec.add(versionInt)
val sequence = DERSequence(vec)
// AOSP sorts by encoded name only, not full sequence
ModuleEntry(
nameEncoded = nameOctet.encoded,
fullEncoded = sequence.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)
}
.onSuccess {
SystemLogger.debug { "module-hash source=rederive hash=${it.toHex().take(8)}" }
}
.onFailure { SystemLogger.debug { "module-hash source=zero hash=00000000" } }
.getOrElse {
SystemLogger.error("Failed to compute module hash.", it)
ByteArray(32)
}
}
/**
* Reads the module-hash DER pre-image straight from the framework's own KeyStoreManager and
* SHA-256's it, so the value byte-matches what a verifier derives from the same
* getSupplementaryAttestationInfo call. Returns null on any failure (e.g. the API is
* unreachable from this process) so the caller falls back to local re-derivation.
*/
private fun supplementaryModuleHash(): ByteArray? =
runCatching {
// @SystemApi surface added in Android 16, absent from the compile SDK, so reflect.
val managerClass = Class.forName("android.security.keystore.KeyStoreManager")
val manager = managerClass.getMethod("getInstance").invoke(null)
// MODULE_HASH is the KeyMint tag (TagType.BYTES | 724 = 0x900002D4), read from the
// framework so it matches the verifier's argument exactly.
val moduleHashTag = managerClass.getField("MODULE_HASH").getInt(null)
val derPreImage =
managerClass
.getMethod("getSupplementaryAttestationInfo", Int::class.java)
.invoke(manager, moduleHashTag) as ByteArray
MessageDigest.getInstance("SHA-256").digest(derPreImage)
val encodables =
apexInfos.flatMap { (packageName, versionCode) ->
listOf(DEROctetString(packageName.toByteArray()), ASN1Integer(versionCode))
}
val sequence = DERSequence(encodables.toTypedArray())
MessageDigest.getInstance("SHA-256").digest(sequence.encoded)
}
.getOrNull()
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
.getOrElse {
SystemLogger.error("Failed to compute module hash.", it)
ByteArray(32) // Return empty hash on failure
}
}
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,77 +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")
}
}
@@ -6,8 +6,7 @@ package org.matrix.TEESimulator.util
*
* @return A new string with each line individually trimmed.
*/
fun String.trimLines(): String =
this.trim().lines().filter { !it.trim().startsWith("<!--") }.joinToString("\n") { it.trim() }
fun String.trimLines(): String = this.trim().lines().joinToString("\n") { it.trim() }
/**
* Converts a ByteArray to its hexadecimal string representation.
@@ -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)
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
[versions]
agp = "8.13.2"
agp = "8.13.1"
annotation = "1.9.1"
jdk18on = "1.83"
kotlin = "2.3.0"
kotlin = "2.2.21"
ktfmt = "0.25.0"
[libraries]
-60
View File
@@ -1,60 +0,0 @@
#!/system/bin/sh
MODDIR=${0%/*}
CONFIG_DIR=/data/adb/tricky_store
. "$MODDIR/action_i18n.sh"
confirm() {
# Sample getevent in 1s bursts; a piped stream block-buffers and misses
# a single key-press before the timeout.
deadline=$(( $(date +%s) + 10 ))
while [ "$(date +%s)" -lt "$deadline" ]; do
events=$(/system/bin/timeout 1 /system/bin/getevent -l 2>/dev/null)
case "$events" in
*KEY_VOLUMEUP*) return 0 ;;
*KEY_VOLUMEDOWN*) return 1 ;;
esac
done
return 1
}
# Debug builds ship diag.sh, adding a one-tap log export before the destructive clear-keys action.
if [ -f "$MODDIR/diag.sh" ]; then
. "$MODDIR/diag.sh"
echo " "
echo " 📦 Export diagnostic logs to /sdcard/Download?"
echo " 🔊 Vol-Up = export logs"
echo " 🔉 Vol-Down = skip to clear keys"
echo " "
if confirm; then
diag_export
exit 0
fi
fi
echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " ⚠️ $(_msg confirm_header)"
echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " "
echo " $(_msg confirm_warning_1)"
echo " $(_msg confirm_warning_2)"
echo " "
echo " 🔊 $(_msg confirm_vol_up)"
echo " 🔉 $(_msg confirm_vol_down)"
echo " "
if ! confirm; then
echo " "
echo "$(_msg confirm_cancelled)"
exit 0
fi
if [ -d "$CONFIG_DIR/persistent_keys" ]; then
rm -rf "$CONFIG_DIR/persistent_keys"
mkdir -p "$CONFIG_DIR/persistent_keys"
echo " "
echo "$(_msg confirm_cleared)"
else
echo " "
echo " $(_msg confirm_not_found)"
fi
-255
View File
@@ -1,255 +0,0 @@
ACTION_LANG="en"
_detect_lang() {
local raw
raw=$(getprop persist.sys.locale 2>/dev/null)
[ -z "$raw" ] && raw=$(getprop ro.product.locale 2>/dev/null)
[ -z "$raw" ] && raw=$(getprop ro.system.locale 2>/dev/null)
local code=$(printf '%s' "$raw" | sed 's/_/-/g')
case "$code" in
zh-Hans*|zh-CN*) code="zh-CN" ;;
zh-Hant*|zh-TW*|zh-HK*) code="zh-TW" ;;
pt-BR*) code="pt-BR" ;;
pt*) code="pt-BR" ;;
es-ES*|es*) code="es-ES" ;;
*-*) code="${code%%-*}" ;;
esac
case "$code" in
ar|az|bn|de|el|es-ES|fa|fr|id|it|ja|ko|pl|pt-BR|ru|th|tl|tr|uk|vi|zh-CN|zh-TW) ACTION_LANG="$code" ;;
esac
}
_detect_lang
_msg() {
case "$ACTION_LANG" in
zh-CN) case "$1" in
confirm_header) echo "清除持久化密钥存储" ;;
confirm_warning_1) echo "这将删除所有缓存的证明密钥。" ;;
confirm_warning_2) echo "使用证明的应用将在下次使用时重新注册。" ;;
confirm_vol_up) echo "音量+ = 确认清除" ;;
confirm_vol_down) echo "音量- = 取消(10秒后默认)" ;;
confirm_cancelled) echo "已取消 - 密钥已保留" ;;
confirm_cleared) echo "持久化密钥存储已清除" ;;
confirm_not_found) echo "未找到持久化密钥存储" ;;
esac ;;
zh-TW) case "$1" in
confirm_header) echo "清除持久化金鑰儲存" ;;
confirm_warning_1) echo "這將刪除所有快取的證明金鑰。" ;;
confirm_warning_2) echo "使用證明的應用程式將在下次使用時重新註冊。" ;;
confirm_vol_up) echo "音量+ = 確認清除" ;;
confirm_vol_down) echo "音量- = 取消(10秒後預設)" ;;
confirm_cancelled) echo "已取消 - 金鑰已保留" ;;
confirm_cleared) echo "持久化金鑰儲存已清除" ;;
confirm_not_found) echo "未找到持久化金鑰儲存" ;;
esac ;;
ja) case "$1" in
confirm_header) echo "永続キーストレージを消去" ;;
confirm_warning_1) echo "キャッシュされた証明キーをすべて削除します。" ;;
confirm_warning_2) echo "証明を使用するアプリは次回使用時に再登録されます。" ;;
confirm_vol_up) echo "音量+ = 消去を確認" ;;
confirm_vol_down) echo "音量- = キャンセル(10秒後デフォルト)" ;;
confirm_cancelled) echo "キャンセルされました - キーは保持されます" ;;
confirm_cleared) echo "永続キーストレージを消去しました" ;;
confirm_not_found) echo "永続キーストレージが見つかりません" ;;
esac ;;
ko) case "$1" in
confirm_header) echo "영구 키 저장소 지우기" ;;
confirm_warning_1) echo "캐시된 모든 증명 키를 삭제합니다." ;;
confirm_warning_2) echo "증명을 사용하는 앱은 다음 사용 시 재등록됩니다." ;;
confirm_vol_up) echo "볼륨+ = 지우기 확인" ;;
confirm_vol_down) echo "볼륨- = 취소 (10초 후 기본값)" ;;
confirm_cancelled) echo "취소됨 - 키 유지됨" ;;
confirm_cleared) echo "영구 키 저장소가 지워졌습니다" ;;
confirm_not_found) echo "영구 키 저장소를 찾을 수 없습니다" ;;
esac ;;
ru) case "$1" in
confirm_header) echo "Очистить постоянное хранилище ключей" ;;
confirm_warning_1) echo "Это удалит все кэшированные ключи аттестации." ;;
confirm_warning_2) echo "Приложения, использующие аттестацию, перерегистрируются при следующем использовании." ;;
confirm_vol_up) echo "Громкость+ = Подтвердить очистку" ;;
confirm_vol_down) echo "Громкость- = Отмена (по умолчанию через 10с)" ;;
confirm_cancelled) echo "Отменено - ключи сохранены" ;;
confirm_cleared) echo "Постоянное хранилище ключей очищено" ;;
confirm_not_found) echo "Постоянное хранилище ключей не найдено" ;;
esac ;;
de) case "$1" in
confirm_header) echo "Persistenten Schlüsselspeicher löschen" ;;
confirm_warning_1) echo "Dies löscht alle zwischengespeicherten Attestierungsschlüssel." ;;
confirm_warning_2) echo "Apps mit Attestierung registrieren sich bei der nächsten Nutzung neu." ;;
confirm_vol_up) echo "Laut+ = Löschen bestätigen" ;;
confirm_vol_down) echo "Leise- = Abbrechen (Standard nach 10s)" ;;
confirm_cancelled) echo "Abgebrochen - Schlüssel beibehalten" ;;
confirm_cleared) echo "Persistenter Schlüsselspeicher gelöscht" ;;
confirm_not_found) echo "Kein persistenter Schlüsselspeicher gefunden" ;;
esac ;;
fr) case "$1" in
confirm_header) echo "Effacer le stockage de clés persistant" ;;
confirm_warning_1) echo "Ceci supprime toutes les clés d'attestation en cache." ;;
confirm_warning_2) echo "Les apps utilisant l'attestation se réinscriront à la prochaine utilisation." ;;
confirm_vol_up) echo "Vol+ = Confirmer l'effacement" ;;
confirm_vol_down) echo "Vol- = Annuler (par défaut après 10s)" ;;
confirm_cancelled) echo "Annulé - clés conservées" ;;
confirm_cleared) echo "Stockage de clés persistant effacé" ;;
confirm_not_found) echo "Aucun stockage de clés persistant trouvé" ;;
esac ;;
es-ES) case "$1" in
confirm_header) echo "Borrar almacenamiento persistente de claves" ;;
confirm_warning_1) echo "Esto elimina todas las claves de atestación en caché." ;;
confirm_warning_2) echo "Las apps que usan atestación se volverán a registrar en el próximo uso." ;;
confirm_vol_up) echo "Vol+ = Confirmar borrado" ;;
confirm_vol_down) echo "Vol- = Cancelar (predeterminado tras 10s)" ;;
confirm_cancelled) echo "Cancelado - claves conservadas" ;;
confirm_cleared) echo "Almacenamiento persistente de claves borrado" ;;
confirm_not_found) echo "No se encontró almacenamiento persistente de claves" ;;
esac ;;
pt-BR) case "$1" in
confirm_header) echo "Limpar armazenamento persistente de chaves" ;;
confirm_warning_1) echo "Isso exclui todas as chaves de atestação em cache." ;;
confirm_warning_2) echo "Apps que usam atestação serão re-registrados no próximo uso." ;;
confirm_vol_up) echo "Vol+ = Confirmar limpeza" ;;
confirm_vol_down) echo "Vol- = Cancelar (padrão após 10s)" ;;
confirm_cancelled) echo "Cancelado - chaves preservadas" ;;
confirm_cleared) echo "Armazenamento persistente de chaves limpo" ;;
confirm_not_found) echo "Nenhum armazenamento persistente de chaves encontrado" ;;
esac ;;
it) case "$1" in
confirm_header) echo "Cancella archivio chiavi persistente" ;;
confirm_warning_1) echo "Questo elimina tutte le chiavi di attestazione in cache." ;;
confirm_warning_2) echo "Le app che usano l'attestazione si re-registreranno al prossimo utilizzo." ;;
confirm_vol_up) echo "Vol+ = Conferma cancellazione" ;;
confirm_vol_down) echo "Vol- = Annulla (predefinito dopo 10s)" ;;
confirm_cancelled) echo "Annullato - chiavi conservate" ;;
confirm_cleared) echo "Archivio chiavi persistente cancellato" ;;
confirm_not_found) echo "Nessun archivio chiavi persistente trovato" ;;
esac ;;
tr) case "$1" in
confirm_header) echo "Kalıcı Anahtar Deposunu Temizle" ;;
confirm_warning_1) echo "Bu, önbelleğe alınmış tüm doğrulama anahtarlarını siler." ;;
confirm_warning_2) echo "Doğrulama kullanan uygulamalar bir sonraki kullanımda yeniden kaydolacak." ;;
confirm_vol_up) echo "Ses+ = Temizlemeyi onayla" ;;
confirm_vol_down) echo "Ses- = İptal (10sn sonra varsayılan)" ;;
confirm_cancelled) echo "İptal edildi - anahtarlar korundu" ;;
confirm_cleared) echo "Kalıcı anahtar deposu temizlendi" ;;
confirm_not_found) echo "Kalıcı anahtar deposu bulunamadı" ;;
esac ;;
id) case "$1" in
confirm_header) echo "Hapus Penyimpanan Kunci Persisten" ;;
confirm_warning_1) echo "Ini menghapus semua kunci atestasi yang di-cache." ;;
confirm_warning_2) echo "Aplikasi yang menggunakan atestasi akan mendaftar ulang saat digunakan." ;;
confirm_vol_up) echo "Vol+ = Konfirmasi hapus" ;;
confirm_vol_down) echo "Vol- = Batal (default setelah 10 detik)" ;;
confirm_cancelled) echo "Dibatalkan - kunci dipertahankan" ;;
confirm_cleared) echo "Penyimpanan kunci persisten dihapus" ;;
confirm_not_found) echo "Penyimpanan kunci persisten tidak ditemukan" ;;
esac ;;
vi) case "$1" in
confirm_header) echo "Xóa lưu trữ khóa cố định" ;;
confirm_warning_1) echo "Thao tác này xóa tất cả khóa chứng thực được lưu cache." ;;
confirm_warning_2) echo "Các ứng dụng dùng chứng thực sẽ đăng ký lại khi sử dụng tiếp theo." ;;
confirm_vol_up) echo "Vol+ = Xác nhận xóa" ;;
confirm_vol_down) echo "Vol- = Hủy (mặc định sau 10s)" ;;
confirm_cancelled) echo "Đã hủy - giữ nguyên khóa" ;;
confirm_cleared) echo "Đã xóa lưu trữ khóa cố định" ;;
confirm_not_found) echo "Không tìm thấy lưu trữ khóa cố định" ;;
esac ;;
ar) case "$1" in
confirm_header) echo "مسح تخزين المفاتيح الدائم" ;;
confirm_warning_1) echo "يؤدي هذا إلى حذف جميع مفاتيح التصديق المخزنة مؤقتاً." ;;
confirm_warning_2) echo "التطبيقات التي تستخدم التصديق ستعيد التسجيل في الاستخدام التالي." ;;
confirm_vol_up) echo "رفع الصوت = تأكيد المسح" ;;
confirm_vol_down) echo "خفض الصوت = إلغاء (افتراضي بعد 10 ثوانٍ)" ;;
confirm_cancelled) echo "تم الإلغاء - تم الاحتفاظ بالمفاتيح" ;;
confirm_cleared) echo "تم مسح تخزين المفاتيح الدائم" ;;
confirm_not_found) echo "لم يتم العثور على تخزين مفاتيح دائم" ;;
esac ;;
th) case "$1" in
confirm_header) echo "ล้างที่จัดเก็บคีย์ถาวร" ;;
confirm_warning_1) echo "การดำเนินการนี้จะลบคีย์การรับรองที่แคชไว้ทั้งหมด" ;;
confirm_warning_2) echo "แอปที่ใช้การรับรองจะลงทะเบียนใหม่ในการใช้งานครั้งถัดไป" ;;
confirm_vol_up) echo "เพิ่มเสียง = ยืนยันการล้าง" ;;
confirm_vol_down) echo "ลดเสียง = ยกเลิก (ค่าเริ่มต้นหลัง 10 วินาที)" ;;
confirm_cancelled) echo "ยกเลิกแล้ว - คีย์ยังคงอยู่" ;;
confirm_cleared) echo "ล้างที่จัดเก็บคีย์ถาวรแล้ว" ;;
confirm_not_found) echo "ไม่พบที่จัดเก็บคีย์ถาวร" ;;
esac ;;
uk) case "$1" in
confirm_header) echo "Очистити постійне сховище ключів" ;;
confirm_warning_1) echo "Це видаляє всі кешовані ключі атестації." ;;
confirm_warning_2) echo "Програми, що використовують атестацію, повторно зареєструються при наступному використанні." ;;
confirm_vol_up) echo "Гучність+ = Підтвердити очищення" ;;
confirm_vol_down) echo "Гучність- = Скасувати (за замовчуванням через 10с)" ;;
confirm_cancelled) echo "Скасовано - ключі збережено" ;;
confirm_cleared) echo "Постійне сховище ключів очищено" ;;
confirm_not_found) echo "Постійне сховище ключів не знайдено" ;;
esac ;;
pl) case "$1" in
confirm_header) echo "Wyczyść trwały magazyn kluczy" ;;
confirm_warning_1) echo "To usuwa wszystkie buforowane klucze atestacji." ;;
confirm_warning_2) echo "Aplikacje używające atestacji zarejestrują się ponownie przy następnym użyciu." ;;
confirm_vol_up) echo "Głośność+ = Potwierdź czyszczenie" ;;
confirm_vol_down) echo "Głośność- = Anuluj (domyślnie po 10s)" ;;
confirm_cancelled) echo "Anulowano - klucze zachowane" ;;
confirm_cleared) echo "Trwały magazyn kluczy wyczyszczony" ;;
confirm_not_found) echo "Nie znaleziono trwałego magazynu kluczy" ;;
esac ;;
az) case "$1" in
confirm_header) echo "Davamlı Açar Yaddaşını Təmizlə" ;;
confirm_warning_1) echo "Bu, keşlənmiş bütün təsdiqləmə açarlarını silir." ;;
confirm_warning_2) echo "Təsdiqləmədən istifadə edən tətbiqlər növbəti istifadədə yenidən qeydiyyatdan keçəcək." ;;
confirm_vol_up) echo "Səs+ = Təmizləməni təsdiqlə" ;;
confirm_vol_down) echo "Səs- = Ləğv et (10 saniyə sonra defolt)" ;;
confirm_cancelled) echo "Ləğv edildi - açarlar saxlanıldı" ;;
confirm_cleared) echo "Davamlı açar yaddaşı təmizləndi" ;;
confirm_not_found) echo "Davamlı açar yaddaşı tapılmadı" ;;
esac ;;
bn) case "$1" in
confirm_header) echo "স্থায়ী কী সংরক্ষণ পরিষ্কার করুন" ;;
confirm_warning_1) echo "এটি সমস্ত ক্যাশড অ্যাটেস্টেশন কী মুছে ফেলে।" ;;
confirm_warning_2) echo "অ্যাটেস্টেশন ব্যবহারকারী অ্যাপগুলি পরবর্তী ব্যবহারে পুনরায় নিবন্ধন করবে।" ;;
confirm_vol_up) echo "ভলিউম+ = পরিষ্কার নিশ্চিত করুন" ;;
confirm_vol_down) echo "ভলিউম- = বাতিল (১০ সেকেন্ডে ডিফল্ট)" ;;
confirm_cancelled) echo "বাতিল করা হয়েছে - কী সংরক্ষিত" ;;
confirm_cleared) echo "স্থায়ী কী সংরক্ষণ পরিষ্কার করা হয়েছে" ;;
confirm_not_found) echo "কোনো স্থায়ী কী সংরক্ষণ পাওয়া যায়নি" ;;
esac ;;
el) case "$1" in
confirm_header) echo "Εκκαθάριση Μόνιμου Αποθηκευτικού Χώρου Κλειδιών" ;;
confirm_warning_1) echo "Διαγράφει όλα τα προσωρινά αποθηκευμένα κλειδιά πιστοποίησης." ;;
confirm_warning_2) echo "Οι εφαρμογές που χρησιμοποιούν πιστοποίηση θα επανεγγραφούν στην επόμενη χρήση." ;;
confirm_vol_up) echo "Ένταση+ = Επιβεβαίωση εκκαθάρισης" ;;
confirm_vol_down) echo "Ένταση- = Ακύρωση (προεπιλογή μετά από 10 δευτ)" ;;
confirm_cancelled) echo "Ακυρώθηκε - τα κλειδιά διατηρήθηκαν" ;;
confirm_cleared) echo "Ο μόνιμος αποθηκευτικός χώρος κλειδιών εκκαθαρίστηκε" ;;
confirm_not_found) echo "Δεν βρέθηκε μόνιμος αποθηκευτικός χώρος κλειδιών" ;;
esac ;;
fa) case "$1" in
confirm_header) echo "پاک کردن ذخیره‌سازی دائمی کلید" ;;
confirm_warning_1) echo "این کار همه کلیدهای تأیید کش‌شده را حذف می‌کند." ;;
confirm_warning_2) echo "برنامه‌های استفاده‌کننده از تأیید در استفاده بعدی دوباره ثبت‌نام می‌کنند." ;;
confirm_vol_up) echo "صدا+ = تأیید پاک کردن" ;;
confirm_vol_down) echo "صدا- = لغو (پیش‌فرض پس از ۱۰ ثانیه)" ;;
confirm_cancelled) echo "لغو شد - کلیدها حفظ شدند" ;;
confirm_cleared) echo "ذخیره‌سازی دائمی کلید پاک شد" ;;
confirm_not_found) echo "ذخیره‌سازی دائمی کلید یافت نشد" ;;
esac ;;
tl) case "$1" in
confirm_header) echo "Burahin ang Persistent Key Storage" ;;
confirm_warning_1) echo "Buburahin nito ang lahat ng naka-cache na attestation keys." ;;
confirm_warning_2) echo "Magre-rehistro muli ang mga app na gumagamit ng attestation sa susunod na paggamit." ;;
confirm_vol_up) echo "Vol+ = Kumpirmahin ang pagbura" ;;
confirm_vol_down) echo "Vol- = Kanselahin (default pagkatapos ng 10s)" ;;
confirm_cancelled) echo "Nakansela - napanatili ang mga key" ;;
confirm_cleared) echo "Nabura ang persistent key storage" ;;
confirm_not_found) echo "Walang nahanap na persistent key storage" ;;
esac ;;
*) case "$1" in
confirm_header) echo "Clear Persistent Key Storage" ;;
confirm_warning_1) echo "This deletes all cached attestation keys." ;;
confirm_warning_2) echo "Apps using attestation will re-enroll on next use." ;;
confirm_vol_up) echo "Vol+ = Confirm clear" ;;
confirm_vol_down) echo "Vol- = Cancel (default after 10s)" ;;
confirm_cancelled) echo "Cancelled - keys preserved" ;;
confirm_cleared) echo "Persistent key storage cleared" ;;
confirm_not_found) echo "No persistent key storage found" ;;
esac ;;
esac
}
+9 -394
View File
@@ -1,399 +1,14 @@
> [!NOTE]
> The project is going through a heavy refactor at the moment, so public commits may lag behind for a while.
🚀 **TEESimulator v2.1 Hotfix Release is Live!** 🚀
---
This urgent hotfix addresses several critical issues identified in the previous v2.0 release.
## TEESimulator-RS v6.0.1-307
The v2.0 update, a significant refactoring effort, unfortunately introduced a few unexpected behaviors and bugs that we are now rectifying.
Fixes five gaps in the module's TEE key-operation and attestation emulation. Two of them fix crashes in real app crypto on a broken-TEE device: any app using an AndroidKeyStore HMAC key or an RSA-OAEP-SHA256 key was throwing. This is a beta; the confirmation logs are in the debug build only, and nothing is field-verified yet.
**Key fixes in this release include:**
1. **Google Play Integrity:** Resolved an issue preventing the attainment of STRONG integrity for Google Play verdicts, caused by an incorrect vendor patch level format. ✅
2. **Application Stability:** Fixed a critical crash related to an incorrect signature for the `SystemProperties.set` stub method. 🐛
3. **Stealth Enhancement:** Implemented a fix to bypass detection by the `Android Native Detector`. 👻
### App crypto correctness
- HMAC operations now run instead of throwing. An AndroidKeyStore HMAC key is symmetric, so an HMAC SIGN fell into the asymmetric SIGN path and failed on a null key pair, and no MAC primitive existed. SIGN and VERIFY now work: the tag is computed with Mac (HmacSHA256/384/512), truncated to the requested MAC_LENGTH (full digest when unspecified), and checked with a constant-time compare.
- RSA-OAEP-SHA256 decrypt no longer fails with BadPaddingException. The cipher ran with no OAEPParameterSpec, so JCA fell back to SHA-1 and rejected SHA-256 ciphertext. It now applies the correct main and MGF1 digests. A key that authorizes several MGF1 digests uses the one the operation requested, not the key's first.
- Grant-domain attestation keys now resolve. A self-granted PURPOSE_ATTEST_KEY (a Domain.GRANT descriptor) could not resolve its signer alias, so generateKey failed and the subject key was never stored, returning KEY_NOT_FOUND on readback. The grant now resolves to the owner key's alias, and the subject key stays readable under Domain.APP.
🔬 We are actively investigating a recent detection method to further enhance stealth capabilities.
### Supplementary attestation
- MODULE_HASH now comes from the framework's own getSupplementaryAttestationInfo, so it matches the value a verifier computes. It falls back to local re-derivation when that API is unreachable.
### Diagnostics (debug builds only)
- New per-operation (oaep-op, hmac-op, attest-grant) and device-level (module-hash, vintf-version) source logs. R8 strips them from release builds.
### 中文说明
修复本模块在 TEE 密钥操作与证明模拟中的五处缺陷。其中两处修复的是真实应用在 TEE 损坏设备上的加密崩溃:任何使用 AndroidKeyStore HMAC 密钥或 RSA-OAEP-SHA256 密钥的应用此前都会抛出异常。本版本为测试版;确认日志仅存在于 debug 构建中,且尚未经过真机验证。
**应用加密正确性**
- HMAC 操作现在可正常执行,不再抛出异常。AndroidKeyStore 的 HMAC 密钥是对称密钥,因此 HMAC SIGN 此前落入了非对称的 SIGN 分支,并因 key pair 为空而失败,当时也没有 MAC 原语。现在 SIGN 与 VERIFY 均可工作:用 Mac (HmacSHA256/384/512) 计算标签,按请求的 MAC_LENGTH 截断(未指定时取完整摘要长度),并用恒定时间比较进行校验。
- RSA-OAEP-SHA256 解密不再抛出 BadPaddingException。此前 cipher 未传入 OAEPParameterSpecJCA 因而回退到 SHA-1 并拒绝 SHA-256 密文。现在会应用正确的主摘要与 MGF1 摘要。若密钥授权了多个 MGF1 摘要,将使用本次操作请求的那个,而非密钥的第一个。
- Grant 域证明密钥现在可以解析。自授权的 PURPOSE_ATTEST_KEYDomain.GRANT 描述符)此前无法解析其签名者别名,导致 generateKey 失败且从不存储主体密钥,读取时返回 KEY_NOT_FOUND。现在该 grant 会解析为属主密钥的别名,主体密钥在 Domain.APP 下仍可读取。
**补充证明**
- MODULE_HASH 现在取自框架自带的 getSupplementaryAttestationInfo,因而与验证方计算出的值一致。当该 API 不可用时,回退到本地重新推导。
**诊断(仅 debug 构建)**
- 新增按操作 (oaep-op、hmac-op、attest-grant) 与设备级 (module-hash、vintf-version) 的来源日志。R8 会在 release 构建中将其剥离。
---
## TEESimulator-RS v6.0.1-282
AUTO-mode key attestation now forges plain attestation from the keybox instead of deferring to the real TEE.
### Detection coverage
- AUTO dispatch probed the device with `checkTeeFunctionality`, which only proves the TEE can mint one EC key. It says nothing about RSA attestation, device-ID attestation, or whether a patched chain survives RSA verify. Plain attestation requests (attest-key OFF, challenge present) were routed to PATCH and deferred to hardware, so devices that can't back that surfaced KeyAttestation reds: `ATTESTATION_KEYS_NOT_PROVISIONED` (-49) and `BLOCK_TYPE_IS_NOT_01`.
- AUTO targets carrying an attestation challenge now take the FORGE path, the same one attest-key-ON already used: a synthetic chain built from the keybox and rooted under the Google root key. Requests with no challenge still pass through to real hardware, so KeyDetector's hardware-backed checks are unaffected.
### Verified
- Offline conformance against real FORGE captures: uid10389 and uid10154 chains are GREEN; the root SPKI byte-matches `GOOGLE_ROOT_PUBLIC_KEY`.
---
## TEESimulator-RS v6.0.1-280
Clears the Duck Detector generate-mode parcel fingerprint that real Android 16 hardware also trips, fixes RSA attestation under an EC-only keybox, and restores device-property attestation for Play Integrity hardware apps such as BHIM and UPI. Generate-mode fix field-confirmed on Android 16.
### Detection coverage
- Generate-mode fingerprint: Duck reads the reply at a flat 12-byte stride and flags the sentinel tuple at positions 12 and 13 that the device's native ALGORITHM-first authorization order lands on. Real A16 silicon trips the same probe, so faithful mirroring stayed flagged. `normalizeAuthorizationLayout` marshals the auth array, runs Duck's exact predicate, and applies a minimal deterministic reorder only when it would match. Count, values, security levels, and the cert chain are untouched, and the reorder keys on the byte condition, never on a package. Applied on both the patch and forge reply paths. (#33)
- `updateAad` on a non-AEAD operation now answers per vendor: Samsung and Xiaomi-MTK TEEs return success, others return INVALID_TAG, matching Duck's OperationErrorPathProbe on both the sign/verify and cipher paths.
### Attestation correctness (Android 16, EC and RSA)
- RSA leaf under an EC-only keybox: patching used to catch the no-RSA-key throw and return the chain untouched, leaking the device's real unlocked Root of Trust for RSA keys while EC keys patched cleanly. It now falls back to any keybox key (EC preferred) and signs the patched leaf with the keybox key's own algorithm, so the RSA leaf re-roots to the Google keybox under a forged locked RoT.
- RSA attest-key forge on an EC-only keybox: the forge path matched the algorithm exactly and threw -75 ATTESTATION_KEYS_NOT_PROVISIONED on a miss, so an RSA ATTEST_KEY request never rooted and verifiers reported an unknown certificate. It now falls back to any attestation key, since an EC key validly ECDSA-signs an RSA-subject leaf. No-op on a dual keybox.
- A16 attestVersion: the device's KeyMint reports version 100 and the lazy cache shadowed the BAKLAVA-to-400 map, so the forge presented 100. It now caches the AOSP value per SDK and presents the correct 400.
- Algorithm-split key on restore: a persisted record holding an EC private key under an RSA leaf failed every signature as DATA_TOO_LARGE_FOR_MODULUS. Restore now drops the record when the private key and served leaf disagree, so the next generateKey rebuilds a coherent key.
- Stale chain on regenerate: reusing an alias in generateKey now evicts the cached chain, matching keystore2, so getKeyEntry serves the current key instead of a stale forge from an earlier generation.
### App compatibility
- Device-property attestation (BRAND, MODEL, and the rest) now forges unconditionally. The old gate probed the live TEE, which is dead on every device the module serves, so it rejected GMS Play Integrity's hardware path and broke BHIM and other UPI and Play-Integrity apps. Device-ID attestation (IMEI, serial) stays governed by the real KeyMint caller-permission rule: privileged callers get it, ordinary apps do not.
- getKeyEntry now reaches the owned-key lookup for skipped privileged UIDs, so framework attestKeyAlias resolution no longer returns "Invalid attestKeyAlias" for Key Attestation over Shizuku. Non-owned keys still skip post-processing, so a real app's key is never patched.
- Device-ID attestation over Shizuku (a privileged UID absent from target.txt) now takes the forge path instead of hitting the real TEE's CANNOT_ATTEST_IDS (-66). "Use attest key" no longer double-roots: a reused persistent attest key is resolved by KEY_ID as well as alias, and an unresolved designated attest key refuses to emit a leaf rather than silently re-rooting under the keybox.
### Diagnostics (debug builds only)
- Per-UID attestation dossier for targeted UIDs at /data/local/tmp/teesim/, recording the decoded chain on both forge and patch paths, key params, the keybox pick (including EC fail-safe), prop sources, forge failures, the emitted authorization shape, and served-versus-verified chains. Release builds strip this through R8 and stay silent. Keybox certificate serials log on every fetch for revocation triage.
### Verified
- Android 16: generate-mode fingerprint signal gone, confirmed on device 2026-06-19.
---
## TEESimulator-RS v6.0.1-251
14 commits since v6.0.0-235. Clears the remaining Duck Detector grant-domain rows (incl. the Android 16 OnePlus report), restores Google Wallet and fingerprint compatibility, and removes the in-module patch-level/bulletin resolvers. Test device (SDK 35) TEE tamper score 28 → 8.
### Detection coverage
- Grant plane virtualized: owner read and cross-app `Domain.GRANT` read return one identical chain. 6 RED rows cleared. (28 → 18)
- Generate-mode fingerprint: dropped 2 surplus authorizations (both patchlevels), USER_ID moved to SOFTWARE to mirror a captured device. (18 → 8)
- Android 16 grant: patch-mode keys now served on the grant plane, so owner and grant reads match, fixes CHAIN_SPLIT.
- Grant gated to SDK ≥ 36: Android 15 answers PERMISSION_DENIED, no synthetic over-capability.
- Stale-chain eviction: import and updateSubcomponent drop the cached attestation; no pre-mutation chain replays.
- Lifecycle coherence: clearNamespace / deleteAllKeys / migrateKeyNamespace mirror synthetic key and grant state, defeats delete-then-read probes.
- Device-ID attestation mirrors the real TEE: returns CANNOT_ATTEST_IDS where silicon can't attest, instead of forging it.
### App compatibility
- Google Wallet: INCLUDE_UNIQUE_ID stripped (not rejected) when the caller lacks the permission; card binding works. (PR #27)
- Fingerprint / vendor keys: KEY_ID miss skips the post-handler, so real HAL operations are no longer wrapped and broken. (PR #26)
### Removed
- PatchLevelManager, auto-resolved the security-patch date from an installed PlayIntegrityFix module (with hot-reload) and applied it to props.
- BulletinPoller, scheduled security-bulletin refresh.
### Other
- Release builds purge stale `teesim-*.bin` diagnostics from `/data/local/tmp` at boot.
- Vol-key confirmation rewritten to 1s `getevent` bursts (piped stream missed single presses on Magisk).
### Verified
- SDK 35, Xiaomi 23106RN0DA: tamper 28 → 8; generate-mode signal gone; 4 grant rows UNAVAILABLE (correct for Android 15); no regressions.
- Android 16 grant fix built but unconfirmed on SDK 36, needs an affected OnePlus user to confirm the grant rows clear.
---
## TEESimulator-RS v6.0.0-235
11 commits since v6.0.0-224. Duck Detector generate-mode fingerprint cleared. Shizuku-routed BYO attestation fixed. Vol-key confirmation restored on Magisk.
### Detection Coverage
- Duck Detector "TEE Simulator generate-mode fingerprint" cleared. `toAuthorizations` reordered to AOSP keymint reference order; KEY_SIZE moves from auth#4 to auth#2, breaking the byte-224 anchor the probe relied on. 0/31 matches on fresh self-probes (was 15/36).
- `persist.logd.size` variants blanked at boot via `service.sh`. Removes a logd-tuning side-channel.
### BYO & Shizuku Routing
- Shizuku-routed BYO attestation no longer fails with `-49 UNSUPPORTED_TAG`. `shouldSkipUid` moved into `handleGenerateKey`, evaluated after BYO parameters are parsed.
- `createOperation` parallel fix: outer UID gate removed; the cache-or-forward lookup is the sole gate. BYO keys created under Shizuku UID can now be used for signing under the same UID.
- `forceGenerate` simplified: any attest-key or BYO request routes to software unconditionally.
- BYO attest-key miss returns the full keybox chain instead of a malformed depth-1 chain.
- AUTO TEE race dispatch removed. Resolution uses `DeviceAttestationService.isTeeFunctional` only.
- Symmetric gen rejects `attestationKey != null` early with `INVALID_ARGUMENT`. Unsupported-algorithm branch returns `-38` instead of `-49`.
### Action Button
- Vol+ / Vol- confirmation restored on Magisk. Streaming `getevent -lq` matched inline against `KEY_VOLUMEUP DOWN` / `KEY_VOLUMEDOWN DOWN`, wrapped in `/system/bin/timeout 10`. The prior polled approach timed out on six-events-per-keypress kernels.
### Verified
- Android 15 (SDK 35), daemon PID 1466.
- Cross-device confirmation pending on OnePlus PKX110 and Samsung SM-S928B.
---
## TEESimulator-RS v6.0.0-224
59 commits since v6.0.0-162. Self-sufficient spoofing infrastructure, Duck Detector TamperScore-4 cleared on Xiaomi A16, persistent symmetric key storage (PR #22), 22-language action button hardening.
### Detection Coverage
- Duck Detector TimingSideChannelProbe cleared on Xiaomi A16 (SDK 35). Timing ratio dropped 1.555x to 1.055x, verdict WARNING to CLEAR. Threshold is > 1.1x.
- `KEY_ID` resolved from `teeResponses` instead of synthesized, matching real KeyMint binder behavior.
- Non-attested key cache mirrors attested path for byte-level metadata parity.
- `KEY_SIZE` emitted for EC keys; omitted when `ecCurve` is present, matching AOSP attestation_record.h.
- SSE messages synthesized canonically on non-AEAD `updateAad`; passthrough shape normalized.
- StrongBox attest version no longer hardcoded; resolved from device context.
- TEE op latency floor enforced to defeat micro-timing probes.
- Attest key resolution restored to nspace-aware lookup after revert/restore cycle.
### Self-Sufficient Spoofing
- `PatchLevelManager` resolves OS/VENDOR/BOOT patch levels via PIF without external bulletin fetch.
- `BulletinPoller` refreshes bulletin data on a schedule, isolated from boot path via umbrella `try/catch`.
- Bootloader-lock props pushed via `resetprop` at boot; absent vbmeta complement props filled; `vbmeta.device_state` included.
- PIF hot-reload via `FileObserver`; empty source files skipped; future patch dates bounded by `MAX_FUTURE_DAYS`.
- Default `security_patch.txt` dropped at install time.
- `sepolicy.rule` allows UDP egress for DNS resolution.
### Key Persistence (PR #22)
- Symmetric keys persist across reboots with byte-identical metadata.
- Keybox edits no longer wipe stored keys.
- Delete marker dropped on key regeneration to prevent stale state.
- Defensive symmetric fallback path with clean error codes.
### Reliability
- `atomicWrite` preserves `[pkg]` sections; errors guarded in `updateTo`.
- `applyToProps` serialized against concurrent callers.
- `pollOnce` wrapped in umbrella `try/catch`; `BulletinPoller.start` failure isolated from spoofer init.
- Spoofer ordering fixed: runs before keystore hook to prevent attest-time prop drift.
- `isAutoMode` reads raw package mode; `system=prop` passive default respected.
- `mergedContents` propagates read errors instead of swallowing them.
- Date regex validation on `currentPatch`; YYYY-MM input skips day synthesis.
- Global key-assignment check requires `=` delimiter (no more partial matches).
- `validation_rejected` status emitted on invalid spoof input.
### Action Button UX
- Vol+ required to clear `persistent_keys`. Vol- cancels. 10-second timeout defaults to cancel.
- Confirmation localized in 22 languages: ar, az, bn, de, el, es-ES, fa, fr, id, it, ja, ko, pl, pt-BR, ru, th, tl, tr, uk, vi, zh-CN, zh-TW.
- Every echoed string resolves through `_msg()` against device locale.
### Build & Ops
- Kotlin `jvmTarget` raised to JVM 21.
- Gradle auto-rewrites `module/update.json` on packaging.
- `scripts/package.sh` locates user-local cargo; rust task receives cargo bin path.
- Verified on Xiaomi Android 16 (SDK 35) `v6.0.0-224-Release`. Daemon alive PID 1392. Pending cross-device confirm on OnePlus PKX110 (qcom sun) and Samsung SM-S928B (pineapple).
---
## TEESimulator-RS v6.0.0
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).
### 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.
### 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.
### Attestation Fixes
- 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
### 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)
### Stability
- FileObserver NPE on config deletion fixed
- Global uncaught exception handler, daemon stays alive on unexpected errors
- PEM parsing hardened against malformed keybox files
### Tested Against
DuckDetector, Luna, Play Integrity, Key Attestation Demo, all passing on Redmi 14C (Android 14, Beanpod KeyMaster, KSU).
🙏 Support for TEE-broken devices and Android 10/11 remains an area of ongoing improvement. We highly encourage you to submit any issues you encounter to help us refine these aspects! 🤝
+2 -38
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 ---
@@ -48,7 +48,7 @@ install_file() {
# --- Installation ---
ui_print "- Extracting module files"
for file in customize.sh module.prop service.sh sepolicy.rule daemon action.sh action_i18n.sh uninstall.sh; do
for file in customize.sh module.prop service.sh sepolicy.rule daemon; do
install_file "$file" "$MODPATH"
done
@@ -67,28 +67,10 @@ ui_print ""
ui_print "- Extracting $ARCH libraries"
install_file "lib/$ABI_DIR/libTEESimulator.so" "$MODPATH"
install_file "lib/$ABI_DIR/libinject.so" "$MODPATH"
install_file "lib/$ABI_DIR/libsupervisor.so" "$MODPATH"
install_file "lib/$ABI_DIR/libcertgen.so" "$MODPATH"
ui_print ""
mv "$MODPATH/libinject.so" "$MODPATH/inject"
mv "$MODPATH/libsupervisor.so" "$MODPATH/supervisor"
chmod 755 "$MODPATH/inject"
chmod 755 "$MODPATH/supervisor"
# Debug builds carry diag.sh (the diagnostic plane); release builds do not. Extract it when
# present; otherwise sweep any external-storage diagnostics a prior debug install left behind,
# since the release keystore domain has no grant to remove them itself.
# Detect presence by the extracted FILE, not unzip's exit code: the busybox/toybox unzip in
# the install environment exits 0 even when the entry is absent, so the sweep never ran.
unzip -qqjo "$ZIPFILE" "diag.sh" -d "$MODPATH" 2>/dev/null
if [ -f "$MODPATH/diag.sh" ]; then
chmod 644 "$MODPATH/diag.sh"
ui_print "- Debug diagnostic plane enabled"
else
rm -rf /data/media/0/TEESimulator /data/local/tmp/teesim
ui_print "- Release build: swept stale diagnostics"
fi
# --- Configuration Files ---
if [ ! -d "$CONFIG_DIR" ]; then
@@ -105,21 +87,3 @@ if [ ! -f "$CONFIG_DIR/target.txt" ]; then
ui_print "- Adding default target scope"
install_file "target.txt" "$CONFIG_DIR"
fi
if [ ! -f "$CONFIG_DIR/security_patch.txt" ]; then
ui_print "- Adding default security patch config (mirror device props)"
printf '%s\n' \
'# TEESimulator default: mirror live device props.' \
'# system=prop reads ro.build.version.security_patch at cert-gen time;' \
'# boot and vendor are auto-forced to prop too (ConfigurationManager.kt:253-256).' \
'# Override with explicit YYYY-MM-DD dates if you want active spoofing.' \
'system=prop' > "$CONFIG_DIR/security_patch.txt"
chmod 644 "$CONFIG_DIR/security_patch.txt"
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
-20
View File
@@ -1,20 +0,0 @@
#!/system/bin/sh
# Debug-only diagnostic plane. Shipped solely in debug ZIPs; its presence is the gate that
# service.sh (setup) and action.sh (export) test before touching external storage.
DIAG_DIR=/data/media/0/TEESimulator
diag_setup() {
mkdir -p "$DIAG_DIR"
chmod 0777 "$DIAG_DIR"
chcon u:object_r:media_rw_data_file:s0 "$DIAG_DIR" 2>/dev/null
}
diag_export() {
_ts=$(date +%Y%m%d-%H%M%S)
_dest=/sdcard/Download/teesim-logs-$_ts
mkdir -p "$_dest"
cp -f "$DIAG_DIR"/teesim-uid-* "$_dest"/ 2>/dev/null
cp -f /data/adb/tricky_store/logs/certgen.log* "$_dest"/ 2>/dev/null
logcat -d -s TEESimulator > "$_dest/logcat.txt" 2>/dev/null
echo " ✅ Saved to $_dest"
}
+3 -3
View File
@@ -1,7 +1,7 @@
id=tricky_store
name=TEESimulator-RS
name=TEESimulator
version=${REPLACEMEVER}
versionCode=${REPLACEMEVERCODE}
author=JingMatrix, Enginex0
author=JingMatrix
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/JingMatrix/TEESimulator/main/module/update.json
+3 -19
View File
@@ -1,20 +1,4 @@
allow keystore {adb_data_file shell_data_file} file *
allow keystore system_file unix_dgram_socket *
allow system_file keystore unix_dgram_socket *
allow keystore system_file file *
allow crash_dump keystore process *
# SOTER Layer-A (10.C): ptrace inject into soterserver (platform_app). The debug NDJSON
# media_rw_data_file grant is debug-only — appended for debug builds in app/build.gradle.kts.
allow crash_dump platform_app process *
allow ksu self:tcp_socket { create connect read write getopt setopt }
allow ksu node:tcp_socket node_bind
allow ksu port:tcp_socket name_connect
allow magisk self:tcp_socket { create connect read write getopt setopt }
allow magisk node:tcp_socket node_bind
allow magisk port:tcp_socket name_connect
allow ksu self:udp_socket { create connect read write getopt setopt }
allow ksu node:udp_socket node_bind
allow ksu port:udp_socket name_connect
allow magisk self:udp_socket { create connect read write getopt setopt }
allow magisk node:udp_socket node_bind
allow magisk port:udp_socket name_connect
+8 -19
View File
@@ -1,22 +1,11 @@
DEBUG=false
MODDIR=${0%/*}
cd $MODDIR
# Fork-based supervisor for instant restart
./supervisor ./daemon "$MODDIR" &
# Debug builds ship diag.sh; its presence enables the external-storage diagnostic plane.
if [ -f "$MODDIR/diag.sh" ]; then
. "$MODDIR/diag.sh"
diag_setup
fi
# Clear logd size persist properties once boot completes
(
until [ "$(getprop sys.boot_completed)" = "1" ]; do
sleep 1
done
setprop persist.logd.size ""
setprop persist.logd.size.crash ""
setprop persist.logd.size.system ""
setprop persist.logd.size.main ""
) &
while true; do
./daemon "$MODDIR" || exit 1
# ensure keystore initialized
sleep 2
done &
-16
View File
@@ -1,16 +0,0 @@
#!/system/bin/sh
MODDIR=${0%/*}
CONFIG_DIR=/data/adb/tricky_store
# Kill daemon and supervisor
for pid in $(pidof TEESimulator) $(pidof supervisor) $(pidof daemon); do
kill -9 "$pid" 2>/dev/null
done
rm -rf "$CONFIG_DIR/persistent_keys"
rm -f "$CONFIG_DIR/tee_status.txt"
rm -f "$CONFIG_DIR/boot_hash.bin" "$CONFIG_DIR/boot_key.bin"
rm -f "$CONFIG_DIR/security_patch.txt" "$CONFIG_DIR/security_patch.txt.next" "$CONFIG_DIR/last_bulletin_fetch.json"
# Debug diagnostics live on external storage; remove them on uninstall.
rm -rf /data/media/0/TEESimulator
+4 -4
View File
@@ -1,6 +1,6 @@
{
"version": "v6.0.1-307",
"versionCode": 307,
"zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/download/v6.0.1-307/TEESimulator-RS-v6.0.1-307-Release.zip",
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/changelog.md"
"version": "v2.1",
"versionCode": 20,
"zipUrl": "https://github.com/JingMatrix/TEESimulator/releases/download/v2.1/TEESimulator-v2.1-20-release.zip",
"changelog": "https://raw.githubusercontent.com/JingMatrix/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"
-1009
View File
File diff suppressed because it is too large Load Diff
-30
View File
@@ -1,30 +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"] }
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(),
})
}
-331
View File
@@ -1,331 +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};
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)?;
// Ground truth of what the Rust forger emitted, keyed to the app. Gated on the APK debug
// variant so release builds never dump the extension.
if params.debug_logging {
tracing::info!(
uid = params.uid,
ext_hex = %hex_encode(&attest_ext),
"produced attestation extension"
);
}
certbuilder::build_certificate_chain(&key_pair, Some(&attest_ext), &keybox, &params)?
} else {
tracing::info!(
uid = params.uid,
"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);
tracing::info!(uid = params.uid, certs = cert_chain.len(), "assembled native cert result");
let out = env.byte_array_from_slice(&blob)?;
Ok(out.into_raw())
}
/// Lowercase hex of a byte slice for diagnostic dumps; the crate has no `hex` dependency.
fn hex_encode(bytes: &[u8]) -> String {
use std::fmt::Write as _;
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
let _ = write!(out, "{:02x}", b);
}
out
}
// ---------------------------------------------------------------------------
// 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(())
}
// ---------------------------------------------------------------------------
// 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")?;
let uid = get_int(env, config, "uid")?;
let debug_logging = get_boolean(env, config, "debugLogging")?;
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,
uid,
debug_logging,
})
}
// ---------------------------------------------------------------------------
// 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
}
-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());
}
}
-39
View File
@@ -1,39 +0,0 @@
mod kmsg;
mod rotating;
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;
}
}
}
-105
View File
@@ -1,105 +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,
/// Calling app UID, used only to key diagnostic log lines to the requesting app.
pub uid: i32,
/// Mirrors the APK debug variant; gates the produced-extension dump so release stays quiet.
pub debug_logging: bool,
}
pub struct GeneratedKeyPair {
pub private_key_pkcs8: Vec<u8>,
}
-276
View File
@@ -1,276 +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
# Gradle's buildRustCertgen resolves `cargo` against the daemon's inherited PATH,
# not the env we inject via gradle's Exec.environment(). Prepend the per-user
# rustup install so non-login shells (CI, IDE-launched terminals, fresh tmux)
# still find it without sourcing /etc/profile.d/cargo-path.sh.
[ -d "$HOME/.cargo/bin" ] && PATH="$HOME/.cargo/bin:$PATH"
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
CLEAR_LOGS=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
--clear-logs Clear per-UID diagnostic logs 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 ;;
--clear-logs) CLEAR_LOGS=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
if [[ "$CLEAR_LOGS" == true ]]; then
bold "==> Clearing per-UID diagnostic logs"
adb shell "rm -rf /data/media/0/TEESimulator /data/local/tmp/teesim" 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")
@@ -4,12 +4,4 @@ public class ActivityThread {
public static void initializeMainlineModules() {
throw new UnsupportedOperationException("STUB!");
}
public static ActivityThread systemMain() {
throw new UnsupportedOperationException("STUB!");
}
public ContextImpl getSystemContext() {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -1,4 +0,0 @@
package android.app;
public class ContextImpl {
}
@@ -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.hardware.security.keymint;
public @interface BlockMode {
public static final int ECB = 1;
public static final int CBC = 2;
public static final int CTR = 3;
public static final int GCM = 32;
}
@@ -1,10 +0,0 @@
package android.hardware.security.keymint;
public @interface PaddingMode {
public static final int NONE = 1;
public static final int RSA_OAEP = 2;
public static final int RSA_PSS = 3;
public static final int RSA_PKCS1_1_5_ENCRYPT = 4;
public static final int RSA_PKCS1_1_5_SIGN = 5;
public static final int PKCS7 = 64;
}
@@ -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;
}
}
@@ -1,38 +0,0 @@
package android.security.keymaster;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class ExportResult implements Parcelable {
public final byte[] exportData;
public final int resultCode;
public ExportResult(int resultCode) {
this.resultCode = resultCode;
this.exportData = new byte[0];
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
public static final Creator<ExportResult> CREATOR = new Creator<ExportResult>() {
@Override
public ExportResult createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public ExportResult[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
}
@@ -1,33 +0,0 @@
package android.security.keymaster;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class KeyCharacteristics implements Parcelable {
public KeymasterArguments hwEnforced;
public KeymasterArguments swEnforced;
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
public static final Creator<KeyCharacteristics> CREATOR = new Creator<KeyCharacteristics>() {
@Override
public KeyCharacteristics createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public KeyCharacteristics[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
}
@@ -1,36 +0,0 @@
package android.security.keymaster;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
abstract class KeymasterArgument implements Parcelable {
public final int tag;
protected KeymasterArgument(int tag) {
this.tag = tag;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
public static final Creator<KeymasterArgument> CREATOR = new Creator<KeymasterArgument>() {
@Override
public KeymasterArgument createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public KeymasterArgument[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
}
@@ -1,147 +0,0 @@
package android.security.keymaster;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import java.math.BigInteger;
import java.util.Date;
import java.util.List;
public class KeymasterArguments implements Parcelable {
private static final long UINT32_RANGE = 1L << 32;
public static final long UINT32_MAX_VALUE = UINT32_RANGE - 1;
private static final BigInteger UINT64_RANGE = BigInteger.ONE.shiftLeft(64);
public static final BigInteger UINT64_MAX_VALUE = UINT64_RANGE.subtract(BigInteger.ONE);
private List<KeymasterArgument> mArguments;
public static final @NonNull Parcelable.Creator<KeymasterArguments> CREATOR = new Parcelable.Creator<KeymasterArguments>() {
@Override
public KeymasterArguments createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public KeymasterArguments[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
public KeymasterArguments() {
throw new UnsupportedOperationException("STUB!");
}
private KeymasterArguments(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
public void addEnum(int tag, int value) {
throw new UnsupportedOperationException("STUB!");
}
public void addEnums(int tag, int... values) {
throw new UnsupportedOperationException("STUB!");
}
public int getEnum(int tag, int defaultValue) {
throw new UnsupportedOperationException("STUB!");
}
public List<Integer> getEnums(int tag) {
throw new UnsupportedOperationException("STUB!");
}
private void addEnumTag(int tag, int value) {
throw new UnsupportedOperationException("STUB!");
}
private int getEnumTagValue(KeymasterArgument arg) {
throw new UnsupportedOperationException("STUB!");
}
public void addUnsignedInt(int tag, long value) {
throw new UnsupportedOperationException("STUB!");
}
public long getUnsignedInt(int tag, long defaultValue) {
throw new UnsupportedOperationException("STUB!");
}
public void addUnsignedLong(int tag, BigInteger value) {
throw new UnsupportedOperationException("STUB!");
}
public List<BigInteger> getUnsignedLongs(int tag) {
throw new UnsupportedOperationException("STUB!");
}
private void addLongTag(int tag, BigInteger value) {
throw new UnsupportedOperationException("STUB!");
}
private BigInteger getLongTagValue(KeymasterArgument arg) {
throw new UnsupportedOperationException("STUB!");
}
public void addBoolean(int tag) {
throw new UnsupportedOperationException("STUB!");
}
public boolean getBoolean(int tag) {
throw new UnsupportedOperationException("STUB!");
}
public void addBytes(int tag, byte[] value) {
throw new UnsupportedOperationException("STUB!");
}
public byte[] getBytes(int tag, byte[] defaultValue) {
throw new UnsupportedOperationException("STUB!");
}
public void addDate(int tag, Date value) {
throw new UnsupportedOperationException("STUB!");
}
public void addDateIfNotNull(int tag, Date value) {
throw new UnsupportedOperationException("STUB!");
}
public Date getDate(int tag, Date defaultValue) {
throw new UnsupportedOperationException("STUB!");
}
private KeymasterArgument getArgumentByTag(int tag) {
throw new UnsupportedOperationException("STUB!");
}
public boolean containsTag(int tag) {
throw new UnsupportedOperationException("STUB!");
}
public int size() {
throw new UnsupportedOperationException("STUB!");
}
@Override
public void writeToParcel(Parcel out, int flags) {
throw new UnsupportedOperationException("STUB!");
}
public void readFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
public static BigInteger toUint64(long value) {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -1,42 +0,0 @@
package android.security.keymaster;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import java.util.List;
public class KeymasterCertificateChain implements Parcelable {
private List<byte[]> mCertificates;
public KeymasterCertificateChain() {
this.mCertificates = null;
}
public KeymasterCertificateChain(List<byte[]> mCertificates) {
this.mCertificates = mCertificates;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
public static final Creator<KeymasterCertificateChain> CREATOR = new Creator<KeymasterCertificateChain>() {
@Override
public KeymasterCertificateChain createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public KeymasterCertificateChain[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
}
@@ -1,263 +0,0 @@
package android.security.keymaster;
import java.util.HashMap;
import java.util.Map;
public final class KeymasterDefs {
private KeymasterDefs() {
}
// Tag types.
public static final int KM_INVALID = 0 << 28;
public static final int KM_ENUM = 1 << 28;
public static final int KM_ENUM_REP = 2 << 28;
public static final int KM_UINT = 3 << 28;
public static final int KM_UINT_REP = 4 << 28;
public static final int KM_ULONG = 5 << 28;
public static final int KM_DATE = 6 << 28;
public static final int KM_BOOL = 7 << 28;
public static final int KM_BIGNUM = 8 << 28;
public static final int KM_BYTES = 9 << 28;
public static final int KM_ULONG_REP = 10 << 28;
// Tag values.
public static final int KM_TAG_INVALID = KM_INVALID | 0;
public static final int KM_TAG_PURPOSE = KM_ENUM_REP | 1;
public static final int KM_TAG_ALGORITHM = KM_ENUM | 2;
public static final int KM_TAG_KEY_SIZE = KM_UINT | 3;
public static final int KM_TAG_BLOCK_MODE = KM_ENUM_REP | 4;
public static final int KM_TAG_DIGEST = KM_ENUM_REP | 5;
public static final int KM_TAG_PADDING = KM_ENUM_REP | 6;
public static final int KM_TAG_CALLER_NONCE = KM_BOOL | 7;
public static final int KM_TAG_MIN_MAC_LENGTH = KM_UINT | 8;
public static final int KM_TAG_RESCOPING_ADD = KM_ENUM_REP | 101;
public static final int KM_TAG_RESCOPING_DEL = KM_ENUM_REP | 102;
public static final int KM_TAG_BLOB_USAGE_REQUIREMENTS = KM_ENUM | 705;
public static final int KM_TAG_RSA_PUBLIC_EXPONENT = KM_ULONG | 200;
public static final int KM_TAG_INCLUDE_UNIQUE_ID = KM_BOOL | 202;
public static final int KM_TAG_ACTIVE_DATETIME = KM_DATE | 400;
public static final int KM_TAG_ORIGINATION_EXPIRE_DATETIME = KM_DATE | 401;
public static final int KM_TAG_USAGE_EXPIRE_DATETIME = KM_DATE | 402;
public static final int KM_TAG_MIN_SECONDS_BETWEEN_OPS = KM_UINT | 403;
public static final int KM_TAG_MAX_USES_PER_BOOT = KM_UINT | 404;
public static final int KM_TAG_ALL_USERS = KM_BOOL | 500;
public static final int KM_TAG_USER_ID = KM_UINT | 501;
public static final int KM_TAG_USER_SECURE_ID = KM_ULONG_REP | 502;
public static final int KM_TAG_NO_AUTH_REQUIRED = KM_BOOL | 503;
public static final int KM_TAG_USER_AUTH_TYPE = KM_ENUM | 504;
public static final int KM_TAG_AUTH_TIMEOUT = KM_UINT | 505;
public static final int KM_TAG_ALLOW_WHILE_ON_BODY = KM_BOOL | 506;
public static final int KM_TAG_TRUSTED_USER_PRESENCE_REQUIRED = KM_BOOL | 507;
public static final int KM_TAG_TRUSTED_CONFIRMATION_REQUIRED = KM_BOOL | 508;
public static final int KM_TAG_UNLOCKED_DEVICE_REQUIRED = KM_BOOL | 509;
public static final int KM_TAG_ALL_APPLICATIONS = KM_BOOL | 600;
public static final int KM_TAG_APPLICATION_ID = KM_BYTES | 601;
public static final int KM_TAG_CREATION_DATETIME = KM_DATE | 701;
public static final int KM_TAG_ORIGIN = KM_ENUM | 702;
public static final int KM_TAG_ROLLBACK_RESISTANT = KM_BOOL | 703;
public static final int KM_TAG_ROOT_OF_TRUST = KM_BYTES | 704;
public static final int KM_TAG_UNIQUE_ID = KM_BYTES | 707;
public static final int KM_TAG_ATTESTATION_CHALLENGE = KM_BYTES | 708;
public static final int KM_TAG_ATTESTATION_ID_BRAND = KM_BYTES | 710;
public static final int KM_TAG_ATTESTATION_ID_DEVICE = KM_BYTES | 711;
public static final int KM_TAG_ATTESTATION_ID_PRODUCT = KM_BYTES | 712;
public static final int KM_TAG_ATTESTATION_ID_SERIAL = KM_BYTES | 713;
public static final int KM_TAG_ATTESTATION_ID_IMEI = KM_BYTES | 714;
public static final int KM_TAG_ATTESTATION_ID_MEID = KM_BYTES | 715;
public static final int KM_TAG_ATTESTATION_ID_MANUFACTURER = KM_BYTES | 716;
public static final int KM_TAG_ATTESTATION_ID_MODEL = KM_BYTES | 717;
public static final int KM_TAG_DEVICE_UNIQUE_ATTESTATION = KM_BOOL | 720;
public static final int KM_TAG_ASSOCIATED_DATA = KM_BYTES | 1000;
public static final int KM_TAG_NONCE = KM_BYTES | 1001;
public static final int KM_TAG_AUTH_TOKEN = KM_BYTES | 1002;
public static final int KM_TAG_MAC_LENGTH = KM_UINT | 1003;
// Algorithm values.
public static final int KM_ALGORITHM_RSA = 1;
public static final int KM_ALGORITHM_EC = 3;
public static final int KM_ALGORITHM_AES = 32;
public static final int KM_ALGORITHM_3DES = 33;
public static final int KM_ALGORITHM_HMAC = 128;
// Block modes.
public static final int KM_MODE_ECB = 1;
public static final int KM_MODE_CBC = 2;
public static final int KM_MODE_CTR = 3;
public static final int KM_MODE_GCM = 32;
// Padding modes.
public static final int KM_PAD_NONE = 1;
public static final int KM_PAD_RSA_OAEP = 2;
public static final int KM_PAD_RSA_PSS = 3;
public static final int KM_PAD_RSA_PKCS1_1_5_ENCRYPT = 4;
public static final int KM_PAD_RSA_PKCS1_1_5_SIGN = 5;
public static final int KM_PAD_PKCS7 = 64;
// Digest modes.
public static final int KM_DIGEST_NONE = 0;
public static final int KM_DIGEST_MD5 = 1;
public static final int KM_DIGEST_SHA1 = 2;
public static final int KM_DIGEST_SHA_2_224 = 3;
public static final int KM_DIGEST_SHA_2_256 = 4;
public static final int KM_DIGEST_SHA_2_384 = 5;
public static final int KM_DIGEST_SHA_2_512 = 6;
// Key origins.
public static final int KM_ORIGIN_GENERATED = 0;
public static final int KM_ORIGIN_IMPORTED = 2;
public static final int KM_ORIGIN_UNKNOWN = 3;
public static final int KM_ORIGIN_SECURELY_IMPORTED = 4;
// Key usability requirements.
public static final int KM_BLOB_STANDALONE = 0;
public static final int KM_BLOB_REQUIRES_FILE_SYSTEM = 1;
// Operation Purposes.
public static final int KM_PURPOSE_ENCRYPT = 0;
public static final int KM_PURPOSE_DECRYPT = 1;
public static final int KM_PURPOSE_SIGN = 2;
public static final int KM_PURPOSE_VERIFY = 3;
public static final int KM_PURPOSE_WRAP = 5;
// Key formats.
public static final int KM_KEY_FORMAT_X509 = 0;
public static final int KM_KEY_FORMAT_PKCS8 = 1;
public static final int KM_KEY_FORMAT_RAW = 3;
// User authenticators.
public static final int HW_AUTH_PASSWORD = 1 << 0;
public static final int HW_AUTH_BIOMETRIC = 1 << 1;
// Error codes.
public static final int KM_ERROR_OK = 0;
public static final int KM_ERROR_ROOT_OF_TRUST_ALREADY_SET = -1;
public static final int KM_ERROR_UNSUPPORTED_PURPOSE = -2;
public static final int KM_ERROR_INCOMPATIBLE_PURPOSE = -3;
public static final int KM_ERROR_UNSUPPORTED_ALGORITHM = -4;
public static final int KM_ERROR_INCOMPATIBLE_ALGORITHM = -5;
public static final int KM_ERROR_UNSUPPORTED_KEY_SIZE = -6;
public static final int KM_ERROR_UNSUPPORTED_BLOCK_MODE = -7;
public static final int KM_ERROR_INCOMPATIBLE_BLOCK_MODE = -8;
public static final int KM_ERROR_UNSUPPORTED_MAC_LENGTH = -9;
public static final int KM_ERROR_UNSUPPORTED_PADDING_MODE = -10;
public static final int KM_ERROR_INCOMPATIBLE_PADDING_MODE = -11;
public static final int KM_ERROR_UNSUPPORTED_DIGEST = -12;
public static final int KM_ERROR_INCOMPATIBLE_DIGEST = -13;
public static final int KM_ERROR_INVALID_EXPIRATION_TIME = -14;
public static final int KM_ERROR_INVALID_USER_ID = -15;
public static final int KM_ERROR_INVALID_AUTHORIZATION_TIMEOUT = -16;
public static final int KM_ERROR_UNSUPPORTED_KEY_FORMAT = -17;
public static final int KM_ERROR_INCOMPATIBLE_KEY_FORMAT = -18;
public static final int KM_ERROR_UNSUPPORTED_KEY_ENCRYPTION_ALGORITHM = -19;
public static final int KM_ERROR_UNSUPPORTED_KEY_VERIFICATION_ALGORITHM = -20;
public static final int KM_ERROR_INVALID_INPUT_LENGTH = -21;
public static final int KM_ERROR_KEY_EXPORT_OPTIONS_INVALID = -22;
public static final int KM_ERROR_DELEGATION_NOT_ALLOWED = -23;
public static final int KM_ERROR_KEY_NOT_YET_VALID = -24;
public static final int KM_ERROR_KEY_EXPIRED = -25;
public static final int KM_ERROR_KEY_USER_NOT_AUTHENTICATED = -26;
public static final int KM_ERROR_OUTPUT_PARAMETER_NULL = -27;
public static final int KM_ERROR_INVALID_OPERATION_HANDLE = -28;
public static final int KM_ERROR_INSUFFICIENT_BUFFER_SPACE = -29;
public static final int KM_ERROR_VERIFICATION_FAILED = -30;
public static final int KM_ERROR_TOO_MANY_OPERATIONS = -31;
public static final int KM_ERROR_UNEXPECTED_NULL_POINTER = -32;
public static final int KM_ERROR_INVALID_KEY_BLOB = -33;
public static final int KM_ERROR_IMPORTED_KEY_NOT_ENCRYPTED = -34;
public static final int KM_ERROR_IMPORTED_KEY_DECRYPTION_FAILED = -35;
public static final int KM_ERROR_IMPORTED_KEY_NOT_SIGNED = -36;
public static final int KM_ERROR_IMPORTED_KEY_VERIFICATION_FAILED = -37;
public static final int KM_ERROR_INVALID_ARGUMENT = -38;
public static final int KM_ERROR_UNSUPPORTED_TAG = -39;
public static final int KM_ERROR_INVALID_TAG = -40;
public static final int KM_ERROR_MEMORY_ALLOCATION_FAILED = -41;
public static final int KM_ERROR_INVALID_RESCOPING = -42;
public static final int KM_ERROR_IMPORT_PARAMETER_MISMATCH = -44;
public static final int KM_ERROR_SECURE_HW_ACCESS_DENIED = -45;
public static final int KM_ERROR_OPERATION_CANCELLED = -46;
public static final int KM_ERROR_CONCURRENT_ACCESS_CONFLICT = -47;
public static final int KM_ERROR_SECURE_HW_BUSY = -48;
public static final int KM_ERROR_SECURE_HW_COMMUNICATION_FAILED = -49;
public static final int KM_ERROR_UNSUPPORTED_EC_FIELD = -50;
public static final int KM_ERROR_MISSING_NONCE = -51;
public static final int KM_ERROR_INVALID_NONCE = -52;
public static final int KM_ERROR_MISSING_MAC_LENGTH = -53;
public static final int KM_ERROR_KEY_RATE_LIMIT_EXCEEDED = -54;
public static final int KM_ERROR_CALLER_NONCE_PROHIBITED = -55;
public static final int KM_ERROR_KEY_MAX_OPS_EXCEEDED = -56;
public static final int KM_ERROR_INVALID_MAC_LENGTH = -57;
public static final int KM_ERROR_MISSING_MIN_MAC_LENGTH = -58;
public static final int KM_ERROR_UNSUPPORTED_MIN_MAC_LENGTH = -59;
public static final int KM_ERROR_CANNOT_ATTEST_IDS = -66;
public static final int KM_ERROR_DEVICE_LOCKED = -72;
public static final int KM_ERROR_UNIMPLEMENTED = -100;
public static final int KM_ERROR_VERSION_MISMATCH = -101;
public static final int KM_ERROR_UNKNOWN_ERROR = -1000;
public static final Map<Integer, String> sErrorCodeToString = new HashMap<Integer, String>();
static {
sErrorCodeToString.put(KM_ERROR_OK, "OK");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_PURPOSE, "Unsupported purpose");
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_PURPOSE, "Incompatible purpose");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_ALGORITHM, "Unsupported algorithm");
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_ALGORITHM, "Incompatible algorithm");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_KEY_SIZE, "Unsupported key size");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_BLOCK_MODE, "Unsupported block mode");
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_BLOCK_MODE, "Incompatible block mode");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_MAC_LENGTH,
"Unsupported MAC or authentication tag length");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_PADDING_MODE, "Unsupported padding mode");
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_PADDING_MODE, "Incompatible padding mode");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_DIGEST, "Unsupported digest");
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_DIGEST, "Incompatible digest");
sErrorCodeToString.put(KM_ERROR_INVALID_EXPIRATION_TIME, "Invalid expiration time");
sErrorCodeToString.put(KM_ERROR_INVALID_USER_ID, "Invalid user ID");
sErrorCodeToString.put(KM_ERROR_INVALID_AUTHORIZATION_TIMEOUT,
"Invalid user authorization timeout");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_KEY_FORMAT, "Unsupported key format");
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_KEY_FORMAT, "Incompatible key format");
sErrorCodeToString.put(KM_ERROR_INVALID_INPUT_LENGTH, "Invalid input length");
sErrorCodeToString.put(KM_ERROR_KEY_NOT_YET_VALID, "Key not yet valid");
sErrorCodeToString.put(KM_ERROR_KEY_EXPIRED, "Key expired");
sErrorCodeToString.put(KM_ERROR_KEY_USER_NOT_AUTHENTICATED, "Key user not authenticated");
sErrorCodeToString.put(KM_ERROR_INVALID_OPERATION_HANDLE, "Invalid operation handle");
sErrorCodeToString.put(KM_ERROR_VERIFICATION_FAILED, "Signature/MAC verification failed");
sErrorCodeToString.put(KM_ERROR_TOO_MANY_OPERATIONS, "Too many operations");
sErrorCodeToString.put(KM_ERROR_INVALID_KEY_BLOB, "Invalid key blob");
sErrorCodeToString.put(KM_ERROR_INVALID_ARGUMENT, "Invalid argument");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_TAG, "Unsupported tag");
sErrorCodeToString.put(KM_ERROR_INVALID_TAG, "Invalid tag");
sErrorCodeToString.put(KM_ERROR_MEMORY_ALLOCATION_FAILED, "Memory allocation failed");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_EC_FIELD, "Unsupported EC field");
sErrorCodeToString.put(KM_ERROR_MISSING_NONCE, "Required IV missing");
sErrorCodeToString.put(KM_ERROR_INVALID_NONCE, "Invalid IV");
sErrorCodeToString.put(KM_ERROR_CALLER_NONCE_PROHIBITED,
"Caller-provided IV not permitted");
sErrorCodeToString.put(KM_ERROR_INVALID_MAC_LENGTH,
"Invalid MAC or authentication tag length");
sErrorCodeToString.put(KM_ERROR_CANNOT_ATTEST_IDS, "Unable to attest device ids");
sErrorCodeToString.put(KM_ERROR_DEVICE_LOCKED, "Device locked");
sErrorCodeToString.put(KM_ERROR_UNIMPLEMENTED, "Not implemented");
sErrorCodeToString.put(KM_ERROR_UNKNOWN_ERROR, "Unknown error");
}
public static int getTagType(int tag) {
return tag & (0xF << 28);
}
public static String getErrorMessage(int errorCode) {
String result = sErrorCodeToString.get(errorCode);
if (result != null) {
return result;
}
return String.valueOf(errorCode);
}
}
@@ -1,16 +0,0 @@
package android.security.keystore;
import android.os.IBinder;
import android.os.RemoteException;
import android.security.keymaster.KeymasterCertificateChain;
public interface IKeystoreCertificateChainCallback {
void onFinished(KeystoreResponse keystoreResponse, KeymasterCertificateChain keymasterCertificateChain)
throws RemoteException;
public static abstract class Stub {
public static IKeystoreCertificateChainCallback asInterface(IBinder b) {
throw new UnsupportedOperationException("STUB!");
}
}
}
@@ -1,15 +0,0 @@
package android.security.keystore;
import android.os.IBinder;
import android.os.RemoteException;
import android.security.keymaster.ExportResult;
public interface IKeystoreExportKeyCallback {
void onFinished(ExportResult exportResult) throws RemoteException;
public static abstract class Stub {
public static IKeystoreExportKeyCallback asInterface(IBinder b) {
throw new UnsupportedOperationException("STUB!");
}
}
}
@@ -1,16 +0,0 @@
package android.security.keystore;
import android.os.IBinder;
import android.os.IInterface;
import android.os.RemoteException;
import android.security.keymaster.KeyCharacteristics;
public interface IKeystoreKeyCharacteristicsCallback extends IInterface {
void onFinished(KeystoreResponse keystoreResponse, KeyCharacteristics keyCharacteristics) throws RemoteException;
public static abstract class Stub {
public static IKeystoreKeyCharacteristicsCallback asInterface(IBinder b) {
throw new UnsupportedOperationException("STUB!");
}
}
}
@@ -1,10 +1,7 @@
package android.security.keystore;
import java.lang.String;
public interface IKeystoreService {
public static final String DESCRIPTOR = "android.security.keystore.IKeystoreService";
class Stub {
}
}
@@ -1,38 +0,0 @@
package android.security.keystore;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class KeystoreResponse implements Parcelable {
public final int error_code_;
public final String error_msg_;
protected KeystoreResponse(int error_code, String error_msg) {
this.error_code_ = error_code;
this.error_msg_ = error_msg;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
public static final Creator<KeystoreResponse> CREATOR = new Creator<KeystoreResponse>() {
@Override
public KeystoreResponse createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public KeystoreResponse[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
}
@@ -1,22 +0,0 @@
package android.security.maintenance;
import android.os.IBinder;
/**
* Compile-time stub for the hidden keystore2 maintenance binder
* ({@code android.security.maintenance.IKeystoreMaintenance}).
*
* <p>This module is a {@code compileOnly} dependency, so the real framework class
* (which carries the actual {@code TRANSACTION_*} codes) is loaded at runtime. We
* only need the {@link #DESCRIPTOR} token to parse the transaction parcel and the
* inner {@code Stub} class so {@code getTransactCode} can reflect the real codes.
*/
public interface IKeystoreMaintenance {
String DESCRIPTOR = "android.security.maintenance.IKeystoreMaintenance";
class Stub {
public static IKeystoreMaintenance asInterface(IBinder b) {
throw new UnsupportedOperationException("STUB!");
}
}
}
@@ -1,38 +0,0 @@
package android.system.keystore2;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class CreateOperationResponse implements Parcelable {
public IKeystoreOperation iOperation;
public OperationChallenge operationChallenge;
public KeyParameters parameters;
public byte[] upgradedBlob;
public static final Creator<CreateOperationResponse> CREATOR = new Creator<CreateOperationResponse>() {
@Override
public CreateOperationResponse createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public CreateOperationResponse[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
@Override
public void writeToParcel(@NonNull Parcel parcel, int i) {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -1,9 +0,0 @@
package android.system.keystore2;
public @interface Domain {
public static final int APP = 0;
public static final int GRANT = 1;
public static final int SELINUX = 2;
public static final int BLOB = 3;
public static final int KEY_ID = 4;
}
@@ -1,33 +0,0 @@
package android.system.keystore2;
import android.os.IBinder;
import android.os.Binder;
import android.os.IInterface;
public interface IKeystoreOperation extends IInterface {
public static final java.lang.String DESCRIPTOR = "android.system.keystore2.IKeystoreOperation";
public void updateAad(byte[] aadInput);
public byte[] update(byte[] input);
public byte[] finish(byte[] input, byte[] signature);
public void abort() throws android.os.RemoteException;
abstract class Stub extends Binder implements IKeystoreOperation {
public static IKeystoreOperation asInterface(IBinder b) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public IBinder asBinder() {
return this;
}
@Override
public void updateAad(byte[] aadInput) {
throw new UnsupportedOperationException("STUB!");
}
}
}
@@ -1,34 +0,0 @@
package android.system.keystore2;
import android.os.Parcel;
import android.os.Parcelable;
import android.hardware.security.keymint.KeyParameter;
import androidx.annotation.NonNull;
public class KeyParameters implements Parcelable {
public KeyParameter[] keyParameter;
public static final Creator<KeyParameters> CREATOR = new Creator<KeyParameters>() {
@Override
public KeyParameters createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public KeyParameters[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
@Override
public void writeToParcel(@NonNull Parcel parcel, int i) {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -1,32 +0,0 @@
package android.system.keystore2;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class OperationChallenge implements Parcelable {
public long challenge = 0L;
public static final Creator<OperationChallenge> CREATOR = new Creator<OperationChallenge>() {
@Override
public OperationChallenge createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public OperationChallenge[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
@Override
public void writeToParcel(@NonNull Parcel parcel, int i) {
throw new UnsupportedOperationException("STUB!");
}
}