Compare commits
154
Commits
v4.0
..
v6.0.1-251
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42842e22c0 | ||
|
|
77fc96db37 | ||
|
|
b27a33b444 | ||
|
|
bae65ac47c | ||
|
|
217a5dc7f3 | ||
|
|
0229368c04 | ||
|
|
0f61bb841a | ||
|
|
50f2e98375 | ||
|
|
f4e2619eba | ||
|
|
649136ab4e | ||
|
|
d155a0ded6 | ||
|
|
edac284972 | ||
|
|
cbb73a0b0e | ||
|
|
3140ff5e96 | ||
|
|
8544aac260 | ||
|
|
134d5111ad | ||
|
|
4c801f2089 | ||
|
|
afc5caeb1b | ||
|
|
0e9ea10b50 | ||
|
|
6ae5ea391c | ||
|
|
f554b36416 | ||
|
|
684542f4b1 | ||
|
|
240728f98d | ||
|
|
95b8c27a9f | ||
|
|
66a8c7fbf8 | ||
|
|
36c93decc6 | ||
|
|
55e39c7f01 | ||
|
|
44816c1a8d | ||
|
|
60b6ec64c2 | ||
|
|
2f21cd57a0 | ||
|
|
fb7f0ca098 | ||
|
|
b323f41b08 | ||
|
|
c46aaa34f8 | ||
|
|
ca86148633 | ||
|
|
0fbdf42e9d | ||
|
|
91ce9485fe | ||
|
|
69fbdc112d | ||
|
|
1ee66be05a | ||
|
|
0b2c34ff8c | ||
|
|
2704eff797 | ||
|
|
22d1972bc7 | ||
|
|
5f72acb1e7 | ||
|
|
d7dc5e0b63 | ||
|
|
59836e143c | ||
|
|
a7e7e454e7 | ||
|
|
bba4a9ebfa | ||
|
|
58b98fd308 | ||
|
|
0617297b22 | ||
|
|
aef80c3105 | ||
|
|
032c87d50e | ||
|
|
62d666fd63 | ||
|
|
39b3811dc3 | ||
|
|
1446090da9 | ||
|
|
52ff39d130 | ||
|
|
3dea767058 | ||
|
|
17b359e94d | ||
|
|
57035b2c94 | ||
|
|
e8d12c4165 | ||
|
|
d048174402 | ||
|
|
21fb3ba879 | ||
|
|
ca56928add | ||
|
|
521e28cece | ||
|
|
372001e8de | ||
|
|
6fa10d7111 | ||
|
|
7c58e2f039 | ||
|
|
6dc7755658 | ||
|
|
921edecb86 | ||
|
|
756aa2efb2 | ||
|
|
0ebfef55b6 | ||
|
|
94c7d00fb5 | ||
|
|
fe21106151 | ||
|
|
3d8d193a44 | ||
|
|
85eef8054d | ||
|
|
051a003b33 | ||
|
|
29b2a85e9f | ||
|
|
890f47009b | ||
|
|
b85b3dea48 | ||
|
|
c8b3e9e528 | ||
|
|
a5375f7426 | ||
|
|
9e1b459b74 | ||
|
|
6476216aa3 | ||
|
|
bc7b11a380 | ||
|
|
22cadc125e | ||
|
|
5267c9dd00 | ||
|
|
dfc8aac920 | ||
|
|
bdb460411a | ||
|
|
ea792c7b78 | ||
|
|
18724cf40d | ||
|
|
1b7800345d | ||
|
|
54c12a9fd5 | ||
|
|
1bc47840d5 | ||
|
|
c8fadb07ae | ||
|
|
c0b14eeeb1 | ||
|
|
47ab0225e1 | ||
|
|
ebb6336281 | ||
|
|
784373c8b5 | ||
|
|
2241bfb13d | ||
|
|
954478b89b | ||
|
|
191085087b | ||
|
|
f870598e77 | ||
|
|
8fdc59a142 | ||
|
|
b6f9d7b486 | ||
|
|
99e6b0b5ea | ||
|
|
397bb6338b | ||
|
|
cb81116701 | ||
|
|
45477a7898 | ||
|
|
634a1293c1 | ||
|
|
f115eda2dc | ||
|
|
217edf61fe | ||
|
|
ee5bf2e1a7 | ||
|
|
2181157cb6 | ||
|
|
aa4917e623 | ||
|
|
f06cb30b40 | ||
|
|
03c71bd202 | ||
|
|
7e2fc0b288 | ||
|
|
258a65ba59 | ||
|
|
d2b8a92fbd | ||
|
|
0723865eab | ||
|
|
7cb44b9999 | ||
|
|
eddd9908af | ||
|
|
36ccd22cdc | ||
|
|
81e6fbf97e | ||
|
|
7f63713f07 | ||
|
|
5df76eacd1 | ||
|
|
ca3978888e | ||
|
|
023d7f929d | ||
|
|
bd40f4b950 | ||
|
|
23696d2f61 | ||
|
|
dfacb34cf9 | ||
|
|
06d9db443c | ||
|
|
7e87766493 | ||
|
|
f8bfa0dfd8 | ||
|
|
90ff59e0aa | ||
|
|
8bdf0d59fa | ||
|
|
6ab09f4889 | ||
|
|
f4559bcd19 | ||
|
|
3b5043a1bb | ||
|
|
8001a8678a | ||
|
|
d21822eb9d | ||
|
|
095a658996 | ||
|
|
70e8968e44 | ||
|
|
c122ded7bf | ||
|
|
7b510a9915 | ||
|
|
8f63dda31b | ||
|
|
9896df93de | ||
|
|
0280bcf189 | ||
|
|
438a462bdf | ||
|
|
40b08cd648 | ||
|
|
c5ed627f68 | ||
|
|
bee73eb39b | ||
|
|
a0ee77202c | ||
|
|
09d9896228 | ||
|
|
5a599025ad | ||
|
|
f5c2bcc024 |
+110
-73
@@ -3,16 +3,10 @@ name: Build
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.github/**'
|
||||
- '!.github/workflows/**'
|
||||
paths-ignore: [ '**.md' ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.github/**'
|
||||
- '!.github/workflows/**'
|
||||
paths-ignore: [ '**.md' ]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
@@ -22,18 +16,9 @@ concurrency:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
attestations: write
|
||||
contents: read
|
||||
|
||||
outputs:
|
||||
releaseName: ${{ steps.prepareArtifact.outputs.releaseName }}
|
||||
debugName: ${{ steps.prepareArtifact.outputs.debugName }}
|
||||
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
@@ -45,6 +30,25 @@ jobs:
|
||||
java-version: 21
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android
|
||||
|
||||
- name: Cache Rust artifacts
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
~/.cargo/bin/cargo-ndk
|
||||
native-certgen/target
|
||||
key: rust-${{ runner.os }}-${{ hashFiles('native-certgen/Cargo.lock') }}
|
||||
restore-keys: rust-${{ runner.os }}-
|
||||
|
||||
- name: Install cargo-ndk
|
||||
run: command -v cargo-ndk || cargo install cargo-ndk
|
||||
|
||||
- name: Set up ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
@@ -60,73 +64,106 @@ jobs:
|
||||
- name: Build with Gradle
|
||||
run: |
|
||||
chmod +x ./gradlew
|
||||
|
||||
./gradlew zipRelease zipDebug -Porg.gradle.parallel=true -Porg.gradle.vfs.watch=true -Dorg.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
- name: Prepare artifact
|
||||
if: success()
|
||||
id: prepareArtifact
|
||||
- name: Read version
|
||||
id: ver
|
||||
run: |
|
||||
set -e
|
||||
RELEASE_FILE=$(find out -name "*Release*.zip" | head -1)
|
||||
DEBUG_FILE=$(find out -name "*Debug*.zip" | head -1)
|
||||
|
||||
if [[ -z "$RELEASE_FILE" || -z "$DEBUG_FILE" ]]; then
|
||||
echo "Error: Could not find release or debug files in out/"
|
||||
echo "Contents of out/ directory:"
|
||||
ls -la out/ || echo "out/ directory does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract names
|
||||
RELEASE_NAME=$(basename "$RELEASE_FILE" .zip)
|
||||
DEBUG_NAME=$(basename "$DEBUG_FILE" .zip)
|
||||
|
||||
echo "releaseName=$RELEASE_NAME" >> $GITHUB_OUTPUT
|
||||
echo "debugName=$DEBUG_NAME" >> $GITHUB_OUTPUT
|
||||
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
|
||||
count=$(git rev-list HEAD --count)
|
||||
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
mkdir -p module-release module-debug
|
||||
unzip -q "$RELEASE_FILE" -d module-release
|
||||
unzip -q "$DEBUG_FILE" -d module-debug
|
||||
echo " Release: $RELEASE_NAME"
|
||||
echo " Debug: $DEBUG_NAME"
|
||||
- name: List build artifacts
|
||||
run: |
|
||||
echo "Release: $(ls out/*Release*.zip | head -1) ($(du -h out/*Release*.zip | head -1 | cut -f1))"
|
||||
echo "Debug: $(ls out/*Debug*.zip | head -1) ($(du -h out/*Debug*.zip | head -1 | cut -f1))"
|
||||
|
||||
- name: Upload release
|
||||
if: success()
|
||||
id: release
|
||||
uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.prepareArtifact.outputs.releaseName }}
|
||||
path: "./module-release/*"
|
||||
name: TEESimulator-RS-release-zip
|
||||
path: out/TEESimulator-RS-*-Release.zip
|
||||
retention-days: 30
|
||||
compression-level: 6
|
||||
compression-level: 0
|
||||
|
||||
- name: Upload debug
|
||||
if: success()
|
||||
id: debug
|
||||
uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.prepareArtifact.outputs.debugName }}
|
||||
path: "./module-debug/*"
|
||||
name: TEESimulator-RS-debug-zip
|
||||
path: out/TEESimulator-RS-*-Debug.zip
|
||||
retention-days: 7
|
||||
compression-level: 6
|
||||
compression-level: 0
|
||||
|
||||
- name: Upload release mappings
|
||||
if: success()
|
||||
uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-mappings-${{ github.run_number }}
|
||||
path: "./app/build/outputs/mapping/release"
|
||||
name: release-mappings
|
||||
path: app/build/outputs/mapping/release
|
||||
retention-days: 30
|
||||
compression-level: 9
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
release:
|
||||
needs: build
|
||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Read version
|
||||
id: ver
|
||||
run: |
|
||||
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Status**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Gradle Tasks**: assembleRelease, assembleDebug" >> $GITHUB_STEP_SUMMARY
|
||||
if [[ "${{ job.status }}" == "success" ]]; then
|
||||
echo "- **Release Artifact**: ${{ steps.prepareArtifact.outputs.releaseName }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Debug Artifact**: ${{ steps.prepareArtifact.outputs.debugName }}" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
|
||||
count=$(git rev-list HEAD --count)
|
||||
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: TEESimulator-RS-release-zip
|
||||
path: zips
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: TEESimulator-RS-debug-zip
|
||||
path: zips
|
||||
|
||||
- name: Extract changelog
|
||||
run: |
|
||||
ver="${VER#v}"
|
||||
awk "/^## TEESimulator-RS v${ver%%-*}/{flag=1; next} /^## TEESimulator-RS v/{if(flag) exit} flag" module/changelog.md > /tmp/notes.md
|
||||
cat /tmp/notes.md
|
||||
env:
|
||||
VER: ${{ steps.ver.outputs.version }}
|
||||
|
||||
- name: Create release
|
||||
run: |
|
||||
gh release delete "$VER" --yes 2>/dev/null || true
|
||||
RELEASE=$(ls zips/*Release*.zip | head -1)
|
||||
DEBUG=$(ls zips/*Debug*.zip | head -1)
|
||||
gh release create "$VER" \
|
||||
--title "$VER" \
|
||||
--latest \
|
||||
--notes-file /tmp/notes.md \
|
||||
"$RELEASE" \
|
||||
"$DEBUG"
|
||||
env:
|
||||
VER: ${{ steps.ver.outputs.version }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- 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 }}
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
out
|
||||
.gradle
|
||||
.kotlin
|
||||
app/build
|
||||
build
|
||||
native-certgen/target
|
||||
app/src/main/jniLibs
|
||||
|
||||
@@ -1,237 +1,139 @@
|
||||
<p align="center">
|
||||
<h1 align="center">🔐 TEESimulator</h1>
|
||||
<h1 align="center">TEESimulator-RS</h1>
|
||||
<p align="center"><b>Full TEE Emulation for Rooted Android</b></p>
|
||||
<p align="center">Hardware attestation. Software keys. Zero detection.</p>
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-v4.0-blue?style=for-the-badge" alt="v4.0">
|
||||
<img src="https://img.shields.io/badge/Android-10%2B-green?style=for-the-badge&logo=android" alt="Android 10+">
|
||||
<img src="https://img.shields.io/badge/Telegram-community-blue?style=for-the-badge&logo=telegram" alt="Telegram">
|
||||
<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>
|
||||
|
||||
---
|
||||
|
||||
> [!NOTE]
|
||||
> **This is a personal fork of [JingMatrix/TEESimulator](https://github.com/JingMatrix/TEESimulator)** with additional hardening, native Rust certificate generation, key persistence, and anti-detection features. For the upstream project, see the original repo.
|
||||
> Fork of [JingMatrix/TEESimulator](https://github.com/JingMatrix/TEESimulator) with native Rust certificate generation, key persistence, and AOSP-compliant attestation behavior. For the upstream project, see the original repo.
|
||||
|
||||
---
|
||||
## What It Does
|
||||
|
||||
## 🧬 What is TEESimulator?
|
||||
TEESimulator intercepts Binder IPC at the `ioctl` level inside the `keystore2` process and generates entire certificate chains from scratch, signed by your keybox, with correct attestation extensions. Apps that verify hardware attestation see a legitimate device.
|
||||
|
||||
TEESimulator is a **complete software simulation** of Android's hardware-backed [Trusted Execution Environment](https://source.android.com/docs/security/features/trusty) for [Key Attestation](https://developer.android.com/privacy-and-security/security-key-attestation). Instead of patching certificates from the real TEE after the fact, TEESimulator intercepts Binder IPC at the `ioctl` level and generates entire certificate chains from scratch — signed by your keybox, with correct attestation extensions, indistinguishable from hardware-generated keys.
|
||||
This is not TrickyStore. TEESimulator replaces TrickyStore and its forks entirely. It shares the same config paths for drop-in compatibility, but the internals are different: native Rust cert generation, binder-level interception via `lsplt`, per-UID rate limiting, key persistence, and AOSP-spec attestation behavior.
|
||||
|
||||
The result: **apps that verify hardware attestation see a legitimate, unmodified device** — even on rooted hardware with an unlocked bootloader.
|
||||
|
||||
> **This is not TrickyStore.** TEESimulator replaces TrickyStore and its forks entirely. It shares the same config paths for drop-in compatibility, but the architecture is fundamentally different: native Rust certificate generation, binder-level interception via `lsplt`, per-UID rate limiting, key persistence, and a multi-layer defense against detector apps.
|
||||
|
||||
---
|
||||
|
||||
## 🔥 Why TEESimulator?
|
||||
|
||||
🔐 **Native Cert Generation** — v4.0 generates X.509 certificate chains in Rust with `ring` and manual DER encoding. No BouncyCastle overhead, no Java crypto quirks, byte-perfect issuer chain linkage.
|
||||
|
||||
🎯 **Binder-Level Interception** — Hooks `ioctl()` on `libc.so` via `lsplt` inside the `keystore2` process. Intercepts `generateKey`, `importKey`, and `getKeyEntry` transactions before the HAL ever sees them.
|
||||
|
||||
🛡️ **Detector Resistant** — Per-UID rate limiting blocks DuckDetector-style keygen flooding. Oversized challenges rejected with real KeyMint error codes. Chain consistency verified byte-for-byte.
|
||||
|
||||
💾 **Key Persistence** — Generated keys survive reboots. Apps that store attestation keys (banking, biometrics) don't break after a restart.
|
||||
|
||||
🔧 **Drop-In Replacement** — Same config paths as TrickyStore (`/data/adb/tricky_store/`). Swap the module ZIP, keep your keybox and target list.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
**Core Attestation Engine**
|
||||
- [x] **Full certificate chain generation** — leaf + intermediates + root, signed by your keybox
|
||||
- [x] **Native Rust certgen** — `libcertgen.so` built with `ring`, `rsa`, and manual DER assembly
|
||||
- [x] **BouncyCastle fallback** — unsupported curves (P-224, P-521, Curve25519) fall back to Java
|
||||
- [x] **ASN.1 attestation extensions** — OID 1.3.6.1.4.1.11129.2.1.17 with all AOSP-specified tags
|
||||
- [x] **Multi-keybox support** — different keybox files per app group via `target.txt`
|
||||
|
||||
**Interception Layer**
|
||||
- [x] **Binder ioctl hook** — `lsplt` PLT hook on `libc.so` inside `keystore2` process
|
||||
- [x] **generateKey / importKey / getKeyEntry** — all three transaction types intercepted
|
||||
- [x] **256KB native payload cap** — oversized binder payloads bypass interception cleanly
|
||||
- [x] **Challenge validation** — rejects >128-byte attestation challenges with `INVALID_INPUT_LENGTH`
|
||||
|
||||
**Hardening**
|
||||
- [x] **Per-UID rate limiter** — 2 hardware keygens per 30s burst window, software fallback on overflow
|
||||
- [x] **importKey eviction guard** — retained patch chains prevent generate-then-import cache attacks
|
||||
- [x] **Key persistence** — file-backed storage with file-level locking, survives reboots and keybox rotations
|
||||
- [x] **Global exception handler** — uncaught exceptions logged, daemon stays alive
|
||||
|
||||
**Configuration**
|
||||
- [x] **Live config reload** — `FileObserver` watches all config files, changes apply immediately
|
||||
- [x] **Security patch spoofing** — per-package `system`, `vendor`, `boot` patch levels with dynamic templates
|
||||
- [x] **Lifecycle scripts** — KSU Action button clears key cache, uninstall removes all traces
|
||||
|
||||
---
|
||||
|
||||
## 📋 Requirements
|
||||
## Requirements
|
||||
|
||||
> [!IMPORTANT]
|
||||
> TEESimulator requires root access and a valid `keybox.xml` for hardware-level attestation results. Without a keybox, the module generates software-level certificates that won't pass strict hardware attestation checks.
|
||||
> A valid `keybox.xml` is required for hardware-level attestation. Without one, the module generates software-level certificates that won't pass strict hardware checks.
|
||||
|
||||
**You need:**
|
||||
1. Android 10 or above
|
||||
2. A supported root manager (KernelSU, Magisk, or APatch)
|
||||
3. A hardware-backed `keybox.xml` placed at `/data/adb/tricky_store/keybox.xml`
|
||||
1. Android 10+
|
||||
2. Root manager: KernelSU, Magisk, or APatch
|
||||
3. `keybox.xml` at `/data/adb/tricky_store/keybox.xml`
|
||||
|
||||
---
|
||||
## Quick Start
|
||||
|
||||
## 📱 Compatibility
|
||||
1. Download the latest ZIP from [Releases](https://github.com/Enginex0/TEESimulator-RS/releases)
|
||||
2. Install via your root manager and reboot
|
||||
3. Place your keybox at `/data/adb/tricky_store/keybox.xml`
|
||||
4. Configure targets in `/data/adb/tricky_store/target.txt`
|
||||
5. Verify with Play Integrity or Key Attestation Demo
|
||||
|
||||
### Root Managers
|
||||
## Architecture
|
||||
|
||||
| Manager | Status | Notes |
|
||||
|---|---|---|
|
||||
| KernelSU | ✅ Tested | Full support including Action button and lifecycle scripts |
|
||||
| Magisk | ✅ Supported | Standard module install |
|
||||
| APatch | ✅ Supported | Standard module install |
|
||||
**Native Cert Generation** — `libcertgen.so` generates X.509 chains in Rust using `ring` and manual DER encoding. BouncyCastle fallback for unsupported curves (P-224, P-521, Curve25519).
|
||||
|
||||
### Tested Devices
|
||||
**Binder Interception** — PLT hook on `ioctl()` in `libc.so` via `lsplt` inside `keystore2`. Intercepts `generateKey`, `importKey`, and `getKeyEntry` transactions.
|
||||
|
||||
| Device | Android | TEE | Status |
|
||||
|---|---|---|---|
|
||||
| Redmi 14C (2409BRN2CA) | 14 (SDK 34) | Beanpod KeyMaster | ✅ Daily driver |
|
||||
**AOSP Compliance** — Self-signed certs for non-attested keys (matching `ta/src/keys.rs`), correct AuthorizationList tag ordering, version-guarded extension fields, `authorize_create` enforcement.
|
||||
|
||||
> Tested against DuckDetector, Luna, Play Integrity, and Key Attestation Demo. If you test on a different device, [open an issue](https://github.com/Enginex0/TEESimulator/issues) with your results.
|
||||
**Key Persistence** — Generated keys survive reboots. File-backed with file-level locking.
|
||||
|
||||
---
|
||||
**Rate Limiting** — Per-UID hardware keygen cap (2/30s window, 2 concurrent). Overflow falls to software certs.
|
||||
|
||||
## 🚀 Quick Start
|
||||
## Configuration
|
||||
|
||||
1. **Download** the latest release ZIP from [Releases](https://github.com/Enginex0/TEESimulator/releases)
|
||||
2. **Install** via your root manager (KSU / Magisk / APatch) and reboot
|
||||
3. **Place your keybox** at `/data/adb/tricky_store/keybox.xml`
|
||||
4. **Configure targets** in `/data/adb/tricky_store/target.txt`
|
||||
5. **Verify** — check Play Integrity or run Key Attestation Demo
|
||||
All config files live at `/data/adb/tricky_store/` and are hot-reloaded via `FileObserver`.
|
||||
|
||||
TEESimulator replaces TrickyStore, TrickyStoreOSS, and their forks. Existing config files are compatible.
|
||||
### target.txt
|
||||
|
||||
---
|
||||
Controls which apps get intercepted and the simulation mode.
|
||||
|
||||
## ⚙️ Configuration
|
||||
| Suffix | Mode |
|
||||
|--------|------|
|
||||
| `!` | Force software key generation |
|
||||
| `?` | Force leaf certificate patching (real TEE key, patched cert) |
|
||||
| *(none)* | Automatic selection |
|
||||
|
||||
All configuration files live at `/data/adb/tricky_store/` and are monitored by `FileObserver` — changes take effect immediately without rebooting.
|
||||
|
||||
### The `keybox.xml` Root of Trust
|
||||
|
||||
This file provides the master cryptographic identity. It contains a private key and a hardware-backed certificate chain from a real device. TEESimulator signs all generated certificates with this key, making them appear legitimate to verifiers.
|
||||
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<AndroidAttestation>
|
||||
<Keybox DeviceID="...">
|
||||
<Key algorithm="ecdsa|rsa">
|
||||
<PrivateKey format="pem">...</PrivateKey>
|
||||
<CertificateChain>...</CertificateChain>
|
||||
</Key>
|
||||
</Keybox>
|
||||
</AndroidAttestation>
|
||||
```
|
||||
|
||||
### Target Packages (`target.txt`)
|
||||
|
||||
Controls which apps get intercepted and what simulation mode to use.
|
||||
|
||||
#### Mode Suffixes
|
||||
|
||||
* **`!` → Force Generation** — Creates a complete software-based virtual key. Full TEE simulation.
|
||||
* **`?` → Force Leaf Hacking** — Real TEE key generated, but its attestation certificate is intercepted and patched.
|
||||
* **No symbol → Automatic** — Module selects the best mode for your device.
|
||||
|
||||
#### Multi-Keybox
|
||||
|
||||
Specify different keybox files for different app groups. Apps listed after a `[filename.xml]` line use that keybox. Apps before any declaration use the default `keybox.xml`.
|
||||
Multi-keybox support via `[filename.xml]` headers:
|
||||
|
||||
```
|
||||
# Default keybox
|
||||
com.google.android.gms!
|
||||
io.github.vvb2060.keyattestation?
|
||||
|
||||
# Switch to a different keybox for the following apps
|
||||
[aosp_keybox.xml]
|
||||
com.google.android.gsf
|
||||
|
||||
# Another keybox
|
||||
[demo_keybox.xml]
|
||||
org.matrix.demo
|
||||
```
|
||||
|
||||
### Security Patch Level (`security_patch.txt`)
|
||||
### security_patch.txt
|
||||
|
||||
Configure the `osPatchLevel`, `vendorPatchLevel`, and `bootPatchLevel` reported in attestation certificates. This only affects attestation data — it does not change actual system properties.
|
||||
|
||||
#### Global and Per-Package
|
||||
|
||||
Settings at the top of the file are global defaults. Add `[package.name]` to override for specific apps.
|
||||
|
||||
#### Keys
|
||||
Override patch levels reported in attestation certificates. Global defaults at top, per-package overrides with `[package.name]`.
|
||||
|
||||
| Key | Scope |
|
||||
|---|---|
|
||||
|-----|-------|
|
||||
| `system` | OS patch level |
|
||||
| `vendor` | Vendor patch level |
|
||||
| `boot` | Boot/kernel patch level |
|
||||
| `all` | Shorthand — sets all three at once |
|
||||
| `all` | Sets all three |
|
||||
|
||||
#### Special Keywords
|
||||
|
||||
| Keyword | Effect |
|
||||
|---|---|
|
||||
| `today` | Current date, dynamically resolved on each attestation |
|
||||
| `YYYY-MM-DD` templates | Semi-dynamic — `YYYY-MM-05` resolves to the 5th of the current month |
|
||||
| `no` | Omit this patch level tag entirely from the attestation |
|
||||
| `device_default` | Use the device's real hardware value |
|
||||
| `prop` | Read from `ro.build.version.security_patch` (matches what detectors see via getprop) |
|
||||
|
||||
#### Example
|
||||
Special values: `today`, `YYYY-MM-DD` templates, `no` (omit tag), `device_default`, `prop` (read from system property).
|
||||
|
||||
```
|
||||
# Global — default for all apps
|
||||
system=YYYY-MM-05
|
||||
vendor=device_default
|
||||
boot=no
|
||||
|
||||
# Override for GMS
|
||||
[com.google.android.gms]
|
||||
system=2024-10-01
|
||||
|
||||
# Custom config for a demo app
|
||||
[org.matrix.demo]
|
||||
all=2025-09-15
|
||||
boot=device_default
|
||||
system=2025-10-01
|
||||
```
|
||||
|
||||
---
|
||||
## Building from Source
|
||||
|
||||
## 💬 Community
|
||||
Prerequisites: JDK 21, Android SDK/NDK 27, Rust stable with `aarch64-linux-android` target, `cargo-ndk`.
|
||||
|
||||
```bash
|
||||
git clone --recursive https://github.com/Enginex0/TEESimulator-RS.git
|
||||
cd TEESimulator-RS
|
||||
./gradlew zipRelease zipDebug
|
||||
```
|
||||
|
||||
Output ZIPs in `out/`. Gradle invokes `cargo ndk` automatically to cross-compile `libcertgen.so`.
|
||||
|
||||
Push to `main` or use **Actions > Build > Run workflow** to trigger CI.
|
||||
|
||||
## Compatibility
|
||||
|
||||
| Root Manager | Status |
|
||||
|---|---|
|
||||
| KernelSU | Tested (Action button + lifecycle scripts) |
|
||||
| Magisk | Supported |
|
||||
| APatch | Supported |
|
||||
|
||||
## Community
|
||||
|
||||
<p align="center">
|
||||
<a href="https://t.me/superpowers9">
|
||||
<img src="https://img.shields.io/badge/⚡_JOIN_THE_GRID-SuperPowers_Telegram-black?style=for-the-badge&logo=telegram&logoColor=cyan&labelColor=0d1117&color=00d4ff" alt="Telegram">
|
||||
<img src="https://img.shields.io/badge/SuperPowers_Telegram-Join-blue?style=for-the-badge&logo=telegram" alt="Telegram">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
## Credits
|
||||
|
||||
## 🙏 Credits
|
||||
- [JingMatrix](https://github.com/JingMatrix/TEESimulator) — original TEESimulator and interception architecture
|
||||
- [5ec1cff](https://github.com/5ec1cff/TrickyStore) — TrickyStore, the project that pioneered keystore interception
|
||||
- [LSPlt](https://github.com/LSPosed/LSPlt) — PLT hook library
|
||||
- [ring](https://github.com/briansmith/ring) — Rust cryptography library
|
||||
- [MhmRdd](https://github.com/MhmRdd) — AOSP compliance work via upstream [PR #157](https://github.com/JingMatrix/TEESimulator/pull/157)
|
||||
- [fatalcoder524](https://github.com/fatalcoder524) — contributor and collaborator
|
||||
- [huguangares](https://github.com/huguangares) — collaborator and tester
|
||||
|
||||
- **[JingMatrix](https://github.com/JingMatrix/TEESimulator)** — original author of TEESimulator and the interception architecture
|
||||
- **[5ec1cff](https://github.com/5ec1cff/TrickyStore)** — TrickyStore, the project that pioneered keystore interception on Android
|
||||
- **[LSPlt](https://github.com/LSPosed/LSPlt)** — PLT hook library used for binder interception
|
||||
- **[ring](https://github.com/briansmith/ring)** — Rust cryptography library powering native cert generation
|
||||
## License
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the [GNU General Public License v3.0](LICENSE).
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<b>🔐 Because the best attestation is the one the TEE never generated.</b>
|
||||
</p>
|
||||
[GNU General Public License v3.0](LICENSE)
|
||||
|
||||
+54
-17
@@ -2,6 +2,7 @@ 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)
|
||||
@@ -29,7 +30,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java)
|
||||
|
||||
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
|
||||
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
|
||||
val verName = "v4.0"
|
||||
val verName = "v6.0.1"
|
||||
|
||||
android {
|
||||
namespace = "org.matrix.TEESimulator"
|
||||
@@ -65,6 +66,12 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_21)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":stub"))
|
||||
compileOnly(libs.annotation)
|
||||
@@ -73,7 +80,7 @@ dependencies {
|
||||
|
||||
// --- Rust native cert gen build task ---
|
||||
val buildRustCertgen by tasks.registering(Exec::class) {
|
||||
group = "TEESimulator Native Build"
|
||||
group = "TEESimulator-RS Native Build"
|
||||
description = "Builds libcertgen.so via cargo-ndk for arm64-v8a."
|
||||
|
||||
workingDir = rootProject.projectDir.resolve("native-certgen")
|
||||
@@ -91,6 +98,7 @@ val buildRustCertgen by tasks.registering(Exec::class) {
|
||||
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
|
||||
@@ -100,6 +108,34 @@ tasks.configureEach {
|
||||
}
|
||||
}
|
||||
|
||||
// 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() }
|
||||
@@ -108,22 +144,23 @@ androidComponents {
|
||||
// --- Define output locations and file names ---
|
||||
// Stage all files in a temporary directory inside 'build' before zipping
|
||||
val tempModuleDir = project.layout.buildDirectory.dir("module/${variant.name}")
|
||||
val zipFileName = "TEESimulator-$verName-$gitCommitCount-$gitCommitHash-$capitalized.zip"
|
||||
val zipFileName = "TEESimulator-RS-$verName-$gitCommitCount-$capitalized.zip"
|
||||
|
||||
// Task 1: Prepare all module files in the temporary build directory.
|
||||
// Using Sync ensures that stale files from previous runs are removed.
|
||||
val prepareModuleFilesTask =
|
||||
tasks.register<Sync>("prepareModuleFiles${capitalized}") {
|
||||
group = "TEESimulator Module Packaging"
|
||||
group = "TEESimulator-RS Module Packaging"
|
||||
description = "Prepares all files for the ${variant.name} module zip."
|
||||
|
||||
if (isDebug) {
|
||||
dependsOn("package${capitalized}")
|
||||
} else {
|
||||
dependsOn("minify${capitalized}WithR8")
|
||||
dependsOn("strip${capitalized}DebugSymbols")
|
||||
}
|
||||
dependsOn("strip${capitalized}DebugSymbols")
|
||||
dependsOn(buildRustCertgen)
|
||||
dependsOn(refreshUpdateJson)
|
||||
|
||||
if (isDebug) {
|
||||
from(variant.artifacts.get(SingleArtifact.APK)) {
|
||||
@@ -140,12 +177,13 @@ androidComponents {
|
||||
}
|
||||
}
|
||||
|
||||
from(
|
||||
project.layout.buildDirectory.dir(
|
||||
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
|
||||
)
|
||||
) {
|
||||
into("lib") // Place them in the 'lib' subfolder of the staging directory.
|
||||
val nativeLibsDir = if (isDebug) {
|
||||
"intermediates/merged_native_libs/${variant.name}/merge${capitalized}NativeLibs/out/lib"
|
||||
} else {
|
||||
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
|
||||
}
|
||||
from(project.layout.buildDirectory.dir(nativeLibsDir)) {
|
||||
into("lib")
|
||||
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so", "**/libcertgen.so")
|
||||
}
|
||||
|
||||
@@ -161,8 +199,7 @@ androidComponents {
|
||||
// Use expand() for simple key-value replacement.
|
||||
expand(
|
||||
"REPLACEMEVERCODE" to gitCommitCount.toString(),
|
||||
"REPLACEMEVER" to
|
||||
"$verName ($gitCommitCount-$gitCommitHash-${variant.name})",
|
||||
"REPLACEMEVER" to "$verName-$gitCommitCount",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -173,7 +210,7 @@ androidComponents {
|
||||
// Task 2: Zip the prepared files from the temporary directory.
|
||||
val zipTask =
|
||||
tasks.register<Zip>("zip${capitalized}") {
|
||||
group = "TEESimulator Module Packaging"
|
||||
group = "TEESimulator-RS Module Packaging"
|
||||
description = "Creates the flashable zip for the ${variant.name} module."
|
||||
dependsOn(prepareModuleFilesTask)
|
||||
|
||||
@@ -186,7 +223,7 @@ androidComponents {
|
||||
fun createInstallTasks(rootProvider: String, installCli: String) {
|
||||
val pushTask =
|
||||
tasks.register<Exec>("push${rootProvider}Module${capitalized}") {
|
||||
group = "TEESimulator Module Installation"
|
||||
group = "TEESimulator-RS Module Installation"
|
||||
description =
|
||||
"Pushes the ${variant.name} module to the device for $rootProvider."
|
||||
dependsOn(zipTask)
|
||||
@@ -200,7 +237,7 @@ androidComponents {
|
||||
|
||||
val installTask =
|
||||
tasks.register<Exec>("install${rootProvider}${capitalized}") {
|
||||
group = "TEESimulator Module Installation"
|
||||
group = "TEESimulator-RS Module Installation"
|
||||
description = "Installs the ${variant.name} module via $rootProvider."
|
||||
dependsOn(pushTask)
|
||||
commandLine(
|
||||
@@ -213,7 +250,7 @@ androidComponents {
|
||||
}
|
||||
|
||||
tasks.register<Exec>("install${rootProvider}AndReboot${capitalized}") {
|
||||
group = "TEESimulator Module Installation"
|
||||
group = "TEESimulator-RS Module Installation"
|
||||
description = "Installs the ${variant.name} module via $rootProvider and reboots."
|
||||
dependsOn(installTask)
|
||||
commandLine("adb", "reboot")
|
||||
|
||||
@@ -5,6 +5,7 @@ set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions")
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -DNDEBUG")
|
||||
|
||||
# LSPlt configuration
|
||||
OPTION(LSPLT_BUILD_SHARED OFF)
|
||||
|
||||
@@ -235,19 +235,21 @@ 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;
|
||||
|
||||
// Checks if a specific Binder instance is currently registered for interception
|
||||
bool isBinderIntercepted(const wp<BBinder> &target) const {
|
||||
bool shouldIntercept(const wp<BBinder> &target, uint32_t code) const {
|
||||
std::shared_lock lock(registry_mutex_);
|
||||
return registry_.find(target) != registry_.end();
|
||||
auto it = registry_.find(target);
|
||||
if (it == registry_.end()) return false;
|
||||
const auto &codes = it->second.filtered_codes;
|
||||
return codes.empty() || std::find(codes.begin(), codes.end(), code) != codes.end();
|
||||
}
|
||||
|
||||
// Main entry point for processing the "Man-in-the-Middle" logic
|
||||
@@ -348,14 +350,14 @@ static sp<BinderStub> g_stub_instance = nullptr;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr binder_size_t kMaxInterceptableDataSize = 256 * 1024;
|
||||
|
||||
void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
||||
if (!txn_data || txn_data->target.ptr == 0)
|
||||
return;
|
||||
|
||||
// Bypass interception for oversized payloads to prevent thread starvation from flood attacks
|
||||
if (txn_data->data_size > kMaxInterceptableDataSize)
|
||||
// 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;
|
||||
@@ -387,7 +389,7 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
||||
// This is safe because we are holding a strong reference.
|
||||
wp<BBinder> wp_target = target_binder_ptr;
|
||||
|
||||
if (g_interceptor_instance->isBinderIntercepted(wp_target)) {
|
||||
if (g_interceptor_instance->shouldIntercept(wp_target, txn_data->code)) {
|
||||
info.transaction_code = txn_data->code;
|
||||
info.target_binder = wp_target; // Assign the valid weak pointer
|
||||
hijack = true;
|
||||
@@ -426,44 +428,29 @@ 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 overflow detected while parsing command %s", getBinderReturnCommandName(cmd));
|
||||
LOGE("[Hook] Buffer overrun parsing command 0x%x", cmd);
|
||||
break;
|
||||
}
|
||||
|
||||
// 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 (__builtin_expect(cmd == BR_TRANSACTION || cmd == BR_TRANSACTION_SEC_CTX, 0)) {
|
||||
binder_transaction_data *txn;
|
||||
if (cmd == BR_TRANSACTION_SEC_CTX) {
|
||||
// The data is wrapped in a secctx struct
|
||||
auto *wrapper = reinterpret_cast<binder_transaction_data_secctx *>(ptr);
|
||||
txn = &wrapper->transaction_data;
|
||||
txn = &reinterpret_cast<binder_transaction_data_secctx *>(ptr)->transaction_data;
|
||||
} else {
|
||||
txn = reinterpret_cast<binder_transaction_data *>(ptr);
|
||||
}
|
||||
|
||||
inspectAndRewriteTransaction(txn);
|
||||
}
|
||||
|
||||
// Advance pointer to the next command
|
||||
ptr += cmd_size;
|
||||
}
|
||||
}
|
||||
@@ -483,13 +470,17 @@ 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);
|
||||
|
||||
// We only care about data read FROM the driver (i.e., incoming commands)
|
||||
if (bwr->read_consumed > 0) {
|
||||
processBinderReadBuffer(*bwr);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,18 +523,29 @@ 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};
|
||||
registry_[weak_target] = {weak_target, callback, std::move(codes)};
|
||||
|
||||
LOGI("Interceptor registered for binder %p", target.get());
|
||||
return OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/resource.h>
|
||||
#include <signal.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
|
||||
static volatile sig_atomic_t should_exit = 0;
|
||||
|
||||
@@ -27,7 +29,12 @@ int main(int argc, char *argv[]) {
|
||||
const char *daemon_path = argv[1];
|
||||
char **daemon_argv = &argv[1];
|
||||
|
||||
int backoff_ms = 500;
|
||||
|
||||
while (!should_exit) {
|
||||
struct timespec child_start;
|
||||
clock_gettime(CLOCK_MONOTONIC, &child_start);
|
||||
|
||||
pid_t pid = fork();
|
||||
|
||||
if (pid < 0) {
|
||||
@@ -39,6 +46,7 @@ int main(int argc, char *argv[]) {
|
||||
if (pid == 0) {
|
||||
// Child: become the daemon
|
||||
prctl(PR_SET_PDEATHSIG, SIGKILL); // Die if parent dies
|
||||
setpriority(PRIO_PROCESS, 0, 10); // lower CPU priority than foreground
|
||||
execv(daemon_path, daemon_argv);
|
||||
perror("execv failed");
|
||||
_exit(127);
|
||||
@@ -50,7 +58,18 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
if (should_exit) break;
|
||||
|
||||
// Instant restart - no delay
|
||||
// Exponential backoff on rapid crashes, reset if child was stable
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
long lived_ms = (now.tv_sec - child_start.tv_sec) * 1000 +
|
||||
(now.tv_nsec - child_start.tv_nsec) / 1000000;
|
||||
|
||||
if (lived_ms > 30000) {
|
||||
backoff_ms = 500;
|
||||
} else {
|
||||
usleep(backoff_ms * 1000);
|
||||
if (backoff_ms < 30000) backoff_ms *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -6,8 +6,10 @@ import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.os.Build
|
||||
import android.os.Looper
|
||||
import java.io.File
|
||||
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
|
||||
@@ -23,8 +25,6 @@ import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||
object App {
|
||||
// The delay in milliseconds before retrying to initialize the interceptor.
|
||||
private const val RETRY_DELAY_MS = 1000L
|
||||
// The sleep duration in milliseconds for the main service loop to keep the process alive.
|
||||
private const val SERVICE_SLEEP_MS = 1000000L
|
||||
|
||||
/**
|
||||
* The main entry point of the TEESimulator application.
|
||||
@@ -35,14 +35,24 @@ object App {
|
||||
fun main(args: Array<String>) {
|
||||
SystemLogger.info("Welcome to TEESimulator!")
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||
SystemLogger.error("Uncaught exception on ${thread.name}", throwable)
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize the Android framework environment
|
||||
purgeDebugDiagnostics()
|
||||
prepareEnvironment()
|
||||
// Initialize and start the appropriate keystore interceptors.
|
||||
initializeInterceptors()
|
||||
|
||||
// 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()
|
||||
|
||||
// Initialize and start the appropriate keystore interceptors.
|
||||
initializeInterceptors()
|
||||
|
||||
// Set up the device's boot key and hash, which are crucial for attestation.
|
||||
AndroidDeviceUtils.setupBootKeyAndHash()
|
||||
|
||||
@@ -63,6 +73,25 @@ object App {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release builds never emit diagnostics. Sweep any `.bin` dumps a prior
|
||||
* debug install left in the world-readable temp dir so they can't act as a
|
||||
* detection artifact for apps that probe /data/local/tmp.
|
||||
*/
|
||||
private fun purgeDebugDiagnostics() {
|
||||
if (SystemLogger.isDebugBuild) return
|
||||
val stale =
|
||||
File("/data/local/tmp").listFiles { _, name ->
|
||||
name.startsWith("teesim-") && name.endsWith(".bin")
|
||||
} ?: return
|
||||
stale.forEach { runCatching { it.delete() } }
|
||||
if (stale.isNotEmpty()) {
|
||||
// warning() bypasses the rate limiter, so this once-per-boot audit
|
||||
// line survives the noisy startup window.
|
||||
SystemLogger.warning("Purged ${stale.size} stale debug diagnostic(s) from /data/local/tmp")
|
||||
}
|
||||
}
|
||||
|
||||
/** Initializes the necessary Android framework internals to satisfy KeyStore requirements. */
|
||||
private fun prepareEnvironment() {
|
||||
// 1. Prepare Main Looper
|
||||
|
||||
@@ -2,8 +2,11 @@ package org.matrix.TEESimulator.attestation
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import org.bouncycastle.asn1.ASN1Boolean
|
||||
import org.bouncycastle.asn1.ASN1Encodable
|
||||
import org.bouncycastle.asn1.ASN1Enumerated
|
||||
@@ -40,11 +43,12 @@ object AttestationBuilder {
|
||||
securityLevel: Int,
|
||||
): Extension {
|
||||
val keyDescription = buildKeyDescription(params, uid, securityLevel)
|
||||
var formattedString =
|
||||
keyDescription.joinToString(separator = ", ") {
|
||||
SystemLogger.verbose {
|
||||
val formattedString = keyDescription.joinToString(separator = ", ") {
|
||||
AttestationPatcher.formatAsn1Primitive(it)
|
||||
}
|
||||
SystemLogger.verbose("Forged attestation data: ${formattedString}")
|
||||
"Forged attestation data: $formattedString"
|
||||
}
|
||||
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
|
||||
}
|
||||
|
||||
@@ -127,33 +131,59 @@ object AttestationBuilder {
|
||||
return properties
|
||||
}
|
||||
|
||||
/** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */
|
||||
private fun buildKeyDescription(
|
||||
params: KeyMintAttestation,
|
||||
uid: Int,
|
||||
securityLevel: Int,
|
||||
): ASN1Sequence {
|
||||
val creationTime = System.currentTimeMillis()
|
||||
val teeEnforced = buildTeeEnforcedList(params, uid, securityLevel)
|
||||
val softwareEnforced = buildSoftwareEnforcedList(uid, securityLevel)
|
||||
val softwareEnforced = buildSoftwareEnforcedList(params, uid, securityLevel, creationTime)
|
||||
|
||||
val uniqueId =
|
||||
if (params.includeUniqueId == true && params.attestationChallenge != null) {
|
||||
computeUniqueId(creationTime, createApplicationId(uid).octets)
|
||||
} else {
|
||||
ByteArray(0)
|
||||
}
|
||||
|
||||
val fields =
|
||||
arrayOf(
|
||||
ASN1Integer(
|
||||
AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()
|
||||
), // attestationVersion
|
||||
ASN1Enumerated(securityLevel), // attestationSecurityLevel
|
||||
ASN1Integer(
|
||||
AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()
|
||||
), // keymasterVersion
|
||||
ASN1Enumerated(securityLevel), // keymasterSecurityLevel
|
||||
DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge
|
||||
DEROctetString(ByteArray(0)), // uniqueId
|
||||
ASN1Integer(AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()),
|
||||
ASN1Enumerated(securityLevel),
|
||||
ASN1Integer(AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()),
|
||||
ASN1Enumerated(securityLevel),
|
||||
DEROctetString(params.attestationChallenge ?: ByteArray(0)),
|
||||
DEROctetString(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,
|
||||
@@ -182,23 +212,110 @@ 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()),
|
||||
),
|
||||
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
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_ORIGIN,
|
||||
ASN1Integer(0L),
|
||||
), // KeyOrigin.GENERATED
|
||||
ASN1Integer((params.origin ?: 0).toLong()),
|
||||
),
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ROOT_OF_TRUST,
|
||||
buildRootOfTrust(null),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
// Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels.
|
||||
val simulatedProperties = getSimulatedHardwareProperties(uid)
|
||||
@@ -295,20 +412,32 @@ object AttestationBuilder {
|
||||
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
|
||||
* Keystore.
|
||||
*/
|
||||
private fun buildSoftwareEnforcedList(uid: Int, securityLevel: Int): DERSequence {
|
||||
val list =
|
||||
mutableListOf<ASN1Encodable>(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_CREATION_DATETIME,
|
||||
ASN1Integer(System.currentTimeMillis()),
|
||||
),
|
||||
private fun buildSoftwareEnforcedList(
|
||||
params: KeyMintAttestation,
|
||||
uid: Int,
|
||||
securityLevel: Int,
|
||||
creationTimeMs: Long = System.currentTimeMillis(),
|
||||
): DERSequence {
|
||||
val list = mutableListOf<ASN1Encodable>()
|
||||
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_CREATION_DATETIME,
|
||||
ASN1Integer(creationTimeMs),
|
||||
)
|
||||
)
|
||||
|
||||
if (params.attestationChallenge != null) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ATTESTATION_APPLICATION_ID,
|
||||
createApplicationId(uid),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
@@ -318,7 +447,39 @@ object AttestationBuilder {
|
||||
)
|
||||
)
|
||||
}
|
||||
return DERSequence(list.toTypedArray())
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -346,6 +507,11 @@ object AttestationBuilder {
|
||||
*/
|
||||
@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!")
|
||||
@@ -353,12 +519,11 @@ object AttestationBuilder {
|
||||
pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid")
|
||||
|
||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||
val packageInfoList = mutableListOf<DERSequence>()
|
||||
val packageInfoList = mutableListOf<Pair<String, Long>>()
|
||||
val signatureDigests = mutableSetOf<Digest>()
|
||||
|
||||
// Process all packages associated with the UID in a single loop.
|
||||
val userId = uid / 100000
|
||||
packages.forEach { packageName ->
|
||||
val userId = uid / 100000
|
||||
val packageInfo =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pm.getPackageInfo(
|
||||
@@ -371,34 +536,36 @@ object AttestationBuilder {
|
||||
pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId)
|
||||
}
|
||||
|
||||
// Add package information (name and version code) to our list.
|
||||
packageInfoList.add(
|
||||
DERSequence(
|
||||
arrayOf(
|
||||
DEROctetString(packageInfo.packageName.toByteArray(StandardCharsets.UTF_8)),
|
||||
ASN1Integer(packageInfo.longVersionCode),
|
||||
)
|
||||
)
|
||||
)
|
||||
packageInfoList.add(packageInfo.packageName to packageInfo.longVersionCode)
|
||||
|
||||
// Collect unique signature digests from the signing history.
|
||||
packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature ->
|
||||
val digest = sha256.digest(signature.toByteArray())
|
||||
signatureDigests.add(Digest(digest))
|
||||
signatureDigests.add(Digest(sha256.digest(signature.toByteArray())))
|
||||
}
|
||||
}
|
||||
|
||||
// The application ID is a sequence of two sets:
|
||||
// 1. A set of package information (name and version).
|
||||
// 2. A set of SHA-256 digests of the signing certificates.
|
||||
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(signatureDigests.map { DEROctetString(it.digest) }.toTypedArray()),
|
||||
DERSet(digests.map { DEROctetString(it.digest) }.toTypedArray()),
|
||||
)
|
||||
)
|
||||
|
||||
return DEROctetString(applicationIdSequence.encoded)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +44,11 @@ object AttestationConstants {
|
||||
|
||||
// --- Key Lifetime and Usage Control ---
|
||||
const val TAG_ROLLBACK_RESISTANCE = 303
|
||||
const val TAG_EARLY_BOOT_ONLY = 305
|
||||
const val TAG_ACTIVE_DATETIME = 400
|
||||
const val TAG_ORIGINATION_EXPIRE_DATETIME = 401
|
||||
const val TAG_USAGE_EXPIRE_DATETIME = 402
|
||||
const val TAG_MAX_BOOT_LEVEL = 403
|
||||
const val TAG_MAX_USES_PER_BOOT = 404
|
||||
const val TAG_USAGE_COUNT_LIMIT = 405
|
||||
|
||||
@@ -56,6 +58,10 @@ object AttestationConstants {
|
||||
const val TAG_NO_AUTH_REQUIRED = 503
|
||||
const val TAG_USER_AUTH_TYPE = 504
|
||||
const val TAG_AUTH_TIMEOUT = 505
|
||||
const val TAG_ALLOW_WHILE_ON_BODY = 506
|
||||
const val TAG_TRUSTED_USER_PRESENCE_REQUIRED = 507
|
||||
const val TAG_TRUSTED_CONFIRMATION_REQUIRED = 508
|
||||
const val TAG_UNLOCKED_DEVICE_REQUIRED = 509
|
||||
|
||||
// --- Attestation and Application Info ---
|
||||
const val TAG_APPLICATION_ID = 601
|
||||
@@ -89,5 +95,5 @@ object AttestationConstants {
|
||||
|
||||
// --- Other Constants ---
|
||||
// https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp
|
||||
const val CHALLENGE_LENGTH_LIMIT = 128 // kMaximumAttestationChallengeLength
|
||||
const val CHALLENGE_LENGTH_LIMIT = 128
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
||||
import org.matrix.TEESimulator.pki.KeyBox
|
||||
import org.matrix.TEESimulator.pki.KeyBoxManager
|
||||
import org.matrix.TEESimulator.util.toHex
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Handles the modification (patching) of Android Key Attestation extensions within certificates.
|
||||
@@ -36,7 +37,12 @@ object AttestationPatcher {
|
||||
* @return A new, cryptographically valid, patched certificate chain. Returns the original chain
|
||||
* on any failure.
|
||||
*/
|
||||
fun patchCertificateChain(originalChain: Array<Certificate>?, uid: Int): Array<Certificate> {
|
||||
fun patchCertificateChain(
|
||||
originalChain: Array<Certificate>?,
|
||||
uid: Int,
|
||||
notBefore: Date? = null,
|
||||
notAfter: Date? = null,
|
||||
): Array<Certificate> {
|
||||
if (originalChain.isNullOrEmpty()) {
|
||||
SystemLogger.error("Attempted to patch a null or empty certificate chain for UID $uid.")
|
||||
return originalChain ?: emptyArray()
|
||||
@@ -63,6 +69,8 @@ object AttestationPatcher {
|
||||
keybox,
|
||||
originalLeaf.sigAlgName,
|
||||
uid,
|
||||
notBefore,
|
||||
notAfter,
|
||||
)
|
||||
|
||||
// 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's
|
||||
@@ -111,17 +119,27 @@ object AttestationPatcher {
|
||||
keybox: KeyBox,
|
||||
sigAlgName: String,
|
||||
uid: Int,
|
||||
notBefore: Date? = null,
|
||||
notAfter: Date? = null,
|
||||
): Certificate {
|
||||
// The issuer of our new leaf is the subject of the first certificate in our custom keybox
|
||||
// chain.
|
||||
val newIssuer = X509CertificateHolder(keybox.certificates[0].encoded).subject
|
||||
|
||||
val effectiveNotBefore = notBefore ?: originalLeafHolder.notBefore
|
||||
val effectiveNotAfter = notAfter ?: originalLeafHolder.notAfter
|
||||
if (notBefore != null || notAfter != null) {
|
||||
SystemLogger.debug(
|
||||
"Overriding cert dates: notBefore=${effectiveNotBefore} (was ${originalLeafHolder.notBefore}), notAfter=${effectiveNotAfter} (was ${originalLeafHolder.notAfter})"
|
||||
)
|
||||
}
|
||||
|
||||
val builder =
|
||||
X509v3CertificateBuilder(
|
||||
newIssuer,
|
||||
originalLeafHolder.serialNumber,
|
||||
originalLeafHolder.notBefore,
|
||||
originalLeafHolder.notAfter,
|
||||
effectiveNotBefore,
|
||||
effectiveNotAfter,
|
||||
originalLeafHolder.subject,
|
||||
originalLeafHolder.subjectPublicKeyInfo,
|
||||
)
|
||||
@@ -146,7 +164,7 @@ object AttestationPatcher {
|
||||
// Log the signature of the newly created certificate to observe its non-deterministic
|
||||
// nature.
|
||||
val signatureBytes = (newCertificate as X509Certificate).signature
|
||||
SystemLogger.verbose("Signature of patched leaf cert: ${signatureBytes.toHex()}")
|
||||
SystemLogger.verbose { "Signature of patched leaf cert: ${signatureBytes.toHex()}" }
|
||||
|
||||
return newCertificate
|
||||
}
|
||||
@@ -268,8 +286,10 @@ object AttestationPatcher {
|
||||
private fun createPatchedAttestationExtension(parsed: ParsedAttestation, uid: Int): Extension {
|
||||
val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed
|
||||
|
||||
var formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
||||
SystemLogger.verbose("Original attestation data: ${formattedString}")
|
||||
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.
|
||||
val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust)
|
||||
@@ -296,8 +316,10 @@ object AttestationPatcher {
|
||||
|
||||
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
|
||||
val patchedSequence = DERSequence(allFields)
|
||||
formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
||||
SystemLogger.verbose("Patched attestation data: ${formattedString}")
|
||||
SystemLogger.verbose {
|
||||
val formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
||||
"Patched attestation data: $formattedString"
|
||||
}
|
||||
val patchedOctets = DEROctetString(patchedSequence)
|
||||
|
||||
return Extension(ATTESTATION_OID, false, patchedOctets)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.matrix.TEESimulator.attestation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import java.security.KeyPairGenerator
|
||||
@@ -60,12 +61,23 @@ object DeviceAttestationService {
|
||||
// A unique alias for the key used to perform the TEE functionality check.
|
||||
private const val TEE_CHECK_KEY_ALIAS = "TEESimulator_AttestationCheck"
|
||||
|
||||
// Alias for the device-ID attestation capability probe.
|
||||
private const val DEVICE_ID_CHECK_KEY_ALIAS = "TEESimulator_DeviceIdCheck"
|
||||
|
||||
/**
|
||||
* Lazily determines if the device's TEE is functional by attempting to generate an
|
||||
* attestation-backed key pair. The result is cached.
|
||||
*/
|
||||
val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() }
|
||||
|
||||
/**
|
||||
* Lazily mirrors whether the real TEE can attest device identifiers/properties (the tags added
|
||||
* by `setDevicePropertiesAttestationIncluded`). Hardware that never provisioned device IDs
|
||||
* returns CANNOT_ATTEST_IDS; the synthesizer consults this so it never forges a capability the
|
||||
* real silicon lacks. Cached.
|
||||
*/
|
||||
val canAttestDeviceIds: Boolean by lazy { checkDeviceIdAttestation() }
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -106,6 +118,37 @@ object DeviceAttestationService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Probes whether the real TEE can satisfy device-ID/property attestation, mirroring its actual
|
||||
* capability. Gated behind [isTeeFunctional] so a dead TEE never triggers a second doomed
|
||||
* probe — it simply reports `false` (cannot attest), the faithful result for such hardware.
|
||||
*/
|
||||
private fun checkDeviceIdAttestation(): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return false
|
||||
if (!isTeeFunctional) return false
|
||||
return try {
|
||||
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
||||
val keyPairGenerator =
|
||||
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
|
||||
val challenge = ByteArray(16).apply { SecureRandom().nextBytes(this) }
|
||||
val spec =
|
||||
KeyGenParameterSpec.Builder(DEVICE_ID_CHECK_KEY_ALIAS, KeyProperties.PURPOSE_SIGN)
|
||||
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
|
||||
.setDigests(KeyProperties.DIGEST_SHA256)
|
||||
.setAttestationChallenge(challenge)
|
||||
.setDevicePropertiesAttestationIncluded(true)
|
||||
.build()
|
||||
keyPairGenerator.initialize(spec)
|
||||
keyPairGenerator.generateKeyPair()
|
||||
runCatching { keyStore.deleteEntry(DEVICE_ID_CHECK_KEY_ALIAS) }
|
||||
SystemLogger.info("Device-ID attestation supported by TEE.")
|
||||
true
|
||||
} catch (_: Exception) {
|
||||
SystemLogger.info("Device-ID attestation not supported by TEE; mirroring as cannot-attest.")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the attestation certificate generated during the TEE check. The key entry is
|
||||
* deleted after retrieval to clean up.
|
||||
@@ -148,11 +191,12 @@ object DeviceAttestationService {
|
||||
|
||||
// The extension's value is an ASN.1 sequence.
|
||||
val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets)
|
||||
var formattedString =
|
||||
keyDescriptionSeq.joinToString(separator = ", ") {
|
||||
SystemLogger.verbose {
|
||||
val formattedString = keyDescriptionSeq.joinToString(separator = ", ") {
|
||||
AttestationPatcher.formatAsn1Primitive(it)
|
||||
}
|
||||
SystemLogger.verbose("Cached attestation data: ${formattedString}")
|
||||
"Cached attestation data: $formattedString"
|
||||
}
|
||||
val fields = keyDescriptionSeq.toArray()
|
||||
|
||||
val attestVersion =
|
||||
|
||||
@@ -19,7 +19,7 @@ import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||
data class KeyMintAttestation(
|
||||
val keySize: Int,
|
||||
val algorithm: Int,
|
||||
val ecCurve: Int,
|
||||
val ecCurve: Int?,
|
||||
val ecCurveName: String,
|
||||
val origin: Int?,
|
||||
val blockMode: List<Int>,
|
||||
@@ -41,19 +41,36 @@ data class KeyMintAttestation(
|
||||
val manufacturer: ByteArray?,
|
||||
val model: ByteArray?,
|
||||
val secondImei: ByteArray?,
|
||||
val activeDateTime: Date?,
|
||||
val originationExpireDateTime: Date?,
|
||||
val usageExpireDateTime: Date?,
|
||||
val usageCountLimit: Int?,
|
||||
val callerNonce: Boolean?,
|
||||
val nonce: ByteArray?,
|
||||
val unlockedDeviceRequired: Boolean?,
|
||||
val includeUniqueId: Boolean?,
|
||||
val rollbackResistance: Boolean?,
|
||||
val earlyBootOnly: Boolean?,
|
||||
val allowWhileOnBody: Boolean?,
|
||||
val trustedUserPresenceRequired: Boolean?,
|
||||
val trustedConfirmationRequired: Boolean?,
|
||||
val noAuthRequired: Boolean?,
|
||||
val maxUsesPerBoot: Int?,
|
||||
val maxBootLevel: Int?,
|
||||
val minMacLength: Int?,
|
||||
val rsaOaepMgfDigest: List<Int>,
|
||||
) {
|
||||
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */
|
||||
constructor(
|
||||
params: Array<KeyParameter>
|
||||
) : this(
|
||||
// AOSP: [key_param(tag = KEY_SIZE, field = Integer)]
|
||||
keySize = params.findInteger(Tag.KEY_SIZE) ?: 0,
|
||||
keySize = params.findInteger(Tag.KEY_SIZE) ?: params.deriveKeySizeFromCurve(),
|
||||
|
||||
// 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) ?: 0,
|
||||
ecCurve = params.findEcCurve(Tag.EC_CURVE),
|
||||
ecCurveName = params.deriveEcCurveName(),
|
||||
|
||||
// AOSP: [key_param(tag = ORIGIN, field = Origin)]
|
||||
@@ -100,6 +117,24 @@ data class KeyMintAttestation(
|
||||
manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER),
|
||||
model = params.findBlob(Tag.ATTESTATION_ID_MODEL),
|
||||
secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI),
|
||||
activeDateTime = params.findDate(Tag.ACTIVE_DATETIME),
|
||||
originationExpireDateTime = params.findDate(Tag.ORIGINATION_EXPIRE_DATETIME),
|
||||
usageExpireDateTime = params.findDate(Tag.USAGE_EXPIRE_DATETIME),
|
||||
usageCountLimit = params.findInteger(Tag.USAGE_COUNT_LIMIT),
|
||||
callerNonce = params.findBoolean(Tag.CALLER_NONCE),
|
||||
nonce = params.findBlob(Tag.NONCE),
|
||||
unlockedDeviceRequired = params.findBoolean(Tag.UNLOCKED_DEVICE_REQUIRED),
|
||||
includeUniqueId = params.findBoolean(Tag.INCLUDE_UNIQUE_ID),
|
||||
rollbackResistance = params.findBoolean(Tag.ROLLBACK_RESISTANCE),
|
||||
earlyBootOnly = params.findBoolean(Tag.EARLY_BOOT_ONLY),
|
||||
allowWhileOnBody = params.findBoolean(Tag.ALLOW_WHILE_ON_BODY),
|
||||
trustedUserPresenceRequired = params.findBoolean(Tag.TRUSTED_USER_PRESENCE_REQUIRED),
|
||||
trustedConfirmationRequired = params.findBoolean(Tag.TRUSTED_CONFIRMATION_REQUIRED),
|
||||
noAuthRequired = params.findBoolean(Tag.NO_AUTH_REQUIRED),
|
||||
maxUsesPerBoot = params.findInteger(Tag.MAX_USES_PER_BOOT),
|
||||
maxBootLevel = params.findInteger(Tag.MAX_BOOT_LEVEL),
|
||||
minMacLength = params.findInteger(Tag.MIN_MAC_LENGTH),
|
||||
rsaOaepMgfDigest = params.findAllDigests(Tag.RSA_OAEP_MGF_DIGEST),
|
||||
) {
|
||||
// Log all parsed parameters for debugging purposes.
|
||||
params.forEach { KeyMintParameterLogger.logParameter(it) }
|
||||
@@ -156,6 +191,21 @@ 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).
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.matrix.TEESimulator.config
|
||||
|
||||
import android.os.SystemProperties
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||
|
||||
object BootStateManager {
|
||||
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() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ object ConfigurationManager {
|
||||
// --- Configuration Paths ---
|
||||
const val CONFIG_PATH = "/data/adb/tricky_store"
|
||||
private const val TARGET_PACKAGES_FILE = "target.txt"
|
||||
private const val TEE_STATUS_FILE = "tee_status.txt"
|
||||
private const val PATCH_LEVEL_FILE = "security_patch.txt"
|
||||
private const val DEFAULT_KEYBOX_FILE = "keybox.xml"
|
||||
private val configRoot = File(CONFIG_PATH)
|
||||
@@ -39,7 +38,6 @@ object ConfigurationManager {
|
||||
// --- In-Memory Configuration State ---
|
||||
@Volatile private var packageModes = mapOf<String, Mode>()
|
||||
@Volatile private var packageKeyboxes = mapOf<String, String>()
|
||||
@Volatile private var isTeeBroken: Boolean? = null
|
||||
@Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null
|
||||
@Volatile private var packagePatchLevels = mapOf<String, CustomPatchLevel>()
|
||||
|
||||
@@ -68,8 +66,6 @@ object ConfigurationManager {
|
||||
// Initial load of all configuration files.
|
||||
loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE))
|
||||
loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE))
|
||||
storeTeeStatus() // Check and store the current TEE status.
|
||||
|
||||
// Start watching for any subsequent file changes.
|
||||
ConfigObserver.startWatching()
|
||||
SystemLogger.info("Configuration initialized and file observer started.")
|
||||
@@ -87,33 +83,40 @@ object ConfigurationManager {
|
||||
return packages.firstNotNullOfOrNull { pkg -> packageKeyboxes[pkg] } ?: DEFAULT_KEYBOX_FILE
|
||||
}
|
||||
|
||||
/** Determines if the certificate for a given UID needs to be patched. */
|
||||
fun shouldPatch(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.PATCH
|
||||
fun shouldPatch(uid: Int): Boolean {
|
||||
val mode = getPackageModeForUid(uid)
|
||||
return mode == Mode.PATCH || mode == Mode.AUTO
|
||||
}
|
||||
|
||||
/** Determines if a new certificate needs to be generated for a given UID. */
|
||||
fun shouldGenerate(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.GENERATE
|
||||
|
||||
/** Determines if no operation is needed for a given UID. */
|
||||
fun shouldSkipUid(uid: Int): Boolean = getPackageModeForUid(uid) == null
|
||||
|
||||
/** Resolves the operating mode for a given UID based on its packages and the TEE status. */
|
||||
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
|
||||
}
|
||||
|
||||
private fun getPackageModeForUid(uid: Int): Mode? {
|
||||
val packages = getPackagesForUid(uid)
|
||||
if (packages.isEmpty()) return null
|
||||
|
||||
// Lazily load TEE status if it hasn't been checked yet.
|
||||
if (isTeeBroken == null) loadTeeStatus()
|
||||
|
||||
// Find the first configured mode for any of the UID's packages.
|
||||
for (pkg in packages) {
|
||||
when (packageModes[pkg]) {
|
||||
Mode.GENERATE -> return Mode.GENERATE
|
||||
Mode.PATCH -> return Mode.PATCH
|
||||
Mode.AUTO -> return if (isTeeBroken == true) Mode.GENERATE else Mode.PATCH
|
||||
null -> continue // No config for this package, check the next one.
|
||||
Mode.AUTO -> return if (DeviceAttestationService.isTeeFunctional) Mode.PATCH else Mode.GENERATE
|
||||
null -> continue
|
||||
}
|
||||
}
|
||||
return null // No configuration found for this UID.
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,7 +174,6 @@ object ConfigurationManager {
|
||||
newModes[pkg] = Mode.PATCH
|
||||
newKeyboxes[pkg] = currentKeybox
|
||||
}
|
||||
// No suffix means AUTO mode.
|
||||
else -> {
|
||||
newModes[trimmedLine] = Mode.AUTO
|
||||
newKeyboxes[trimmedLine] = currentKeybox
|
||||
@@ -280,29 +282,6 @@ object ConfigurationManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks the device's TEE status and writes the result to a file for persistence. */
|
||||
private fun storeTeeStatus() {
|
||||
val statusFile = File(configRoot, TEE_STATUS_FILE)
|
||||
isTeeBroken = !DeviceAttestationService.isTeeFunctional
|
||||
try {
|
||||
statusFile.writeText("tee_broken=$isTeeBroken")
|
||||
SystemLogger.info("TEE status stored: isTeeBroken=$isTeeBroken")
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to write TEE status to file.", e)
|
||||
}
|
||||
}
|
||||
|
||||
/** Loads the TEE status from the file. */
|
||||
private fun loadTeeStatus() {
|
||||
val statusFile = File(configRoot, TEE_STATUS_FILE)
|
||||
isTeeBroken =
|
||||
if (statusFile.exists()) {
|
||||
statusFile.readText().trim() == "tee_broken=true"
|
||||
} else {
|
||||
null // Status is unknown.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A FileObserver that monitors the configuration directory for changes and triggers reloads of
|
||||
* the relevant settings.
|
||||
@@ -327,10 +306,15 @@ object ConfigurationManager {
|
||||
)
|
||||
KeyBoxManager.invalidateCache(path)
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
|
||||
// Clear cached keys possibly containing old certificates
|
||||
// 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.
|
||||
org.matrix.TEESimulator.interception.keystore.shim
|
||||
.KeyMintSecurityLevelInterceptor
|
||||
.clearAllGeneratedKeys("updating $file")
|
||||
.invalidatePatchedChains("updating $file")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -360,7 +344,29 @@ object ConfigurationManager {
|
||||
return iPackageManager
|
||||
}
|
||||
|
||||
/** Retrieves the package names associated with a UID. */
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getPackagesForUid(uid: Int): Array<String> {
|
||||
return uidToPackagesCache.getOrPut(uid) {
|
||||
try {
|
||||
|
||||
@@ -109,17 +109,17 @@ abstract class BinderInterceptor : Binder() {
|
||||
* `handlePostTransact`).
|
||||
*/
|
||||
final override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
|
||||
// The native hook prepends a transaction ID to the data parcel.
|
||||
val txId = data.readLong()
|
||||
val result =
|
||||
val result = try {
|
||||
when (code) {
|
||||
// These codes are defined in the native layer to distinguish hook types.
|
||||
PRE_TRANSACT_CODE -> handlePreTransact(txId, data)
|
||||
POST_TRANSACT_CODE -> handlePostTransact(txId, data)
|
||||
else -> return super.onTransact(code, data, reply, flags)
|
||||
}
|
||||
|
||||
// The reply parcel is guaranteed to be non-null for our custom transactions.
|
||||
} catch (e: Throwable) {
|
||||
SystemLogger.error("[TX_ID: $txId] Interceptor exception, falling through to HAL", e)
|
||||
TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
writeResultToReply(result, reply!!)
|
||||
return true
|
||||
}
|
||||
@@ -293,15 +293,21 @@ abstract class BinderInterceptor : Binder() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Uses the backdoor binder to register an interceptor for a specific target service. */
|
||||
fun register(backdoor: IBinder, target: IBinder, interceptor: BinderInterceptor) {
|
||||
fun register(
|
||||
backdoor: IBinder,
|
||||
target: IBinder,
|
||||
interceptor: BinderInterceptor,
|
||||
filteredCodes: IntArray = intArrayOf(),
|
||||
) {
|
||||
val data = Parcel.obtain()
|
||||
val reply = Parcel.obtain()
|
||||
try {
|
||||
data.writeStrongBinder(target)
|
||||
data.writeStrongBinder(interceptor)
|
||||
data.writeInt(filteredCodes.size)
|
||||
for (code in filteredCodes) data.writeInt(code)
|
||||
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
|
||||
SystemLogger.info("Registered interceptor for target: $target")
|
||||
SystemLogger.info("Registered interceptor for target: $target (${filteredCodes.size} filtered codes)")
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to register binder interceptor.", e)
|
||||
} finally {
|
||||
|
||||
+3
-2
@@ -68,11 +68,12 @@ abstract class AbstractKeystoreInterceptor : BinderInterceptor() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Registers this interceptor with the native hook layer and sets up a death recipient. */
|
||||
protected open val interceptedCodes: IntArray = intArrayOf()
|
||||
|
||||
private fun setupInterceptor(service: IBinder, backdoor: IBinder) {
|
||||
keystoreService = service
|
||||
SystemLogger.info("Registering interceptor for service: $serviceName")
|
||||
register(backdoor, service, this)
|
||||
register(backdoor, service, this, interceptedCodes)
|
||||
service.linkToDeath(createDeathRecipient(), 0)
|
||||
onInterceptorReady(service, backdoor)
|
||||
}
|
||||
|
||||
+98
-2
@@ -1,11 +1,16 @@
|
||||
package org.matrix.TEESimulator.interception.keystore
|
||||
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.KeyParameterValue
|
||||
import android.hardware.security.keymint.Tag
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.security.KeyStore
|
||||
import android.security.keystore.KeystoreResponse
|
||||
import android.system.keystore2.Authorization
|
||||
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||
|
||||
data class KeyIdentifier(val uid: Int, val alias: String)
|
||||
|
||||
@@ -14,11 +19,29 @@ object InterceptorUtils {
|
||||
|
||||
private const val EX_SERVICE_SPECIFIC = -8
|
||||
|
||||
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)
|
||||
writeString(null)
|
||||
}
|
||||
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||
}
|
||||
@@ -93,12 +116,21 @@ object InterceptorUtils {
|
||||
fun <T : Parcelable?> createTypedObjectReply(
|
||||
obj: T,
|
||||
flags: Int = 0,
|
||||
diagnosticTag: String? = null,
|
||||
): BinderInterceptor.TransactionResult.OverrideReply {
|
||||
val parcel =
|
||||
Parcel.obtain().apply {
|
||||
writeNoException()
|
||||
writeTypedObject(obj, flags)
|
||||
}
|
||||
if (diagnosticTag != null && SystemLogger.isDebugBuild) {
|
||||
val savedPos = parcel.dataPosition()
|
||||
val wire = parcel.marshall()
|
||||
parcel.setDataPosition(savedPos)
|
||||
val path = "/data/local/tmp/teesim-$diagnosticTag.bin"
|
||||
runCatching { java.io.File(path).writeBytes(wire) }
|
||||
SystemLogger.debug("[$diagnosticTag] reply len=${wire.size} path=$path")
|
||||
}
|
||||
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||
}
|
||||
|
||||
@@ -119,6 +151,70 @@ object InterceptorUtils {
|
||||
|
||||
/** Checks if a reply parcel contains an exception without consuming it. */
|
||||
fun hasException(reply: Parcel): Boolean {
|
||||
return runCatching { reply.readException() }.exceptionOrNull() != null
|
||||
val exception = runCatching { reply.readException() }.exceptionOrNull()
|
||||
if (exception != null) reply.setDataPosition(0)
|
||||
return exception != null
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return authorizations
|
||||
.map { auth ->
|
||||
val replacement =
|
||||
when (auth.keyParameter.tag) {
|
||||
Tag.OS_PATCHLEVEL ->
|
||||
if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) osPatch else null
|
||||
Tag.VENDOR_PATCHLEVEL ->
|
||||
if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) vendorPatch
|
||||
else null
|
||||
Tag.BOOT_PATCHLEVEL ->
|
||||
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) bootPatch else null
|
||||
else -> null
|
||||
}
|
||||
if (replacement != null) {
|
||||
Authorization().apply {
|
||||
keyParameter =
|
||||
KeyParameter().apply {
|
||||
tag = auth.keyParameter.tag
|
||||
value = KeyParameterValue.integer(replacement)
|
||||
}
|
||||
securityLevel = auth.securityLevel
|
||||
}
|
||||
} else {
|
||||
auth
|
||||
}
|
||||
}
|
||||
.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
+360
-32
@@ -5,11 +5,14 @@ import android.hardware.security.keymint.SecurityLevel
|
||||
import android.os.Build
|
||||
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
|
||||
@@ -44,6 +47,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
if (Build.VERSION.SDK_INT >= 34)
|
||||
InterceptorUtils.getTransactCode(stubBinderClass, "listEntriesBatched")
|
||||
else null
|
||||
private val GET_NUMBER_OF_ENTRIES_TRANSACTION =
|
||||
InterceptorUtils.getTransactCode(stubBinderClass, "getNumberOfEntries")
|
||||
private val GRANT_TRANSACTION = InterceptorUtils.getTransactCode(stubBinderClass, "grant")
|
||||
private val UNGRANT_TRANSACTION = InterceptorUtils.getTransactCode(stubBinderClass, "ungrant")
|
||||
|
||||
private val transactionNames: Map<Int, String> by lazy {
|
||||
stubBinderClass.declaredFields
|
||||
@@ -54,10 +61,40 @@ 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
|
||||
|
||||
// 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).
|
||||
@@ -65,6 +102,27 @@ 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) {
|
||||
@@ -74,7 +132,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...")
|
||||
val interceptor =
|
||||
KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
|
||||
register(backdoor, tee.asBinder(), interceptor)
|
||||
register(
|
||||
backdoor,
|
||||
tee.asBinder(),
|
||||
interceptor,
|
||||
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
|
||||
)
|
||||
interceptor.loadPersistedKeys()
|
||||
}
|
||||
}
|
||||
@@ -86,7 +149,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
|
||||
val interceptor =
|
||||
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
|
||||
register(backdoor, strongbox.asBinder(), interceptor)
|
||||
register(
|
||||
backdoor,
|
||||
strongbox.asBinder(),
|
||||
interceptor,
|
||||
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
|
||||
)
|
||||
interceptor.loadPersistedKeys()
|
||||
}
|
||||
}
|
||||
@@ -102,7 +170,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
callingPid: Int,
|
||||
data: Parcel,
|
||||
): TransactionResult {
|
||||
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) {
|
||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
|
||||
return if (ConfigurationManager.shouldSkipUid(callingUid))
|
||||
TransactionResult.ContinueAndSkipPost
|
||||
else TransactionResult.Continue
|
||||
} else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
|
||||
|
||||
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
|
||||
@@ -134,41 +207,121 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
) {
|
||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||
|
||||
if (ConfigurationManager.shouldSkipUid(callingUid))
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
|
||||
if (code == UPDATE_SUBCOMPONENT_TRANSACTION)
|
||||
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
|
||||
|
||||
if (descriptor.alias != null) {
|
||||
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
|
||||
} else {
|
||||
SystemLogger.info(
|
||||
"Skip ${transactionNames[code]!!} for key [alias, blob, domain, nspace]: [${descriptor.alias}, ${descriptor.blob}, ${descriptor.domain}, ${descriptor.nspace}]"
|
||||
)
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
// 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 0x4) == 0) { // GET_INFO = 0x4 (access-vector gate)
|
||||
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)
|
||||
}
|
||||
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
||||
|
||||
if (ConfigurationManager.shouldSkipUid(callingUid))
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
|
||||
if (code == DELETE_KEY_TRANSACTION) {
|
||||
if (KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null) {
|
||||
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)
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response."
|
||||
)
|
||||
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||
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
|
||||
}
|
||||
|
||||
val response =
|
||||
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
||||
?: return TransactionResult.Continue
|
||||
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}"
|
||||
)
|
||||
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}"
|
||||
)
|
||||
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
|
||||
}
|
||||
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)
|
||||
}
|
||||
return TransactionResult.Continue
|
||||
}
|
||||
|
||||
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
|
||||
SystemLogger.info("${descriptor.alias} was an attestation key")
|
||||
@@ -178,6 +331,57 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
KeyMintParameterLogger.logParameter(it.keyParameter)
|
||||
}
|
||||
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,
|
||||
@@ -203,10 +407,33 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
reply: Parcel?,
|
||||
resultCode: Int,
|
||||
): TransactionResult {
|
||||
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
|
||||
return TransactionResult.SkipTransaction
|
||||
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 (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) {
|
||||
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||
return runCatching {
|
||||
val hardwareCount = reply.readInt()
|
||||
val softwareCount =
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys.keys.count {
|
||||
it.uid == callingUid
|
||||
}
|
||||
val totalCount = hardwareCount + softwareCount
|
||||
val parcel = Parcel.obtain().apply {
|
||||
writeNoException()
|
||||
writeInt(totalCount)
|
||||
}
|
||||
TransactionResult.OverrideReply(parcel)
|
||||
}
|
||||
.getOrElse {
|
||||
SystemLogger.error("[TX_ID: $txId] Failed to modify getNumberOfEntries.", it)
|
||||
TransactionResult.SkipTransaction
|
||||
}
|
||||
} else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||
|
||||
return runCatching {
|
||||
@@ -241,23 +468,44 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
val response = reply.readTypedObject(KeyEntryResponse.CREATOR)!!
|
||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||
|
||||
if (userUpdatedKeys.remove(keyId)) {
|
||||
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: userUpdated=true, skipping patch" }
|
||||
SystemLogger.debug("[TX_ID: $txId] Skipping cert patch for user-updated key $keyId.")
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
val authorizations = response.metadata.authorizations
|
||||
val parsedParameters =
|
||||
KeyMintAttestation(
|
||||
authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
|
||||
)
|
||||
|
||||
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: isImport=${parsedParameters.isImportKey()} origin=${parsedParameters.origin} inImportedKeys=${KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)} hasPatchedChain=${KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) != null} isAttestKey=${parsedParameters.isAttestKey()}" }
|
||||
|
||||
if (parsedParameters.isImportKey()) {
|
||||
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
||||
if (retainedChain == null) {
|
||||
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, no retained chain, skip" }
|
||||
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, SERVING RETAINED CHAIN (detection vector!)" }
|
||||
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
|
||||
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
|
||||
response.metadata.authorizations =
|
||||
InterceptorUtils.patchAuthorizations(
|
||||
response.metadata.authorizations,
|
||||
callingUid,
|
||||
)
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
}
|
||||
|
||||
if (KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)) {
|
||||
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: in importedKeys set, skip" }
|
||||
SystemLogger.debug("[TX_ID: $txId] Skipping attest-key override for imported key $keyId")
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
if (parsedParameters.isAttestKey()) {
|
||||
SystemLogger.warning(
|
||||
"[TX_ID: $txId] Found hardware attest key ${keyId.alias} in the reply."
|
||||
@@ -276,28 +524,52 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
keyData.second.toTypedArray(),
|
||||
)
|
||||
.getOrThrow()
|
||||
response.metadata.authorizations =
|
||||
InterceptorUtils.patchAuthorizations(
|
||||
response.metadata.authorizations,
|
||||
callingUid,
|
||||
)
|
||||
|
||||
keyDescriptor.nspace = SecureRandom().nextLong()
|
||||
val newNspace = SecureRandom().nextLong()
|
||||
response.metadata.key?.let { it.nspace = newNspace }
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys[keyId] =
|
||||
KeyMintSecurityLevelInterceptor.GeneratedKeyInfo(
|
||||
keyData.first,
|
||||
keyDescriptor.nspace,
|
||||
null,
|
||||
newNspace,
|
||||
response,
|
||||
parsedParameters,
|
||||
)
|
||||
KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId)
|
||||
|
||||
// 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,
|
||||
nspace = keyDescriptor.nspace,
|
||||
secretKey = null,
|
||||
nspace = newNspace,
|
||||
securityLevel = response.metadata.keySecurityLevel,
|
||||
certChain = keyData.second,
|
||||
algorithm = parsedParameters.algorithm,
|
||||
keySize = parsedParameters.keySize,
|
||||
ecCurve = parsedParameters.ecCurve,
|
||||
ecCurve = parsedParameters.ecCurve ?: 0,
|
||||
purposes = parsedParameters.purpose,
|
||||
digests = parsedParameters.digest,
|
||||
isAttestationKey = true,
|
||||
metadataBytes = metadataBytesForPersist,
|
||||
)
|
||||
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
@@ -326,10 +598,16 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
)
|
||||
finalChain =
|
||||
AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
||||
KeyMintSecurityLevelInterceptor.patchedChains[keyId] = finalChain
|
||||
}
|
||||
|
||||
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
|
||||
.getOrThrow()
|
||||
response.metadata.authorizations =
|
||||
InterceptorUtils.patchAuthorizations(
|
||||
response.metadata.authorizations,
|
||||
callingUid,
|
||||
)
|
||||
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
}
|
||||
@@ -344,12 +622,62 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
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
|
||||
}
|
||||
|
||||
private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult {
|
||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||
?: return TransactionResult.ContinueAndSkipPost
|
||||
|
||||
val generatedKeyInfo =
|
||||
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(callingUid, descriptor?.nspace)
|
||||
?: return TransactionResult.ContinueAndSkipPost
|
||||
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
|
||||
}
|
||||
|
||||
SystemLogger.info("Updating sub-component with key[${generatedKeyInfo.nspace}]")
|
||||
val metadata = generatedKeyInfo.response.metadata
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
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
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
+18
@@ -431,6 +431,24 @@ private data class LegacyKeygenParameters(
|
||||
manufacturer = null,
|
||||
model = null,
|
||||
secondImei = null,
|
||||
activeDateTime = null,
|
||||
originationExpireDateTime = null,
|
||||
usageExpireDateTime = null,
|
||||
usageCountLimit = null,
|
||||
callerNonce = null,
|
||||
nonce = null,
|
||||
unlockedDeviceRequired = null,
|
||||
includeUniqueId = null,
|
||||
rollbackResistance = null,
|
||||
earlyBootOnly = null,
|
||||
allowWhileOnBody = null,
|
||||
trustedUserPresenceRequired = null,
|
||||
trustedConfirmationRequired = null,
|
||||
noAuthRequired = null,
|
||||
maxUsesPerBoot = null,
|
||||
maxBootLevel = null,
|
||||
minMacLength = null,
|
||||
rsaOaepMgfDigest = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ object ListEntriesHandler {
|
||||
startPastAlias: String?,
|
||||
): List<KeyDescriptor> {
|
||||
return KeyMintSecurityLevelInterceptor.generatedKeys.keys
|
||||
.filter { it.uid == uid && (startPastAlias == null || it.alias < startPastAlias) }
|
||||
.filter { it.uid == uid && (startPastAlias == null || it.alias > startPastAlias) }
|
||||
.map { keyId ->
|
||||
KeyDescriptor().apply {
|
||||
this.domain = Domain.APP
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package org.matrix.TEESimulator.interception.keystore.shim
|
||||
|
||||
import android.hardware.security.keymint.Algorithm
|
||||
import android.hardware.security.keymint.KeyPurpose
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.Tag
|
||||
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||
|
||||
object AuthorizeCreate {
|
||||
|
||||
fun check(
|
||||
keyParams: KeyMintAttestation?,
|
||||
opParams: KeyMintAttestation,
|
||||
rawOpParams: Array<KeyParameter>? = null,
|
||||
): Int? {
|
||||
if (keyParams == null) return null
|
||||
val purpose = opParams.purpose.firstOrNull() ?: return null
|
||||
// Algorithm-level rejection runs before purpose-list check (AOSP HAL behavior)
|
||||
return checkAlgorithmPurpose(keyParams, purpose)
|
||||
?: checkPurpose(keyParams, purpose)
|
||||
?: checkTemporalValidity(keyParams, purpose)
|
||||
?: checkCallerNonce(keyParams, purpose, rawOpParams)
|
||||
}
|
||||
|
||||
private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
|
||||
val algo = keyParams.algorithm
|
||||
if ((algo == Algorithm.EC || algo == Algorithm.RSA) &&
|
||||
(purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.ENCRYPT)
|
||||
) {
|
||||
return KeystoreErrorCodes.unsupportedPurpose
|
||||
}
|
||||
if (algo == Algorithm.RSA && purpose == KeyPurpose.AGREE_KEY)
|
||||
return KeystoreErrorCodes.unsupportedPurpose
|
||||
return null
|
||||
}
|
||||
|
||||
private fun checkPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
|
||||
if (purpose == KeyPurpose.WRAP_KEY)
|
||||
return KeystoreErrorCodes.incompatiblePurpose
|
||||
if (purpose !in keyParams.purpose)
|
||||
return KeystoreErrorCodes.incompatiblePurpose
|
||||
return null
|
||||
}
|
||||
|
||||
private fun checkTemporalValidity(keyParams: KeyMintAttestation, purpose: Int): Int? {
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
keyParams.activeDateTime?.let { activeDate ->
|
||||
if (now < activeDate.time) return KeystoreErrorCodes.keyNotYetValid
|
||||
}
|
||||
|
||||
keyParams.originationExpireDateTime?.let { expireDate ->
|
||||
if (purpose == KeyPurpose.SIGN || purpose == KeyPurpose.ENCRYPT) {
|
||||
if (now > expireDate.time) return KeystoreErrorCodes.keyExpired
|
||||
}
|
||||
}
|
||||
|
||||
keyParams.usageExpireDateTime?.let { expireDate ->
|
||||
if (purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.DECRYPT) {
|
||||
if (now > expireDate.time) return KeystoreErrorCodes.keyExpired
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun checkCallerNonce(keyParams: KeyMintAttestation, purpose: Int, rawOpParams: Array<KeyParameter>?): Int? {
|
||||
if (purpose != KeyPurpose.SIGN && purpose != KeyPurpose.ENCRYPT) return null
|
||||
if (keyParams.callerNonce == true) return null
|
||||
if (rawOpParams?.any { it.tag == Tag.NONCE } == true)
|
||||
return KeystoreErrorCodes.callerNonceProhibited
|
||||
return null
|
||||
}
|
||||
}
|
||||
+128
-10
@@ -29,13 +29,47 @@ data class PersistedKeyData(
|
||||
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 {
|
||||
|
||||
private const val FORMAT_VERSION = 1
|
||||
/**
|
||||
* 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
|
||||
@@ -47,7 +81,8 @@ object GeneratedKeyPersistence {
|
||||
|
||||
fun save(
|
||||
keyId: KeyIdentifier,
|
||||
keyPair: KeyPair,
|
||||
keyPair: KeyPair?,
|
||||
secretKey: javax.crypto.SecretKey?,
|
||||
nspace: Long,
|
||||
securityLevel: Int,
|
||||
certChain: List<Certificate>,
|
||||
@@ -57,7 +92,11 @@ object GeneratedKeyPersistence {
|
||||
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")
|
||||
@@ -87,7 +126,8 @@ object GeneratedKeyPersistence {
|
||||
out.writeInt(digests.size)
|
||||
digests.forEach { out.writeInt(it) }
|
||||
|
||||
val pkBytes = keyPair.private.encoded
|
||||
// Asymmetric key block (empty for symmetric-only).
|
||||
val pkBytes = keyPair?.private?.encoded ?: ByteArray(0)
|
||||
out.writeInt(pkBytes.size)
|
||||
out.write(pkBytes)
|
||||
|
||||
@@ -97,6 +137,23 @@ object GeneratedKeyPersistence {
|
||||
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()
|
||||
@@ -129,6 +186,7 @@ object GeneratedKeyPersistence {
|
||||
val file = File(PERSISTENCE_DIR, keyFileName(keyId.uid, keyId.alias))
|
||||
if (file.exists()) {
|
||||
if (file.delete()) {
|
||||
fileLocks.remove(keyFileName(keyId.uid, keyId.alias))
|
||||
SystemLogger.debug("Deleted persisted key: $keyId")
|
||||
} else {
|
||||
SystemLogger.warning("Failed to delete persisted key file: ${file.name}")
|
||||
@@ -158,6 +216,7 @@ object GeneratedKeyPersistence {
|
||||
if (file.delete()) count++
|
||||
}
|
||||
}
|
||||
fileLocks.clear()
|
||||
SystemLogger.info("Deleted $count persisted key files")
|
||||
}.onFailure { e ->
|
||||
SystemLogger.error("Failed to delete all persisted keys", e)
|
||||
@@ -187,8 +246,17 @@ object GeneratedKeyPersistence {
|
||||
DataInputStream(BufferedInputStream(FileInputStream(file))).use { input ->
|
||||
val version = input.readInt()
|
||||
if (version != FORMAT_VERSION) {
|
||||
SystemLogger.warning(
|
||||
"Skipping ${file.name}: unknown format version $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
|
||||
}
|
||||
@@ -210,7 +278,7 @@ object GeneratedKeyPersistence {
|
||||
|
||||
val pkLen = requireBounds(input.readInt(), 8192, "pkLen")
|
||||
val pkBytes = ByteArray(pkLen)
|
||||
input.readFully(pkBytes)
|
||||
if (pkLen > 0) input.readFully(pkBytes)
|
||||
|
||||
val certCount = requireBounds(input.readInt(), 10, "certCount")
|
||||
val certChainBytes = (0 until certCount).map {
|
||||
@@ -220,6 +288,17 @@ object GeneratedKeyPersistence {
|
||||
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(
|
||||
@@ -235,6 +314,9 @@ object GeneratedKeyPersistence {
|
||||
digests = digests,
|
||||
privateKeyBytes = pkBytes,
|
||||
certChainBytes = certChainBytes,
|
||||
metadataBytes = metadataBytes,
|
||||
symmetricKeyBytes = skBytes,
|
||||
symmetricAlgorithm = skAlgo,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -290,7 +372,7 @@ object GeneratedKeyPersistence {
|
||||
DataInputStream(BufferedInputStream(FileInputStream(existing))).use { input ->
|
||||
val version = input.readInt()
|
||||
if (version != FORMAT_VERSION) {
|
||||
SystemLogger.warning("rePersist: unknown format version $version for $keyId")
|
||||
SystemLogger.warning("rePersist: legacy format version $version for $keyId, will not re-persist (next generateKey replaces it)")
|
||||
return
|
||||
}
|
||||
readPersistedKeyData(input)
|
||||
@@ -301,9 +383,29 @@ object GeneratedKeyPersistence {
|
||||
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 = generatedKeyInfo.keyPair,
|
||||
keyPair = keyPair,
|
||||
secretKey = secretKey,
|
||||
nspace = generatedKeyInfo.nspace,
|
||||
securityLevel = secLevel,
|
||||
certChain = newChain.toList(),
|
||||
@@ -313,6 +415,7 @@ object GeneratedKeyPersistence {
|
||||
purposes = persisted.purposes,
|
||||
digests = persisted.digests,
|
||||
isAttestationKey = persisted.isAttestationKey,
|
||||
metadataBytes = metadataBytes,
|
||||
)
|
||||
SystemLogger.debug("Re-persisted key $keyId with updated cert chain")
|
||||
}
|
||||
@@ -329,7 +432,8 @@ object GeneratedKeyPersistence {
|
||||
return digest.joinToString("") { "%02x".format(it) } + ".bin"
|
||||
}
|
||||
|
||||
// Reads all fields after version has already been consumed
|
||||
// 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()
|
||||
@@ -348,7 +452,7 @@ object GeneratedKeyPersistence {
|
||||
|
||||
val pkLen = requireBounds(input.readInt(), 8192, "pkLen")
|
||||
val pkBytes = ByteArray(pkLen)
|
||||
input.readFully(pkBytes)
|
||||
if (pkLen > 0) input.readFully(pkBytes)
|
||||
|
||||
val certCount = requireBounds(input.readInt(), 10, "certCount")
|
||||
val certChainBytes = (0 until certCount).map {
|
||||
@@ -358,6 +462,17 @@ object GeneratedKeyPersistence {
|
||||
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,
|
||||
@@ -371,6 +486,9 @@ object GeneratedKeyPersistence {
|
||||
digests = digests,
|
||||
privateKeyBytes = pkBytes,
|
||||
certChainBytes = certChainBytes,
|
||||
metadataBytes = metadataBytes,
|
||||
symmetricKeyBytes = skBytes,
|
||||
symmetricAlgorithm = skAlgo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1034
-180
File diff suppressed because it is too large
Load Diff
+8
@@ -13,6 +13,7 @@ import org.matrix.TEESimulator.interception.keystore.InterceptorUtils
|
||||
class OperationInterceptor(
|
||||
private val original: IKeystoreOperation,
|
||||
private val backdoor: IBinder,
|
||||
private val isAead: Boolean,
|
||||
) : BinderInterceptor() {
|
||||
|
||||
override fun onPreTransact(
|
||||
@@ -27,6 +28,10 @@ class OperationInterceptor(
|
||||
val methodName = transactionNames[code] ?: "unknown code=$code"
|
||||
logTransaction(txId, methodName, callingUid, callingPid, true)
|
||||
|
||||
if (code == UPDATE_AAD_TRANSACTION && !isAead) {
|
||||
return InterceptorUtils.createServiceSpecificErrorReply(KeystoreErrorCodes.invalidTag)
|
||||
}
|
||||
|
||||
if (code == FINISH_TRANSACTION || code == ABORT_TRANSACTION) {
|
||||
KeyMintSecurityLevelInterceptor.removeOperationInterceptor(target, backdoor)
|
||||
}
|
||||
@@ -44,6 +49,9 @@ class OperationInterceptor(
|
||||
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
|
||||
|
||||
+302
-48
@@ -3,10 +3,15 @@ package org.matrix.TEESimulator.interception.keystore.shim
|
||||
import android.hardware.security.keymint.Algorithm
|
||||
import android.hardware.security.keymint.BlockMode
|
||||
import android.hardware.security.keymint.Digest
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.KeyParameterValue
|
||||
import android.hardware.security.keymint.KeyPurpose
|
||||
import android.hardware.security.keymint.PaddingMode
|
||||
import android.os.RemoteException
|
||||
import android.hardware.security.keymint.Tag
|
||||
import android.os.ServiceSpecificException
|
||||
import java.util.concurrent.locks.LockSupport
|
||||
import android.system.keystore2.IKeystoreOperation
|
||||
import android.system.keystore2.KeyParameters
|
||||
import java.security.KeyPair
|
||||
import java.security.Signature
|
||||
import java.security.SignatureException
|
||||
@@ -15,16 +20,16 @@ import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
|
||||
// A sealed interface to represent the different cryptographic operations we can perform.
|
||||
private sealed interface CryptoPrimitive {
|
||||
fun updateAad(aadInput: ByteArray?) {
|
||||
throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
|
||||
}
|
||||
fun update(data: ByteArray?): ByteArray?
|
||||
|
||||
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
|
||||
|
||||
fun abort()
|
||||
fun getBeginParameters(): Array<KeyParameter>? = null
|
||||
}
|
||||
|
||||
// Helper object to map KeyMint constants to JCA algorithm strings.
|
||||
private object JcaAlgorithmMapper {
|
||||
fun mapSignatureAlgorithm(params: KeyMintAttestation): String {
|
||||
val digest =
|
||||
@@ -34,16 +39,18 @@ private object JcaAlgorithmMapper {
|
||||
Digest.SHA_2_512 -> "SHA512"
|
||||
else -> "NONE"
|
||||
}
|
||||
val keyAlgo =
|
||||
when (params.algorithm) {
|
||||
Algorithm.EC -> "ECDSA"
|
||||
Algorithm.RSA -> "RSA"
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported signature algorithm: ${params.algorithm}"
|
||||
)
|
||||
return 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"
|
||||
}
|
||||
return "${digest}with${keyAlgo}"
|
||||
else ->
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCodes.incompatibleAlgorithm,
|
||||
"Unsupported signature algorithm: ${params.algorithm}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
|
||||
@@ -52,30 +59,32 @@ private object JcaAlgorithmMapper {
|
||||
Algorithm.RSA -> "RSA"
|
||||
Algorithm.AES -> "AES"
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported cipher algorithm: ${params.algorithm}"
|
||||
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" // Default for RSA
|
||||
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" // Default for GCM
|
||||
else -> "NoPadding"
|
||||
}
|
||||
return "$keyAlgo/$blockMode/$padding"
|
||||
}
|
||||
}
|
||||
|
||||
// Concrete implementation for Signing.
|
||||
private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
|
||||
private val signature: Signature =
|
||||
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
|
||||
@@ -95,7 +104,6 @@ private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimi
|
||||
override fun abort() {}
|
||||
}
|
||||
|
||||
// Concrete implementation for Verification.
|
||||
private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
|
||||
private val signature: Signature =
|
||||
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
|
||||
@@ -109,107 +117,353 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri
|
||||
|
||||
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
if (data != null) update(data)
|
||||
if (signature == null) throw SignatureException("Signature to verify is null")
|
||||
if (signature == null) {
|
||||
throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature to verify is null")
|
||||
}
|
||||
if (!this.signature.verify(signature)) {
|
||||
// Throwing an exception is how Keystore signals verification failure.
|
||||
throw SignatureException("Signature verification failed")
|
||||
throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature verification failed")
|
||||
}
|
||||
// A successful verification returns no data.
|
||||
return null
|
||||
}
|
||||
|
||||
override fun abort() {}
|
||||
}
|
||||
|
||||
// Concrete implementation for Encryption/Decryption.
|
||||
private class CipherPrimitive(
|
||||
keyPair: KeyPair,
|
||||
cryptoKey: java.security.Key,
|
||||
params: KeyMintAttestation,
|
||||
private val opMode: Int,
|
||||
) : CryptoPrimitive {
|
||||
private val isAead = params.blockMode.firstOrNull() == BlockMode.GCM
|
||||
private val cipher: Cipher =
|
||||
Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply {
|
||||
val key = if (opMode == Cipher.ENCRYPT_MODE) keyPair.public else keyPair.private
|
||||
init(opMode, key)
|
||||
val nonce = params.nonce
|
||||
if (nonce != null && isAead) {
|
||||
init(opMode, cryptoKey, javax.crypto.spec.GCMParameterSpec(128, nonce))
|
||||
} else if (nonce != null) {
|
||||
init(opMode, cryptoKey, javax.crypto.spec.IvParameterSpec(nonce))
|
||||
} else {
|
||||
init(opMode, cryptoKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateAad(aadInput: ByteArray?) {
|
||||
if (!isAead) throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
|
||||
if (aadInput != null) cipher.updateAAD(aadInput)
|
||||
}
|
||||
|
||||
override fun update(data: ByteArray?): ByteArray? =
|
||||
if (data != null) cipher.update(data) else null
|
||||
|
||||
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? =
|
||||
if (data != null) cipher.doFinal(data) else cipher.doFinal()
|
||||
|
||||
override fun getBeginParameters(): Array<KeyParameter>? {
|
||||
val iv = cipher.iv ?: return null
|
||||
return arrayOf(
|
||||
KeyParameter().apply {
|
||||
tag = Tag.NONCE
|
||||
value = KeyParameterValue.blob(iv)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun abort() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* A software-only implementation of a cryptographic operation. This class acts as a controller,
|
||||
* delegating to a specific cryptographic primitive based on the operation's purpose.
|
||||
*/
|
||||
class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMintAttestation) {
|
||||
// This now holds the specific strategy object (Signer, Verifier, etc.)
|
||||
private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive {
|
||||
private val agreement: javax.crypto.KeyAgreement =
|
||||
javax.crypto.KeyAgreement.getInstance("ECDH").apply { init(keyPair.private) }
|
||||
|
||||
override fun update(data: ByteArray?): ByteArray? = null
|
||||
|
||||
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
if (data == null)
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCodes.invalidArgument,
|
||||
"Peer public key required for key agreement",
|
||||
)
|
||||
val peerKey =
|
||||
java.security.KeyFactory.getInstance("EC")
|
||||
.generatePublic(java.security.spec.X509EncodedKeySpec(data))
|
||||
agreement.doPhase(peerKey, true)
|
||||
return agreement.generateSecret()
|
||||
}
|
||||
|
||||
override fun abort() {}
|
||||
}
|
||||
|
||||
class SoftwareOperation(
|
||||
private val txId: Long,
|
||||
keyPair: KeyPair?,
|
||||
secretKey: javax.crypto.SecretKey?,
|
||||
params: KeyMintAttestation,
|
||||
private val latencyFloorMs: Long = 0L,
|
||||
) {
|
||||
private val primitive: CryptoPrimitive
|
||||
@Volatile var finalized = false
|
||||
private set
|
||||
|
||||
var onFinishCallback: (() -> Unit)? = null
|
||||
|
||||
val beginParameters: KeyParameters?
|
||||
get() {
|
||||
val params = primitive.getBeginParameters() ?: return null
|
||||
if (params.isEmpty()) return null
|
||||
return KeyParameters().apply { keyParameter = params }
|
||||
}
|
||||
|
||||
init {
|
||||
// The "Strategy" pattern: choose the implementation based on the purpose.
|
||||
// For simplicity, we only consider the first purpose listed.
|
||||
val purpose = params.purpose.firstOrNull()
|
||||
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
|
||||
|
||||
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 =
|
||||
when (purpose) {
|
||||
KeyPurpose.SIGN -> Signer(keyPair, params)
|
||||
KeyPurpose.VERIFY -> Verifier(keyPair, params)
|
||||
KeyPurpose.ENCRYPT -> CipherPrimitive(keyPair, params, Cipher.ENCRYPT_MODE)
|
||||
KeyPurpose.DECRYPT -> CipherPrimitive(keyPair, params, Cipher.DECRYPT_MODE)
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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 UnsupportedOperationException("Unsupported operation purpose: $purpose")
|
||||
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 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)
|
||||
// Re-throw the exception so the binder can report it to the client.
|
||||
throw 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)
|
||||
}
|
||||
|
||||
fun resolveField(className: String, fieldName: String, fallback: Int): Int =
|
||||
runCatching {
|
||||
Class.forName(className).getField(fieldName).getInt(null)
|
||||
}.getOrElse {
|
||||
SystemLogger.debug("Resolved $className.$fieldName via fallback: $fallback")
|
||||
fallback
|
||||
}
|
||||
}
|
||||
|
||||
/** The Binder interface for our [SoftwareOperation]. */
|
||||
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
||||
IKeystoreOperation.Stub() {
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
@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)
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
@Synchronized
|
||||
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
return operation.finish(input, signature)
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
@Synchronized
|
||||
override fun abort() {
|
||||
operation.abort()
|
||||
}
|
||||
|
||||
@@ -1,41 +1,86 @@
|
||||
package org.matrix.TEESimulator.logging
|
||||
|
||||
import android.util.Log
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import org.matrix.TEESimulator.BuildConfig
|
||||
|
||||
/**
|
||||
* A centralized logging utility for the TEESimulator application. This object provides a consistent
|
||||
* logging tag and format for all application logs, making it easier to filter and debug in Logcat.
|
||||
*
|
||||
* Includes a rate limiter that caps logd syscalls during binder stress to prevent thread pool
|
||||
* contention. The first [RATE_LIMIT_BURST] messages per [RATE_LIMIT_WINDOW_MS] window are logged
|
||||
* normally; subsequent messages are suppressed and a summary is emitted when the window resets.
|
||||
*/
|
||||
object SystemLogger {
|
||||
// The tag used for all log messages from this application.
|
||||
private const val TAG = "TEESimulator"
|
||||
@PublishedApi internal const val TAG = "TEESimulator"
|
||||
|
||||
private val isDebugBuild = BuildConfig.DEBUG
|
||||
@PublishedApi internal val isDebugBuild = BuildConfig.DEBUG
|
||||
|
||||
// Rate limiter: allow BURST messages per WINDOW, then suppress until window resets.
|
||||
private const val RATE_LIMIT_BURST = 15
|
||||
private const val RATE_LIMIT_WINDOW_MS = 1000L
|
||||
private val windowStart = AtomicLong(System.currentTimeMillis())
|
||||
private val windowCount = AtomicInteger(0)
|
||||
private val suppressedCount = AtomicInteger(0)
|
||||
|
||||
/**
|
||||
* Returns true if this message should be emitted. Resets the window if expired
|
||||
* and emits a suppression summary for the previous window.
|
||||
*/
|
||||
@PublishedApi internal fun acquireLogPermit(): Boolean {
|
||||
val now = System.currentTimeMillis()
|
||||
val start = windowStart.get()
|
||||
if (now - start > RATE_LIMIT_WINDOW_MS) {
|
||||
// Window expired: reset and emit suppression summary if needed.
|
||||
if (windowStart.compareAndSet(start, now)) {
|
||||
val suppressed = suppressedCount.getAndSet(0)
|
||||
windowCount.set(1) // this call counts as #1 in the new window
|
||||
if (suppressed > 0) {
|
||||
Log.i(TAG, "[rate-limit] suppressed $suppressed log messages in previous window")
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
val count = windowCount.incrementAndGet()
|
||||
if (count <= RATE_LIMIT_BURST) return true
|
||||
suppressedCount.incrementAndGet()
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a debug message. Use this for fine-grained information that is useful for debugging.
|
||||
*
|
||||
* @param message The message to log.
|
||||
*/
|
||||
fun debug(message: String) {
|
||||
if (!isDebugBuild) return
|
||||
if (!acquireLogPermit()) return
|
||||
Log.d(TAG, message)
|
||||
}
|
||||
|
||||
/** Lazy debug: lambda only evaluates if message will be logged. */
|
||||
inline fun debug(message: () -> String) {
|
||||
if (!isDebugBuild) return
|
||||
if (!acquireLogPermit()) return
|
||||
Log.d(TAG, message())
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an informational message. Use this to report major application lifecycle events.
|
||||
*
|
||||
* @param message The message to log.
|
||||
*/
|
||||
fun info(message: String) {
|
||||
if (!acquireLogPermit()) return
|
||||
Log.i(TAG, message)
|
||||
}
|
||||
|
||||
/** Lazy info: lambda only evaluates if message will be logged. */
|
||||
inline fun info(message: () -> String) {
|
||||
if (!acquireLogPermit()) return
|
||||
Log.i(TAG, message())
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a warning message. 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.
|
||||
* Logs a warning message. Warnings are never rate-limited.
|
||||
*/
|
||||
fun warning(message: String, throwable: Throwable? = null) {
|
||||
if (throwable != null) {
|
||||
@@ -46,11 +91,7 @@ object SystemLogger {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Logs an error message. Errors are never rate-limited.
|
||||
*/
|
||||
fun error(message: String, throwable: Throwable? = null) {
|
||||
if (throwable != null) {
|
||||
@@ -63,11 +104,22 @@ object SystemLogger {
|
||||
/**
|
||||
* Logs a verbose message. This level is for highly detailed logs that are generally not needed
|
||||
* unless tracking a very specific issue.
|
||||
*
|
||||
* @param message The message to log.
|
||||
*/
|
||||
fun verbose(message: String) {
|
||||
if (!isDebugBuild) return
|
||||
if (!acquireLogPermit()) return
|
||||
Log.v(TAG, message)
|
||||
}
|
||||
|
||||
/** Lazy verbose: lambda only evaluates if message will be logged. */
|
||||
inline fun verbose(message: () -> String) {
|
||||
if (!isDebugBuild) return
|
||||
if (!acquireLogPermit()) return
|
||||
Log.v(TAG, message())
|
||||
}
|
||||
|
||||
inline fun trace(message: () -> String) {
|
||||
if (!isDebugBuild) return
|
||||
Log.w(TAG, message())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import java.math.BigInteger
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.X509Certificate
|
||||
import java.security.spec.ECGenParameterSpec
|
||||
import java.security.spec.RSAKeyGenParameterSpec
|
||||
import java.util.Date
|
||||
@@ -36,6 +35,8 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
||||
*/
|
||||
object CertificateGenerator {
|
||||
|
||||
private const val UNDEFINED_NOT_AFTER = 253402300799000L
|
||||
|
||||
/**
|
||||
* Generates a software-based cryptographic key pair.
|
||||
*
|
||||
@@ -49,7 +50,10 @@ object CertificateGenerator {
|
||||
Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName)
|
||||
Algorithm.RSA ->
|
||||
"RSA" to
|
||||
RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent)
|
||||
RSAKeyGenParameterSpec(
|
||||
params.keySize,
|
||||
params.rsaPublicExponent ?: RSAKeyGenParameterSpec.F4,
|
||||
)
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported algorithm: ${params.algorithm}"
|
||||
@@ -88,33 +92,38 @@ object CertificateGenerator {
|
||||
"Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})"
|
||||
)
|
||||
|
||||
return runCatching {
|
||||
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)
|
||||
|
||||
// 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) =
|
||||
val attestKeyInfo =
|
||||
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)
|
||||
}
|
||||
getAttestationKeyInfo(uid, attestKeyAlias)
|
||||
} else null
|
||||
|
||||
val (signingKey, issuer) = attestKeyInfo
|
||||
?.let { it.first to it.second }
|
||||
?: (keybox.keyPair to getIssuerFromKeybox(keybox))
|
||||
|
||||
// Build the new leaf certificate with the simulated attestation.
|
||||
val leafCert =
|
||||
buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel)
|
||||
|
||||
// If not self-attesting, the chain is just the leaf. Otherwise, append the keybox
|
||||
// chain.
|
||||
if (attestKeyAlias != null) {
|
||||
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
|
||||
}
|
||||
.onFailure { SystemLogger.error("Failed to generate certificate chain.", it) }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,7 +137,7 @@ object CertificateGenerator {
|
||||
params: KeyMintAttestation,
|
||||
securityLevel: Int,
|
||||
): Pair<KeyPair, List<Certificate>>? {
|
||||
return runCatching {
|
||||
return try {
|
||||
SystemLogger.info(
|
||||
"Generating new attested key pair for alias: '$alias' (UID: $uid)"
|
||||
)
|
||||
@@ -144,11 +153,12 @@ object CertificateGenerator {
|
||||
"Successfully generated new certificate chain for alias: '$alias'."
|
||||
)
|
||||
Pair(newKeyPair, chain)
|
||||
} catch (e: android.os.ServiceSpecificException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", e)
|
||||
null
|
||||
}
|
||||
.onFailure {
|
||||
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", it)
|
||||
}
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
fun getIssuerFromKeybox(keybox: KeyBox) =
|
||||
@@ -163,7 +173,10 @@ object CertificateGenerator {
|
||||
else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm")
|
||||
}
|
||||
return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName)
|
||||
?: throw Exception("Could not load keybox for UID $uid and algorithm $algorithmName")
|
||||
?: throw android.os.ServiceSpecificException(
|
||||
-75, // ATTESTATION_KEYS_NOT_PROVISIONED
|
||||
"No attestation key for algorithm $algorithmName in $keyboxFile",
|
||||
)
|
||||
}
|
||||
|
||||
/** Retrieves the key pair and issuer name for a given attestation key alias. */
|
||||
@@ -213,17 +226,16 @@ object CertificateGenerator {
|
||||
uid: Int,
|
||||
securityLevel: Int,
|
||||
): Certificate {
|
||||
val subject = params.certificateSubject ?: X500Name("CN=Android KeyStore Key")
|
||||
val leafNotAfter =
|
||||
(signingKeyPair.public as? X509Certificate)?.notAfter
|
||||
?: Date(System.currentTimeMillis() + 31536000000L)
|
||||
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(
|
||||
issuer,
|
||||
params.certificateSerial ?: BigInteger.ONE,
|
||||
params.certificateNotBefore ?: Date(),
|
||||
params.certificateNotAfter ?: leafNotAfter,
|
||||
notBefore,
|
||||
notAfter,
|
||||
subject,
|
||||
subjectKeyPair.public,
|
||||
)
|
||||
@@ -233,16 +245,17 @@ object CertificateGenerator {
|
||||
if (keyUsageBits != 0) {
|
||||
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
|
||||
}
|
||||
// Add our custom, simulated attestation extension.
|
||||
builder.addExtension(
|
||||
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
|
||||
)
|
||||
if (params.attestationChallenge != null) {
|
||||
builder.addExtension(
|
||||
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
|
||||
)
|
||||
}
|
||||
|
||||
val signerAlgorithm =
|
||||
when (params.algorithm) {
|
||||
Algorithm.EC -> "SHA256withECDSA"
|
||||
Algorithm.RSA -> "SHA256withRSA"
|
||||
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}")
|
||||
when (signingKeyPair.private.algorithm) {
|
||||
"EC", "ECDSA" -> "SHA256withECDSA"
|
||||
"RSA" -> "SHA256withRSA"
|
||||
else -> throw IllegalArgumentException("Unsupported signing key: ${signingKeyPair.private.algorithm}")
|
||||
}
|
||||
val contentSigner =
|
||||
JcaContentSignerBuilder(signerAlgorithm)
|
||||
@@ -251,4 +264,39 @@ object CertificateGenerator {
|
||||
|
||||
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
||||
}
|
||||
|
||||
// AOSP ta/src/keys.rs:452-478, ta/src/cert.rs:111-114
|
||||
private fun buildSelfSignedCertificate(
|
||||
keyPair: KeyPair,
|
||||
params: KeyMintAttestation,
|
||||
): Certificate {
|
||||
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
|
||||
val notBefore = params.certificateNotBefore ?: Date(0)
|
||||
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
|
||||
|
||||
val builder = JcaX509v3CertificateBuilder(
|
||||
subject,
|
||||
params.certificateSerial ?: BigInteger.ONE,
|
||||
notBefore,
|
||||
notAfter,
|
||||
subject,
|
||||
keyPair.public,
|
||||
)
|
||||
|
||||
val keyUsageBits = buildKeyUsageFromPurposes(params.purpose)
|
||||
if (keyUsageBits != 0) {
|
||||
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
|
||||
}
|
||||
|
||||
val signerAlgorithm = when (keyPair.private.algorithm) {
|
||||
"EC", "ECDSA" -> "SHA256withECDSA"
|
||||
"RSA" -> "SHA256withRSA"
|
||||
else -> throw IllegalArgumentException("Unsupported key: ${keyPair.private.algorithm}")
|
||||
}
|
||||
val contentSigner = JcaContentSignerBuilder(signerAlgorithm)
|
||||
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||
.build(keyPair.private)
|
||||
|
||||
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,13 @@ data class CertGenConfig(
|
||||
val idManufacturer: ByteArray?,
|
||||
val idModel: ByteArray?,
|
||||
val idSecondImei: ByteArray?,
|
||||
val activeDatetime: Long = -1L,
|
||||
val originationExpireDatetime: Long = -1L,
|
||||
val usageExpireDatetime: Long = -1L,
|
||||
val usageCountLimit: Int = -1,
|
||||
val callerNonce: Boolean = false,
|
||||
val unlockedDeviceRequired: Boolean = false,
|
||||
val noAuthRequired: Boolean = true,
|
||||
)
|
||||
|
||||
object NativeCertGen {
|
||||
@@ -105,7 +112,7 @@ object NativeCertGen {
|
||||
}
|
||||
|
||||
val algorithmName = when (certs[0].publicKey.algorithm) {
|
||||
"EC" -> "EC"
|
||||
"EC", "ECDSA" -> "EC"
|
||||
"RSA" -> "RSA"
|
||||
else -> certs[0].publicKey.algorithm
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.matrix.TEESimulator.util
|
||||
|
||||
import android.hardware.security.keymint.SecurityLevel
|
||||
import android.os.Build
|
||||
import android.os.SystemProperties
|
||||
import java.io.ByteArrayOutputStream
|
||||
@@ -91,27 +90,33 @@ object AndroidDeviceUtils {
|
||||
attestationValueProvider: () -> ByteArray?,
|
||||
expectedSize: Int,
|
||||
): ByteArray {
|
||||
// 1. Attempt to get the value from the system property.
|
||||
getProperty(propertyName, expectedSize)?.let {
|
||||
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
|
||||
persistToFile(propertyName, it)
|
||||
return it
|
||||
}
|
||||
|
||||
// 2. Fallback to the value from a cached TEE attestation.
|
||||
try {
|
||||
attestationValueProvider()?.let {
|
||||
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
|
||||
setProperty(propertyName, it) // Persist for consistency
|
||||
setProperty(propertyName, it)
|
||||
persistToFile(propertyName, it)
|
||||
return it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to get $propertyName from attestation.", e)
|
||||
}
|
||||
|
||||
// 3. As a final fallback, generate a random value.
|
||||
readFromFile(propertyName, expectedSize)?.let {
|
||||
SystemLogger.debug("Using $propertyName from persistent file: ${it.toHex()}")
|
||||
setProperty(propertyName, it)
|
||||
return it
|
||||
}
|
||||
|
||||
return generateRandomBytes(expectedSize).also {
|
||||
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
|
||||
setProperty(propertyName, it)
|
||||
persistToFile(propertyName, it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,10 +163,55 @@ object AndroidDeviceUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/** Generates a cryptographically random byte array of a specified length. */
|
||||
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? {
|
||||
return try {
|
||||
val file = fileForProperty(propertyName)
|
||||
if (!file.exists()) return null
|
||||
val bytes = file.readBytes()
|
||||
if (bytes.size == expectedSize) bytes else null
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to read $propertyName from file.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// --- Patch Level Properties ---
|
||||
|
||||
fun getPatchLevel(uid: Int): Int {
|
||||
@@ -295,7 +345,9 @@ object AndroidDeviceUtils {
|
||||
6 -> { // YYYYMM
|
||||
val year = normalized.substring(0, 4).toInt()
|
||||
val month = normalized.substring(4, 6).toInt()
|
||||
if (isLong) year * 10000 + month * 100 + 1 else year * 100 + month
|
||||
// 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
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
@@ -343,20 +395,26 @@ object AndroidDeviceUtils {
|
||||
)
|
||||
|
||||
/**
|
||||
* Retrieves the attestation version based on security level and OS version. StrongBox (level 2)
|
||||
* requires version 300.
|
||||
* Retrieves the attestation version for the given security level. The value follows the device
|
||||
* OS: cached attestation data wins, then attestVersionMap[SDK_INT], then 400 as last resort.
|
||||
* A static StrongBox=300 floor would force a major-version mismatch with the TEE chain on
|
||||
* Android 16 devices that report keymaster 400 across both security levels.
|
||||
*
|
||||
* @param securityLevel The security level of the attestation (1 for TEE, 2 for StrongBox).
|
||||
* @return The appropriate attestation version number.
|
||||
*/
|
||||
fun getAttestVersion(securityLevel: Int): Int {
|
||||
// StrongBox security level requires an attestation version of at least 300.
|
||||
if (securityLevel == SecurityLevel.STRONGBOX) {
|
||||
return 300
|
||||
}
|
||||
return DeviceAttestationService.CachedAttestationData?.attestVersion
|
||||
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")
|
||||
return version
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -365,10 +423,7 @@ object AndroidDeviceUtils {
|
||||
* @param securityLevel The security level, used to determine the correct attestation version.
|
||||
* @return The appropriate Keymaster or KeyMint version number.
|
||||
*/
|
||||
fun getKeymasterVersion(securityLevel: Int): Int {
|
||||
val attestVersion = getAttestVersion(securityLevel)
|
||||
return if (attestVersion >= 100) attestVersion else 41 // Keymaster 4.1 for older versions
|
||||
}
|
||||
fun getKeymasterVersion(securityLevel: Int): Int = getAttestVersion(securityLevel)
|
||||
|
||||
// --- APEX and Module Hash Properties ---
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.matrix.TEESimulator.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
|
||||
object AndroidPermissionUtils {
|
||||
|
||||
@SuppressLint("PrivateApi", "DiscouragedPrivateApi")
|
||||
private fun getGlobalContext(): Context? {
|
||||
return try {
|
||||
// 1. Get the hidden ActivityThread class via reflection
|
||||
val activityThreadClass = Class.forName("android.app.ActivityThread")
|
||||
|
||||
// 2. Invoke the static currentActivityThread() method
|
||||
val currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread")
|
||||
currentActivityThreadMethod.isAccessible = true
|
||||
val activityThread = currentActivityThreadMethod.invoke(null)
|
||||
|
||||
if (activityThread == null) {
|
||||
SystemLogger.warning("Reflection: ActivityThread.currentActivityThread() returned null")
|
||||
return null
|
||||
}
|
||||
|
||||
// 3. Try to get the application context
|
||||
val getApplicationMethod = activityThreadClass.getDeclaredMethod("getApplication")
|
||||
getApplicationMethod.isAccessible = true
|
||||
val application = getApplicationMethod.invoke(activityThread) as? Context
|
||||
|
||||
if (application != null) return application
|
||||
|
||||
// 4. Fallback to getSystemContext() if application is null (often happens in system_server)
|
||||
val getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext")
|
||||
getSystemContextMethod.isAccessible = true
|
||||
getSystemContextMethod.invoke(activityThread) as? Context
|
||||
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Reflection failed to get global context for permission check", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Core permission check.
|
||||
*/
|
||||
fun hasPermission(uid: Int, permission: String): Boolean {
|
||||
val context = getGlobalContext() ?: run {
|
||||
SystemLogger.warning("AndroidPermissionUtils: Context is null, failing permission check safely.")
|
||||
return false
|
||||
}
|
||||
|
||||
val result = context.checkPermission(permission, -1, uid)
|
||||
return result == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
fun hasDeviceAttestationPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.READ_PRIVILEGED_PHONE_STATE")
|
||||
}
|
||||
|
||||
fun hasUniqueIdAttestationPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.REQUEST_UNIQUE_ID_ATTESTATION")
|
||||
}
|
||||
|
||||
fun hasManageUsersPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.MANAGE_USERS")
|
||||
}
|
||||
|
||||
fun hasDumpPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.DUMP")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
+37
-2
@@ -2,10 +2,45 @@
|
||||
MODDIR=${0%/*}
|
||||
CONFIG_DIR=/data/adb/tricky_store
|
||||
|
||||
. "$MODDIR/action_i18n.sh"
|
||||
|
||||
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 " "
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 "Persistent key storage cleared"
|
||||
echo " "
|
||||
echo " ✅ $(_msg confirm_cleared)"
|
||||
else
|
||||
echo "No persistent key storage found"
|
||||
echo " "
|
||||
echo " ℹ️ $(_msg confirm_not_found)"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
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
|
||||
}
|
||||
@@ -1,3 +1,281 @@
|
||||
## 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.
|
||||
|
||||
+20
-2
@@ -15,7 +15,7 @@ fi
|
||||
|
||||
# --- Version Info ---
|
||||
VERSION=$(grep_prop version "${TMPDIR}/module.prop")
|
||||
ui_print "- Installing TEESimulator $VERSION"
|
||||
ui_print "- Installing TEESimulator-RS $VERSION"
|
||||
ui_print ""
|
||||
|
||||
# --- Architecture Handling ---
|
||||
@@ -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 uninstall.sh; do
|
||||
for file in customize.sh module.prop service.sh sepolicy.rule daemon action.sh action_i18n.sh uninstall.sh; do
|
||||
install_file "$file" "$MODPATH"
|
||||
done
|
||||
|
||||
@@ -91,3 +91,21 @@ 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
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
id=tricky_store
|
||||
name=TEESimulator
|
||||
name=TEESimulator-RS
|
||||
version=${REPLACEMEVER}
|
||||
versionCode=${REPLACEMEVERCODE}
|
||||
author=JingMatrix, Enginex0
|
||||
description=Software simulation for Android hardware-backed key pairs with key attestation
|
||||
updateJson=https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/update.json
|
||||
updateJson=https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/update.json
|
||||
|
||||
@@ -1,2 +1,16 @@
|
||||
allow keystore {adb_data_file shell_data_file} file *
|
||||
allow crash_dump keystore 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
|
||||
|
||||
@@ -3,3 +3,14 @@ cd $MODDIR
|
||||
|
||||
# Fork-based supervisor for instant restart
|
||||
./supervisor ./daemon "$MODDIR" &
|
||||
|
||||
# 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 ""
|
||||
) &
|
||||
|
||||
@@ -9,3 +9,5 @@ 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"
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": "v4.0",
|
||||
"versionCode": 90,
|
||||
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.0/TEESimulator-v4.0-Release.zip",
|
||||
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md"
|
||||
"version": "v6.0.1-251",
|
||||
"versionCode": 251,
|
||||
"zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/download/v6.0.1-251/TEESimulator-RS-v6.0.1-251-Release.zip",
|
||||
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/changelog.md"
|
||||
}
|
||||
|
||||
@@ -33,6 +33,36 @@ pub fn build_attestation_extension(params: &CertGenParams) -> Result<Vec<u8>> {
|
||||
fn build_software_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
|
||||
let mut fields: Vec<(u32, Vec<u8>)> = Vec::new();
|
||||
|
||||
// Tag 303: CALLER_NONCE — NULL (presence = true)
|
||||
if params.caller_nonce {
|
||||
fields.push((303, enc_null()));
|
||||
}
|
||||
|
||||
// Tag 400: ACTIVE_DATETIME — INTEGER (milliseconds)
|
||||
if params.active_datetime >= 0 {
|
||||
fields.push((400, enc_integer(params.active_datetime)));
|
||||
}
|
||||
|
||||
// Tag 401: ORIGINATION_EXPIRE_DATETIME — INTEGER (milliseconds)
|
||||
if params.origination_expire_datetime >= 0 {
|
||||
fields.push((401, enc_integer(params.origination_expire_datetime)));
|
||||
}
|
||||
|
||||
// Tag 402: USAGE_EXPIRE_DATETIME — INTEGER (milliseconds)
|
||||
if params.usage_expire_datetime >= 0 {
|
||||
fields.push((402, enc_integer(params.usage_expire_datetime)));
|
||||
}
|
||||
|
||||
// Tag 405: USAGE_COUNT_LIMIT — INTEGER
|
||||
if params.usage_count_limit >= 0 {
|
||||
fields.push((405, enc_integer(params.usage_count_limit as i64)));
|
||||
}
|
||||
|
||||
// Tag 509: UNLOCKED_DEVICE_REQUIRED — NULL
|
||||
if params.unlocked_device_required {
|
||||
fields.push((509, enc_null()));
|
||||
}
|
||||
|
||||
// Tag 701: CREATION_DATETIME — INTEGER (milliseconds)
|
||||
fields.push((701, enc_integer(params.creation_datetime)));
|
||||
|
||||
@@ -77,8 +107,10 @@ fn build_tee_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
|
||||
fields.push((10, enc_integer(curve as i32 as i64)));
|
||||
}
|
||||
|
||||
// Tag 503: NO_AUTH_REQUIRED — NULL (presence = true)
|
||||
fields.push((503, enc_null()));
|
||||
// Tag 503: NO_AUTH_REQUIRED — NULL (conditional)
|
||||
if params.no_auth_required {
|
||||
fields.push((503, enc_null()));
|
||||
}
|
||||
|
||||
// Tag 702: ORIGIN — INTEGER 0 (GENERATED)
|
||||
fields.push((702, enc_integer(0)));
|
||||
@@ -519,6 +551,44 @@ mod tests {
|
||||
assert_eq!(tags, sorted, "AuthorizationList fields must be sorted by tag number");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enforcement_tags_in_software_enforced() {
|
||||
let mut params = make_test_params();
|
||||
params.usage_count_limit = 3;
|
||||
params.unlocked_device_required = true;
|
||||
params.caller_nonce = true;
|
||||
params.active_datetime = 1709913600000;
|
||||
let sw = build_software_enforced(¶ms).unwrap();
|
||||
let inner = skip_tlv_header(&sw);
|
||||
let tags = extract_tag_numbers(inner);
|
||||
assert!(tags.contains(&303), "CALLER_NONCE (303) must be in softwareEnforced");
|
||||
assert!(tags.contains(&400), "ACTIVE_DATETIME (400) must be in softwareEnforced");
|
||||
assert!(tags.contains(&405), "USAGE_COUNT_LIMIT (405) must be in softwareEnforced");
|
||||
assert!(tags.contains(&509), "UNLOCKED_DEVICE_REQUIRED (509) must be in softwareEnforced");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_auth_required_conditional() {
|
||||
let mut params = make_test_params();
|
||||
params.no_auth_required = false;
|
||||
let tee = build_tee_enforced(¶ms).unwrap();
|
||||
let inner = skip_tlv_header(&tee);
|
||||
let tags = extract_tag_numbers(inner);
|
||||
assert!(!tags.contains(&503), "NO_AUTH_REQUIRED (503) must be absent when false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enforcement_tags_omitted_when_unset() {
|
||||
let params = make_test_params();
|
||||
let sw = build_software_enforced(¶ms).unwrap();
|
||||
let inner = skip_tlv_header(&sw);
|
||||
let tags = extract_tag_numbers(inner);
|
||||
assert!(!tags.contains(&303), "CALLER_NONCE should be absent when false");
|
||||
assert!(!tags.contains(&400), "ACTIVE_DATETIME should be absent when -1");
|
||||
assert!(!tags.contains(&405), "USAGE_COUNT_LIMIT should be absent when -1");
|
||||
assert!(!tags.contains(&509), "UNLOCKED_DEVICE_REQUIRED should be absent when false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_extension_roundtrip() {
|
||||
let params = make_test_params();
|
||||
@@ -568,6 +638,13 @@ mod tests {
|
||||
id_manufacturer: None,
|
||||
id_model: None,
|
||||
id_second_imei: None,
|
||||
active_datetime: -1,
|
||||
origination_expire_datetime: -1,
|
||||
usage_expire_datetime: -1,
|
||||
usage_count_limit: -1,
|
||||
caller_nonce: false,
|
||||
unlocked_device_required: false,
|
||||
no_auth_required: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,69 @@ 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, ¶ms.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(¬_before, ¬_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: &[u8],
|
||||
attestation_ext_der: Option<&[u8]>,
|
||||
keybox: &ParsedKeybox,
|
||||
params: &CertGenParams,
|
||||
) -> Result<Vec<Vec<u8>>> {
|
||||
@@ -33,7 +93,7 @@ pub fn build_certificate_chain(
|
||||
|
||||
fn build_leaf_cert(
|
||||
key_pair: &GeneratedKeyPair,
|
||||
attestation_ext_der: &[u8],
|
||||
attestation_ext_der: Option<&[u8]>,
|
||||
keybox: &ParsedKeybox,
|
||||
params: &CertGenParams,
|
||||
) -> Result<Vec<u8>> {
|
||||
@@ -51,7 +111,7 @@ fn build_leaf_cert(
|
||||
let subject_dn_der = if let Some(ref subject) = params.cert_subject {
|
||||
subject.clone()
|
||||
} else {
|
||||
encode_simple_cn_dn("Android KeyStore Key")
|
||||
encode_simple_cn_dn("Android Keystore Key")
|
||||
};
|
||||
|
||||
// Validity
|
||||
@@ -63,7 +123,6 @@ fn build_leaf_cert(
|
||||
timestamp_to_datetime(params.cert_not_after)?
|
||||
};
|
||||
|
||||
// Extensions
|
||||
let extensions_der = build_extensions(attestation_ext_der, ¶ms.purposes)?;
|
||||
|
||||
// TBS Certificate
|
||||
@@ -256,19 +315,19 @@ fn extract_rsa_spki(pkcs8_der: &[u8]) -> Result<Vec<u8>> {
|
||||
Ok(encode_der_sequence(&[&alg_id, &pub_key_bits]))
|
||||
}
|
||||
|
||||
fn build_extensions(attestation_ext_der: &[u8], purposes: &[i32]) -> Result<Vec<u8>> {
|
||||
fn build_extensions(attestation_ext_der: Option<&[u8]>, purposes: &[i32]) -> Result<Vec<u8>> {
|
||||
let mut extensions: Vec<Vec<u8>> = Vec::new();
|
||||
|
||||
// KeyUsage extension (critical)
|
||||
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);
|
||||
}
|
||||
|
||||
// Attestation extension (non-critical)
|
||||
let attest_ext = build_extension(&encode_der_oid(ATTESTATION_OID), false, attestation_ext_der);
|
||||
extensions.push(attest_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))
|
||||
}
|
||||
|
||||
@@ -62,14 +62,13 @@ fn generate_attested_inner(env: &mut JNIEnv, config: &JObject) -> Result<jbyteAr
|
||||
|
||||
let keybox = keybox::parse_keybox(¶ms.keybox_cert_chain, ¶ms.keybox_private_key)?;
|
||||
|
||||
let attest_ext = attestation::build_attestation_extension(¶ms)?;
|
||||
|
||||
let cert_chain = certbuilder::build_certificate_chain(
|
||||
&key_pair,
|
||||
&attest_ext,
|
||||
&keybox,
|
||||
¶ms,
|
||||
)?;
|
||||
let cert_chain = if params.attestation_challenge.is_some() {
|
||||
let attest_ext = attestation::build_attestation_extension(¶ms)?;
|
||||
certbuilder::build_certificate_chain(&key_pair, Some(&attest_ext), &keybox, ¶ms)?
|
||||
} else {
|
||||
tracing::info!("no attestation challenge, generating self-signed cert (depth 1)");
|
||||
certbuilder::build_self_signed_cert(&key_pair, ¶ms)?
|
||||
};
|
||||
|
||||
let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain);
|
||||
|
||||
@@ -199,6 +198,14 @@ fn extract_config(env: &mut JNIEnv, config: &JObject) -> Result<CertGenParams> {
|
||||
let id_model = get_nullable_byte_array(env, config, "idModel")?;
|
||||
let id_second_imei = get_nullable_byte_array(env, config, "idSecondImei")?;
|
||||
|
||||
let active_datetime = get_long(env, config, "activeDatetime")?;
|
||||
let origination_expire_datetime = get_long(env, config, "originationExpireDatetime")?;
|
||||
let usage_expire_datetime = get_long(env, config, "usageExpireDatetime")?;
|
||||
let usage_count_limit = get_int(env, config, "usageCountLimit")?;
|
||||
let caller_nonce = get_boolean(env, config, "callerNonce")?;
|
||||
let unlocked_device_required = get_boolean(env, config, "unlockedDeviceRequired")?;
|
||||
let no_auth_required = get_boolean(env, config, "noAuthRequired")?;
|
||||
|
||||
Ok(CertGenParams {
|
||||
algorithm: Algorithm::try_from(algorithm)?,
|
||||
key_size: key_size as u32,
|
||||
@@ -238,6 +245,13 @@ fn extract_config(env: &mut JNIEnv, config: &JObject) -> Result<CertGenParams> {
|
||||
id_manufacturer,
|
||||
id_model,
|
||||
id_second_imei,
|
||||
active_datetime,
|
||||
origination_expire_datetime,
|
||||
usage_expire_datetime,
|
||||
usage_count_limit,
|
||||
caller_nonce,
|
||||
unlocked_device_required,
|
||||
no_auth_required,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -253,6 +267,10 @@ fn get_long(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<i64> {
|
||||
Ok(env.get_field(obj, name, "J")?.j()?)
|
||||
}
|
||||
|
||||
fn get_boolean(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<bool> {
|
||||
Ok(env.get_field(obj, name, "Z")?.z()?)
|
||||
}
|
||||
|
||||
fn get_byte_array(env: &mut JNIEnv, obj: &JObject, name: &'static str) -> Result<Vec<u8>> {
|
||||
let field = env.get_field(obj, name, "[B")?.l()?;
|
||||
if field.is_null() {
|
||||
|
||||
@@ -42,35 +42,6 @@ impl TryFrom<i32> for EcCurve {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum KeyPurpose {
|
||||
Encrypt = 0,
|
||||
Decrypt = 1,
|
||||
Sign = 2,
|
||||
Verify = 3,
|
||||
WrapKey = 5,
|
||||
AgreeKey = 6,
|
||||
AttestKey = 7,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum SecurityLevel {
|
||||
Software = 0,
|
||||
TrustedEnvironment = 1,
|
||||
StrongBox = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum VerifiedBootState {
|
||||
Verified = 0,
|
||||
SelfSigned = 1,
|
||||
Unverified = 2,
|
||||
Failed = 3,
|
||||
}
|
||||
|
||||
pub struct CertGenParams {
|
||||
pub algorithm: Algorithm,
|
||||
pub key_size: u32,
|
||||
@@ -114,6 +85,14 @@ pub struct CertGenParams {
|
||||
pub id_manufacturer: Option<Vec<u8>>,
|
||||
pub id_model: Option<Vec<u8>>,
|
||||
pub id_second_imei: Option<Vec<u8>>,
|
||||
|
||||
pub active_datetime: i64,
|
||||
pub origination_expire_datetime: i64,
|
||||
pub usage_expire_datetime: i64,
|
||||
pub usage_count_limit: i32,
|
||||
pub caller_nonce: bool,
|
||||
pub unlocked_device_required: bool,
|
||||
pub no_auth_required: bool,
|
||||
}
|
||||
|
||||
pub struct GeneratedKeyPair {
|
||||
|
||||
+7
-1
@@ -10,6 +10,12 @@
|
||||
# ./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"
|
||||
@@ -235,7 +241,7 @@ print_summary() {
|
||||
|
||||
# --- Main ---
|
||||
echo ""
|
||||
bold "TEESimulator package pipeline"
|
||||
bold "TEESimulator-RS package pipeline"
|
||||
echo ""
|
||||
|
||||
[[ "$BUILD_RUST" == true ]] && build_rust
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ dependencyResolutionManagement {
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "TEESimulator"
|
||||
rootProject.name = "TEESimulator-RS"
|
||||
|
||||
include(":stub")
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ public interface IPackageManager {
|
||||
|
||||
ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId);
|
||||
|
||||
int checkPermission(String permName, String pkgName, int userId);
|
||||
|
||||
class Stub {
|
||||
public static IPackageManager asInterface(IBinder binder) {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package android.os;
|
||||
|
||||
public class SELinux {
|
||||
public static boolean checkSELinuxAccess(
|
||||
String scon, String tcon, String tclass, String perm) {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@ public class ServiceManager {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
}
|
||||
|
||||
public static boolean isDeclared(String name) {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
}
|
||||
|
||||
public static String[] listServices() {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package android.os;
|
||||
|
||||
public class ServiceSpecificException extends RuntimeException {
|
||||
public final int errorCode;
|
||||
|
||||
public ServiceSpecificException(int errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public ServiceSpecificException(int errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user