Compare commits
118
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b40754fd8b | ||
|
|
0f32194a89 | ||
|
|
64723b2564 | ||
|
|
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 |
@@ -149,3 +149,21 @@ jobs:
|
||||
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 }}
|
||||
|
||||
@@ -5,3 +5,6 @@ app/build
|
||||
build
|
||||
native-certgen/target
|
||||
app/src/main/jniLibs
|
||||
|
||||
# Internal planning/strategy plane, never publish
|
||||
.omc/
|
||||
|
||||
@@ -1,256 +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">
|
||||
<a href="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml"><img src="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml/badge.svg" alt="Build"></a>
|
||||
<img src="https://img.shields.io/badge/version-v4.2-blue?style=for-the-badge" alt="v4.2">
|
||||
<img src="https://img.shields.io/badge/Android-10%2B-green?style=for-the-badge&logo=android" alt="Android 10+">
|
||||
<img src="https://img.shields.io/badge/Telegram-community-blue?style=for-the-badge&logo=telegram" alt="Telegram">
|
||||
<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.
|
||||
|
||||
## 🔨 Building from Source
|
||||
| Suffix | Mode |
|
||||
|--------|------|
|
||||
| `!` | Force software key generation |
|
||||
| `?` | Force leaf certificate patching (real TEE key, patched cert) |
|
||||
| *(none)* | Automatic selection |
|
||||
|
||||
The CI workflow builds on every push to `main`. You can also build locally or trigger a build from your own fork.
|
||||
|
||||
**Prerequisites:** JDK 21, Android SDK/NDK 27, Rust stable with `aarch64-linux-android` target, `cargo-ndk`.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Enginex0/TEESimulator.git
|
||||
cd TEESimulator
|
||||
./gradlew zipRelease zipDebug
|
||||
```
|
||||
|
||||
Output ZIPs land in `out/`. The Gradle build automatically invokes `cargo ndk` to cross-compile `libcertgen.so` before packaging.
|
||||
|
||||
To rebuild from a fork, push to `main` or use **Actions → Build → Run workflow**. The workflow installs all toolchains (Java, Rust, cargo-ndk, ccache) and uploads Release + Debug ZIPs as artifacts.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
All configuration files live at `/data/adb/tricky_store/` and are monitored by `FileObserver` — changes take effect immediately without rebooting.
|
||||
|
||||
### 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)
|
||||
|
||||
+38
-1
@@ -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.8"
|
||||
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)
|
||||
@@ -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() }
|
||||
@@ -124,6 +160,7 @@ androidComponents {
|
||||
dependsOn("strip${capitalized}DebugSymbols")
|
||||
}
|
||||
dependsOn(buildRustCertgen)
|
||||
dependsOn(refreshUpdateJson)
|
||||
|
||||
if (isDebug) {
|
||||
from(variant.artifacts.get(SingleArtifact.APK)) {
|
||||
|
||||
@@ -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,16 +350,10 @@ static sp<BinderStub> g_stub_instance = nullptr;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr binder_size_t kMaxInterceptableDataSize = 256 * 1024;
|
||||
|
||||
void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
||||
if (!txn_data || txn_data->target.ptr == 0)
|
||||
return;
|
||||
|
||||
// Bypass interception for oversized payloads to prevent thread starvation from flood attacks
|
||||
if (txn_data->data_size > kMaxInterceptableDataSize)
|
||||
return;
|
||||
|
||||
// AIDL methods use codes in [FIRST_CALL_TRANSACTION, LAST_CALL_TRANSACTION] (1..0x00ffffff).
|
||||
// System transactions (PING, INTERFACE, DUMP, SHELL_COMMAND) use codes above that range.
|
||||
// Skip those — intercepting a ping adds measurable latency that timing detectors flag.
|
||||
@@ -393,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;
|
||||
@@ -432,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;
|
||||
}
|
||||
}
|
||||
@@ -489,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -33,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()
|
||||
|
||||
@@ -61,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,
|
||||
@@ -194,6 +224,16 @@ object AttestationBuilder {
|
||||
)
|
||||
}
|
||||
|
||||
if (params.blockMode.isNotEmpty()) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_BLOCK_MODE,
|
||||
DERSet(params.blockMode.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.padding.isNotEmpty()) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
@@ -214,14 +254,61 @@ object AttestationBuilder {
|
||||
)
|
||||
}
|
||||
|
||||
val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel)
|
||||
|
||||
if (params.rsaOaepMgfDigest.isNotEmpty() && attestVersion >= 100) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_RSA_OAEP_MGF_DIGEST,
|
||||
DERSet(params.rsaOaepMgfDigest.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.rollbackResistance == true && attestVersion >= 3) {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_ROLLBACK_RESISTANCE, DERNull.INSTANCE)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.earlyBootOnly == true && attestVersion >= 4) {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_EARLY_BOOT_ONLY, DERNull.INSTANCE)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.noAuthRequired == true) {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.allowWhileOnBody == true) {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_ALLOW_WHILE_ON_BODY, DERNull.INSTANCE)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.trustedUserPresenceRequired == true && attestVersion >= 3) {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_TRUSTED_USER_PRESENCE_REQUIRED, DERNull.INSTANCE)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.trustedConfirmationRequired == true && attestVersion >= 3) {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_TRUSTED_CONFIRMATION_REQUIRED, DERNull.INSTANCE)
|
||||
)
|
||||
}
|
||||
|
||||
list.addAll(
|
||||
listOf(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE),
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ORIGIN,
|
||||
ASN1Integer(0L),
|
||||
), // KeyOrigin.GENERATED
|
||||
ASN1Integer((params.origin ?: 0).toLong()),
|
||||
),
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ROOT_OF_TRUST,
|
||||
@@ -325,20 +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(
|
||||
@@ -348,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())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -376,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!")
|
||||
@@ -383,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(
|
||||
@@ -401,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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -41,13 +41,30 @@ 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,
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+94
-1
@@ -1,11 +1,16 @@
|
||||
package org.matrix.TEESimulator.interception.keystore
|
||||
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.KeyParameterValue
|
||||
import android.hardware.security.keymint.Tag
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.security.KeyStore
|
||||
import android.security.keystore.KeystoreResponse
|
||||
import android.system.keystore2.Authorization
|
||||
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||
|
||||
data class KeyIdentifier(val uid: Int, val alias: String)
|
||||
|
||||
@@ -14,10 +19,27 @@ 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(null)
|
||||
writeString(synthesizeSseMessage(errorCode))
|
||||
writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196)
|
||||
writeInt(errorCode)
|
||||
}
|
||||
@@ -94,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)
|
||||
}
|
||||
|
||||
@@ -124,4 +155,66 @@ object InterceptorUtils {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
+347
-31
@@ -5,6 +5,8 @@ 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
|
||||
@@ -45,6 +47,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
if (Build.VERSION.SDK_INT >= 34)
|
||||
InterceptorUtils.getTransactCode(stubBinderClass, "listEntriesBatched")
|
||||
else null
|
||||
private val GET_NUMBER_OF_ENTRIES_TRANSACTION =
|
||||
InterceptorUtils.getTransactCode(stubBinderClass, "getNumberOfEntries")
|
||||
private val GRANT_TRANSACTION = InterceptorUtils.getTransactCode(stubBinderClass, "grant")
|
||||
private val UNGRANT_TRANSACTION = InterceptorUtils.getTransactCode(stubBinderClass, "ungrant")
|
||||
|
||||
private val transactionNames: Map<Int, String> by lazy {
|
||||
stubBinderClass.declaredFields
|
||||
@@ -56,12 +62,39 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
}
|
||||
|
||||
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).
|
||||
@@ -69,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) {
|
||||
@@ -78,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()
|
||||
}
|
||||
}
|
||||
@@ -90,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()
|
||||
}
|
||||
}
|
||||
@@ -106,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()
|
||||
@@ -138,40 +207,113 @@ 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) {
|
||||
val wasSoftwareKey = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null
|
||||
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
||||
if (wasSoftwareKey) {
|
||||
deletedSoftwareKeys.add(keyId)
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response."
|
||||
)
|
||||
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||
val keyId =
|
||||
if (descriptor.alias != null) {
|
||||
KeyIdentifier(callingUid, descriptor.alias)
|
||||
} else if (descriptor.domain == Domain.KEY_ID) {
|
||||
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||
callingUid, descriptor.nspace
|
||||
)?.let { info ->
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys.entries
|
||||
.find { it.value.nspace == info.nspace && it.key.uid == callingUid }
|
||||
?.key
|
||||
}
|
||||
} else null
|
||||
|
||||
if (keyId != null) {
|
||||
val isSoftwareKey =
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(keyId)
|
||||
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
||||
if (isSoftwareKey) {
|
||||
deletedSoftwareKeys.add(keyId)
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] Deleted cached keypair ${keyId.alias}, replying with empty response."
|
||||
)
|
||||
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||
}
|
||||
}
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
if (descriptor.alias == null) {
|
||||
if (descriptor.domain == Domain.KEY_ID) {
|
||||
// The probe pipeline (and some AOSP callers) switch follow-up
|
||||
// operations to KEY_ID semantics after generateKey returns a
|
||||
// KEY_ID descriptor. Without this branch, our software keys
|
||||
// are invisible to KEY_ID-based getKeyEntry calls and the
|
||||
// request falls through to the real keystore2 daemon, which
|
||||
// legitimately responds with KEY_NOT_FOUND. Duck Detector's
|
||||
// TimingSideChannelProbe captures that exception during its
|
||||
// warmup phase and surfaces it as
|
||||
// "Captured private binder exception during timing skip".
|
||||
// Resolving by KEY_ID and returning the cached response keeps
|
||||
// the call on the happy path, eliminating the warmup signal.
|
||||
val info = KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||
callingUid, descriptor.nspace
|
||||
)
|
||||
if (info?.response != null) {
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] Found generated response via KEY_ID nspace=${descriptor.nspace}"
|
||||
)
|
||||
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)) {
|
||||
@@ -189,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,
|
||||
@@ -214,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 {
|
||||
@@ -252,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."
|
||||
@@ -287,20 +524,43 @@ 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,
|
||||
@@ -309,6 +569,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
purposes = parsedParameters.purpose,
|
||||
digests = parsedParameters.digest,
|
||||
isAttestationKey = true,
|
||||
metadataBytes = metadataBytesForPersist,
|
||||
)
|
||||
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
@@ -342,6 +603,11 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
|
||||
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
|
||||
.getOrThrow()
|
||||
response.metadata.authorizations =
|
||||
InterceptorUtils.patchAuthorizations(
|
||||
response.metadata.authorizations,
|
||||
callingUid,
|
||||
)
|
||||
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
}
|
||||
@@ -356,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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+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
|
||||
}
|
||||
}
|
||||
+126
-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()
|
||||
@@ -189,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
|
||||
}
|
||||
@@ -212,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 {
|
||||
@@ -222,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(
|
||||
@@ -237,6 +314,9 @@ object GeneratedKeyPersistence {
|
||||
digests = digests,
|
||||
privateKeyBytes = pkBytes,
|
||||
certChainBytes = certChainBytes,
|
||||
metadataBytes = metadataBytes,
|
||||
symmetricKeyBytes = skBytes,
|
||||
symmetricAlgorithm = skAlgo,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -292,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)
|
||||
@@ -303,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(),
|
||||
@@ -315,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")
|
||||
}
|
||||
@@ -331,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()
|
||||
@@ -350,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 {
|
||||
@@ -360,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,
|
||||
@@ -373,6 +486,9 @@ object GeneratedKeyPersistence {
|
||||
digests = digests,
|
||||
privateKeyBytes = pkBytes,
|
||||
certChainBytes = certChainBytes,
|
||||
metadataBytes = metadataBytes,
|
||||
symmetricKeyBytes = skBytes,
|
||||
symmetricAlgorithm = skAlgo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+899
-202
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
|
||||
|
||||
+224
-33
@@ -3,11 +3,15 @@ package org.matrix.TEESimulator.interception.keystore.shim
|
||||
import android.hardware.security.keymint.Algorithm
|
||||
import android.hardware.security.keymint.BlockMode
|
||||
import android.hardware.security.keymint.Digest
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.KeyParameterValue
|
||||
import android.hardware.security.keymint.KeyPurpose
|
||||
import android.hardware.security.keymint.PaddingMode
|
||||
import android.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
|
||||
@@ -16,15 +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?) {}
|
||||
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 =
|
||||
@@ -41,8 +46,9 @@ private object JcaAlgorithmMapper {
|
||||
if (isPss) "${digest}withRSA/PSS" else "${digest}withRSA"
|
||||
}
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported signature algorithm: ${params.algorithm}"
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCodes.incompatibleAlgorithm,
|
||||
"Unsupported signature algorithm: ${params.algorithm}",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -53,8 +59,9 @@ 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 =
|
||||
@@ -78,7 +85,6 @@ private object JcaAlgorithmMapper {
|
||||
}
|
||||
}
|
||||
|
||||
// Concrete implementation for Signing.
|
||||
private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
|
||||
private val signature: Signature =
|
||||
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
|
||||
@@ -98,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 {
|
||||
@@ -112,42 +117,86 @@ 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() {}
|
||||
}
|
||||
|
||||
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,
|
||||
keyPair: KeyPair?,
|
||||
secretKey: javax.crypto.SecretKey?,
|
||||
params: KeyMintAttestation,
|
||||
private val latencyFloorMs: Long = 0L,
|
||||
) {
|
||||
@@ -155,19 +204,85 @@ class SoftwareOperation(
|
||||
@Volatile var finalized = false
|
||||
private set
|
||||
|
||||
var onFinishCallback: (() -> Unit)? = null
|
||||
|
||||
val beginParameters: KeyParameters?
|
||||
get() {
|
||||
val params = primitive.getBeginParameters() ?: return null
|
||||
if (params.isEmpty()) return null
|
||||
return KeyParameters().apply { keyParameter = params }
|
||||
}
|
||||
|
||||
init {
|
||||
val purpose = params.purpose.firstOrNull()
|
||||
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
|
||||
|
||||
if (purpose == null) {
|
||||
// Defensive: if params somehow restored without a PURPOSE tag
|
||||
// (corrupt v2 metadata, mismatched authorizations array on load,
|
||||
// or future format drift) the original code crashed with NPE
|
||||
// because Signer/Verifier/Cipher all dereference keyPair!!
|
||||
// before checking purpose. Surface a clean keystore error
|
||||
// instead so callers see a normal-looking operation failure
|
||||
// they can recover from rather than the process appearing to
|
||||
// silently corrupt their session.
|
||||
SystemLogger.warning(
|
||||
"[SoftwareOp TX_ID: $txId] Purpose missing on restored key " +
|
||||
"(authorizations=${params.purpose}, keyPair=${if (keyPair != null) "present" else "null"}, " +
|
||||
"secretKey=${if (secretKey != null) "present" else "null"}). " +
|
||||
"Returning unsupportedPurpose."
|
||||
)
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCodes.unsupportedPurpose,
|
||||
"Restored key has no PURPOSE authorization",
|
||||
)
|
||||
}
|
||||
|
||||
primitive =
|
||||
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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,10 +301,18 @@ class SoftwareOperation(
|
||||
}
|
||||
|
||||
fun updateAad(aadInput: ByteArray?) {
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] updateAad() inputSize=${aadInput?.size ?: 0}")
|
||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] updateAad() ENTRY inputSize=${aadInput?.size ?: 0} primitive=${primitive::class.simpleName}")
|
||||
checkActive()
|
||||
checkInputLength(aadInput)
|
||||
primitive.updateAad(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? {
|
||||
@@ -202,7 +325,7 @@ class SoftwareOperation(
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
|
||||
throw e
|
||||
throw mapToServiceSpecificException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,16 +338,17 @@ class SoftwareOperation(
|
||||
if (latencyFloorMs > 0) {
|
||||
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
|
||||
val delayMs = latencyFloorMs - elapsedMs
|
||||
if (delayMs > 0) Thread.sleep(delayMs)
|
||||
if (delayMs > 0) LockSupport.parkNanos(delayMs * 1_000_000)
|
||||
}
|
||||
finalized = true
|
||||
onFinishCallback?.invoke()
|
||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
|
||||
return result
|
||||
} catch (e: ServiceSpecificException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
|
||||
throw e
|
||||
throw mapToServiceSpecificException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,13 +358,20 @@ class SoftwareOperation(
|
||||
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 {
|
||||
// AOSP keystore2 operation.rs: const MAX_RECEIVE_DATA: usize = 0x8000
|
||||
private const val MAX_RECEIVE_DATA = 0x8000
|
||||
}
|
||||
}
|
||||
|
||||
private object KeystoreErrorCodes {
|
||||
internal object KeystoreErrorCodes {
|
||||
val tooMuchData: Int by lazy {
|
||||
resolveField("android.system.keystore2.ResponseCode", "TOO_MUCH_DATA", 21)
|
||||
}
|
||||
@@ -249,7 +380,55 @@ private object KeystoreErrorCodes {
|
||||
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_OPERATION_HANDLE", -28)
|
||||
}
|
||||
|
||||
private fun resolveField(className: String, fieldName: String, fallback: Int): Int =
|
||||
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 {
|
||||
@@ -261,18 +440,30 @@ private object KeystoreErrorCodes {
|
||||
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
||||
IKeystoreOperation.Stub() {
|
||||
|
||||
@Synchronized
|
||||
override fun updateAad(aadInput: ByteArray?) {
|
||||
operation.updateAad(aadInput)
|
||||
SystemLogger.info("[SoftwareOpBinder] updateAad() ENTRY callingUid=${android.os.Binder.getCallingUid()} size=${aadInput?.size ?: 0}")
|
||||
try {
|
||||
operation.updateAad(aadInput)
|
||||
SystemLogger.info("[SoftwareOpBinder] updateAad() RETURNED_NORMALLY")
|
||||
} catch (throwable: Throwable) {
|
||||
val code = (throwable as? ServiceSpecificException)?.errorCode
|
||||
SystemLogger.info("[SoftwareOpBinder] updateAad() PROPAGATING class=${throwable::class.java.name} code=$code msg=${throwable.message}")
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun update(input: ByteArray?): ByteArray? {
|
||||
return operation.update(input)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
return operation.finish(input, signature)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun abort() {
|
||||
operation.abort()
|
||||
}
|
||||
|
||||
@@ -1,42 +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) {
|
||||
@@ -47,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) {
|
||||
@@ -64,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. */
|
||||
@@ -214,16 +227,15 @@ object CertificateGenerator {
|
||||
securityLevel: Int,
|
||||
): Certificate {
|
||||
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
|
||||
val leafNotAfter =
|
||||
(signingKeyPair.public as? X509Certificate)?.notAfter
|
||||
?: Date(System.currentTimeMillis() + 31536000000L)
|
||||
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,10 +245,11 @@ 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 (signingKeyPair.private.algorithm) {
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -164,6 +163,24 @@ object AndroidDeviceUtils {
|
||||
}
|
||||
}
|
||||
|
||||
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) }
|
||||
|
||||
@@ -328,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
|
||||
}
|
||||
@@ -376,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
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -398,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,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,226 @@
|
||||
## TEESimulator-RS v6.0.1-282
|
||||
|
||||
AUTO-mode key attestation now forges plain attestation from the keybox instead of deferring to the real TEE.
|
||||
|
||||
### Detection coverage
|
||||
- AUTO dispatch probed the device with `checkTeeFunctionality`, which only proves the TEE can mint one EC key. It says nothing about RSA attestation, device-ID attestation, or whether a patched chain survives RSA verify. Plain attestation requests (attest-key OFF, challenge present) were routed to PATCH and deferred to hardware, so devices that can't back that surfaced KeyAttestation reds: `ATTESTATION_KEYS_NOT_PROVISIONED` (-49) and `BLOCK_TYPE_IS_NOT_01`.
|
||||
- AUTO targets carrying an attestation challenge now take the FORGE path, the same one attest-key-ON already used: a synthetic chain built from the keybox and rooted under the Google root key. Requests with no challenge still pass through to real hardware, so KeyDetector's hardware-backed checks are unaffected.
|
||||
|
||||
### Verified
|
||||
- Offline conformance against real FORGE captures: uid10389 and uid10154 chains are GREEN; the root SPKI byte-matches `GOOGLE_ROOT_PUBLIC_KEY`.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v6.0.1-280
|
||||
|
||||
Clears the Duck Detector generate-mode parcel fingerprint that real Android 16 hardware also trips, fixes RSA attestation under an EC-only keybox, and restores device-property attestation for Play Integrity hardware apps such as BHIM and UPI. Generate-mode fix field-confirmed on Android 16.
|
||||
|
||||
### Detection coverage
|
||||
- Generate-mode fingerprint: Duck reads the reply at a flat 12-byte stride and flags the sentinel tuple at positions 12 and 13 that the device's native ALGORITHM-first authorization order lands on. Real A16 silicon trips the same probe, so faithful mirroring stayed flagged. `normalizeAuthorizationLayout` marshals the auth array, runs Duck's exact predicate, and applies a minimal deterministic reorder only when it would match. Count, values, security levels, and the cert chain are untouched, and the reorder keys on the byte condition, never on a package. Applied on both the patch and forge reply paths. (#33)
|
||||
- `updateAad` on a non-AEAD operation now answers per vendor: Samsung and Xiaomi-MTK TEEs return success, others return INVALID_TAG, matching Duck's OperationErrorPathProbe on both the sign/verify and cipher paths.
|
||||
|
||||
### Attestation correctness (Android 16, EC and RSA)
|
||||
- RSA leaf under an EC-only keybox: patching used to catch the no-RSA-key throw and return the chain untouched, leaking the device's real unlocked Root of Trust for RSA keys while EC keys patched cleanly. It now falls back to any keybox key (EC preferred) and signs the patched leaf with the keybox key's own algorithm, so the RSA leaf re-roots to the Google keybox under a forged locked RoT.
|
||||
- RSA attest-key forge on an EC-only keybox: the forge path matched the algorithm exactly and threw -75 ATTESTATION_KEYS_NOT_PROVISIONED on a miss, so an RSA ATTEST_KEY request never rooted and verifiers reported an unknown certificate. It now falls back to any attestation key, since an EC key validly ECDSA-signs an RSA-subject leaf. No-op on a dual keybox.
|
||||
- A16 attestVersion: the device's KeyMint reports version 100 and the lazy cache shadowed the BAKLAVA-to-400 map, so the forge presented 100. It now caches the AOSP value per SDK and presents the correct 400.
|
||||
- Algorithm-split key on restore: a persisted record holding an EC private key under an RSA leaf failed every signature as DATA_TOO_LARGE_FOR_MODULUS. Restore now drops the record when the private key and served leaf disagree, so the next generateKey rebuilds a coherent key.
|
||||
- Stale chain on regenerate: reusing an alias in generateKey now evicts the cached chain, matching keystore2, so getKeyEntry serves the current key instead of a stale forge from an earlier generation.
|
||||
|
||||
### App compatibility
|
||||
- Device-property attestation (BRAND, MODEL, and the rest) now forges unconditionally. The old gate probed the live TEE, which is dead on every device the module serves, so it rejected GMS Play Integrity's hardware path and broke BHIM and other UPI and Play-Integrity apps. Device-ID attestation (IMEI, serial) stays governed by the real KeyMint caller-permission rule: privileged callers get it, ordinary apps do not.
|
||||
- getKeyEntry now reaches the owned-key lookup for skipped privileged UIDs, so framework attestKeyAlias resolution no longer returns "Invalid attestKeyAlias" for Key Attestation over Shizuku. Non-owned keys still skip post-processing, so a real app's key is never patched.
|
||||
- Device-ID attestation over Shizuku (a privileged UID absent from target.txt) now takes the forge path instead of hitting the real TEE's CANNOT_ATTEST_IDS (-66). "Use attest key" no longer double-roots: a reused persistent attest key is resolved by KEY_ID as well as alias, and an unresolved designated attest key refuses to emit a leaf rather than silently re-rooting under the keybox.
|
||||
|
||||
### Diagnostics (debug builds only)
|
||||
- Per-UID attestation dossier for targeted UIDs at /data/local/tmp/teesim/, recording the decoded chain on both forge and patch paths, key params, the keybox pick (including EC fail-safe), prop sources, forge failures, the emitted authorization shape, and served-versus-verified chains. Release builds strip this through R8 and stay silent. Keybox certificate serials log on every fetch for revocation triage.
|
||||
|
||||
### Verified
|
||||
- Android 16: generate-mode fingerprint signal gone, confirmed on device 2026-06-19.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v6.0.1-251
|
||||
|
||||
14 commits since v6.0.0-235. Clears the remaining Duck Detector grant-domain rows (incl. the Android 16 OnePlus report), restores Google Wallet and fingerprint compatibility, and removes the in-module patch-level/bulletin resolvers. Test device (SDK 35) TEE tamper score 28 → 8.
|
||||
|
||||
### Detection coverage
|
||||
- Grant plane virtualized: owner read and cross-app `Domain.GRANT` read return one identical chain. 6 RED rows cleared. (28 → 18)
|
||||
- Generate-mode fingerprint: dropped 2 surplus authorizations (both patchlevels), USER_ID moved to SOFTWARE to mirror a captured device. (18 → 8)
|
||||
- Android 16 grant: patch-mode keys now served on the grant plane, so owner and grant reads match — fixes CHAIN_SPLIT.
|
||||
- Grant gated to SDK ≥ 36: Android 15 answers PERMISSION_DENIED, no synthetic over-capability.
|
||||
- Stale-chain eviction: import and updateSubcomponent drop the cached attestation; no pre-mutation chain replays.
|
||||
- Lifecycle coherence: clearNamespace / deleteAllKeys / migrateKeyNamespace mirror synthetic key and grant state — defeats delete-then-read probes.
|
||||
- Device-ID attestation mirrors the real TEE: returns CANNOT_ATTEST_IDS where silicon can't attest, instead of forging it.
|
||||
|
||||
### App compatibility
|
||||
- Google Wallet: INCLUDE_UNIQUE_ID stripped (not rejected) when the caller lacks the permission; card binding works. (PR #27)
|
||||
- Fingerprint / vendor keys: KEY_ID miss skips the post-handler, so real HAL operations are no longer wrapped and broken. (PR #26)
|
||||
|
||||
### Removed
|
||||
- PatchLevelManager — auto-resolved the security-patch date from an installed PlayIntegrityFix module (with hot-reload) and applied it to props.
|
||||
- BulletinPoller — scheduled security-bulletin refresh.
|
||||
|
||||
### Other
|
||||
- Release builds purge stale `teesim-*.bin` diagnostics from `/data/local/tmp` at boot.
|
||||
- Vol-key confirmation rewritten to 1s `getevent` bursts (piped stream missed single presses on Magisk).
|
||||
|
||||
### Verified
|
||||
- SDK 35, Xiaomi 23106RN0DA: tamper 28 → 8; generate-mode signal gone; 4 grant rows UNAVAILABLE (correct for Android 15); no regressions.
|
||||
- Android 16 grant fix built but unconfirmed on SDK 36 — needs an affected OnePlus user to confirm the grant rows clear.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v6.0.0-235
|
||||
|
||||
11 commits since v6.0.0-224. Duck Detector generate-mode fingerprint cleared. Shizuku-routed BYO attestation fixed. Vol-key confirmation restored on Magisk.
|
||||
|
||||
### Detection Coverage
|
||||
- Duck Detector "TEE Simulator generate-mode fingerprint" cleared. `toAuthorizations` reordered to AOSP keymint reference order; KEY_SIZE moves from auth#4 to auth#2, breaking the byte-224 anchor the probe relied on. 0/31 matches on fresh self-probes (was 15/36).
|
||||
- `persist.logd.size` variants blanked at boot via `service.sh`. Removes a logd-tuning side-channel.
|
||||
|
||||
### BYO & Shizuku Routing
|
||||
- Shizuku-routed BYO attestation no longer fails with `-49 UNSUPPORTED_TAG`. `shouldSkipUid` moved into `handleGenerateKey`, evaluated after BYO parameters are parsed.
|
||||
- `createOperation` parallel fix: outer UID gate removed; the cache-or-forward lookup is the sole gate. BYO keys created under Shizuku UID can now be used for signing under the same UID.
|
||||
- `forceGenerate` simplified: any attest-key or BYO request routes to software unconditionally.
|
||||
- BYO attest-key miss returns the full keybox chain instead of a malformed depth-1 chain.
|
||||
- AUTO TEE race dispatch removed. Resolution uses `DeviceAttestationService.isTeeFunctional` only.
|
||||
- Symmetric gen rejects `attestationKey != null` early with `INVALID_ARGUMENT`. Unsupported-algorithm branch returns `-38` instead of `-49`.
|
||||
|
||||
### Action Button
|
||||
- Vol+ / Vol- confirmation restored on Magisk. Streaming `getevent -lq` matched inline against `KEY_VOLUMEUP DOWN` / `KEY_VOLUMEDOWN DOWN`, wrapped in `/system/bin/timeout 10`. The prior polled approach timed out on six-events-per-keypress kernels.
|
||||
|
||||
### Verified
|
||||
- Android 15 (SDK 35), daemon PID 1466.
|
||||
- Cross-device confirmation pending on OnePlus PKX110 and Samsung SM-S928B.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v6.0.0-224
|
||||
|
||||
59 commits since v6.0.0-162. Self-sufficient spoofing infrastructure, Duck Detector TamperScore-4 cleared on Xiaomi A16, persistent symmetric key storage (PR #22), 22-language action button hardening.
|
||||
|
||||
### Detection Coverage
|
||||
- Duck Detector TimingSideChannelProbe cleared on Xiaomi A16 (SDK 35). Timing ratio dropped 1.555x to 1.055x, verdict WARNING to CLEAR. Threshold is > 1.1x.
|
||||
- `KEY_ID` resolved from `teeResponses` instead of synthesized, matching real KeyMint binder behavior.
|
||||
- Non-attested key cache mirrors attested path for byte-level metadata parity.
|
||||
- `KEY_SIZE` emitted for EC keys; omitted when `ecCurve` is present, matching AOSP attestation_record.h.
|
||||
- SSE messages synthesized canonically on non-AEAD `updateAad`; passthrough shape normalized.
|
||||
- StrongBox attest version no longer hardcoded; resolved from device context.
|
||||
- TEE op latency floor enforced to defeat micro-timing probes.
|
||||
- Attest key resolution restored to nspace-aware lookup after revert/restore cycle.
|
||||
|
||||
### Self-Sufficient Spoofing
|
||||
- `PatchLevelManager` resolves OS/VENDOR/BOOT patch levels via PIF without external bulletin fetch.
|
||||
- `BulletinPoller` refreshes bulletin data on a schedule, isolated from boot path via umbrella `try/catch`.
|
||||
- Bootloader-lock props pushed via `resetprop` at boot; absent vbmeta complement props filled; `vbmeta.device_state` included.
|
||||
- PIF hot-reload via `FileObserver`; empty source files skipped; future patch dates bounded by `MAX_FUTURE_DAYS`.
|
||||
- Default `security_patch.txt` dropped at install time.
|
||||
- `sepolicy.rule` allows UDP egress for DNS resolution.
|
||||
|
||||
### Key Persistence (PR #22)
|
||||
- Symmetric keys persist across reboots with byte-identical metadata.
|
||||
- Keybox edits no longer wipe stored keys.
|
||||
- Delete marker dropped on key regeneration to prevent stale state.
|
||||
- Defensive symmetric fallback path with clean error codes.
|
||||
|
||||
### Reliability
|
||||
- `atomicWrite` preserves `[pkg]` sections; errors guarded in `updateTo`.
|
||||
- `applyToProps` serialized against concurrent callers.
|
||||
- `pollOnce` wrapped in umbrella `try/catch`; `BulletinPoller.start` failure isolated from spoofer init.
|
||||
- Spoofer ordering fixed: runs before keystore hook to prevent attest-time prop drift.
|
||||
- `isAutoMode` reads raw package mode; `system=prop` passive default respected.
|
||||
- `mergedContents` propagates read errors instead of swallowing them.
|
||||
- Date regex validation on `currentPatch`; YYYY-MM input skips day synthesis.
|
||||
- Global key-assignment check requires `=` delimiter (no more partial matches).
|
||||
- `validation_rejected` status emitted on invalid spoof input.
|
||||
|
||||
### Action Button UX
|
||||
- Vol+ required to clear `persistent_keys`. Vol- cancels. 10-second timeout defaults to cancel.
|
||||
- Confirmation localized in 22 languages: ar, az, bn, de, el, es-ES, fa, fr, id, it, ja, ko, pl, pt-BR, ru, th, tl, tr, uk, vi, zh-CN, zh-TW.
|
||||
- Every echoed string resolves through `_msg()` against device locale.
|
||||
|
||||
### Build & Ops
|
||||
- Kotlin `jvmTarget` raised to JVM 21.
|
||||
- Gradle auto-rewrites `module/update.json` on packaging.
|
||||
- `scripts/package.sh` locates user-local cargo; rust task receives cargo bin path.
|
||||
- Verified on Xiaomi Android 16 (SDK 35) `v6.0.0-224-Release`. Daemon alive PID 1392. Pending cross-device confirm on OnePlus PKX110 (qcom sun) and Samsung SM-S928B (pineapple).
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v6.0.0
|
||||
|
||||
Repository consolidation release. All tee-rebuild work merged as the new main branch.
|
||||
|
||||
### AOSP Self-Signed Cert Compliance
|
||||
- No-challenge keys now generate self-signed certs (subject == issuer, depth 1), matching AOSP `ta/src/keys.rs:451-478`
|
||||
- Both Kotlin (BouncyCastle) and Rust (native-certgen) paths corrected
|
||||
- Eliminates attestation behavioral probes that detect keybox issuer on non-attested keys
|
||||
|
||||
### Stability
|
||||
- Binder stress crash hardening for concurrent generateKey calls
|
||||
- AUTO mode TEE race for consistent attestation on devices with working G10
|
||||
- Oversized transactions routed to software gen instead of crashing
|
||||
- Operation-time params (BLOCK_MODE, PADDING, DIGEST) passed through to CipherPrimitive
|
||||
|
||||
### Banking App Compatibility
|
||||
- Bare `target.txt` entries now default to AUTO mode, resolved at config level to PATCH (working TEE) or GENERATE (broken TEE)
|
||||
- Fixes BHIM and similar banking apps that require TEE-backed attestation keys
|
||||
- Restores v5.0 behavior where AUTO was resolved before the interceptor dispatch, avoiding the non-deterministic `raceTeePatch` path
|
||||
|
||||
### Infrastructure
|
||||
- Version scheme changed to semver (v6.0.0)
|
||||
- Repository moved to TEESimulator-RS as canonical source
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v5.0: AOSP Compliance Overhaul
|
||||
|
||||
Major release integrating 30+ AOSP compliance improvements from upstream PR #157 analysis, layered on top of our StrongBox hardening and native cert gen architecture.
|
||||
|
||||
### Attestation Extension Alignment
|
||||
- 17 enforcement tags added to KeyMintAttestation (ACTIVE_DATETIME, ORIGINATION_EXPIRE, USAGE_EXPIRE, USAGE_COUNT_LIMIT, CALLER_NONCE, UNLOCKED_DEVICE_REQUIRED, INCLUDE_UNIQUE_ID, ROLLBACK_RESISTANCE, EARLY_BOOT_ONLY, ALLOW_WHILE_ON_BODY, TRUSTED_USER_PRESENCE_REQUIRED, TRUSTED_CONFIRMATION_REQUIRED, NO_AUTH_REQUIRED, MAX_USES_PER_BOOT, MAX_BOOT_LEVEL, MIN_MAC_LENGTH, RSA_OAEP_MGF_DIGEST)
|
||||
- BLOCK_MODE encoded as SET OF INTEGER per AOSP attestation_record.h
|
||||
- Version-guarded tags (RSA_OAEP_MGF_DIGEST >=100, ROLLBACK_RESISTANCE >=3, EARLY_BOOT_ONLY >=4)
|
||||
- INCLUDE_UNIQUE_ID computed via HMAC-SHA256 per KeyMint HAL spec using device HBK
|
||||
- AAID gated on attestation challenge presence
|
||||
- Certificate validity defaults aligned with AOSP (epoch notBefore, 9999-12-31 notAfter)
|
||||
|
||||
### Binder Infrastructure
|
||||
- Native transaction code filtering at C++ level, skipping JNI for non-intercepted codes
|
||||
- getNumberOfEntries includes software-generated key count
|
||||
- deleteKey resolves KEY_ID domain via generatedKeys lookup
|
||||
- patchAuthorizations for OS/VENDOR/BOOT patch levels in authorization arrays
|
||||
|
||||
### Software Operation AOSP Conformance
|
||||
- updateAad on non-AEAD operations returns INVALID_TAG (-76), matching AOSP operation.rs
|
||||
- All crypto exceptions wrapped as ServiceSpecificException with correct KeyMint error codes
|
||||
- GCM IV returned in CreateOperationResponse.parameters for encrypt operations
|
||||
- SoftwareOperationBinder methods @Synchronized, matching AOSP Mutex per operation
|
||||
- authorize_create enforcement: PURPOSE validation, algorithm-purpose compatibility, temporal constraints, CALLER_NONCE prohibition, WRAP_KEY rejection
|
||||
|
||||
### Security and Configuration
|
||||
- SELinux permission checks via /proc/pid/attr/current
|
||||
- Per-UID permission verification through IPackageManager.checkPermission
|
||||
- Imported key tracking prevents stale attest-key overrides in getKeyEntry
|
||||
- nspace consistency fix in attest-key override path
|
||||
- TeeLatencySimulator with log-normal distribution matching real hardware profiles
|
||||
- Device-unique HBK seed generated on install (32 bytes from /dev/random)
|
||||
|
||||
### Preserved from v4.8
|
||||
- StrongBox op limits (4 concurrent max, TOO_MANY_OPERATIONS rejection)
|
||||
- LRU operation pruning per security level
|
||||
- Hardware keygen rate limiting (2/30s sliding window, 2 concurrent cap)
|
||||
- Native Rust cert generation with BouncyCastle fallback
|
||||
- Key persistence across reboots
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v4.8.1: StrongBox Op Rejection Fix
|
||||
|
||||
- **StrongBox op limit gate fix** — `trackAndEnforceOpLimit` was only called in the `Domain.KEY_ID` not-found path, so software-generated keys (found via `Domain.APP`) bypassed `STRONGBOX_MAX_CONCURRENT_OPS=4` entirely. DuckDetector's concurrent signing handles test created 24+ operations that all succeeded via LRU pruning instead of being rejected with `TOO_MANY_OPERATIONS (-29)`. Now enforced for all StrongBox createOperation paths.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v4.8: StrongBox Hardening & LRU Pruning
|
||||
|
||||
Tested against DuckDetector on OnePlus (Android 16, KSU). Tamper score dropped from 32 to 8.
|
||||
|
||||
+19
-1
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
) &
|
||||
|
||||
@@ -10,3 +10,4 @@ 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.5",
|
||||
"versionCode": 111,
|
||||
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.5/TEESimulator-v4.5-Release.zip",
|
||||
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md"
|
||||
"version": "v6.0.1-282",
|
||||
"versionCode": 282,
|
||||
"zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/download/v6.0.1-282/TEESimulator-RS-v6.0.1-282-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>> {
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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