Compare commits
300
Commits
v6.0.1-251
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d241e56d6 | ||
|
|
1f96c8a13e | ||
|
|
000e926693 | ||
|
|
5b7373ad7a | ||
|
|
eb94820192 | ||
|
|
d7109421ef | ||
|
|
c9918952f4 | ||
|
|
d2cb950cb6 | ||
|
|
2cf6526b5c | ||
|
|
ab4f41eb2a | ||
|
|
422f78ebcf | ||
|
|
dc2d894647 | ||
|
|
a811919d0c | ||
|
|
0f1143a445 | ||
|
|
4429f0ed60 | ||
|
|
5ddd8137df | ||
|
|
a4875dc200 | ||
|
|
6b84c76f10 | ||
|
|
43a2301982 | ||
|
|
7de3ef1ba3 | ||
|
|
9a8c06bca6 | ||
|
|
4e61f1523d | ||
|
|
ab40677dac | ||
|
|
07dde7c756 | ||
|
|
718c80a68b | ||
|
|
9ffa00332b | ||
|
|
5c36288461 | ||
|
|
d0139003a6 | ||
|
|
28f48f2e01 | ||
|
|
c2552ba164 | ||
|
|
e2dc7aa210 | ||
|
|
9193b79a6a | ||
|
|
b2d84b4661 | ||
|
|
af3c27451d | ||
|
|
0586db18d9 | ||
|
|
ec5df08574 | ||
|
|
d65526aaa5 | ||
|
|
ca226bd7de | ||
|
|
e5d24c3907 | ||
|
|
a54e8a5315 | ||
|
|
0b67700763 | ||
|
|
fbee59688d | ||
|
|
2e5155fb75 | ||
|
|
ce22327147 | ||
|
|
f69fee21a8 | ||
|
|
9561f7d9c0 | ||
|
|
79e4fe905e | ||
|
|
5bbb0bffe0 | ||
|
|
59a2357312 | ||
|
|
7f33bd737d | ||
|
|
b90af0b716 | ||
|
|
e5483afc70 | ||
|
|
f826312fc4 | ||
|
|
40519b94ea | ||
|
|
37e9d007de | ||
|
|
5c300ff47b | ||
|
|
5bd563d8db | ||
|
|
254fb0f0a9 | ||
|
|
8d195010fd | ||
|
|
d6ddc925ce | ||
|
|
a67efa4102 | ||
|
|
00e8cc36fe | ||
|
|
ca857c30ac | ||
|
|
a58c4798c3 | ||
|
|
8cebcf14a8 | ||
|
|
ab58b11e4d | ||
|
|
bed32b7454 | ||
|
|
43e948efb3 | ||
|
|
cf4fb127f2 | ||
|
|
8dd644a2e5 | ||
|
|
a0d7418504 | ||
|
|
ebe2040b2b | ||
|
|
a731b33f09 | ||
|
|
67c283b75b | ||
|
|
40f652f725 | ||
|
|
25fd28a32b | ||
|
|
108e027a98 | ||
|
|
be4c418893 | ||
|
|
0829bc98ca | ||
|
|
49763cdca7 | ||
|
|
937058a7ff | ||
|
|
76e033700c | ||
|
|
f384871f5a | ||
|
|
1cea3ab8f1 | ||
|
|
a6452c3a26 | ||
|
|
da9a99bfbf | ||
|
|
b9defa70ae | ||
|
|
60fee55a0f | ||
|
|
182450d3b4 | ||
|
|
6d11362034 | ||
|
|
20a58f8a58 | ||
|
|
5a12970426 | ||
|
|
cfbba4cddb | ||
|
|
c4ea3e0bb2 | ||
|
|
ea37792653 | ||
|
|
80f65b02ac | ||
|
|
be04f16a50 | ||
|
|
9a7e4d7251 | ||
|
|
180dc039cd | ||
|
|
eea60018ca | ||
|
|
17c63120f5 | ||
|
|
5d33701601 | ||
|
|
2e55d56426 | ||
|
|
c69e2d47b8 | ||
|
|
3b1e908670 | ||
|
|
259d27c3c7 | ||
|
|
d15abe3c62 | ||
|
|
99957e18c4 | ||
|
|
393ae6073f | ||
|
|
fbe819d6d8 | ||
|
|
ad1a9b8c32 | ||
|
|
d146ad8234 | ||
|
|
e737453e02 | ||
|
|
429e033b7f | ||
|
|
f706ffdf64 | ||
|
|
bbeab27f11 | ||
|
|
864c8841c1 | ||
|
|
4e55ba4e77 | ||
|
|
c511cc48e9 | ||
|
|
37179bbbfc | ||
|
|
a0e7fcf400 | ||
|
|
c4e0a6ee48 | ||
|
|
a79d7e3637 | ||
|
|
8cb8616068 | ||
|
|
4b4b7ec626 | ||
|
|
439a9d8254 | ||
|
|
128783dfd4 | ||
|
|
da9ade723d | ||
|
|
5a6d336fa8 | ||
|
|
813ff814ea | ||
|
|
ee6770bcc7 | ||
|
|
bf29946fdc | ||
|
|
59dfb2eb85 | ||
|
|
ee7e5ba698 | ||
|
|
04c310e8d3 | ||
|
|
f4d72a641e | ||
|
|
5803039309 | ||
|
|
8b0acb649d | ||
|
|
15a1c0ca40 | ||
|
|
4ecbfc7259 | ||
|
|
db882ec326 | ||
|
|
37454e6262 | ||
|
|
feee04b95d | ||
|
|
161be9fc36 | ||
|
|
23b5497f57 | ||
|
|
0081e93eb0 | ||
|
|
6ebc6d0bb1 | ||
|
|
0f749dded9 | ||
|
|
23d40b8975 | ||
|
|
80387b9516 | ||
|
|
7d470cf830 | ||
|
|
76461ad39a | ||
|
|
08e8c769ab | ||
|
|
45d54f9369 | ||
|
|
75acfb9235 | ||
|
|
1959f0a780 | ||
|
|
7a98b35666 | ||
|
|
0b8985d8bd | ||
|
|
b3aa7950c5 | ||
|
|
4c89acced3 | ||
|
|
da75e08d58 | ||
|
|
4d5e94f835 | ||
|
|
fef17c07ec | ||
|
|
9f03b84364 | ||
|
|
3acf73210d | ||
|
|
c6587c5447 | ||
|
|
91070d7ede | ||
|
|
77462cb42a | ||
|
|
c80aaef7ae | ||
|
|
27eeaec384 | ||
|
|
468b6f5121 | ||
|
|
4a103c9231 | ||
|
|
8c27e43ab0 | ||
|
|
444afadc3b | ||
|
|
5b8ee6d278 | ||
|
|
69a6648d92 | ||
|
|
981cefb506 | ||
|
|
6856d4cb47 | ||
|
|
ea7e770a6c | ||
|
|
e74dd8318d | ||
|
|
745b7d2f8f | ||
|
|
5defc0d832 | ||
|
|
2b000b738d | ||
|
|
6fc3269229 | ||
|
|
b83769ff80 | ||
|
|
5d270ce1ad | ||
|
|
46ebde8d4f | ||
|
|
0dbeeca15b | ||
|
|
f388529bda | ||
|
|
1fa6f5a12a | ||
|
|
93c6761990 | ||
|
|
806ec26a03 | ||
|
|
70e1ffb12c | ||
|
|
3749dec58b | ||
|
|
2a9ce5e0c8 | ||
|
|
1475c0be02 | ||
|
|
4b9c5fe1d0 | ||
|
|
256bb91a6c | ||
|
|
93ec464d02 | ||
|
|
ff7539f158 | ||
|
|
3e47a3a9b3 | ||
|
|
19b87e64b8 | ||
|
|
88177ab59e | ||
|
|
58afe2e0ef | ||
|
|
dc2f31ff74 | ||
|
|
938d414ebf | ||
|
|
8876f5bc5f | ||
|
|
1f076468db | ||
|
|
4a96491e63 | ||
|
|
9807f89b71 | ||
|
|
25f3f753ff | ||
|
|
1bfe628317 | ||
|
|
3ecc72bcb7 | ||
|
|
5964eb7e45 | ||
|
|
e02bae6f43 | ||
|
|
71c30e68d0 | ||
|
|
e5fb27c8f9 | ||
|
|
5bace3ad30 | ||
|
|
20603572f3 | ||
|
|
a781b29e0d | ||
|
|
f781f61f44 | ||
|
|
6aba82edb1 | ||
|
|
7f3d72ba20 | ||
|
|
e82abe1ce9 | ||
|
|
95724116b7 | ||
|
|
d335809682 | ||
|
|
0b680c578e | ||
|
|
98f7e0f08b | ||
|
|
e8672459e0 | ||
|
|
9dc8ec1530 | ||
|
|
5fcd4ab7b6 | ||
|
|
30d5188c69 | ||
|
|
770a4f0134 | ||
|
|
bb28a8d30c | ||
|
|
3e2aaa2ff0 | ||
|
|
ff88543c98 | ||
|
|
c99a2ab302 | ||
|
|
b33e1ae3ac | ||
|
|
fc64789dc3 | ||
|
|
945f3cac79 | ||
|
|
4e50a70366 | ||
|
|
8b00d7985f | ||
|
|
e7444bb62a | ||
|
|
6fdf5c766b | ||
|
|
33397f244c | ||
|
|
23bef3f88e | ||
|
|
129cec06bf | ||
|
|
54f68b99b1 | ||
|
|
8649b8b928 | ||
|
|
68af5ac680 | ||
|
|
10d673b606 | ||
|
|
b5251c0418 | ||
|
|
1d2c60c510 | ||
|
|
b997304f02 | ||
|
|
5e394f1b72 | ||
|
|
b1f3b5d28d | ||
|
|
e9d7321b5f | ||
|
|
4097ffde6a | ||
|
|
dcb961d084 | ||
|
|
151410c75e | ||
|
|
205fda43ba | ||
|
|
d77508d0c1 | ||
|
|
aff7cf9c32 | ||
|
|
fa2956ce1b | ||
|
|
19ad610cec | ||
|
|
1a1334e3f3 | ||
|
|
17ab5b0e2c | ||
|
|
ccd4ae7f5f | ||
|
|
b0206c10ec | ||
|
|
bfc15cba62 | ||
|
|
f76a4dd977 | ||
|
|
a0ac3f71bb | ||
|
|
a30459628f | ||
|
|
cd3970869d | ||
|
|
31a0906c02 | ||
|
|
eeefdc48eb | ||
|
|
4d9787367c | ||
|
|
987c7ba35c | ||
|
|
2446461aab | ||
|
|
0275eb7ad2 | ||
|
|
f5d4ab1f44 | ||
|
|
79145e3bff | ||
|
|
ecc1dbdaeb | ||
|
|
9b57e14752 | ||
|
|
6a58f804d7 | ||
|
|
78e80391d3 | ||
|
|
0afcaeeb53 | ||
|
|
1e644d71e9 | ||
|
|
3351a1c932 | ||
|
|
ee02216534 | ||
|
|
6193da0e66 | ||
|
|
ee9863009a | ||
|
|
fa64f941b3 | ||
|
|
8aa93f1427 | ||
|
|
0894b44166 | ||
|
|
c017c9b0ab | ||
|
|
9bd75d15f7 | ||
|
|
cc52307ca8 | ||
|
|
ffb27915e2 | ||
|
|
9fe8919696 |
@@ -1,7 +0,0 @@
|
|||||||
out
|
|
||||||
.gradle
|
|
||||||
.kotlin
|
|
||||||
app/build
|
|
||||||
build
|
|
||||||
native-certgen/target
|
|
||||||
app/src/main/jniLibs
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<h1 align="center">TEESimulator-RS</h1>
|
<h1 align="center">TEESimulator-RS</h1>
|
||||||
<p align="center"><b>Full TEE Emulation for Rooted Android</b></p>
|
<p align="center"><b>Pass hardware security checks on a rooted Android phone</b></p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/Enginex0/TEESimulator-RS/actions/workflows/build.yml"><img src="https://github.com/Enginex0/TEESimulator-RS/actions/workflows/build.yml/badge.svg" alt="Build"></a>
|
<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+">
|
<img src="https://img.shields.io/badge/Android-10%2B-green?logo=android" alt="Android 10+">
|
||||||
@@ -11,58 +11,78 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Fork of [JingMatrix/TEESimulator](https://github.com/JingMatrix/TEESimulator) with native Rust certificate generation, key persistence, and AOSP-compliant attestation behavior. For the upstream project, see the original repo.
|
> This is a fork of [JingMatrix/TEESimulator](https://github.com/JingMatrix/TEESimulator). It adds certificate generation written in Rust, generated keys that survive reboots, and attestation behavior that matches stock Android. See the upstream repo for the original project.
|
||||||
|
|
||||||
## What It Does
|
## What it does
|
||||||
|
|
||||||
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.
|
Some Android apps refuse to run on a rooted phone. They ask the phone to prove it still has a genuine security chip, a check called hardware attestation. A rooted phone normally fails that check.
|
||||||
|
|
||||||
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.
|
TEESimulator makes it pass. Android runs a system process named `keystore2` that answers these proof requests. TEESimulator sits in front of `keystore2`, watches for the requests apps make to create keys and read their certificates, and builds the proof itself: a full chain of certificates signed by your `keybox.xml`. To the app, the phone looks genuine.
|
||||||
|
|
||||||
|
It replaces TrickyStore and its forks completely. It reads config from the same files, so you can switch without moving anything, but the internals are rewritten: certificates are generated in Rust, keys are saved across reboots, and each app gets its own limit on how fast it can request hardware-backed keys.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> A valid `keybox.xml` is required for hardware-level attestation. Without one, the module generates software-level certificates that won't pass strict hardware checks.
|
> You need a valid `keybox.xml`. This is the file used to sign the proof. Without it, TEESimulator can only produce software-only certificates, which strict apps reject.
|
||||||
|
|
||||||
1. Android 10+
|
1. Android 10 or newer
|
||||||
2. Root manager: KernelSU, Magisk, or APatch
|
2. A root manager: KernelSU, Magisk, or APatch
|
||||||
3. `keybox.xml` at `/data/adb/tricky_store/keybox.xml`
|
3. A `keybox.xml` file at `/data/adb/tricky_store/keybox.xml`
|
||||||
|
|
||||||
## Quick Start
|
## Quick start
|
||||||
|
|
||||||
1. Download the latest ZIP from [Releases](https://github.com/Enginex0/TEESimulator-RS/releases)
|
1. Download the latest ZIP from [Releases](https://github.com/Enginex0/TEESimulator-RS/releases).
|
||||||
2. Install via your root manager and reboot
|
2. Install it with your root manager, then reboot.
|
||||||
3. Place your keybox at `/data/adb/tricky_store/keybox.xml`
|
3. Put your `keybox.xml` at `/data/adb/tricky_store/keybox.xml`.
|
||||||
4. Configure targets in `/data/adb/tricky_store/target.txt`
|
4. List the apps you want to cover in `/data/adb/tricky_store/target.txt`.
|
||||||
5. Verify with Play Integrity or Key Attestation Demo
|
5. Check that it works with Play Integrity or the Key Attestation Demo app.
|
||||||
|
|
||||||
## Architecture
|
## How it works
|
||||||
|
|
||||||
**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).
|
```
|
||||||
|
App
|
||||||
|
| asks the phone to prove it has real security hardware
|
||||||
|
v
|
||||||
|
+----------------------------------------------------+
|
||||||
|
| keystore2 (the Android process that answers) |
|
||||||
|
| |
|
||||||
|
| ioctl <- TEESimulator hooks the call here |
|
||||||
|
| | |
|
||||||
|
| v |
|
||||||
|
| builds a certificate chain and signs it |
|
||||||
|
| with your keybox.xml |
|
||||||
|
+----------------------------------------------------+
|
||||||
|
| the signed chain goes back to the app
|
||||||
|
v
|
||||||
|
App -> sees a genuine, hardware-backed device
|
||||||
|
```
|
||||||
|
|
||||||
**Binder Interception** — PLT hook on `ioctl()` in `libc.so` via `lsplt` inside `keystore2`. Intercepts `generateKey`, `importKey`, and `getKeyEntry` transactions.
|
**Certificate generation in Rust.** A native library, `libcertgen.so`, builds the X.509 certificate chains in Rust with the `ring` crypto library, encoding the bytes by hand in DER, the standard certificate format. Three key types fall outside `ring`'s support (the P-224, P-521, and Curve25519 curves); for those it falls back to Java's BouncyCastle.
|
||||||
|
|
||||||
**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.
|
**Hooking keystore2.** Inside the `keystore2` process, TEESimulator redirects `ioctl`, the low-level system call Android uses to pass messages between processes. It does this with `lsplt`, a hooking library. From there it can read and answer three kinds of request: creating a key, importing a key, and fetching a key's certificate.
|
||||||
|
|
||||||
**Key Persistence** — Generated keys survive reboots. File-backed with file-level locking.
|
**Matching stock Android.** The output matches what a real device produces. Keys that are not attested get self-signed certificates. The fields inside the attestation record keep the same order. Fields that only exist on certain Android versions appear only on those versions. The same usage checks run before a key is used.
|
||||||
|
|
||||||
**Rate Limiting** — Per-UID hardware keygen cap (2/30s window, 2 concurrent). Overflow falls to software certs.
|
**Keys that survive reboots.** Generated keys are written to disk and stay valid after a restart. File locking stops two writers from corrupting the store.
|
||||||
|
|
||||||
|
**Per-app rate limit.** Each app may request at most 2 hardware-backed keys per 30 seconds, and only 2 at a time. Past that, it receives a software-only certificate.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
All config files live at `/data/adb/tricky_store/` and are hot-reloaded via `FileObserver`.
|
All config files live in `/data/adb/tricky_store/`. TEESimulator reloads them the moment you save, so a reboot is not needed.
|
||||||
|
|
||||||
### target.txt
|
### target.txt
|
||||||
|
|
||||||
Controls which apps get intercepted and the simulation mode.
|
Lists the apps TEESimulator handles, one package name per line. A suffix sets how each app is handled.
|
||||||
|
|
||||||
| Suffix | Mode |
|
| Suffix | What it does |
|
||||||
|--------|------|
|
|--------|--------------|
|
||||||
| `!` | Force software key generation |
|
| `!` | Always make a software key |
|
||||||
| `?` | Force leaf certificate patching (real TEE key, patched cert) |
|
| `?` | Keep the real hardware key, patch only its certificate |
|
||||||
| *(none)* | Automatic selection |
|
| none | Decide automatically |
|
||||||
|
|
||||||
Multi-keybox support via `[filename.xml]` headers:
|
To use more than one keybox, add a `[filename.xml]` header above the apps that should use that file:
|
||||||
|
|
||||||
```
|
```
|
||||||
com.google.android.gms!
|
com.google.android.gms!
|
||||||
@@ -74,16 +94,16 @@ com.google.android.gsf
|
|||||||
|
|
||||||
### security_patch.txt
|
### security_patch.txt
|
||||||
|
|
||||||
Override patch levels reported in attestation certificates. Global defaults at top, per-package overrides with `[package.name]`.
|
Sets the security patch dates reported in the attestation certificates. Global defaults go at the top. Override them for one app with a `[package.name]` header.
|
||||||
|
|
||||||
| Key | Scope |
|
| Key | What it sets |
|
||||||
|-----|-------|
|
|-----|--------------|
|
||||||
| `system` | OS patch level |
|
| `system` | OS patch level |
|
||||||
| `vendor` | Vendor patch level |
|
| `vendor` | Vendor patch level |
|
||||||
| `boot` | Boot/kernel patch level |
|
| `boot` | Boot and kernel patch level |
|
||||||
| `all` | Sets all three |
|
| `all` | All three at once |
|
||||||
|
|
||||||
Special values: `today`, `YYYY-MM-DD` templates, `no` (omit tag), `device_default`, `prop` (read from system property).
|
Accepted values: `today`, a `YYYY-MM-DD` template, `no` to omit the field, `device_default`, or `prop` to read the value from a system property.
|
||||||
|
|
||||||
```
|
```
|
||||||
system=YYYY-MM-05
|
system=YYYY-MM-05
|
||||||
@@ -94,9 +114,15 @@ boot=no
|
|||||||
system=2025-10-01
|
system=2025-10-01
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building from Source
|
### boot_props_mode
|
||||||
|
|
||||||
Prerequisites: JDK 21, Android SDK/NDK 27, Rust stable with `aarch64-linux-android` target, `cargo-ndk`.
|
Controls global `ro.boot.*` property spoofing. Values: `auto` (default), `force`, or `disable`.
|
||||||
|
|
||||||
|
In `auto`, Oplus-family devices (OnePlus/OPPO/realme/Oplus) skip boot-state prop spoofing to avoid conflicts with vendor TEE services such as ultrasonic fingerprint calibration. Create `/data/adb/tricky_store/boot_props_mode` with `force` to restore the old behavior, or `disable` to turn it off on any device.
|
||||||
|
|
||||||
|
## Building from source
|
||||||
|
|
||||||
|
You need JDK 21, the Android SDK and NDK 29, Rust (stable) with the `aarch64-linux-android` target, and `cargo-ndk`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone --recursive https://github.com/Enginex0/TEESimulator-RS.git
|
git clone --recursive https://github.com/Enginex0/TEESimulator-RS.git
|
||||||
@@ -104,15 +130,13 @@ cd TEESimulator-RS
|
|||||||
./gradlew zipRelease zipDebug
|
./gradlew zipRelease zipDebug
|
||||||
```
|
```
|
||||||
|
|
||||||
Output ZIPs in `out/`. Gradle invokes `cargo ndk` automatically to cross-compile `libcertgen.so`.
|
The ZIPs land in `out/`. Gradle runs `cargo ndk` for you to cross-compile `libcertgen.so`. To build on CI instead, push to `main` or run Actions > Build > Run workflow.
|
||||||
|
|
||||||
Push to `main` or use **Actions > Build > Run workflow** to trigger CI.
|
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
| Root Manager | Status |
|
| Root manager | Status |
|
||||||
|---|---|
|
|---|---|
|
||||||
| KernelSU | Tested (Action button + lifecycle scripts) |
|
| KernelSU | Tested, including the Action button and lifecycle scripts |
|
||||||
| Magisk | Supported |
|
| Magisk | Supported |
|
||||||
| APatch | Supported |
|
| APatch | Supported |
|
||||||
|
|
||||||
@@ -126,13 +150,10 @@ Push to `main` or use **Actions > Build > Run workflow** to trigger CI.
|
|||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- [JingMatrix](https://github.com/JingMatrix/TEESimulator) — original TEESimulator and interception architecture
|
- [JingMatrix](https://github.com/JingMatrix/TEESimulator) for the original TEESimulator and its interception design
|
||||||
- [5ec1cff](https://github.com/5ec1cff/TrickyStore) — TrickyStore, the project that pioneered keystore interception
|
- [ring](https://github.com/briansmith/ring) for the Rust cryptography
|
||||||
- [LSPlt](https://github.com/LSPosed/LSPlt) — PLT hook library
|
- [fatalcoder524](https://github.com/fatalcoder524) for contributions and collaboration
|
||||||
- [ring](https://github.com/briansmith/ring) — Rust cryptography library
|
- [huguangares](https://github.com/huguangares) for collaboration and testing
|
||||||
- [MhmRdd](https://github.com/MhmRdd) — AOSP compliance work via upstream [PR #157](https://github.com/JingMatrix/TEESimulator/pull/157)
|
|
||||||
- [fatalcoder524](https://github.com/fatalcoder524) — contributor and collaborator
|
|
||||||
- [huguangares](https://github.com/huguangares) — collaborator and tester
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+80
-44
@@ -28,7 +28,14 @@ abstract class GitExecutor @Inject constructor(private val execOperations: ExecO
|
|||||||
// Instantiate the helper class using Gradle's object factory
|
// Instantiate the helper class using Gradle's object factory
|
||||||
val gitExecutor = objects.newInstance(GitExecutor::class.java)
|
val gitExecutor = objects.newInstance(GitExecutor::class.java)
|
||||||
|
|
||||||
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
|
// versionCode = git commit count + floor offset. The 2026-07-08 public-release
|
||||||
|
// history scrub (0f1143a) rewrote history and dropped the raw commit count below
|
||||||
|
// the build number already shipped to testers (298), so post-scrub counts read as
|
||||||
|
// downgrades. The floor offset lifts versionCode back above that peak and keeps it
|
||||||
|
// monotonic across the rewrite; each later commit still bumps it by one.
|
||||||
|
val versionCodeFloorOffset = 5
|
||||||
|
val gitCommitCount =
|
||||||
|
gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt() + versionCodeFloorOffset
|
||||||
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
|
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
|
||||||
val verName = "v6.0.1"
|
val verName = "v6.0.1"
|
||||||
|
|
||||||
@@ -66,11 +73,7 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } }
|
||||||
compilerOptions {
|
|
||||||
jvmTarget.set(JvmTarget.JVM_21)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(project(":stub"))
|
compileOnly(project(":stub"))
|
||||||
@@ -79,27 +82,35 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Rust native cert gen build task ---
|
// --- Rust native cert gen build task ---
|
||||||
val buildRustCertgen by tasks.registering(Exec::class) {
|
val buildRustCertgen by
|
||||||
group = "TEESimulator-RS Native Build"
|
tasks.registering(Exec::class) {
|
||||||
description = "Builds libcertgen.so via cargo-ndk for arm64-v8a."
|
group = "TEESimulator-RS Native Build"
|
||||||
|
description = "Builds libcertgen.so via cargo-ndk for arm64-v8a."
|
||||||
|
|
||||||
workingDir = rootProject.projectDir.resolve("native-certgen")
|
workingDir = rootProject.projectDir.resolve("native-certgen")
|
||||||
|
|
||||||
commandLine(
|
commandLine(
|
||||||
"cargo", "ndk",
|
"cargo",
|
||||||
"-t", "arm64-v8a",
|
"ndk",
|
||||||
"-o", rootProject.projectDir.resolve("app/src/main/jniLibs").absolutePath,
|
"-t",
|
||||||
"build", "--release"
|
"arm64-v8a",
|
||||||
)
|
"-o",
|
||||||
|
rootProject.projectDir.resolve("app/src/main/jniLibs").absolutePath,
|
||||||
|
"build",
|
||||||
|
"--release",
|
||||||
|
)
|
||||||
|
|
||||||
inputs.dir(rootProject.projectDir.resolve("native-certgen/src"))
|
inputs.dir(rootProject.projectDir.resolve("native-certgen/src"))
|
||||||
inputs.file(rootProject.projectDir.resolve("native-certgen/Cargo.toml"))
|
inputs.file(rootProject.projectDir.resolve("native-certgen/Cargo.toml"))
|
||||||
inputs.file(rootProject.projectDir.resolve("native-certgen/Cargo.lock"))
|
inputs.file(rootProject.projectDir.resolve("native-certgen/Cargo.lock"))
|
||||||
outputs.dir(rootProject.projectDir.resolve("app/src/main/jniLibs"))
|
outputs.dir(rootProject.projectDir.resolve("app/src/main/jniLibs"))
|
||||||
|
|
||||||
environment("ANDROID_NDK_HOME", android.ndkDirectory.absolutePath)
|
environment("ANDROID_NDK_HOME", android.ndkDirectory.absolutePath)
|
||||||
environment("PATH", "${System.getProperty("user.home")}/.cargo/bin:${System.getenv("PATH") ?: ""}")
|
environment(
|
||||||
}
|
"PATH",
|
||||||
|
"${System.getProperty("user.home")}/.cargo/bin:${System.getenv("PATH") ?: ""}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// AGP auto-detects jniLibs/ as an input to mergeJniLibFolders — wire the dependency
|
// AGP auto-detects jniLibs/ as an input to mergeJniLibFolders — wire the dependency
|
||||||
tasks.configureEach {
|
tasks.configureEach {
|
||||||
@@ -110,31 +121,32 @@ tasks.configureEach {
|
|||||||
|
|
||||||
// Auto-rewrite module/update.json on every packaging build so versionCode and
|
// Auto-rewrite module/update.json on every packaging build so versionCode and
|
||||||
// zipUrl track gitCommitCount automatically, matching module.prop.
|
// zipUrl track gitCommitCount automatically, matching module.prop.
|
||||||
val refreshUpdateJson by tasks.registering {
|
val refreshUpdateJson by
|
||||||
group = "TEESimulator-RS Module Packaging"
|
tasks.registering {
|
||||||
description = "Rewrite module/update.json to match current verName and gitCommitCount."
|
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 updateJsonFile = rootProject.projectDir.resolve("module/update.json")
|
||||||
val capturedVerName = verName
|
val capturedVerName = verName
|
||||||
val capturedCount = gitCommitCount
|
val capturedCount = gitCommitCount
|
||||||
|
|
||||||
inputs.property("verName", capturedVerName)
|
inputs.property("verName", capturedVerName)
|
||||||
inputs.property("gitCommitCount", capturedCount)
|
inputs.property("gitCommitCount", capturedCount)
|
||||||
outputs.file(updateJsonFile)
|
outputs.file(updateJsonFile)
|
||||||
|
|
||||||
doLast {
|
doLast {
|
||||||
val fullVer = "$capturedVerName-$capturedCount"
|
val fullVer = "$capturedVerName-$capturedCount"
|
||||||
updateJsonFile.writeText(
|
updateJsonFile.writeText(
|
||||||
"""{
|
"""{
|
||||||
"version": "$fullVer",
|
"version": "$fullVer",
|
||||||
"versionCode": $capturedCount,
|
"versionCode": $capturedCount,
|
||||||
"zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/download/$fullVer/TEESimulator-RS-$fullVer-Release.zip",
|
"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"
|
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/changelog.md"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
androidComponents {
|
androidComponents {
|
||||||
onVariants(selector().all()) { variant ->
|
onVariants(selector().all()) { variant ->
|
||||||
@@ -177,20 +189,27 @@ androidComponents {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val nativeLibsDir = if (isDebug) {
|
val nativeLibsDir =
|
||||||
"intermediates/merged_native_libs/${variant.name}/merge${capitalized}NativeLibs/out/lib"
|
if (isDebug) {
|
||||||
} else {
|
"intermediates/merged_native_libs/${variant.name}/merge${capitalized}NativeLibs/out/lib"
|
||||||
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
|
} else {
|
||||||
}
|
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
|
||||||
|
}
|
||||||
from(project.layout.buildDirectory.dir(nativeLibsDir)) {
|
from(project.layout.buildDirectory.dir(nativeLibsDir)) {
|
||||||
into("lib")
|
into("lib")
|
||||||
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so", "**/libcertgen.so")
|
include(
|
||||||
|
"**/libinject.so",
|
||||||
|
"**/libTEESimulator.so",
|
||||||
|
"**/libsupervisor.so",
|
||||||
|
"**/libcertgen.so",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now, copy and process the files from 'module' directory.
|
// Now, copy and process the files from 'module' directory.
|
||||||
val sourceModuleDir = rootProject.projectDir.resolve("module")
|
val sourceModuleDir = rootProject.projectDir.resolve("module")
|
||||||
from(sourceModuleDir) {
|
from(sourceModuleDir) {
|
||||||
exclude("module.prop") // Exclude the template file.
|
exclude("module.prop") // Exclude the template file.
|
||||||
|
exclude("diag.sh") // Debug-only diagnostic plane; included for debug below.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy and filter the module.prop template separately.
|
// Copy and filter the module.prop template separately.
|
||||||
@@ -203,8 +222,25 @@ androidComponents {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
from(sourceModuleDir) { include("diag.sh") }
|
||||||
|
}
|
||||||
|
|
||||||
// The destination for all the above 'from' operations.
|
// The destination for all the above 'from' operations.
|
||||||
into(tempModuleDir)
|
into(tempModuleDir)
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
doLast {
|
||||||
|
// Debug-only: grant the keystore + soterserver (platform_app) domains
|
||||||
|
// external-storage access for the per-UID NDJSON sink. diag.sh (shipped
|
||||||
|
// only in debug) carries the shell side of the diagnostic plane.
|
||||||
|
tempModuleDir.get().asFile.resolve("sepolicy.rule")
|
||||||
|
.appendText(
|
||||||
|
"\nallow keystore media_rw_data_file { dir file } *" +
|
||||||
|
"\nallow platform_app media_rw_data_file { dir file } *\n",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Task 2: Zip the prepared files from the temporary directory.
|
// Task 2: Zip the prepared files from the temporary directory.
|
||||||
|
|||||||
@@ -403,7 +403,10 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
|||||||
uint64_t tx_id = ++g_transaction_id_counter;
|
uint64_t tx_id = ++g_transaction_id_counter;
|
||||||
info.transaction_id = tx_id;
|
info.transaction_id = tx_id;
|
||||||
|
|
||||||
LOGV("[Hook] Hijacking Transaction %" PRIu64 " (Code: %u)", tx_id, txn_data->code);
|
// tx_id is the same counter handed to the Kotlin interceptor, and sender_euid is the
|
||||||
|
// calling app; together they correlate this native hijack with that UID's per-UID file.
|
||||||
|
LOGV("[Hook] Hijacking Transaction %" PRIu64 " (Code: %u, uid=%u)", tx_id, txn_data->code,
|
||||||
|
txn_data->sender_euid);
|
||||||
|
|
||||||
// Rewrite the destination to our Stub
|
// Rewrite the destination to our Stub
|
||||||
txn_data->target.ptr = reinterpret_cast<uintptr_t>(g_stub_instance->getWeakRefs());
|
txn_data->target.ptr = reinterpret_cast<uintptr_t>(g_stub_instance->getWeakRefs());
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.content.Context
|
|||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import java.io.File
|
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
import org.matrix.TEESimulator.config.BootStateManager
|
import org.matrix.TEESimulator.config.BootStateManager
|
||||||
@@ -14,6 +13,7 @@ import org.matrix.TEESimulator.config.ConfigurationManager
|
|||||||
import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor
|
import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor
|
||||||
import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor
|
import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor
|
||||||
import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor
|
import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor
|
||||||
|
import org.matrix.TEESimulator.interception.soter.SoterProcessSupervisor
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
import org.matrix.TEESimulator.pki.NativeCertGen
|
import org.matrix.TEESimulator.pki.NativeCertGen
|
||||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||||
@@ -40,8 +40,7 @@ object App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
purgeDebugDiagnostics()
|
val systemContext = prepareEnvironment()
|
||||||
prepareEnvironment()
|
|
||||||
|
|
||||||
// Spoof boot-state props before any hook attaches, so keystore2's
|
// Spoof boot-state props before any hook attaches, so keystore2's
|
||||||
// cached snapshot reflects the spoofed values.
|
// cached snapshot reflects the spoofed values.
|
||||||
@@ -64,6 +63,10 @@ object App {
|
|||||||
|
|
||||||
NativeCertGen.initialize("/data/adb/modules/tricky_store/libcertgen.so")
|
NativeCertGen.initialize("/data/adb/modules/tricky_store/libcertgen.so")
|
||||||
|
|
||||||
|
// Mount the SOTER forge on the on-demand soterserver process. The supervisor
|
||||||
|
// binds and (re)injects on its own thread, returning at once so it never blocks the loop.
|
||||||
|
SoterProcessSupervisor.start(systemContext)
|
||||||
|
|
||||||
// This starts the message queue processing. It blocks here indefinitely
|
// This starts the message queue processing. It blocks here indefinitely
|
||||||
// processing messages until Looper.myLooper().quit() is called.
|
// processing messages until Looper.myLooper().quit() is called.
|
||||||
Looper.loop()
|
Looper.loop()
|
||||||
@@ -73,27 +76,8 @@ 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. */
|
/** Initializes the necessary Android framework internals to satisfy KeyStore requirements. */
|
||||||
private fun prepareEnvironment() {
|
private fun prepareEnvironment(): Context {
|
||||||
// 1. Prepare Main Looper
|
// 1. Prepare Main Looper
|
||||||
if (Looper.getMainLooper() == null) {
|
if (Looper.getMainLooper() == null) {
|
||||||
@Suppress("deprecation") Looper.prepareMainLooper()
|
@Suppress("deprecation") Looper.prepareMainLooper()
|
||||||
@@ -102,8 +86,10 @@ object App {
|
|||||||
// 2. Initialize ActivityThread for the current process
|
// 2. Initialize ActivityThread for the current process
|
||||||
val activityThread = ActivityThread.systemMain()
|
val activityThread = ActivityThread.systemMain()
|
||||||
|
|
||||||
// 3. Get the system context
|
// 3. Get the system context. The stub declares getSystemContext(): ContextImpl
|
||||||
val systemContext = activityThread.getSystemContext()
|
// (a bare class), so cast to the Context it really is at runtime for the wiring.
|
||||||
|
@Suppress("CAST_NEVER_SUCCEEDS")
|
||||||
|
val systemContext = activityThread.getSystemContext() as Context
|
||||||
|
|
||||||
// 4. Create a dummy Application object and attach the context
|
// 4. Create a dummy Application object and attach the context
|
||||||
val app = Application()
|
val app = Application()
|
||||||
@@ -118,6 +104,8 @@ object App {
|
|||||||
ActivityThread::class.java.getDeclaredField("mInitialApplication")
|
ActivityThread::class.java.getDeclaredField("mInitialApplication")
|
||||||
mInitialApplicationField.isAccessible = true
|
mInitialApplicationField.isAccessible = true
|
||||||
mInitialApplicationField.set(activityThread, app)
|
mInitialApplicationField.set(activityThread, app)
|
||||||
|
|
||||||
|
return systemContext
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -44,9 +44,10 @@ object AttestationBuilder {
|
|||||||
): Extension {
|
): Extension {
|
||||||
val keyDescription = buildKeyDescription(params, uid, securityLevel)
|
val keyDescription = buildKeyDescription(params, uid, securityLevel)
|
||||||
SystemLogger.verbose {
|
SystemLogger.verbose {
|
||||||
val formattedString = keyDescription.joinToString(separator = ", ") {
|
val formattedString =
|
||||||
AttestationPatcher.formatAsn1Primitive(it)
|
keyDescription.joinToString(separator = ", ") {
|
||||||
}
|
AttestationPatcher.formatAsn1Primitive(it)
|
||||||
|
}
|
||||||
"Forged attestation data: $formattedString"
|
"Forged attestation data: $formattedString"
|
||||||
}
|
}
|
||||||
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
|
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
|
||||||
@@ -116,7 +117,9 @@ object AttestationBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
|
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
|
||||||
SystemLogger.info("Attestation patch levels for uid=$uid: os=$osPatch, vendor=$vendorPatch, boot=$bootPatch")
|
SystemLogger.info(
|
||||||
|
"Attestation patch levels for uid=$uid: os=$osPatch, vendor=$vendorPatch, boot=$bootPatch"
|
||||||
|
)
|
||||||
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
|
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
|
||||||
if (bootPatch != DO_NOT_REPORT) {
|
if (bootPatch != DO_NOT_REPORT) {
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
@@ -268,7 +271,11 @@ object AttestationBuilder {
|
|||||||
|
|
||||||
if (params.rollbackResistance == true && attestVersion >= 3) {
|
if (params.rollbackResistance == true && attestVersion >= 3) {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_ROLLBACK_RESISTANCE, DERNull.INSTANCE)
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ROLLBACK_RESISTANCE,
|
||||||
|
DERNull.INSTANCE,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,19 +293,31 @@ object AttestationBuilder {
|
|||||||
|
|
||||||
if (params.allowWhileOnBody == true) {
|
if (params.allowWhileOnBody == true) {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_ALLOW_WHILE_ON_BODY, DERNull.INSTANCE)
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ALLOW_WHILE_ON_BODY,
|
||||||
|
DERNull.INSTANCE,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.trustedUserPresenceRequired == true && attestVersion >= 3) {
|
if (params.trustedUserPresenceRequired == true && attestVersion >= 3) {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_TRUSTED_USER_PRESENCE_REQUIRED, DERNull.INSTANCE)
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_TRUSTED_USER_PRESENCE_REQUIRED,
|
||||||
|
DERNull.INSTANCE,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.trustedConfirmationRequired == true && attestVersion >= 3) {
|
if (params.trustedConfirmationRequired == true && attestVersion >= 3) {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_TRUSTED_CONFIRMATION_REQUIRED, DERNull.INSTANCE)
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_TRUSTED_CONFIRMATION_REQUIRED,
|
||||||
|
DERNull.INSTANCE,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,33 +468,51 @@ object AttestationBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (params.callerNonce == true) {
|
if (params.callerNonce == true) {
|
||||||
list.add(
|
list.add(DERTaggedObject(true, AttestationConstants.TAG_CALLER_NONCE, DERNull.INSTANCE))
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_CALLER_NONCE, DERNull.INSTANCE)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
params.activeDateTime?.let {
|
params.activeDateTime?.let {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time))
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ACTIVE_DATETIME,
|
||||||
|
ASN1Integer(it.time),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
params.originationExpireDateTime?.let {
|
params.originationExpireDateTime?.let {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_ORIGINATION_EXPIRE_DATETIME, ASN1Integer(it.time))
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ORIGINATION_EXPIRE_DATETIME,
|
||||||
|
ASN1Integer(it.time),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
params.usageExpireDateTime?.let {
|
params.usageExpireDateTime?.let {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_USAGE_EXPIRE_DATETIME, ASN1Integer(it.time))
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_USAGE_EXPIRE_DATETIME,
|
||||||
|
ASN1Integer(it.time),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
params.usageCountLimit?.let {
|
params.usageCountLimit?.let {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_USAGE_COUNT_LIMIT, ASN1Integer(it.toLong()))
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_USAGE_COUNT_LIMIT,
|
||||||
|
ASN1Integer(it.toLong()),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (params.unlockedDeviceRequired == true) {
|
if (params.unlockedDeviceRequired == true) {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_UNLOCKED_DEVICE_REQUIRED, DERNull.INSTANCE)
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_UNLOCKED_DEVICE_REQUIRED,
|
||||||
|
DERNull.INSTANCE,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,15 @@ package org.matrix.TEESimulator.attestation
|
|||||||
|
|
||||||
import android.security.keystore.KeyProperties
|
import android.security.keystore.KeyProperties
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.PublicKey
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
|
import java.security.interfaces.ECPrivateKey
|
||||||
|
import java.security.interfaces.ECPublicKey
|
||||||
|
import java.security.interfaces.RSAPrivateKey
|
||||||
|
import java.security.interfaces.RSAPublicKey
|
||||||
|
import java.util.Date
|
||||||
import org.bouncycastle.asn1.*
|
import org.bouncycastle.asn1.*
|
||||||
import org.bouncycastle.asn1.x509.Extension
|
import org.bouncycastle.asn1.x509.Extension
|
||||||
import org.bouncycastle.cert.X509CertificateHolder
|
import org.bouncycastle.cert.X509CertificateHolder
|
||||||
@@ -16,7 +23,6 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
|||||||
import org.matrix.TEESimulator.pki.KeyBox
|
import org.matrix.TEESimulator.pki.KeyBox
|
||||||
import org.matrix.TEESimulator.pki.KeyBoxManager
|
import org.matrix.TEESimulator.pki.KeyBoxManager
|
||||||
import org.matrix.TEESimulator.util.toHex
|
import org.matrix.TEESimulator.util.toHex
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the modification (patching) of Android Key Attestation extensions within certificates.
|
* Handles the modification (patching) of Android Key Attestation extensions within certificates.
|
||||||
@@ -67,7 +73,6 @@ object AttestationPatcher {
|
|||||||
originalLeafHolder,
|
originalLeafHolder,
|
||||||
parsedAttestation,
|
parsedAttestation,
|
||||||
keybox,
|
keybox,
|
||||||
originalLeaf.sigAlgName,
|
|
||||||
uid,
|
uid,
|
||||||
notBefore,
|
notBefore,
|
||||||
notAfter,
|
notAfter,
|
||||||
@@ -91,25 +96,12 @@ object AttestationPatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to normalize algorithm names for Bouncy Castle. Old Android versions might reports
|
|
||||||
* "SHA256WITHECDSA", but Bouncy Castle expects "SHA256withECDSA".
|
|
||||||
*/
|
|
||||||
private fun normalizeSignatureAlgorithm(algoName: String): String {
|
|
||||||
// 1. Force uppercase to handle "sha256withecdsa"
|
|
||||||
// 2. Replace "WITH" with "with" to satisfy Bouncy Castle's naming convention
|
|
||||||
return algoName.uppercase().replace("WITH", "with")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new leaf certificate with a modified attestation extension.
|
* Creates a new leaf certificate with a modified attestation extension.
|
||||||
*
|
*
|
||||||
* @param originalLeafHolder A Bouncy Castle holder for the original leaf certificate.
|
* @param originalLeafHolder A Bouncy Castle holder for the original leaf certificate.
|
||||||
* @param parsedAttestation The parsed components of the original attestation.
|
* @param parsedAttestation The parsed components of the original attestation.
|
||||||
* @param keybox The KeyBox containing the new issuer certificate and signing key.
|
* @param keybox The KeyBox containing the new issuer certificate and signing key.
|
||||||
* @param sigAlgName The signature algorithm name (e.g., "SHA256withECDSA") from the original
|
|
||||||
* certificate. This is required to ensure the new certificate is signed using a compatible
|
|
||||||
* algorithm.
|
|
||||||
* @param uid The UID of the application requesting the certificate.
|
* @param uid The UID of the application requesting the certificate.
|
||||||
* @return A new [Certificate] object.
|
* @return A new [Certificate] object.
|
||||||
*/
|
*/
|
||||||
@@ -117,7 +109,6 @@ object AttestationPatcher {
|
|||||||
originalLeafHolder: X509CertificateHolder,
|
originalLeafHolder: X509CertificateHolder,
|
||||||
parsedAttestation: ParsedAttestation,
|
parsedAttestation: ParsedAttestation,
|
||||||
keybox: KeyBox,
|
keybox: KeyBox,
|
||||||
sigAlgName: String,
|
|
||||||
uid: Int,
|
uid: Int,
|
||||||
notBefore: Date? = null,
|
notBefore: Date? = null,
|
||||||
notAfter: Date? = null,
|
notAfter: Date? = null,
|
||||||
@@ -154,9 +145,12 @@ object AttestationPatcher {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign the newly built certificate with the private key from our keybox.
|
// Sign the new leaf with the keybox key. The signature algorithm must match THAT key, not
|
||||||
|
// the original leaf's: when an RSA leaf is re-rooted under an EC-only keybox, this signs
|
||||||
|
// with ECDSA. The RSA subject public key is untouched and the chain still verifies to the
|
||||||
|
// keybox root.
|
||||||
val signer =
|
val signer =
|
||||||
JcaContentSignerBuilder(normalizeSignatureAlgorithm(sigAlgName))
|
JcaContentSignerBuilder(signatureAlgorithmFor(keybox.keyPair.private))
|
||||||
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
.build(keybox.keyPair.private)
|
.build(keybox.keyPair.private)
|
||||||
val newCertificate = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
val newCertificate = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||||
@@ -178,8 +172,8 @@ object AttestationPatcher {
|
|||||||
* 1. A simple key type like "RSA" or "EC".
|
* 1. A simple key type like "RSA" or "EC".
|
||||||
* 2. A full JCA signature algorithm name like "SHA256withRSA".
|
* 2. A full JCA signature algorithm name like "SHA256withRSA".
|
||||||
*
|
*
|
||||||
* @return The [KeyBox] containing the appropriate key pair for signing.
|
* @return The algorithm-matching [KeyBox] when present, otherwise any available key (fail-safe).
|
||||||
* @throws IllegalArgumentException if no matching KeyBox can be found for the derived key type.
|
* @throws IllegalArgumentException only if the keybox file contains no usable signing key.
|
||||||
*/
|
*/
|
||||||
private fun getKeyboxForUidAndAlgorithm(uid: Int, algorithm: String): KeyBox {
|
private fun getKeyboxForUidAndAlgorithm(uid: Int, algorithm: String): KeyBox {
|
||||||
val keyboxFile = ConfigurationManager.getKeyboxFileForUid(uid)
|
val keyboxFile = ConfigurationManager.getKeyboxFileForUid(uid)
|
||||||
@@ -194,12 +188,37 @@ object AttestationPatcher {
|
|||||||
else -> algorithm // If no match, assume it's already a simple key type string.
|
else -> algorithm // If no match, assume it's already a simple key type string.
|
||||||
}
|
}
|
||||||
|
|
||||||
return KeyBoxManager.getAttestationKey(keyboxFile, keyType)
|
val matching = KeyBoxManager.getAttestationKey(keyboxFile, keyType)
|
||||||
|
if (matching != null) return matching
|
||||||
|
|
||||||
|
// Fail-safe: no algorithm-matching key (e.g. an EC-only Google keybox asked to re-root an
|
||||||
|
// RSA leaf). Fall back to any available key instead of throwing -- a throw here aborts the
|
||||||
|
// patch and the caller hands back the device's REAL, unlocked attestation. Re-signing under
|
||||||
|
// the available key keeps the chain rooted at the keybox with our forged, locked Root of
|
||||||
|
// Trust; a leaf's signature algorithm is independent of its subject key, so an RSA subject
|
||||||
|
// key signs validly under an EC keybox key.
|
||||||
|
return KeyBoxManager.getAnyAttestationKey(keyboxFile)?.also {
|
||||||
|
SystemLogger.debug(
|
||||||
|
"No '$keyType' attestation key in $keyboxFile for UID $uid; re-signing under the " +
|
||||||
|
"available keybox key to avoid leaking the device's real attestation."
|
||||||
|
)
|
||||||
|
}
|
||||||
?: throw IllegalArgumentException(
|
?: throw IllegalArgumentException(
|
||||||
"No keybox found for UID $uid and algorithm '$keyType' (derived from input '$algorithm') in file $keyboxFile"
|
"No usable attestation key for UID $uid in file $keyboxFile (requested '$keyType')"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** SHA-256 signature algorithm name matching the keybox signing key's type. */
|
||||||
|
private fun signatureAlgorithmFor(signingKey: PrivateKey): String =
|
||||||
|
when (signingKey) {
|
||||||
|
is ECPrivateKey -> "SHA256withECDSA"
|
||||||
|
is RSAPrivateKey -> "SHA256withRSA"
|
||||||
|
else ->
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Unsupported keybox signing key type: ${signingKey.algorithm}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** Recursively formats an ASN1Primitive into a concise, readable string. */
|
/** Recursively formats an ASN1Primitive into a concise, readable string. */
|
||||||
fun formatAsn1Primitive(obj: ASN1Encodable?): String {
|
fun formatAsn1Primitive(obj: ASN1Encodable?): String {
|
||||||
val primitive = obj?.toASN1Primitive()
|
val primitive = obj?.toASN1Primitive()
|
||||||
@@ -234,6 +253,157 @@ object AttestationPatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Reverse map of attestation tag number to its symbolic name, e.g. 704 -> "ROOT_OF_TRUST". */
|
||||||
|
private val attestTagNames: Map<Int, String> by lazy {
|
||||||
|
AttestationConstants::class
|
||||||
|
.java
|
||||||
|
.fields
|
||||||
|
.filter { it.name.startsWith("TAG_") && it.type == Int::class.java }
|
||||||
|
.associate { (it.get(null) as Int) to it.name.removePrefix("TAG_") }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the full key-attestation extension of [cert] as a single structured line for the
|
||||||
|
* diagnostic dossier, or null when the certificate carries no attestation extension. This is the
|
||||||
|
* ground-truth view of what we actually emitted, so any divergence from a genuine TEE surfaces
|
||||||
|
* directly as a differing field rather than having to be guessed.
|
||||||
|
*/
|
||||||
|
fun formatAttestationExtension(cert: X509Certificate): String? {
|
||||||
|
val rawExtension = cert.getExtensionValue(ATTESTATION_OID.id) ?: return null
|
||||||
|
return runCatching {
|
||||||
|
val keyDescriptionDer = ASN1OctetString.getInstance(rawExtension).octets
|
||||||
|
formatKeyDescription(ASN1Sequence.getInstance(keyDescriptionDer))
|
||||||
|
}
|
||||||
|
.getOrElse { "<unparseable attestation extension: ${it.message}>" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Renders the identity fields of every certificate in a returned chain for the dossier. */
|
||||||
|
fun formatCertChain(chain: List<Certificate>): String =
|
||||||
|
chain
|
||||||
|
.mapIndexed { index, cert ->
|
||||||
|
val x509 = cert as? X509Certificate ?: return@mapIndexed "[$index] <non-X509>"
|
||||||
|
"[$index] subject=${x509.subjectX500Principal.name} " +
|
||||||
|
"issuer=${x509.issuerX500Principal.name} " +
|
||||||
|
"serial=${x509.serialNumber.toString(16)} " +
|
||||||
|
"notBefore=${x509.notBefore} notAfter=${x509.notAfter}"
|
||||||
|
}
|
||||||
|
.joinToString(separator = " ; ")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies every certificate in [chain] against its issuer and renders the outcome for the
|
||||||
|
* dossier. The forged chain is [leaf] + keybox certs, so edge 0<-1 proves the leaf was signed by
|
||||||
|
* the key matching the issuer cert and later edges test the keybox's own chain. For an RSA issuer
|
||||||
|
* it also reports signature-bytes vs modulus-bytes: a signature longer than the modulus is the
|
||||||
|
* exact DATA_TOO_LARGE_FOR_KEY_SIZE the app's verifier throws, so the offending edge is
|
||||||
|
* identifiable from the log alone.
|
||||||
|
*/
|
||||||
|
fun formatChainVerification(chain: List<Certificate>): String {
|
||||||
|
if (chain.size < 2) return "<single cert; nothing to chain-verify>"
|
||||||
|
return (0 until chain.size - 1).joinToString(separator = " ; ") { i ->
|
||||||
|
val child = chain[i] as? X509Certificate ?: return@joinToString "[$i]<non-X509>"
|
||||||
|
val parent =
|
||||||
|
chain[i + 1] as? X509Certificate ?: return@joinToString "[$i]<parent non-X509>"
|
||||||
|
val outcome =
|
||||||
|
runCatching {
|
||||||
|
child.verify(parent.publicKey)
|
||||||
|
"OK"
|
||||||
|
}
|
||||||
|
.getOrElse { "FAIL(${it.javaClass.simpleName}: ${it.message?.take(80)})" }
|
||||||
|
val rsaSizes =
|
||||||
|
(parent.publicKey as? RSAPublicKey)?.let {
|
||||||
|
val sigBytes = child.signature.size
|
||||||
|
val modBytes = (it.modulus.bitLength() + 7) / 8
|
||||||
|
" sig=${sigBytes}B mod=${modBytes}B" + if (sigBytes > modBytes) " OVERSIZE" else ""
|
||||||
|
} ?: ""
|
||||||
|
"[$i]${describeKey(child.publicKey)}<-[${i + 1}]${describeKey(parent.publicKey)}:" +
|
||||||
|
"$outcome$rsaSizes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-cert key type/size, subject, issuer, and signature length, for reconstructing the chain a
|
||||||
|
* caller verifies. The signature length reveals the signer's key size, so a 4096-bit signature
|
||||||
|
* landing on a 2048-bit issuer (DATA_TOO_LARGE) is visible without the certificate bytes.
|
||||||
|
*/
|
||||||
|
fun formatChainKeys(chain: List<Certificate>): String =
|
||||||
|
chain
|
||||||
|
.mapIndexed { index, cert ->
|
||||||
|
val x509 = cert as? X509Certificate ?: return@mapIndexed "[$index]<non-X509>"
|
||||||
|
"[$index]${describeKey(x509.publicKey)} " +
|
||||||
|
"subj=${x509.subjectX500Principal.name} " +
|
||||||
|
"iss=${x509.issuerX500Principal.name} " +
|
||||||
|
"sigLen=${x509.signature.size}B"
|
||||||
|
}
|
||||||
|
.joinToString(separator = " ; ")
|
||||||
|
|
||||||
|
private fun describeKey(key: PublicKey): String =
|
||||||
|
when (key) {
|
||||||
|
is RSAPublicKey -> "RSA${key.modulus.bitLength()}"
|
||||||
|
is ECPublicKey -> "EC${key.params.curve.field.fieldSize}"
|
||||||
|
else -> key.algorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatKeyDescription(seq: ASN1Sequence): String {
|
||||||
|
val fields = seq.toArray()
|
||||||
|
return "attestVer=${formatAsn1Primitive(fields[AttestationConstants.KEY_DESCRIPTION_ATTESTATION_VERSION_INDEX])} " +
|
||||||
|
"attestSecLvl=${formatSecurityLevel(fields[AttestationConstants.KEY_DESCRIPTION_ATTESTATION_SECURITY_LEVEL_INDEX])} " +
|
||||||
|
"kmVer=${formatAsn1Primitive(fields[AttestationConstants.KEY_DESCRIPTION_KEYMINT_VERSION_INDEX])} " +
|
||||||
|
"kmSecLvl=${formatSecurityLevel(fields[AttestationConstants.KEY_DESCRIPTION_KEYMINT_SECURITY_LEVEL_INDEX])} " +
|
||||||
|
"challenge=${formatAsn1Primitive(fields[AttestationConstants.KEY_DESCRIPTION_ATTESTATION_CHALLENGE_INDEX])} " +
|
||||||
|
"uniqueId=${formatAsn1Primitive(fields[AttestationConstants.KEY_DESCRIPTION_UNIQUE_ID_INDEX])} " +
|
||||||
|
"sw=${formatAuthorizationList(fields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX])} " +
|
||||||
|
"tee=${formatAuthorizationList(fields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX])}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatSecurityLevel(obj: ASN1Encodable): String {
|
||||||
|
val level = (obj.toASN1Primitive() as? ASN1Enumerated)?.value?.toInt()
|
||||||
|
val name =
|
||||||
|
when (level) {
|
||||||
|
0 -> "Software"
|
||||||
|
1 -> "TEE"
|
||||||
|
2 -> "StrongBox"
|
||||||
|
else -> "?"
|
||||||
|
}
|
||||||
|
return "$level($name)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatAuthorizationList(obj: ASN1Encodable): String {
|
||||||
|
val seq = obj.toASN1Primitive() as? ASN1Sequence ?: return formatAsn1Primitive(obj)
|
||||||
|
return seq
|
||||||
|
.map { element ->
|
||||||
|
val tagged = element as? ASN1TaggedObject ?: return@map formatAsn1Primitive(element)
|
||||||
|
val name = attestTagNames[tagged.tagNo] ?: "TAG"
|
||||||
|
val value =
|
||||||
|
if (tagged.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST)
|
||||||
|
formatRootOfTrust(tagged.baseObject)
|
||||||
|
else formatAsn1Primitive(tagged.baseObject)
|
||||||
|
"${tagged.tagNo}($name)=$value"
|
||||||
|
}
|
||||||
|
.joinToString(prefix = "[", postfix = "]", separator = ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the Root of Trust sub-sequence explicitly — it is the field a detector most often uses
|
||||||
|
* to unmask a simulated TEE (a random verifiedBootKey, an unexpected verifiedBootState, or a
|
||||||
|
* deviceLocked that disagrees with the bootloader all live here).
|
||||||
|
*/
|
||||||
|
private fun formatRootOfTrust(obj: ASN1Encodable): String {
|
||||||
|
val fields = (obj.toASN1Primitive() as? ASN1Sequence)?.toArray() ?: return formatAsn1Primitive(obj)
|
||||||
|
val state = fields.getOrNull(AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_STATE_INDEX)
|
||||||
|
val stateName =
|
||||||
|
when ((state?.toASN1Primitive() as? ASN1Enumerated)?.value?.toInt()) {
|
||||||
|
0 -> "Verified"
|
||||||
|
1 -> "SelfSigned"
|
||||||
|
2 -> "Unverified"
|
||||||
|
3 -> "Failed"
|
||||||
|
else -> "?"
|
||||||
|
}
|
||||||
|
return "[bootKey=${formatAsn1Primitive(fields.getOrNull(AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX))}, " +
|
||||||
|
"deviceLocked=${formatAsn1Primitive(fields.getOrNull(AttestationConstants.ROOT_OF_TRUST_DEVICE_LOCKED_INDEX))}, " +
|
||||||
|
"verifiedBootState=${formatAsn1Primitive(state)}($stateName), " +
|
||||||
|
"bootHash=${formatAsn1Primitive(fields.getOrNull(AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX))}]"
|
||||||
|
}
|
||||||
|
|
||||||
// Function to check if a given ASN1Sequence contains the Root of Trust tag.
|
// Function to check if a given ASN1Sequence contains the Root of Trust tag.
|
||||||
private fun sequenceContainsRootOfTrust(seq: ASN1Encodable): Boolean {
|
private fun sequenceContainsRootOfTrust(seq: ASN1Encodable): Boolean {
|
||||||
if (seq !is ASN1Sequence) return false
|
if (seq !is ASN1Sequence) return false
|
||||||
@@ -287,7 +457,8 @@ object AttestationPatcher {
|
|||||||
val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed
|
val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed
|
||||||
|
|
||||||
SystemLogger.verbose {
|
SystemLogger.verbose {
|
||||||
val formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
val formattedString =
|
||||||
|
allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
||||||
"Original attestation data: $formattedString"
|
"Original attestation data: $formattedString"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +488,8 @@ object AttestationPatcher {
|
|||||||
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
|
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
|
||||||
val patchedSequence = DERSequence(allFields)
|
val patchedSequence = DERSequence(allFields)
|
||||||
SystemLogger.verbose {
|
SystemLogger.verbose {
|
||||||
val formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
val formattedString =
|
||||||
|
patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
||||||
"Patched attestation data: $formattedString"
|
"Patched attestation data: $formattedString"
|
||||||
}
|
}
|
||||||
val patchedOctets = DEROctetString(patchedSequence)
|
val patchedOctets = DEROctetString(patchedSequence)
|
||||||
|
|||||||
+129
-33
@@ -1,7 +1,7 @@
|
|||||||
package org.matrix.TEESimulator.attestation
|
package org.matrix.TEESimulator.attestation
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.os.Build
|
import android.security.KeyStoreException
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
import android.security.keystore.KeyProperties
|
import android.security.keystore.KeyProperties
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
@@ -9,6 +9,9 @@ import java.security.KeyStore
|
|||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.security.spec.ECGenParameterSpec
|
import java.security.spec.ECGenParameterSpec
|
||||||
|
import java.security.spec.RSAKeyGenParameterSpec
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import org.bouncycastle.asn1.ASN1Integer
|
import org.bouncycastle.asn1.ASN1Integer
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||||
import org.bouncycastle.asn1.ASN1OctetString
|
import org.bouncycastle.asn1.ASN1OctetString
|
||||||
@@ -17,6 +20,7 @@ import org.bouncycastle.asn1.ASN1TaggedObject
|
|||||||
import org.bouncycastle.asn1.x509.Extension
|
import org.bouncycastle.asn1.x509.Extension
|
||||||
import org.bouncycastle.cert.X509CertificateHolder
|
import org.bouncycastle.cert.X509CertificateHolder
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||||
import org.matrix.TEESimulator.util.toHex
|
import org.matrix.TEESimulator.util.toHex
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,22 +65,63 @@ object DeviceAttestationService {
|
|||||||
// A unique alias for the key used to perform the TEE functionality check.
|
// A unique alias for the key used to perform the TEE functionality check.
|
||||||
private const val TEE_CHECK_KEY_ALIAS = "TEESimulator_AttestationCheck"
|
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
|
* Lazily determines if the device's TEE is functional by attempting to generate an
|
||||||
* attestation-backed key pair. The result is cached.
|
* attestation-backed key pair. The result is cached.
|
||||||
*/
|
*/
|
||||||
val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() }
|
val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() }
|
||||||
|
|
||||||
|
// Per (algorithm, security-level) attestation-capability verdicts, keyed by probe-key alias.
|
||||||
|
// A device may attest one algorithm or security level yet lack a provisioned attestation key
|
||||||
|
// for another (e.g. a TEE that attests RSA over a StrongBox that cannot), so each pair is
|
||||||
|
// probed and cached on its own.
|
||||||
|
private data class ProbeSpec(
|
||||||
|
val algorithm: String,
|
||||||
|
val strongBox: Boolean,
|
||||||
|
val keyAlias: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val rsaTeeProbe =
|
||||||
|
ProbeSpec(KeyProperties.KEY_ALGORITHM_RSA, false, "TEESimulator_RsaAttestCheck")
|
||||||
|
private val rsaStrongBoxProbe =
|
||||||
|
ProbeSpec(KeyProperties.KEY_ALGORITHM_RSA, true, "TEESimulator_RsaAttestCheckSb")
|
||||||
|
private val ecTeeProbe =
|
||||||
|
ProbeSpec(KeyProperties.KEY_ALGORITHM_EC, false, "TEESimulator_EcAttestCheck")
|
||||||
|
private val ecStrongBoxProbe =
|
||||||
|
ProbeSpec(KeyProperties.KEY_ALGORITHM_EC, true, "TEESimulator_EcAttestCheckSb")
|
||||||
|
|
||||||
|
private val attestableVerdicts = ConcurrentHashMap<String, Boolean>()
|
||||||
|
private val attestProbesInFlight = ConcurrentHashMap<String, AtomicBoolean>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazily mirrors whether the real TEE can attest device identifiers/properties (the tags added
|
* Whether the real hardware can attest an RSA key at the requested security level. AUTO dispatch
|
||||||
* by `setDevicePropertiesAttestationIncluded`). Hardware that never provisioned device IDs
|
* reads this to forge RSA attestation only where the hardware genuinely cannot serve it.
|
||||||
* returns CANNOT_ATTEST_IDS; the synthesizer consults this so it never forges a capability the
|
*
|
||||||
* real silicon lacks. Cached.
|
* Only a definitive verdict is cached: a successful probe, or a permanent keystore failure. A
|
||||||
|
* transient or unrecognized failure leaves the verdict unset and reports attestable, so dispatch
|
||||||
|
* PATCHes the genuine chain and re-probes next read — a one-off keystore hiccup can never freeze
|
||||||
|
* the device into forging an attestation it could serve.
|
||||||
*/
|
*/
|
||||||
val canAttestDeviceIds: Boolean by lazy { checkDeviceIdAttestation() }
|
fun isRsaAttestable(strongBox: Boolean): Boolean =
|
||||||
|
isHardwareAttestable(if (strongBox) rsaStrongBoxProbe else rsaTeeProbe)
|
||||||
|
|
||||||
|
/** Whether the real hardware can attest an EC key at the requested security level. */
|
||||||
|
fun isEcAttestable(strongBox: Boolean): Boolean =
|
||||||
|
isHardwareAttestable(if (strongBox) ecStrongBoxProbe else ecTeeProbe)
|
||||||
|
|
||||||
|
private fun isHardwareAttestable(probe: ProbeSpec): Boolean {
|
||||||
|
attestableVerdicts[probe.keyAlias]?.let { return it }
|
||||||
|
val probeInFlight =
|
||||||
|
attestProbesInFlight.computeIfAbsent(probe.keyAlias) { AtomicBoolean(false) }
|
||||||
|
if (probeInFlight.compareAndSet(false, true)) {
|
||||||
|
try {
|
||||||
|
probeAttestability(probe)?.let { attestableVerdicts[probe.keyAlias] = it }
|
||||||
|
} finally {
|
||||||
|
probeInFlight.set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attestableVerdicts[probe.keyAlias] ?: true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazily fetches and parses attestation data from a genuinely generated certificate. The result
|
* Lazily fetches and parses attestation data from a genuinely generated certificate. The result
|
||||||
@@ -119,33 +164,80 @@ object DeviceAttestationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Probes whether the real TEE can satisfy device-ID/property attestation, mirroring its actual
|
* Probes whether the real hardware can attest a key matching [probe] by generating one with an
|
||||||
* capability. Gated behind [isTeeFunctional] so a dead TEE never triggers a second doomed
|
* attestation challenge at the probe's algorithm and security level. Mirrors
|
||||||
* probe — it simply reports `false` (cannot attest), the faithful result for such hardware.
|
* [checkTeeFunctionality]; the request runs as the module UID, so it is skipped by interception
|
||||||
|
* and reaches genuine hardware rather than the forge path.
|
||||||
|
*
|
||||||
|
* @return `true` if attestation succeeded, `false` only on a confirmed attestation-keys-
|
||||||
|
* unavailable failure, or `null` on a transient or unrecognized failure where the caller
|
||||||
|
* fails open and re-probes.
|
||||||
*/
|
*/
|
||||||
private fun checkDeviceIdAttestation(): Boolean {
|
private fun probeAttestability(probe: ProbeSpec): Boolean? {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return false
|
val label = "${probe.algorithm} attestation (strongBox=${probe.strongBox})"
|
||||||
if (!isTeeFunctional) return false
|
SystemLogger.info("Performing $label capability check...")
|
||||||
return try {
|
return try {
|
||||||
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
|
||||||
val keyPairGenerator =
|
val keyPairGenerator =
|
||||||
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
|
KeyPairGenerator.getInstance(probe.algorithm, "AndroidKeyStore")
|
||||||
|
|
||||||
val challenge = ByteArray(16).apply { SecureRandom().nextBytes(this) }
|
val challenge = ByteArray(16).apply { SecureRandom().nextBytes(this) }
|
||||||
val spec =
|
|
||||||
KeyGenParameterSpec.Builder(DEVICE_ID_CHECK_KEY_ALIAS, KeyProperties.PURPOSE_SIGN)
|
val builder =
|
||||||
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
|
KeyGenParameterSpec.Builder(probe.keyAlias, KeyProperties.PURPOSE_SIGN)
|
||||||
.setDigests(KeyProperties.DIGEST_SHA256)
|
.setDigests(KeyProperties.DIGEST_SHA256)
|
||||||
.setAttestationChallenge(challenge)
|
.setAttestationChallenge(challenge)
|
||||||
.setDevicePropertiesAttestationIncluded(true)
|
.setIsStrongBoxBacked(probe.strongBox)
|
||||||
.build()
|
if (probe.algorithm == KeyProperties.KEY_ALGORITHM_RSA) {
|
||||||
keyPairGenerator.initialize(spec)
|
builder
|
||||||
|
.setAlgorithmParameterSpec(RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4))
|
||||||
|
.setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
|
||||||
|
} else {
|
||||||
|
builder.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPairGenerator.initialize(builder.build())
|
||||||
keyPairGenerator.generateKeyPair()
|
keyPairGenerator.generateKeyPair()
|
||||||
runCatching { keyStore.deleteEntry(DEVICE_ID_CHECK_KEY_ALIAS) }
|
|
||||||
SystemLogger.info("Device-ID attestation supported by TEE.")
|
SystemLogger.info("$label capability check successful.")
|
||||||
true
|
true
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.info("Device-ID attestation not supported by TEE; mirroring as cannot-attest.")
|
if (isAttestationUnavailable(e)) {
|
||||||
false
|
SystemLogger.info("$label unsupported by hardware; AUTO will forge attestation.")
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
SystemLogger.warning(
|
||||||
|
"$label capability check failed transiently; treating as capable.",
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
deleteProbeKey(probe.keyAlias)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether [error] definitively means the hardware cannot attest the probed key: a permanent
|
||||||
|
* [KeyStoreException] from the keystore. Transient failures and non-keystore errors return
|
||||||
|
* `false`, so the caller fails open and re-probes rather than caching a guess. The probe runs a
|
||||||
|
* fixed, valid spec as root, so its only permanent keystore failure mode is missing attestation
|
||||||
|
* support; [KeyStoreException.isTransientFailure] draws the transient/permanent line.
|
||||||
|
*/
|
||||||
|
private fun isAttestationUnavailable(error: Throwable): Boolean {
|
||||||
|
var cause: Throwable? = error
|
||||||
|
while (cause != null) {
|
||||||
|
val keyStoreError = cause as? KeyStoreException
|
||||||
|
if (keyStoreError != null) return !keyStoreError.isTransientFailure
|
||||||
|
cause = cause.cause
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteProbeKey(keyAlias: String) {
|
||||||
|
try {
|
||||||
|
KeyStore.getInstance("AndroidKeyStore").apply { load(null) }.deleteEntry(keyAlias)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.warning("Failed to delete attestation probe key.", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,19 +284,23 @@ object DeviceAttestationService {
|
|||||||
// The extension's value is an ASN.1 sequence.
|
// The extension's value is an ASN.1 sequence.
|
||||||
val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets)
|
val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets)
|
||||||
SystemLogger.verbose {
|
SystemLogger.verbose {
|
||||||
val formattedString = keyDescriptionSeq.joinToString(separator = ", ") {
|
val formattedString =
|
||||||
AttestationPatcher.formatAsn1Primitive(it)
|
keyDescriptionSeq.joinToString(separator = ", ") {
|
||||||
}
|
AttestationPatcher.formatAsn1Primitive(it)
|
||||||
|
}
|
||||||
"Cached attestation data: $formattedString"
|
"Cached attestation data: $formattedString"
|
||||||
}
|
}
|
||||||
val fields = keyDescriptionSeq.toArray()
|
val fields = keyDescriptionSeq.toArray()
|
||||||
|
|
||||||
val attestVersion =
|
val deviceAttestVersion =
|
||||||
ASN1Integer.getInstance(
|
ASN1Integer.getInstance(
|
||||||
fields[AttestationConstants.KEY_DESCRIPTION_ATTESTATION_VERSION_INDEX]
|
fields[AttestationConstants.KEY_DESCRIPTION_ATTESTATION_VERSION_INDEX]
|
||||||
)
|
)
|
||||||
.positiveValue
|
.positiveValue
|
||||||
.toInt()
|
.toInt()
|
||||||
|
// The device KeyMint HAL can report a version below its OS's AOSP value (100 on an A16
|
||||||
|
// where BAKLAVA mandates 400); cache the AOSP value so the forge matches an updated device.
|
||||||
|
val attestVersion = AndroidDeviceUtils.aospAttestVersion ?: deviceAttestVersion
|
||||||
val keymasterVersion =
|
val keymasterVersion =
|
||||||
ASN1Integer.getInstance(
|
ASN1Integer.getInstance(
|
||||||
fields[AttestationConstants.KEY_DESCRIPTION_KEYMINT_VERSION_INDEX]
|
fields[AttestationConstants.KEY_DESCRIPTION_KEYMINT_VERSION_INDEX]
|
||||||
@@ -298,7 +394,7 @@ object DeviceAttestationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, osPatch=$osPatchLevel, vendorPatch=$vendorPatchLevel, bootPatch=$bootPatchLevel, moduleHash=${moduleHash?.toHex()}, bootKey=${verifiedBootKey?.toHex()}, bootHash=${verifiedBootHash?.toHex()}"
|
"Successfully extracted attestation data: version=$deviceAttestVersion, osVersion=$osVersion, osPatch=$osPatchLevel, vendorPatch=$vendorPatchLevel, bootPatch=$bootPatchLevel, moduleHash=${moduleHash?.toHex()}, bootKey=${verifiedBootKey?.toHex()}, bootHash=${verifiedBootHash?.toHex()}"
|
||||||
)
|
)
|
||||||
return AttestationData(
|
return AttestationData(
|
||||||
moduleHash,
|
moduleHash,
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ data class KeyMintAttestation(
|
|||||||
val maxUsesPerBoot: Int?,
|
val maxUsesPerBoot: Int?,
|
||||||
val maxBootLevel: Int?,
|
val maxBootLevel: Int?,
|
||||||
val minMacLength: Int?,
|
val minMacLength: Int?,
|
||||||
|
val macLength: Int? = null,
|
||||||
val rsaOaepMgfDigest: List<Int>,
|
val rsaOaepMgfDigest: List<Int>,
|
||||||
) {
|
) {
|
||||||
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */
|
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */
|
||||||
@@ -134,6 +135,7 @@ data class KeyMintAttestation(
|
|||||||
maxUsesPerBoot = params.findInteger(Tag.MAX_USES_PER_BOOT),
|
maxUsesPerBoot = params.findInteger(Tag.MAX_USES_PER_BOOT),
|
||||||
maxBootLevel = params.findInteger(Tag.MAX_BOOT_LEVEL),
|
maxBootLevel = params.findInteger(Tag.MAX_BOOT_LEVEL),
|
||||||
minMacLength = params.findInteger(Tag.MIN_MAC_LENGTH),
|
minMacLength = params.findInteger(Tag.MIN_MAC_LENGTH),
|
||||||
|
macLength = params.findInteger(Tag.MAC_LENGTH),
|
||||||
rsaOaepMgfDigest = params.findAllDigests(Tag.RSA_OAEP_MGF_DIGEST),
|
rsaOaepMgfDigest = params.findAllDigests(Tag.RSA_OAEP_MGF_DIGEST),
|
||||||
) {
|
) {
|
||||||
// Log all parsed parameters for debugging purposes.
|
// Log all parsed parameters for debugging purposes.
|
||||||
@@ -142,7 +144,8 @@ data class KeyMintAttestation(
|
|||||||
|
|
||||||
fun isAttestKey(): Boolean = purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY)
|
fun isAttestKey(): Boolean = purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY)
|
||||||
|
|
||||||
fun isImportKey(): Boolean = origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED
|
fun isImportKey(): Boolean =
|
||||||
|
origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Private helper extension functions for parsing KeyParameter arrays ---
|
// --- Private helper extension functions for parsing KeyParameter arrays ---
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
package org.matrix.TEESimulator.config
|
package org.matrix.TEESimulator.config
|
||||||
|
|
||||||
import android.os.SystemProperties
|
import android.os.SystemProperties
|
||||||
|
import java.io.File
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||||
|
|
||||||
object BootStateManager {
|
object BootStateManager {
|
||||||
|
private const val CONFIG_PATH = "/data/adb/tricky_store"
|
||||||
|
private const val BOOT_PROPS_MODE_FILE = "boot_props_mode"
|
||||||
|
|
||||||
|
private enum class BootPropsMode {
|
||||||
|
AUTO,
|
||||||
|
FORCE,
|
||||||
|
DISABLE,
|
||||||
|
}
|
||||||
|
|
||||||
private val targets =
|
private val targets =
|
||||||
linkedMapOf(
|
linkedMapOf(
|
||||||
"ro.boot.verifiedbootstate" to "green",
|
"ro.boot.verifiedbootstate" to "green",
|
||||||
@@ -22,6 +32,25 @@ object BootStateManager {
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun apply() {
|
fun apply() {
|
||||||
|
val mode = readBootPropsMode()
|
||||||
|
when (mode) {
|
||||||
|
BootPropsMode.DISABLE -> {
|
||||||
|
SystemLogger.info("BootStateManager: disabled by $BOOT_PROPS_MODE_FILE")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
BootPropsMode.AUTO -> {
|
||||||
|
if (isOplusFamilyDevice()) {
|
||||||
|
SystemLogger.warning(
|
||||||
|
"BootStateManager: skipping boot-state prop spoofing on Oplus-family device in auto mode"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BootPropsMode.FORCE -> {
|
||||||
|
SystemLogger.info("BootStateManager: force-enabled by $BOOT_PROPS_MODE_FILE")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for ((name, target) in targets) {
|
for ((name, target) in targets) {
|
||||||
val current = SystemProperties.get(name, "")
|
val current = SystemProperties.get(name, "")
|
||||||
if (current.isEmpty()) {
|
if (current.isEmpty()) {
|
||||||
@@ -45,4 +74,50 @@ object BootStateManager {
|
|||||||
AndroidDeviceUtils.setProperty(name, value)
|
AndroidDeviceUtils.setProperty(name, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun shouldSpoofBootProps(): Boolean =
|
||||||
|
when (readBootPropsMode()) {
|
||||||
|
BootPropsMode.DISABLE -> false
|
||||||
|
BootPropsMode.AUTO -> !isOplusFamilyDevice()
|
||||||
|
BootPropsMode.FORCE -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readBootPropsMode(): BootPropsMode {
|
||||||
|
val file = File(CONFIG_PATH, BOOT_PROPS_MODE_FILE)
|
||||||
|
if (!file.exists()) return BootPropsMode.AUTO
|
||||||
|
|
||||||
|
val raw =
|
||||||
|
runCatching { file.readText().trim().lowercase() }
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.warning("BootStateManager: failed to read ${file.absolutePath}", it)
|
||||||
|
return BootPropsMode.AUTO
|
||||||
|
}
|
||||||
|
|
||||||
|
return when (raw) {
|
||||||
|
"1", "true", "on", "enable", "enabled", "force" -> BootPropsMode.FORCE
|
||||||
|
"0", "false", "off", "disable", "disabled", "none" -> BootPropsMode.DISABLE
|
||||||
|
else -> BootPropsMode.AUTO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isOplusFamilyDevice(): Boolean {
|
||||||
|
val props =
|
||||||
|
listOf(
|
||||||
|
"ro.product.manufacturer",
|
||||||
|
"ro.product.brand",
|
||||||
|
"ro.product.vendor.manufacturer",
|
||||||
|
"ro.product.vendor.brand",
|
||||||
|
"ro.product.odm.manufacturer",
|
||||||
|
"ro.product.odm.brand",
|
||||||
|
"ro.boot.hardware.sku",
|
||||||
|
"ro.boot.project_name",
|
||||||
|
)
|
||||||
|
|
||||||
|
val joined =
|
||||||
|
props.joinToString(separator = " ") { name ->
|
||||||
|
SystemProperties.get(name, "")
|
||||||
|
}.lowercase()
|
||||||
|
|
||||||
|
return listOf("oneplus", "oplus", "oppo", "realme").any { joined.contains(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,8 @@ object ConfigurationManager {
|
|||||||
fun isAutoMode(uid: Int): Boolean {
|
fun isAutoMode(uid: Int): Boolean {
|
||||||
for (pkg in getPackagesForUid(uid)) {
|
for (pkg in getPackagesForUid(uid)) {
|
||||||
when (packageModes[pkg]) {
|
when (packageModes[pkg]) {
|
||||||
Mode.GENERATE, Mode.PATCH -> return false
|
Mode.GENERATE,
|
||||||
|
Mode.PATCH -> return false
|
||||||
Mode.AUTO -> return true
|
Mode.AUTO -> return true
|
||||||
null -> continue
|
null -> continue
|
||||||
}
|
}
|
||||||
@@ -112,7 +113,9 @@ object ConfigurationManager {
|
|||||||
when (packageModes[pkg]) {
|
when (packageModes[pkg]) {
|
||||||
Mode.GENERATE -> return Mode.GENERATE
|
Mode.GENERATE -> return Mode.GENERATE
|
||||||
Mode.PATCH -> return Mode.PATCH
|
Mode.PATCH -> return Mode.PATCH
|
||||||
Mode.AUTO -> return if (DeviceAttestationService.isTeeFunctional) Mode.PATCH else Mode.GENERATE
|
Mode.AUTO ->
|
||||||
|
return if (DeviceAttestationService.isTeeFunctional) Mode.PATCH
|
||||||
|
else Mode.GENERATE
|
||||||
null -> continue
|
null -> continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,7 +263,9 @@ object ConfigurationManager {
|
|||||||
// resolves to the real device prop — force boot/vendor through the same path
|
// resolves to the real device prop — force boot/vendor through the same path
|
||||||
// to prevent cross-component date mismatches on non-Pixel devices.
|
// to prevent cross-component date mismatches on non-Pixel devices.
|
||||||
if (newGlobalLevel?.system.equals("prop", ignoreCase = true)) {
|
if (newGlobalLevel?.system.equals("prop", ignoreCase = true)) {
|
||||||
SystemLogger.info("system=prop: forcing boot/vendor to derive from device props (were: boot=${newGlobalLevel?.boot}, vendor=${newGlobalLevel?.vendor})")
|
SystemLogger.info(
|
||||||
|
"system=prop: forcing boot/vendor to derive from device props (were: boot=${newGlobalLevel?.boot}, vendor=${newGlobalLevel?.vendor})"
|
||||||
|
)
|
||||||
newGlobalLevel = newGlobalLevel?.copy(boot = "prop", vendor = "prop")
|
newGlobalLevel = newGlobalLevel?.copy(boot = "prop", vendor = "prop")
|
||||||
}
|
}
|
||||||
contextLines.remove("") // Remove global context to iterate over packages next
|
contextLines.remove("") // Remove global context to iterate over packages next
|
||||||
@@ -293,10 +298,12 @@ object ConfigurationManager {
|
|||||||
|
|
||||||
val file = if (event != DELETE) File(configRoot, path) else null
|
val file = if (event != DELETE) File(configRoot, path) else null
|
||||||
when (path) {
|
when (path) {
|
||||||
TARGET_PACKAGES_FILE -> file?.let { loadTargetPackages(it) }
|
TARGET_PACKAGES_FILE ->
|
||||||
?: SystemLogger.warning("$TARGET_PACKAGES_FILE was deleted.")
|
file?.let { loadTargetPackages(it) }
|
||||||
PATCH_LEVEL_FILE -> file?.let { loadPatchLevelConfig(it) }
|
?: SystemLogger.warning("$TARGET_PACKAGES_FILE was deleted.")
|
||||||
?: SystemLogger.warning("$PATCH_LEVEL_FILE was deleted.")
|
PATCH_LEVEL_FILE ->
|
||||||
|
file?.let { loadPatchLevelConfig(it) }
|
||||||
|
?: SystemLogger.warning("$PATCH_LEVEL_FILE was deleted.")
|
||||||
// Any change to an XML file is assumed to be a keybox.
|
// Any change to an XML file is assumed to be a keybox.
|
||||||
// The cache in KeyBoxManager will handle reloading it on its next use.
|
// The cache in KeyBoxManager will handle reloading it on its next use.
|
||||||
else ->
|
else ->
|
||||||
|
|||||||
@@ -110,16 +110,20 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
*/
|
*/
|
||||||
final override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
|
final override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
|
||||||
val txId = data.readLong()
|
val txId = data.readLong()
|
||||||
val result = try {
|
val result =
|
||||||
when (code) {
|
try {
|
||||||
PRE_TRANSACT_CODE -> handlePreTransact(txId, data)
|
when (code) {
|
||||||
POST_TRANSACT_CODE -> handlePostTransact(txId, data)
|
PRE_TRANSACT_CODE -> handlePreTransact(txId, data)
|
||||||
else -> return super.onTransact(code, data, reply, flags)
|
POST_TRANSACT_CODE -> handlePostTransact(txId, data)
|
||||||
|
else -> return super.onTransact(code, data, reply, flags)
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
SystemLogger.error(
|
||||||
|
"[TX_ID: $txId] Interceptor exception, falling through to HAL",
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
|
||||||
SystemLogger.error("[TX_ID: $txId] Interceptor exception, falling through to HAL", e)
|
|
||||||
TransactionResult.ContinueAndSkipPost
|
|
||||||
}
|
|
||||||
writeResultToReply(result, reply!!)
|
writeResultToReply(result, reply!!)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -220,7 +224,11 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Helper function for consistent logging of intercepted transactions. */
|
/**
|
||||||
|
* Logs an intercepted transaction. For a targeted UID every transaction — whether we intercept
|
||||||
|
* or merely observe it — is recorded on that UID's own diagnostic plane, so its keystore
|
||||||
|
* timeline reads cleanly end to end. Untargeted UIDs get a single terse, rate-limited line.
|
||||||
|
*/
|
||||||
protected fun logTransaction(
|
protected fun logTransaction(
|
||||||
txId: Long,
|
txId: Long,
|
||||||
methodName: String,
|
methodName: String,
|
||||||
@@ -228,15 +236,14 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
callingPid: Int,
|
callingPid: Int,
|
||||||
skipPost: Boolean = false,
|
skipPost: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val isIntercepting = !skipPost && !ConfigurationManager.shouldSkipUid(callingUid)
|
if (SystemLogger.isUidLogged(callingUid)) {
|
||||||
val action = if (isIntercepting) "Intercept" else "Observe"
|
val action = if (skipPost) "observe" else "intercept"
|
||||||
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
|
SystemLogger.uidLog(callingUid, txId, "tx", "$methodName action=$action pid=$callingPid")
|
||||||
val message =
|
return
|
||||||
"[TX_ID: $txId] $action $methodName for packages=[$packages] (uid=$callingUid, pid=$callingPid)"
|
}
|
||||||
if (isIntercepting) {
|
SystemLogger.verbose {
|
||||||
SystemLogger.debug(message)
|
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
|
||||||
} else {
|
"[TX_ID: $txId] Observe $methodName for packages=[$packages] (uid=$callingUid, pid=$callingPid)"
|
||||||
SystemLogger.verbose(message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,18 +305,26 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
target: IBinder,
|
target: IBinder,
|
||||||
interceptor: BinderInterceptor,
|
interceptor: BinderInterceptor,
|
||||||
filteredCodes: IntArray = intArrayOf(),
|
filteredCodes: IntArray = intArrayOf(),
|
||||||
) {
|
): Boolean {
|
||||||
val data = Parcel.obtain()
|
val data = Parcel.obtain()
|
||||||
val reply = Parcel.obtain()
|
val reply = Parcel.obtain()
|
||||||
try {
|
return try {
|
||||||
data.writeStrongBinder(target)
|
data.writeStrongBinder(target)
|
||||||
data.writeStrongBinder(interceptor)
|
data.writeStrongBinder(interceptor)
|
||||||
data.writeInt(filteredCodes.size)
|
data.writeInt(filteredCodes.size)
|
||||||
for (code in filteredCodes) data.writeInt(code)
|
for (code in filteredCodes) data.writeInt(code)
|
||||||
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
|
val ok = backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
|
||||||
SystemLogger.info("Registered interceptor for target: $target (${filteredCodes.size} filtered codes)")
|
if (ok) {
|
||||||
|
SystemLogger.info(
|
||||||
|
"Registered interceptor for target: $target (${filteredCodes.size} filtered codes)"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SystemLogger.error("Register transact returned false for target: $target")
|
||||||
|
}
|
||||||
|
ok
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("Failed to register binder interceptor.", e)
|
SystemLogger.error("Failed to register binder interceptor.", e)
|
||||||
|
false
|
||||||
} finally {
|
} finally {
|
||||||
data.recycle()
|
data.recycle()
|
||||||
reply.recycle()
|
reply.recycle()
|
||||||
|
|||||||
+118
-13
@@ -8,6 +8,8 @@ import android.os.Parcelable
|
|||||||
import android.security.KeyStore
|
import android.security.KeyStore
|
||||||
import android.security.keystore.KeystoreResponse
|
import android.security.keystore.KeystoreResponse
|
||||||
import android.system.keystore2.Authorization
|
import android.system.keystore2.Authorization
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||||
@@ -19,6 +21,11 @@ object InterceptorUtils {
|
|||||||
|
|
||||||
private const val EX_SERVICE_SPECIFIC = -8
|
private const val EX_SERVICE_SPECIFIC = -8
|
||||||
|
|
||||||
|
private const val FLAT_STRIDE_HEADER = 12
|
||||||
|
private const val MAX_AUTH_COUNT = 256
|
||||||
|
private const val SENTINEL_MODTIME = 4_294_967_297L
|
||||||
|
private const val HIGH_MODTIME = 4_999_999_999L
|
||||||
|
|
||||||
private fun synthesizeSseMessage(errorCode: Int): String =
|
private fun synthesizeSseMessage(errorCode: Int): String =
|
||||||
when (errorCode) {
|
when (errorCode) {
|
||||||
2 -> "Error::Rc(SYSTEM_ERROR)"
|
2 -> "Error::Rc(SYSTEM_ERROR)"
|
||||||
@@ -37,12 +44,13 @@ object InterceptorUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply {
|
fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply {
|
||||||
val parcel = Parcel.obtain().apply {
|
val parcel =
|
||||||
writeInt(EX_SERVICE_SPECIFIC)
|
Parcel.obtain().apply {
|
||||||
writeString(synthesizeSseMessage(errorCode))
|
writeInt(EX_SERVICE_SPECIFIC)
|
||||||
writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196)
|
writeString(synthesizeSseMessage(errorCode))
|
||||||
writeInt(errorCode)
|
writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196)
|
||||||
}
|
writeInt(errorCode)
|
||||||
|
}
|
||||||
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,24 +120,25 @@ object InterceptorUtils {
|
|||||||
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Correlates a captured reply parcel to the app that triggered it, for [createTypedObjectReply]. */
|
||||||
|
data class ReplyDiagnostic(val uid: Int, val txId: Long?, val event: String)
|
||||||
|
|
||||||
/** Creates an `OverrideReply` parcel containing a Parcelable object. */
|
/** Creates an `OverrideReply` parcel containing a Parcelable object. */
|
||||||
fun <T : Parcelable?> createTypedObjectReply(
|
fun <T : Parcelable?> createTypedObjectReply(
|
||||||
obj: T,
|
obj: T,
|
||||||
flags: Int = 0,
|
flags: Int = 0,
|
||||||
diagnosticTag: String? = null,
|
diagnostic: ReplyDiagnostic? = null,
|
||||||
): BinderInterceptor.TransactionResult.OverrideReply {
|
): BinderInterceptor.TransactionResult.OverrideReply {
|
||||||
val parcel =
|
val parcel =
|
||||||
Parcel.obtain().apply {
|
Parcel.obtain().apply {
|
||||||
writeNoException()
|
writeNoException()
|
||||||
writeTypedObject(obj, flags)
|
writeTypedObject(obj, flags)
|
||||||
}
|
}
|
||||||
if (diagnosticTag != null && SystemLogger.isDebugBuild) {
|
if (diagnostic != null && SystemLogger.isUidLogged(diagnostic.uid)) {
|
||||||
val savedPos = parcel.dataPosition()
|
val savedPos = parcel.dataPosition()
|
||||||
val wire = parcel.marshall()
|
val wire = parcel.marshall()
|
||||||
parcel.setDataPosition(savedPos)
|
parcel.setDataPosition(savedPos)
|
||||||
val path = "/data/local/tmp/teesim-$diagnosticTag.bin"
|
SystemLogger.uidLogRaw(diagnostic.uid, diagnostic.txId, diagnostic.event, "len=${wire.size}", wire)
|
||||||
runCatching { java.io.File(path).writeBytes(wire) }
|
|
||||||
SystemLogger.debug("[$diagnosticTag] reply len=${wire.size} path=$path")
|
|
||||||
}
|
}
|
||||||
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
}
|
}
|
||||||
@@ -189,8 +198,8 @@ object InterceptorUtils {
|
|||||||
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
|
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
|
||||||
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
|
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
|
||||||
|
|
||||||
return authorizations
|
val patched =
|
||||||
.map { auth ->
|
authorizations.map { auth ->
|
||||||
val replacement =
|
val replacement =
|
||||||
when (auth.keyParameter.tag) {
|
when (auth.keyParameter.tag) {
|
||||||
Tag.OS_PATCHLEVEL ->
|
Tag.OS_PATCHLEVEL ->
|
||||||
@@ -216,5 +225,101 @@ object InterceptorUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.toTypedArray()
|
.toTypedArray()
|
||||||
|
return normalizeAuthorizationLayout(patched)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reorders a generateKey reply's authorizations only when the marshalled reply would read, to a
|
||||||
|
* flat 12-byte-stride parcel fingerprint, as the TEE-simulator sentinel: a last slot of
|
||||||
|
* securityLevel 4 or 256, tag 1, union 32 with the 0x1_0000_0001 pseudo-timestamp, or any
|
||||||
|
* timestamp past [HIGH_MODTIME]. Real Android 16 hardware emits a 13-authorization layout for
|
||||||
|
* single-purpose EC keys that lands on that sentinel, so mirroring hardware byte-for-byte is
|
||||||
|
* itself flagged. Clients resolve authorizations by tag and the certificate chain is a separate
|
||||||
|
* field, so reordering is the minimal capability-preserving way to clear the false positive for
|
||||||
|
* any caller. Already-clean replies are returned unchanged.
|
||||||
|
*/
|
||||||
|
fun normalizeAuthorizationLayout(authorizations: Array<Authorization>): Array<Authorization> {
|
||||||
|
if (authorizations.size < 2) return authorizations
|
||||||
|
if (!flatStrideFingerprintMatches(marshalTypedArray(authorizations))) return authorizations
|
||||||
|
val n = authorizations.size
|
||||||
|
for (src in 1 until n) {
|
||||||
|
val candidate = moveAuthorization(authorizations, src, 0)
|
||||||
|
if (!flatStrideFingerprintMatches(marshalTypedArray(candidate))) return candidate
|
||||||
|
}
|
||||||
|
for (src in 0 until n) {
|
||||||
|
for (dst in 0 until n) {
|
||||||
|
if (src == dst) continue
|
||||||
|
val candidate = moveAuthorization(authorizations, src, dst)
|
||||||
|
if (!flatStrideFingerprintMatches(marshalTypedArray(candidate))) return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return authorizations
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun moveAuthorization(
|
||||||
|
authorizations: Array<Authorization>,
|
||||||
|
src: Int,
|
||||||
|
dst: Int,
|
||||||
|
): Array<Authorization> {
|
||||||
|
val reordered = authorizations.toMutableList()
|
||||||
|
reordered.add(dst, reordered.removeAt(src))
|
||||||
|
return reordered.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun marshalTypedArray(authorizations: Array<Authorization>): ByteArray {
|
||||||
|
val parcel = Parcel.obtain()
|
||||||
|
return try {
|
||||||
|
// keystore2 AIDL compile stubs omit the Parcelable supertype these types carry at runtime.
|
||||||
|
parcel.writeTypedArray(authorizations.map { it as Parcelable }.toTypedArray(), 0)
|
||||||
|
parcel.marshall()
|
||||||
|
} finally {
|
||||||
|
parcel.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun flatStrideFingerprintMatches(marshalled: ByteArray): Boolean =
|
||||||
|
runCatching {
|
||||||
|
val parcel = ByteBuffer.wrap(marshalled).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
val count = parcel.getInt(0)
|
||||||
|
if (count !in 1..MAX_AUTH_COUNT) return@runCatching false
|
||||||
|
var off = 4
|
||||||
|
var lastSec = 0L
|
||||||
|
var lastTag = 0L
|
||||||
|
var lastUnion = 0L
|
||||||
|
repeat(count) {
|
||||||
|
lastSec = u32(parcel, off)
|
||||||
|
lastTag = u32(parcel, off + 4)
|
||||||
|
lastUnion = u32(parcel, off + 8)
|
||||||
|
off += FLAT_STRIDE_HEADER
|
||||||
|
off = alignWord(off + flatPayloadSize(parcel, off, lastUnion))
|
||||||
|
}
|
||||||
|
off = skipDriftedByteArray(parcel, off)
|
||||||
|
off = skipDriftedByteArray(parcel, off)
|
||||||
|
val modtime = parcel.getLong(alignWord(off))
|
||||||
|
val unknownUnion = lastUnion !in 0..14
|
||||||
|
modtime > HIGH_MODTIME ||
|
||||||
|
(modtime == SENTINEL_MODTIME &&
|
||||||
|
(lastSec == 4L || lastSec == 256L) &&
|
||||||
|
lastTag == 1L &&
|
||||||
|
lastUnion == 32L &&
|
||||||
|
unknownUnion)
|
||||||
|
}.getOrDefault(false)
|
||||||
|
|
||||||
|
private fun flatPayloadSize(parcel: ByteBuffer, off: Int, union: Long): Int =
|
||||||
|
when {
|
||||||
|
union in 1..11 -> 4
|
||||||
|
union == 12L || union == 13L -> 8
|
||||||
|
union == 14L -> alignWord(off + 4 + parcel.getInt(off)) - off
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun skipDriftedByteArray(parcel: ByteBuffer, off: Int): Int {
|
||||||
|
if (parcel.getInt(off) == 0) return off + 4
|
||||||
|
val lengthPos = off + 4
|
||||||
|
return alignWord(lengthPos + 4 + parcel.getInt(lengthPos))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun u32(parcel: ByteBuffer, off: Int): Long = parcel.getInt(off).toLong() and 0xFFFFFFFFL
|
||||||
|
|
||||||
|
private fun alignWord(off: Int): Int = (off + 3) and 3.inv()
|
||||||
}
|
}
|
||||||
|
|||||||
+214
-74
@@ -18,6 +18,7 @@ import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
|||||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
import org.matrix.TEESimulator.interception.keystore.shim.GeneratedKeyPersistence
|
import org.matrix.TEESimulator.interception.keystore.shim.GeneratedKeyPersistence
|
||||||
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
|
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
|
||||||
|
import org.matrix.TEESimulator.logging.AttestationDossier
|
||||||
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
import org.matrix.TEESimulator.pki.CertificateGenerator
|
import org.matrix.TEESimulator.pki.CertificateGenerator
|
||||||
@@ -63,6 +64,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
|
|
||||||
private const val RESPONSE_KEY_NOT_FOUND = 7
|
private const val RESPONSE_KEY_NOT_FOUND = 7
|
||||||
private const val RESPONSE_PERMISSION_DENIED = 6
|
private const val RESPONSE_PERMISSION_DENIED = 6
|
||||||
|
private const val KEY_PERMISSION_GET_INFO = 0x4
|
||||||
|
private const val KEY_PERMISSION_UPDATE = 0x80
|
||||||
|
|
||||||
// KeyStoreManager.grantKeyAccess() became a public app API in Android 16 (API 36). Before that,
|
// 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
|
// grant was a hidden API and SELinux denied untrusted_app, so a synthetic-key grant must answer
|
||||||
@@ -120,7 +123,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
Keystore2MaintenanceInterceptor,
|
Keystore2MaintenanceInterceptor,
|
||||||
Keystore2MaintenanceInterceptor.interceptedCodes,
|
Keystore2MaintenanceInterceptor.interceptedCodes,
|
||||||
)
|
)
|
||||||
} ?: SystemLogger.warning("Maintenance binder not found; skipping lifecycle parity.")
|
}
|
||||||
|
?: SystemLogger.warning(
|
||||||
|
"Maintenance binder not found; skipping lifecycle parity."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.onFailure { SystemLogger.error("Failed to intercept maintenance binder.", it) }
|
.onFailure { SystemLogger.error("Failed to intercept maintenance binder.", it) }
|
||||||
}
|
}
|
||||||
@@ -219,22 +225,28 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
?: return TransactionResult.ContinueAndSkipPost
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
|
|
||||||
// Domain.GRANT read (Android 16+ KeyStoreManager grant). Served for ANY grantee uid —
|
// Domain.GRANT read (Android 16+ KeyStoreManager grant). Served for ANY grantee uid —
|
||||||
// including isolated services (bindIsolatedService) with no package mapping — so resolve
|
// including isolated services (bindIsolatedService) with no package mapping — so
|
||||||
// it before the package-scoped skip; caller-binding in resolveGrant() is the real access
|
// resolve
|
||||||
// gate. On Android <= 15 no grants are ever issued (grant() denies), so softwareGrants is
|
// 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.
|
// empty and this falls through to the real keystore2.
|
||||||
if (code == GET_KEY_ENTRY_TRANSACTION && descriptor.domain == Domain.GRANT) {
|
if (code == GET_KEY_ENTRY_TRANSACTION && descriptor.domain == Domain.GRANT) {
|
||||||
val grant =
|
val grant =
|
||||||
KeyMintSecurityLevelInterceptor.resolveGrant(descriptor.nspace, callingUid)
|
KeyMintSecurityLevelInterceptor.resolveGrant(descriptor.nspace, callingUid)
|
||||||
if (grant == null) {
|
if (grant == null) {
|
||||||
// Ours but wrong caller -> KEY_NOT_FOUND (caller-binding); not ours -> real keystore2.
|
// Ours but wrong caller -> KEY_NOT_FOUND (caller-binding); not ours -> real
|
||||||
|
// keystore2.
|
||||||
return if (
|
return if (
|
||||||
KeyMintSecurityLevelInterceptor.softwareGrants.containsKey(descriptor.nspace)
|
KeyMintSecurityLevelInterceptor.softwareGrants.containsKey(
|
||||||
|
descriptor.nspace
|
||||||
|
)
|
||||||
)
|
)
|
||||||
InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
|
InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
|
||||||
else TransactionResult.ContinueAndSkipPost
|
else TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
if ((grant.accessVector and 0x4) == 0) { // GET_INFO = 0x4 (access-vector gate)
|
if ((grant.accessVector and KEY_PERMISSION_GET_INFO) == 0) {
|
||||||
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
|
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
|
||||||
}
|
}
|
||||||
val response =
|
val response =
|
||||||
@@ -244,7 +256,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
return InterceptorUtils.createTypedObjectReply(response)
|
return InterceptorUtils.createTypedObjectReply(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ConfigurationManager.shouldSkipUid(callingUid))
|
// generateKey force-forges attest/device-id keys even for skipped UIDs; getKeyEntry
|
||||||
|
// must serve them back or the framework's attestKeyAlias lookup gets KEY_NOT_FOUND.
|
||||||
|
if (code != GET_KEY_ENTRY_TRANSACTION && ConfigurationManager.shouldSkipUid(callingUid))
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
|
||||||
if (code == DELETE_KEY_TRANSACTION) {
|
if (code == DELETE_KEY_TRANSACTION) {
|
||||||
@@ -253,12 +267,16 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
KeyIdentifier(callingUid, descriptor.alias)
|
KeyIdentifier(callingUid, descriptor.alias)
|
||||||
} else if (descriptor.domain == Domain.KEY_ID) {
|
} else if (descriptor.domain == Domain.KEY_ID) {
|
||||||
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||||
callingUid, descriptor.nspace
|
callingUid,
|
||||||
)?.let { info ->
|
descriptor.nspace,
|
||||||
KeyMintSecurityLevelInterceptor.generatedKeys.entries
|
)
|
||||||
.find { it.value.nspace == info.nspace && it.key.uid == callingUid }
|
?.let { info ->
|
||||||
?.key
|
KeyMintSecurityLevelInterceptor.generatedKeys.entries
|
||||||
}
|
.find {
|
||||||
|
it.value.nspace == info.nspace && it.key.uid == callingUid
|
||||||
|
}
|
||||||
|
?.key
|
||||||
|
}
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
if (keyId != null) {
|
if (keyId != null) {
|
||||||
@@ -289,27 +307,34 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
// "Captured private binder exception during timing skip".
|
// "Captured private binder exception during timing skip".
|
||||||
// Resolving by KEY_ID and returning the cached response keeps
|
// Resolving by KEY_ID and returning the cached response keeps
|
||||||
// the call on the happy path, eliminating the warmup signal.
|
// the call on the happy path, eliminating the warmup signal.
|
||||||
val info = KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
val info =
|
||||||
callingUid, descriptor.nspace
|
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||||
)
|
callingUid,
|
||||||
|
descriptor.nspace,
|
||||||
|
)
|
||||||
if (info?.response != null) {
|
if (info?.response != null) {
|
||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"[TX_ID: $txId] Found generated response via KEY_ID nspace=${descriptor.nspace}"
|
"[TX_ID: $txId] Found generated response via KEY_ID nspace=${descriptor.nspace}"
|
||||||
)
|
)
|
||||||
|
logServedChain(callingUid, txId, "keyid:${descriptor.nspace}", info.response)
|
||||||
return InterceptorUtils.createTypedObjectReply(info.response)
|
return InterceptorUtils.createTypedObjectReply(info.response)
|
||||||
}
|
}
|
||||||
val teeResp = KeyMintSecurityLevelInterceptor.findTeeResponseByKeyId(
|
val teeResp =
|
||||||
callingUid, descriptor.nspace
|
KeyMintSecurityLevelInterceptor.findTeeResponseByKeyId(
|
||||||
)
|
callingUid,
|
||||||
|
descriptor.nspace,
|
||||||
|
)
|
||||||
if (teeResp != null) {
|
if (teeResp != null) {
|
||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"[TX_ID: $txId] Found TEE response via KEY_ID nspace=${descriptor.nspace}"
|
"[TX_ID: $txId] Found TEE response via KEY_ID nspace=${descriptor.nspace}"
|
||||||
)
|
)
|
||||||
|
logServedChain(callingUid, txId, "keyid:${descriptor.nspace}", teeResp)
|
||||||
return InterceptorUtils.createTypedObjectReply(teeResp)
|
return InterceptorUtils.createTypedObjectReply(teeResp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Domain.GRANT is handled earlier (before the package-scoped skip); an alias-less
|
// 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.
|
// read reaching here is KEY_ID or unknown, so it falls through to the real
|
||||||
|
// keystore2.
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
||||||
@@ -317,10 +342,16 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
if (deletedSoftwareKeys.remove(keyId)) {
|
if (deletedSoftwareKeys.remove(keyId)) {
|
||||||
SystemLogger.info("[TX_ID: $txId] Returning KEY_NOT_FOUND for deleted key ${descriptor.alias}")
|
SystemLogger.info(
|
||||||
|
"[TX_ID: $txId] Returning KEY_NOT_FOUND for deleted key ${descriptor.alias}"
|
||||||
|
)
|
||||||
return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
|
return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
|
||||||
}
|
}
|
||||||
return TransactionResult.Continue
|
// Owned keys were served above; for a skipped UID a non-owned key must still skip
|
||||||
|
// post-processing so we never patch an un-targeted app's real key.
|
||||||
|
return if (ConfigurationManager.shouldSkipUid(callingUid))
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
else TransactionResult.Continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
|
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
|
||||||
@@ -328,8 +359,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
|
|
||||||
SystemLogger.info("[TX_ID: $txId] Found generated response for ${descriptor.alias}:")
|
SystemLogger.info("[TX_ID: $txId] Found generated response for ${descriptor.alias}:")
|
||||||
response.metadata?.authorizations?.forEach {
|
response.metadata?.authorizations?.forEach {
|
||||||
KeyMintParameterLogger.logParameter(it.keyParameter)
|
KeyMintParameterLogger.logParameter(callingUid, txId, it.keyParameter)
|
||||||
}
|
}
|
||||||
|
logServedChain(callingUid, txId, descriptor.alias, response)
|
||||||
return InterceptorUtils.createTypedObjectReply(response)
|
return InterceptorUtils.createTypedObjectReply(response)
|
||||||
} else if (code == GRANT_TRANSACTION) {
|
} else if (code == GRANT_TRANSACTION) {
|
||||||
logTransaction(txId, transactionNames[code] ?: "grant", callingUid, callingPid)
|
logTransaction(txId, transactionNames[code] ?: "grant", callingUid, callingPid)
|
||||||
@@ -339,14 +371,16 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
?: return TransactionResult.ContinueAndSkipPost
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
val granteeUid = data.readInt()
|
val granteeUid = data.readInt()
|
||||||
val accessVector = data.readInt()
|
val accessVector = data.readInt()
|
||||||
// Synthetic (generatedKeys) AND patch-mode (teeResponses) keys are ours; both must grant
|
// Synthetic (generatedKeys) AND patch-mode (teeResponses) keys are ours; both must
|
||||||
// coherently so the Domain.GRANT readback returns the same chain the owner read returns.
|
// 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
|
// Real hardware keys fall through to the real keystore2, which applies the same SELinux
|
||||||
// gate the platform would.
|
// gate the platform would.
|
||||||
val ownerKeyId =
|
val ownerKeyId =
|
||||||
resolveOwnerKeyId(key, callingUid)
|
resolveOwnerKeyId(key, callingUid)?.takeIf {
|
||||||
?.takeIf { KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) }
|
KeyMintSecurityLevelInterceptor.ownsKeyResponse(it)
|
||||||
?: return TransactionResult.ContinueAndSkipPost
|
} ?: return TransactionResult.ContinueAndSkipPost
|
||||||
// Version-gated to mirror the real TEE 1:1. Pre-Android-16, grant was a hidden API and
|
// 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
|
// SELinux denied untrusted_app, so keystore2 returns PERMISSION_DENIED. Android 16
|
||||||
// (API 36) exposes KeyStoreManager.grantKeyAccess(), so an app grants its own key:
|
// (API 36) exposes KeyStoreManager.grantKeyAccess(), so an app grants its own key:
|
||||||
@@ -373,9 +407,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
?: return TransactionResult.ContinueAndSkipPost
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
val granteeUid = data.readInt()
|
val granteeUid = data.readInt()
|
||||||
val ownerKeyId =
|
val ownerKeyId =
|
||||||
resolveOwnerKeyId(key, callingUid)
|
resolveOwnerKeyId(key, callingUid)?.takeIf {
|
||||||
?.takeIf { KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) }
|
KeyMintSecurityLevelInterceptor.ownsKeyResponse(it)
|
||||||
?: return TransactionResult.ContinueAndSkipPost
|
} ?: return TransactionResult.ContinueAndSkipPost
|
||||||
// Same version gate as grant(): denied pre-36, revoke the virtualized grant on 36+.
|
// Same version gate as grant(): denied pre-36, revoke the virtualized grant on 36+.
|
||||||
if (Build.VERSION.SDK_INT < GRANT_PUBLIC_API_SDK) {
|
if (Build.VERSION.SDK_INT < GRANT_PUBLIC_API_SDK) {
|
||||||
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
|
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
|
||||||
@@ -423,10 +457,11 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
it.uid == callingUid
|
it.uid == callingUid
|
||||||
}
|
}
|
||||||
val totalCount = hardwareCount + softwareCount
|
val totalCount = hardwareCount + softwareCount
|
||||||
val parcel = Parcel.obtain().apply {
|
val parcel =
|
||||||
writeNoException()
|
Parcel.obtain().apply {
|
||||||
writeInt(totalCount)
|
writeNoException()
|
||||||
}
|
writeInt(totalCount)
|
||||||
|
}
|
||||||
TransactionResult.OverrideReply(parcel)
|
TransactionResult.OverrideReply(parcel)
|
||||||
}
|
}
|
||||||
.getOrElse {
|
.getOrElse {
|
||||||
@@ -469,8 +504,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||||
|
|
||||||
if (userUpdatedKeys.remove(keyId)) {
|
if (userUpdatedKeys.remove(keyId)) {
|
||||||
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: userUpdated=true, skipping patch" }
|
SystemLogger.trace {
|
||||||
SystemLogger.debug("[TX_ID: $txId] Skipping cert patch for user-updated key $keyId.")
|
"[TRACE-$txId] getKeyEntry $keyId: userUpdated=true, skipping patch"
|
||||||
|
}
|
||||||
|
SystemLogger.debug(
|
||||||
|
"[TX_ID: $txId] Skipping cert patch for user-updated key $keyId."
|
||||||
|
)
|
||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,18 +519,29 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
|
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()}" }
|
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()) {
|
if (parsedParameters.isImportKey()) {
|
||||||
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
||||||
if (retainedChain == null) {
|
if (retainedChain == null) {
|
||||||
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, no retained chain, skip" }
|
SystemLogger.trace {
|
||||||
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
|
"[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
|
return TransactionResult.SkipTransaction
|
||||||
}
|
}
|
||||||
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, SERVING RETAINED CHAIN (detection vector!)" }
|
SystemLogger.trace {
|
||||||
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
|
"[TRACE-$txId] getKeyEntry $keyId: imported, SERVING RETAINED CHAIN (detection vector!)"
|
||||||
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
|
}
|
||||||
|
SystemLogger.info(
|
||||||
|
"[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId"
|
||||||
|
)
|
||||||
|
CertificateHelper.updateCertificateChain(response.metadata, retainedChain)
|
||||||
|
.getOrThrow()
|
||||||
response.metadata.authorizations =
|
response.metadata.authorizations =
|
||||||
InterceptorUtils.patchAuthorizations(
|
InterceptorUtils.patchAuthorizations(
|
||||||
response.metadata.authorizations,
|
response.metadata.authorizations,
|
||||||
@@ -501,8 +551,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)) {
|
if (KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)) {
|
||||||
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: in importedKeys set, skip" }
|
SystemLogger.trace {
|
||||||
SystemLogger.debug("[TX_ID: $txId] Skipping attest-key override for imported key $keyId")
|
"[TRACE-$txId] getKeyEntry $keyId: in importedKeys set, skip"
|
||||||
|
}
|
||||||
|
SystemLogger.debug(
|
||||||
|
"[TX_ID: $txId] Skipping attest-key override for imported key $keyId"
|
||||||
|
)
|
||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,17 +599,19 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
// Snapshot metadata bytes for the same reason as the
|
// Snapshot metadata bytes for the same reason as the
|
||||||
// primary doSoftwareKeyGen path — loss-less restore
|
// primary doSoftwareKeyGen path — loss-less restore
|
||||||
// after reboot.
|
// after reboot.
|
||||||
val metadataBytesForPersist = response.metadata?.let { md ->
|
val metadataBytesForPersist =
|
||||||
runCatching {
|
response.metadata?.let { md ->
|
||||||
val parcel = android.os.Parcel.obtain()
|
runCatching {
|
||||||
try {
|
val parcel = android.os.Parcel.obtain()
|
||||||
md.writeToParcel(parcel, 0)
|
try {
|
||||||
parcel.marshall()
|
md.writeToParcel(parcel, 0)
|
||||||
} finally {
|
parcel.marshall()
|
||||||
parcel.recycle()
|
} finally {
|
||||||
}
|
parcel.recycle()
|
||||||
}.getOrNull()
|
}
|
||||||
}
|
}
|
||||||
|
.getOrNull()
|
||||||
|
}
|
||||||
GeneratedKeyPersistence.save(
|
GeneratedKeyPersistence.save(
|
||||||
keyId = keyId,
|
keyId = keyId,
|
||||||
keyPair = keyData.first,
|
keyPair = keyData.first,
|
||||||
@@ -609,6 +665,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
callingUid,
|
callingUid,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PATCH decode point: the patched chain actually served back to the app on
|
||||||
|
// getKeyEntry — the ground truth a patch-mode detector reads.
|
||||||
|
AttestationDossier.log(callingUid, txId, "PATCH", finalChain.asList())
|
||||||
|
|
||||||
return InterceptorUtils.createTypedObjectReply(response)
|
return InterceptorUtils.createTypedObjectReply(response)
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
@@ -623,33 +683,90 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the owner [KeyIdentifier] a grant/ungrant call targets. APP/alias keys map
|
* Resolves the owner [KeyIdentifier] a grant/ungrant call targets. APP/alias keys map directly;
|
||||||
* directly; KEY_ID keys are looked up by nspace (mirrors the deleteKey resolver). Returns
|
* KEY_ID keys are looked up by nspace (mirrors the deleteKey resolver). Returns null for
|
||||||
* null for anything not addressable, so callers fall through to the real keystore2.
|
* anything not addressable, so callers fall through to the real keystore2.
|
||||||
*/
|
*/
|
||||||
private fun resolveOwnerKeyId(descriptor: KeyDescriptor, callingUid: Int): KeyIdentifier? =
|
private fun resolveOwnerKeyId(descriptor: KeyDescriptor, callingUid: Int): KeyIdentifier? =
|
||||||
when {
|
when {
|
||||||
descriptor.alias != null -> KeyIdentifier(callingUid, descriptor.alias)
|
descriptor.alias != null -> KeyIdentifier(callingUid, descriptor.alias)
|
||||||
descriptor.domain == Domain.KEY_ID ->
|
descriptor.domain == Domain.KEY_ID ->
|
||||||
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(callingUid, descriptor.nspace)
|
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||||
|
callingUid,
|
||||||
|
descriptor.nspace,
|
||||||
|
)
|
||||||
?.let { info ->
|
?.let { info ->
|
||||||
KeyMintSecurityLevelInterceptor.generatedKeys.entries
|
KeyMintSecurityLevelInterceptor.generatedKeys.entries
|
||||||
.firstOrNull { it.value.nspace == info.nspace && it.key.uid == callingUid }
|
.firstOrNull {
|
||||||
|
it.value.nspace == info.nspace && it.key.uid == callingUid
|
||||||
|
}
|
||||||
?.key
|
?.key
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the certificate chain actually served back to [uid] on a getKeyEntry, keyed by
|
||||||
|
* [alias]. The app reassembles its final chain from these served chains (the leaf alias plus
|
||||||
|
* the attest-key alias), so logging each one with key sizes and a per-edge verification makes a
|
||||||
|
* verification failure in the app's combined chain reproducible from the log, not inferred.
|
||||||
|
*/
|
||||||
|
private fun logServedChain(uid: Int, txId: Long, alias: String, response: KeyEntryResponse?) {
|
||||||
|
if (response == null || !SystemLogger.isUidLogged(uid)) return
|
||||||
|
val chain = CertificateHelper.getCertificateChain(response)?.asList() ?: return
|
||||||
|
SystemLogger.uidLog(uid, txId, "served", "alias=$alias depth=${chain.size}")
|
||||||
|
SystemLogger.uidLog(uid, txId, "served-keys", AttestationPatcher.formatChainKeys(chain))
|
||||||
|
SystemLogger.uidLog(uid, txId, "served-verify", AttestationPatcher.formatChainVerification(chain))
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult {
|
private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult {
|
||||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
val descriptor =
|
||||||
?: return TransactionResult.ContinueAndSkipPost
|
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
|
|
||||||
|
if (descriptor.domain == Domain.GRANT) {
|
||||||
|
val grant =
|
||||||
|
KeyMintSecurityLevelInterceptor.resolveGrant(descriptor.nspace, callingUid)
|
||||||
|
if (grant == null) {
|
||||||
|
return if (
|
||||||
|
KeyMintSecurityLevelInterceptor.softwareGrants.containsKey(descriptor.nspace)
|
||||||
|
)
|
||||||
|
InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
|
||||||
|
else TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
if ((grant.accessVector and KEY_PERMISSION_UPDATE) == 0) {
|
||||||
|
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
|
||||||
|
}
|
||||||
|
|
||||||
|
val generatedKeyInfo =
|
||||||
|
KeyMintSecurityLevelInterceptor.generatedKeys[grant.ownerKeyId]
|
||||||
|
val response =
|
||||||
|
generatedKeyInfo?.response
|
||||||
|
?: KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(grant.ownerKeyId)
|
||||||
|
?: return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
|
||||||
|
return updateResponseSubcomponent(
|
||||||
|
response = response,
|
||||||
|
publicCert = data.createByteArray(),
|
||||||
|
certificateChain = data.createByteArray(),
|
||||||
|
persist = {
|
||||||
|
if (generatedKeyInfo != null) {
|
||||||
|
GeneratedKeyPersistence.rePersistIfNeeded(
|
||||||
|
grant.ownerKeyId.uid,
|
||||||
|
generatedKeyInfo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = "grant[${descriptor.nspace}] -> ${grant.ownerKeyId}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val generatedKeyInfo =
|
val generatedKeyInfo =
|
||||||
when (descriptor.domain) {
|
when (descriptor.domain) {
|
||||||
Domain.KEY_ID ->
|
Domain.KEY_ID ->
|
||||||
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||||
callingUid, descriptor.nspace
|
callingUid,
|
||||||
|
descriptor.nspace,
|
||||||
)
|
)
|
||||||
Domain.APP ->
|
Domain.APP ->
|
||||||
descriptor.alias?.let {
|
descriptor.alias?.let {
|
||||||
@@ -659,35 +776,58 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (generatedKeyInfo == null) {
|
if (generatedKeyInfo == null) {
|
||||||
// Patch-mode key (cached in teeResponses, not generatedKeys): the real keystore2 applies
|
// Patch-mode key (cached in teeResponses, not generatedKeys): the real keystore2
|
||||||
|
// applies
|
||||||
// the update, so drop our stale cached chain. Otherwise getKeyEntry replays the
|
// the update, so drop our stale cached chain. Otherwise getKeyEntry replays the
|
||||||
// pre-update generated attestation (duck STALE_TEE_RESPONSE_AFTER_KEY_ID_UPDATE).
|
// pre-update generated attestation (duck STALE_TEE_RESPONSE_AFTER_KEY_ID_UPDATE).
|
||||||
when (descriptor.domain) {
|
when (descriptor.domain) {
|
||||||
Domain.KEY_ID ->
|
Domain.KEY_ID ->
|
||||||
KeyMintSecurityLevelInterceptor.evictTeeResponseByKeyId(callingUid, descriptor.nspace)
|
KeyMintSecurityLevelInterceptor.evictTeeResponseByKeyId(
|
||||||
|
callingUid,
|
||||||
|
descriptor.nspace,
|
||||||
|
)
|
||||||
Domain.APP ->
|
Domain.APP ->
|
||||||
descriptor.alias?.let {
|
descriptor.alias?.let {
|
||||||
KeyMintSecurityLevelInterceptor.evictTeeResponse(KeyIdentifier(callingUid, it))
|
KeyMintSecurityLevelInterceptor.evictTeeResponse(
|
||||||
|
KeyIdentifier(callingUid, it)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
descriptor.alias?.let {
|
descriptor.alias?.let {
|
||||||
val kid = KeyIdentifier(callingUid, it)
|
val kid = KeyIdentifier(callingUid, it)
|
||||||
userUpdatedKeys.add(kid)
|
userUpdatedKeys.add(kid)
|
||||||
SystemLogger.trace { "[TRACE] updateSubcomponent $kid: not generated key, added to userUpdatedKeys" }
|
SystemLogger.trace {
|
||||||
|
"[TRACE] updateSubcomponent $kid: not generated key, added to userUpdatedKeys"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemLogger.info("Updating sub-component with key[${generatedKeyInfo.nspace}]")
|
return updateResponseSubcomponent(
|
||||||
val metadata = generatedKeyInfo.response.metadata
|
response = generatedKeyInfo.response,
|
||||||
val publicCert = data.createByteArray()
|
publicCert = data.createByteArray(),
|
||||||
val certificateChain = data.createByteArray()
|
certificateChain = data.createByteArray(),
|
||||||
|
persist = {
|
||||||
|
GeneratedKeyPersistence.rePersistIfNeeded(callingUid, generatedKeyInfo)
|
||||||
|
},
|
||||||
|
label = "key[${generatedKeyInfo.nspace}]",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateResponseSubcomponent(
|
||||||
|
response: KeyEntryResponse,
|
||||||
|
publicCert: ByteArray?,
|
||||||
|
certificateChain: ByteArray?,
|
||||||
|
persist: () -> Unit,
|
||||||
|
label: String,
|
||||||
|
): TransactionResult {
|
||||||
|
SystemLogger.info("Updating sub-component with $label")
|
||||||
|
val metadata = response.metadata
|
||||||
metadata.certificate = publicCert
|
metadata.certificate = publicCert
|
||||||
metadata.certificateChain = certificateChain
|
metadata.certificateChain = certificateChain
|
||||||
|
|
||||||
GeneratedKeyPersistence.rePersistIfNeeded(callingUid, generatedKeyInfo)
|
persist()
|
||||||
|
|
||||||
SystemLogger.verbose(
|
SystemLogger.verbose(
|
||||||
"Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]"
|
"Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]"
|
||||||
|
|||||||
+12
-7
@@ -7,18 +7,18 @@ import android.system.keystore2.Domain
|
|||||||
import android.system.keystore2.KeyDescriptor
|
import android.system.keystore2.KeyDescriptor
|
||||||
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||||
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
|
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
|
* 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.
|
* 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
|
* 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
|
* and then returns [TransactionResult.ContinueAndSkipPost], so the real keystore2 still performs
|
||||||
* real operation. We never fabricate a maintenance reply, so real key lifecycle is never disturbed.
|
* 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
|
* Mounted via `register()` from [Keystore2Interceptor.onInterceptorReady]; the maintenance binder
|
||||||
* hosted by the same keystore2 process, so the already-injected native hook reaches it too.
|
* is hosted by the same keystore2 process, so the already-injected native hook reaches it too.
|
||||||
*/
|
*/
|
||||||
object Keystore2MaintenanceInterceptor : BinderInterceptor() {
|
object Keystore2MaintenanceInterceptor : BinderInterceptor() {
|
||||||
private val stubClass = IKeystoreMaintenance.Stub::class.java
|
private val stubClass = IKeystoreMaintenance.Stub::class.java
|
||||||
@@ -93,13 +93,18 @@ object Keystore2MaintenanceInterceptor : BinderInterceptor() {
|
|||||||
descriptor.alias != null -> KeyIdentifier(callingUid, descriptor.alias)
|
descriptor.alias != null -> KeyIdentifier(callingUid, descriptor.alias)
|
||||||
descriptor.domain == Domain.KEY_ID ->
|
descriptor.domain == Domain.KEY_ID ->
|
||||||
KeyMintSecurityLevelInterceptor.generatedKeys.entries
|
KeyMintSecurityLevelInterceptor.generatedKeys.entries
|
||||||
.firstOrNull { it.key.uid == callingUid && it.value.nspace == descriptor.nspace }
|
.firstOrNull {
|
||||||
|
it.key.uid == callingUid && it.value.nspace == descriptor.nspace
|
||||||
|
}
|
||||||
?.key
|
?.key
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Destination must be an addressable Domain.APP alias for us to keep tracking the key. */
|
/** Destination must be an addressable Domain.APP alias for us to keep tracking the key. */
|
||||||
private fun resolveDestinationKeyId(descriptor: KeyDescriptor, callingUid: Int): KeyIdentifier? {
|
private fun resolveDestinationKeyId(
|
||||||
|
descriptor: KeyDescriptor,
|
||||||
|
callingUid: Int,
|
||||||
|
): KeyIdentifier? {
|
||||||
val alias = descriptor.alias ?: return null
|
val alias = descriptor.alias ?: return null
|
||||||
if (descriptor.domain != Domain.APP) return null
|
if (descriptor.domain != Domain.APP) return null
|
||||||
val uid = if (descriptor.nspace > 0) descriptor.nspace.toInt() else callingUid
|
val uid = if (descriptor.nspace > 0) descriptor.nspace.toInt() else callingUid
|
||||||
|
|||||||
+54
-8
@@ -1,8 +1,10 @@
|
|||||||
package org.matrix.TEESimulator.interception.keystore.shim
|
package org.matrix.TEESimulator.interception.keystore.shim
|
||||||
|
|
||||||
import android.hardware.security.keymint.Algorithm
|
import android.hardware.security.keymint.Algorithm
|
||||||
import android.hardware.security.keymint.KeyPurpose
|
import android.hardware.security.keymint.BlockMode
|
||||||
import android.hardware.security.keymint.KeyParameter
|
import android.hardware.security.keymint.KeyParameter
|
||||||
|
import android.hardware.security.keymint.KeyPurpose
|
||||||
|
import android.hardware.security.keymint.PaddingMode
|
||||||
import android.hardware.security.keymint.Tag
|
import android.hardware.security.keymint.Tag
|
||||||
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||||
|
|
||||||
@@ -18,14 +20,16 @@ object AuthorizeCreate {
|
|||||||
// Algorithm-level rejection runs before purpose-list check (AOSP HAL behavior)
|
// Algorithm-level rejection runs before purpose-list check (AOSP HAL behavior)
|
||||||
return checkAlgorithmPurpose(keyParams, purpose)
|
return checkAlgorithmPurpose(keyParams, purpose)
|
||||||
?: checkPurpose(keyParams, purpose)
|
?: checkPurpose(keyParams, purpose)
|
||||||
|
?: checkOperationAuthorizations(keyParams, opParams)
|
||||||
?: checkTemporalValidity(keyParams, purpose)
|
?: checkTemporalValidity(keyParams, purpose)
|
||||||
?: checkCallerNonce(keyParams, purpose, rawOpParams)
|
?: checkCallerNonce(keyParams, purpose, rawOpParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
|
private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
|
||||||
val algo = keyParams.algorithm
|
val algo = keyParams.algorithm
|
||||||
if ((algo == Algorithm.EC || algo == Algorithm.RSA) &&
|
if (
|
||||||
(purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.ENCRYPT)
|
(algo == Algorithm.EC || algo == Algorithm.RSA) &&
|
||||||
|
(purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.ENCRYPT)
|
||||||
) {
|
) {
|
||||||
return KeystoreErrorCodes.unsupportedPurpose
|
return KeystoreErrorCodes.unsupportedPurpose
|
||||||
}
|
}
|
||||||
@@ -35,10 +39,48 @@ object AuthorizeCreate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun checkPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
|
private fun checkPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
|
||||||
if (purpose == KeyPurpose.WRAP_KEY)
|
if (purpose == KeyPurpose.WRAP_KEY) return KeystoreErrorCodes.incompatiblePurpose
|
||||||
return KeystoreErrorCodes.incompatiblePurpose
|
if (purpose !in keyParams.purpose) return KeystoreErrorCodes.incompatiblePurpose
|
||||||
if (purpose !in keyParams.purpose)
|
return null
|
||||||
return KeystoreErrorCodes.incompatiblePurpose
|
}
|
||||||
|
|
||||||
|
private fun checkOperationAuthorizations(
|
||||||
|
keyParams: KeyMintAttestation,
|
||||||
|
opParams: KeyMintAttestation,
|
||||||
|
): Int? {
|
||||||
|
if (opParams.blockMode.any { it !in keyParams.blockMode }) {
|
||||||
|
return KeystoreErrorCodes.incompatibleBlockMode
|
||||||
|
}
|
||||||
|
if (opParams.padding.any { it !in keyParams.padding }) {
|
||||||
|
return KeystoreErrorCodes.incompatiblePaddingMode
|
||||||
|
}
|
||||||
|
if (opParams.digest.any { it !in keyParams.digest }) {
|
||||||
|
return KeystoreErrorCodes.incompatibleDigest
|
||||||
|
}
|
||||||
|
if (opParams.rsaOaepMgfDigest.any { it !in keyParams.rsaOaepMgfDigest }) {
|
||||||
|
return KeystoreErrorCodes.incompatibleDigest
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyParams.algorithm == Algorithm.AES && opParams.blockMode.contains(BlockMode.GCM)) {
|
||||||
|
val requestedMacLength = opParams.minMacLength
|
||||||
|
val keyMinMacLength = keyParams.minMacLength
|
||||||
|
if (
|
||||||
|
requestedMacLength != null &&
|
||||||
|
keyMinMacLength != null &&
|
||||||
|
requestedMacLength < keyMinMacLength
|
||||||
|
) {
|
||||||
|
return KeystoreErrorCodes.invalidMacLength
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
keyParams.algorithm == Algorithm.RSA &&
|
||||||
|
opParams.padding.contains(PaddingMode.RSA_OAEP) &&
|
||||||
|
opParams.digest.isEmpty()
|
||||||
|
) {
|
||||||
|
return KeystoreErrorCodes.incompatibleDigest
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +106,11 @@ object AuthorizeCreate {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkCallerNonce(keyParams: KeyMintAttestation, purpose: Int, rawOpParams: Array<KeyParameter>?): Int? {
|
private fun checkCallerNonce(
|
||||||
|
keyParams: KeyMintAttestation,
|
||||||
|
purpose: Int,
|
||||||
|
rawOpParams: Array<KeyParameter>?,
|
||||||
|
): Int? {
|
||||||
if (purpose != KeyPurpose.SIGN && purpose != KeyPurpose.ENCRYPT) return null
|
if (purpose != KeyPurpose.SIGN && purpose != KeyPurpose.ENCRYPT) return null
|
||||||
if (keyParams.callerNonce == true) return null
|
if (keyParams.callerNonce == true) return null
|
||||||
if (rawOpParams?.any { it.tag == Tag.NONCE } == true)
|
if (rawOpParams?.any { it.tag == Tag.NONCE } == true)
|
||||||
|
|||||||
+224
-225
@@ -33,19 +33,17 @@ data class PersistedKeyData(
|
|||||||
val privateKeyBytes: ByteArray,
|
val privateKeyBytes: ByteArray,
|
||||||
val certChainBytes: List<ByteArray>,
|
val certChainBytes: List<ByteArray>,
|
||||||
/**
|
/**
|
||||||
* Byte-identical KeyMetadata parcel snapshot. Restoring authorizations
|
* Byte-identical KeyMetadata parcel snapshot. Restoring authorizations directly from these
|
||||||
* directly from these bytes preserves tag count, order, and exact
|
* bytes preserves tag count, order, and exact security-level annotations across reboots — the
|
||||||
* security-level annotations across reboots — the kind of structural
|
* kind of structural details apps fingerprint to decide whether the alias is still "the same
|
||||||
* details apps fingerprint to decide whether the alias is still
|
* key".
|
||||||
* "the same key".
|
|
||||||
*/
|
*/
|
||||||
val metadataBytes: ByteArray,
|
val metadataBytes: ByteArray,
|
||||||
/**
|
/**
|
||||||
* Raw secret material for symmetric records (AES, HMAC, 3DES). Empty
|
* Raw secret material for symmetric records (AES, HMAC, 3DES). Empty for asymmetric. Critical
|
||||||
* for asymmetric. Critical for AndroidX security crypto MasterKey
|
* for AndroidX security crypto MasterKey (AES-GCM-256) — without this every reboot regenerates
|
||||||
* (AES-GCM-256) — without this every reboot regenerates a fresh AES
|
* a fresh AES key and EncryptedSharedPreferences becomes undecryptable, which is what banking
|
||||||
* key and EncryptedSharedPreferences becomes undecryptable, which is
|
* apps interpret as session expiry and force a relogin.
|
||||||
* what banking apps interpret as session expiry and force a relogin.
|
|
||||||
*/
|
*/
|
||||||
val symmetricKeyBytes: ByteArray,
|
val symmetricKeyBytes: ByteArray,
|
||||||
val symmetricAlgorithm: String,
|
val symmetricAlgorithm: String,
|
||||||
@@ -54,20 +52,15 @@ data class PersistedKeyData(
|
|||||||
object GeneratedKeyPersistence {
|
object GeneratedKeyPersistence {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single source of truth for the on-disk format. Bump this every time
|
* Single source of truth for the on-disk format. Bump this every time the layout changes; older
|
||||||
* the layout changes; older numbers are silently skipped on read so
|
* numbers are silently skipped on read so stale dev artifacts and pre-fix upstream files can't
|
||||||
* stale dev artifacts and pre-fix upstream files can't be partially
|
* be partially rehydrated into broken in-memory state.
|
||||||
* rehydrated into broken in-memory state.
|
|
||||||
*
|
*
|
||||||
* History:
|
* History: 1 — original upstream layout (no metadata snapshot, no symmetric block; restored
|
||||||
* 1 — original upstream layout (no metadata snapshot, no symmetric
|
* keys lose authorization tags and AES master keys altogether — apps relying on persisted
|
||||||
* block; restored keys lose authorization tags and AES master
|
* keystore state across reboots get logged out) 2 — transitional dev-only format that added
|
||||||
* keys altogether — apps relying on persisted keystore state
|
* metadata but still missed the symmetric block; never shipped 3 — current: byte-identical
|
||||||
* across reboots get logged out)
|
* KeyMetadata snapshot + raw symmetric key material so AES/HMAC keys survive reboots
|
||||||
* 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 const val FORMAT_VERSION = 3
|
||||||
private val PERSISTENCE_DIR = File(CONFIG_PATH, "persistent_keys")
|
private val PERSISTENCE_DIR = File(CONFIG_PATH, "persistent_keys")
|
||||||
@@ -104,77 +97,80 @@ object GeneratedKeyPersistence {
|
|||||||
try {
|
try {
|
||||||
SystemLogger.debug("[Persistence] Lock acquired for $filename")
|
SystemLogger.debug("[Persistence] Lock acquired for $filename")
|
||||||
runCatching {
|
runCatching {
|
||||||
PERSISTENCE_DIR.mkdirs()
|
PERSISTENCE_DIR.mkdirs()
|
||||||
val finalFile = File(PERSISTENCE_DIR, filename)
|
val finalFile = File(PERSISTENCE_DIR, filename)
|
||||||
val tmpFile = File(PERSISTENCE_DIR, "$filename.tmp")
|
val tmpFile = File(PERSISTENCE_DIR, "$filename.tmp")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DataOutputStream(BufferedOutputStream(FileOutputStream(tmpFile))).use { out ->
|
DataOutputStream(BufferedOutputStream(FileOutputStream(tmpFile))).use { out
|
||||||
out.writeInt(FORMAT_VERSION)
|
->
|
||||||
out.writeInt(securityLevel)
|
out.writeInt(FORMAT_VERSION)
|
||||||
out.writeInt(keyId.uid)
|
out.writeInt(securityLevel)
|
||||||
out.writeUTF(keyId.alias)
|
out.writeInt(keyId.uid)
|
||||||
out.writeLong(nspace)
|
out.writeUTF(keyId.alias)
|
||||||
out.writeBoolean(isAttestationKey)
|
out.writeLong(nspace)
|
||||||
out.writeInt(algorithm)
|
out.writeBoolean(isAttestationKey)
|
||||||
out.writeInt(keySize)
|
out.writeInt(algorithm)
|
||||||
out.writeInt(ecCurve)
|
out.writeInt(keySize)
|
||||||
|
out.writeInt(ecCurve)
|
||||||
|
|
||||||
out.writeInt(purposes.size)
|
out.writeInt(purposes.size)
|
||||||
purposes.forEach { out.writeInt(it) }
|
purposes.forEach { out.writeInt(it) }
|
||||||
|
|
||||||
out.writeInt(digests.size)
|
out.writeInt(digests.size)
|
||||||
digests.forEach { out.writeInt(it) }
|
digests.forEach { out.writeInt(it) }
|
||||||
|
|
||||||
// Asymmetric key block (empty for symmetric-only).
|
// Asymmetric key block (empty for symmetric-only).
|
||||||
val pkBytes = keyPair?.private?.encoded ?: ByteArray(0)
|
val pkBytes = keyPair?.private?.encoded ?: ByteArray(0)
|
||||||
out.writeInt(pkBytes.size)
|
out.writeInt(pkBytes.size)
|
||||||
out.write(pkBytes)
|
out.write(pkBytes)
|
||||||
|
|
||||||
out.writeInt(certChain.size)
|
out.writeInt(certChain.size)
|
||||||
certChain.forEach { cert ->
|
certChain.forEach { cert ->
|
||||||
val encoded = cert.encoded
|
val encoded = cert.encoded
|
||||||
out.writeInt(encoded.size)
|
out.writeInt(encoded.size)
|
||||||
out.write(encoded)
|
out.write(encoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata snapshot (always present, may be empty
|
// Metadata snapshot (always present, may be empty
|
||||||
// if the live KeyMetadata could not be marshalled).
|
// if the live KeyMetadata could not be marshalled).
|
||||||
val mdBytes = metadataBytes ?: ByteArray(0)
|
val mdBytes = metadataBytes ?: ByteArray(0)
|
||||||
out.writeInt(mdBytes.size)
|
out.writeInt(mdBytes.size)
|
||||||
if (mdBytes.isNotEmpty()) out.write(mdBytes)
|
if (mdBytes.isNotEmpty()) out.write(mdBytes)
|
||||||
|
|
||||||
// Symmetric key block (empty for asymmetric keys).
|
// Symmetric key block (empty for asymmetric keys).
|
||||||
if (secretKey != null) {
|
if (secretKey != null) {
|
||||||
val skBytes = secretKey.encoded
|
val skBytes = secretKey.encoded
|
||||||
out.writeUTF(secretKey.algorithm)
|
out.writeUTF(secretKey.algorithm)
|
||||||
out.writeInt(skBytes.size)
|
out.writeInt(skBytes.size)
|
||||||
out.write(skBytes)
|
out.write(skBytes)
|
||||||
} else {
|
} else {
|
||||||
out.writeUTF("")
|
out.writeUTF("")
|
||||||
out.writeInt(0)
|
out.writeInt(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
tmpFile.delete()
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
tmpFile.delete()
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atomic rename — if this fails the tmp is left behind and cleaned on next deleteAll
|
// Atomic rename — if this fails the tmp is left behind and cleaned on next
|
||||||
if (!tmpFile.renameTo(finalFile)) {
|
// deleteAll
|
||||||
tmpFile.delete()
|
if (!tmpFile.renameTo(finalFile)) {
|
||||||
throw IllegalStateException("Failed to atomically rename $tmpFile -> $finalFile")
|
tmpFile.delete()
|
||||||
}
|
throw IllegalStateException(
|
||||||
|
"Failed to atomically rename $tmpFile -> $finalFile"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Verify write succeeded - catches disk-full or filesystem errors
|
// Verify write succeeded - catches disk-full or filesystem errors
|
||||||
if (!finalFile.exists() || finalFile.length() < 20) {
|
if (!finalFile.exists() || finalFile.length() < 20) {
|
||||||
throw IOException("File write verification failed - possible disk full")
|
throw IOException("File write verification failed - possible disk full")
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemLogger.debug("Persisted key: $keyId")
|
SystemLogger.debug("Persisted key: $keyId")
|
||||||
}.onFailure { e ->
|
}
|
||||||
SystemLogger.error("Failed to persist key $keyId", e)
|
.onFailure { e -> SystemLogger.error("Failed to persist key $keyId", e) }
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock()
|
lock.unlock()
|
||||||
SystemLogger.debug("[Persistence] Lock released for $filename")
|
SystemLogger.debug("[Persistence] Lock released for $filename")
|
||||||
@@ -183,44 +179,42 @@ object GeneratedKeyPersistence {
|
|||||||
|
|
||||||
fun delete(keyId: KeyIdentifier) {
|
fun delete(keyId: KeyIdentifier) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val file = File(PERSISTENCE_DIR, keyFileName(keyId.uid, keyId.alias))
|
val file = File(PERSISTENCE_DIR, keyFileName(keyId.uid, keyId.alias))
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
if (file.delete()) {
|
if (file.delete()) {
|
||||||
fileLocks.remove(keyFileName(keyId.uid, keyId.alias))
|
fileLocks.remove(keyFileName(keyId.uid, keyId.alias))
|
||||||
SystemLogger.debug("Deleted persisted key: $keyId")
|
SystemLogger.debug("Deleted persisted key: $keyId")
|
||||||
|
} else {
|
||||||
|
SystemLogger.warning("Failed to delete persisted key file: ${file.name}")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
SystemLogger.warning("Failed to delete persisted key file: ${file.name}")
|
SystemLogger.debug("No persisted file to delete for: $keyId")
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
SystemLogger.debug("No persisted file to delete for: $keyId")
|
|
||||||
}
|
}
|
||||||
}.onFailure { e ->
|
.onFailure { e -> SystemLogger.error("Failed to delete persisted key $keyId", e) }
|
||||||
SystemLogger.error("Failed to delete persisted key $keyId", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAll() {
|
fun deleteAll() {
|
||||||
runCatching {
|
runCatching {
|
||||||
if (!PERSISTENCE_DIR.exists()) {
|
if (!PERSISTENCE_DIR.exists()) {
|
||||||
SystemLogger.debug("No persistent_keys directory, nothing to delete")
|
SystemLogger.debug("No persistent_keys directory, nothing to delete")
|
||||||
return
|
return
|
||||||
}
|
|
||||||
val files = PERSISTENCE_DIR.listFiles()
|
|
||||||
if (files == null) {
|
|
||||||
SystemLogger.warning("Cannot list persistent_keys directory")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var count = 0
|
|
||||||
files.forEach { file ->
|
|
||||||
if (file.name.endsWith(".bin") || file.name.endsWith(".tmp")) {
|
|
||||||
if (file.delete()) count++
|
|
||||||
}
|
}
|
||||||
|
val files = PERSISTENCE_DIR.listFiles()
|
||||||
|
if (files == null) {
|
||||||
|
SystemLogger.warning("Cannot list persistent_keys directory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var count = 0
|
||||||
|
files.forEach { file ->
|
||||||
|
if (file.name.endsWith(".bin") || file.name.endsWith(".tmp")) {
|
||||||
|
if (file.delete()) count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileLocks.clear()
|
||||||
|
SystemLogger.info("Deleted $count persisted key files")
|
||||||
}
|
}
|
||||||
fileLocks.clear()
|
.onFailure { e -> SystemLogger.error("Failed to delete all persisted keys", e) }
|
||||||
SystemLogger.info("Deleted $count persisted key files")
|
|
||||||
}.onFailure { e ->
|
|
||||||
SystemLogger.error("Failed to delete all persisted keys", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadAll(securityLevel: Int): List<PersistedKeyData> {
|
fun loadAll(securityLevel: Int): List<PersistedKeyData> {
|
||||||
@@ -243,87 +237,86 @@ object GeneratedKeyPersistence {
|
|||||||
|
|
||||||
for (file in files) {
|
for (file in files) {
|
||||||
runCatching {
|
runCatching {
|
||||||
DataInputStream(BufferedInputStream(FileInputStream(file))).use { input ->
|
DataInputStream(BufferedInputStream(FileInputStream(file))).use { input ->
|
||||||
val version = input.readInt()
|
val version = input.readInt()
|
||||||
if (version != FORMAT_VERSION) {
|
if (version != FORMAT_VERSION) {
|
||||||
// Old upstream files (v1) and dev-only intermediate
|
// Old upstream files (v1) and dev-only intermediate
|
||||||
// files (v2) are missing the metadata snapshot
|
// files (v2) are missing the metadata snapshot
|
||||||
// and/or symmetric key block — restoring them
|
// and/or symmetric key block — restoring them
|
||||||
// would put broken state in memory (apps relying
|
// would put broken state in memory (apps relying
|
||||||
// on those records get logged out). Skip and let
|
// on those records get logged out). Skip and let
|
||||||
// the next generateKey re-create cleanly with the
|
// the next generateKey re-create cleanly with the
|
||||||
// new format. Affected apps re-login once after
|
// new format. Affected apps re-login once after
|
||||||
// upgrade, then never again.
|
// upgrade, then never again.
|
||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"Skipping ${file.name}: legacy format version $version. " +
|
"Skipping ${file.name}: legacy format version $version. " +
|
||||||
"It will be replaced on next generateKey for this alias."
|
"It will be replaced on next generateKey for this alias."
|
||||||
)
|
|
||||||
return@runCatching
|
|
||||||
}
|
|
||||||
|
|
||||||
val storedSecLevel = input.readInt()
|
|
||||||
val uid = input.readInt()
|
|
||||||
val alias = input.readUTF()
|
|
||||||
val nspace = input.readLong()
|
|
||||||
val isAttestKey = input.readBoolean()
|
|
||||||
val algo = input.readInt()
|
|
||||||
val kSize = input.readInt()
|
|
||||||
val curve = input.readInt()
|
|
||||||
|
|
||||||
val purposeCount = requireBounds(input.readInt(), 64, "purposeCount")
|
|
||||||
val purposes = (0 until purposeCount).map { input.readInt() }
|
|
||||||
|
|
||||||
val digestCount = requireBounds(input.readInt(), 64, "digestCount")
|
|
||||||
val digests = (0 until digestCount).map { input.readInt() }
|
|
||||||
|
|
||||||
val pkLen = requireBounds(input.readInt(), 8192, "pkLen")
|
|
||||||
val pkBytes = ByteArray(pkLen)
|
|
||||||
if (pkLen > 0) input.readFully(pkBytes)
|
|
||||||
|
|
||||||
val certCount = requireBounds(input.readInt(), 10, "certCount")
|
|
||||||
val certChainBytes = (0 until certCount).map {
|
|
||||||
val certLen = requireBounds(input.readInt(), 65536, "certLen")
|
|
||||||
val certBytes = ByteArray(certLen)
|
|
||||||
input.readFully(certBytes)
|
|
||||||
certBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
val metaLen = requireBounds(input.readInt(), 256 * 1024, "metaLen")
|
|
||||||
val metadataBytes = ByteArray(metaLen).also {
|
|
||||||
if (metaLen > 0) input.readFully(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val skAlgo = input.readUTF()
|
|
||||||
val skLen = requireBounds(input.readInt(), 8192, "skLen")
|
|
||||||
val skBytes = ByteArray(skLen).also {
|
|
||||||
if (skLen > 0) input.readFully(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (storedSecLevel == securityLevel) {
|
|
||||||
result.add(
|
|
||||||
PersistedKeyData(
|
|
||||||
uid = uid,
|
|
||||||
alias = alias,
|
|
||||||
nspace = nspace,
|
|
||||||
securityLevel = storedSecLevel,
|
|
||||||
isAttestationKey = isAttestKey,
|
|
||||||
algorithm = algo,
|
|
||||||
keySize = kSize,
|
|
||||||
ecCurve = curve,
|
|
||||||
purposes = purposes,
|
|
||||||
digests = digests,
|
|
||||||
privateKeyBytes = pkBytes,
|
|
||||||
certChainBytes = certChainBytes,
|
|
||||||
metadataBytes = metadataBytes,
|
|
||||||
symmetricKeyBytes = skBytes,
|
|
||||||
symmetricAlgorithm = skAlgo,
|
|
||||||
)
|
)
|
||||||
)
|
return@runCatching
|
||||||
|
}
|
||||||
|
|
||||||
|
val storedSecLevel = input.readInt()
|
||||||
|
val uid = input.readInt()
|
||||||
|
val alias = input.readUTF()
|
||||||
|
val nspace = input.readLong()
|
||||||
|
val isAttestKey = input.readBoolean()
|
||||||
|
val algo = input.readInt()
|
||||||
|
val kSize = input.readInt()
|
||||||
|
val curve = input.readInt()
|
||||||
|
|
||||||
|
val purposeCount = requireBounds(input.readInt(), 64, "purposeCount")
|
||||||
|
val purposes = (0 until purposeCount).map { input.readInt() }
|
||||||
|
|
||||||
|
val digestCount = requireBounds(input.readInt(), 64, "digestCount")
|
||||||
|
val digests = (0 until digestCount).map { input.readInt() }
|
||||||
|
|
||||||
|
val pkLen = requireBounds(input.readInt(), 8192, "pkLen")
|
||||||
|
val pkBytes = ByteArray(pkLen)
|
||||||
|
if (pkLen > 0) input.readFully(pkBytes)
|
||||||
|
|
||||||
|
val certCount = requireBounds(input.readInt(), 10, "certCount")
|
||||||
|
val certChainBytes =
|
||||||
|
(0 until certCount).map {
|
||||||
|
val certLen = requireBounds(input.readInt(), 65536, "certLen")
|
||||||
|
val certBytes = ByteArray(certLen)
|
||||||
|
input.readFully(certBytes)
|
||||||
|
certBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
val metaLen = requireBounds(input.readInt(), 256 * 1024, "metaLen")
|
||||||
|
val metadataBytes =
|
||||||
|
ByteArray(metaLen).also { if (metaLen > 0) input.readFully(it) }
|
||||||
|
|
||||||
|
val skAlgo = input.readUTF()
|
||||||
|
val skLen = requireBounds(input.readInt(), 8192, "skLen")
|
||||||
|
val skBytes = ByteArray(skLen).also { if (skLen > 0) input.readFully(it) }
|
||||||
|
|
||||||
|
if (storedSecLevel == securityLevel) {
|
||||||
|
result.add(
|
||||||
|
PersistedKeyData(
|
||||||
|
uid = uid,
|
||||||
|
alias = alias,
|
||||||
|
nspace = nspace,
|
||||||
|
securityLevel = storedSecLevel,
|
||||||
|
isAttestationKey = isAttestKey,
|
||||||
|
algorithm = algo,
|
||||||
|
keySize = kSize,
|
||||||
|
ecCurve = curve,
|
||||||
|
purposes = purposes,
|
||||||
|
digests = digests,
|
||||||
|
privateKeyBytes = pkBytes,
|
||||||
|
certChainBytes = certChainBytes,
|
||||||
|
metadataBytes = metadataBytes,
|
||||||
|
symmetricKeyBytes = skBytes,
|
||||||
|
symmetricAlgorithm = skAlgo,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onFailure { e ->
|
.onFailure { e ->
|
||||||
SystemLogger.warning("Skipping corrupted persisted key file: ${file.name}", e)
|
SystemLogger.warning("Skipping corrupted persisted key file: ${file.name}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemLogger.info("Loaded ${result.size} persisted keys for security level $securityLevel")
|
SystemLogger.info("Loaded ${result.size} persisted keys for security level $securityLevel")
|
||||||
@@ -345,11 +338,14 @@ object GeneratedKeyPersistence {
|
|||||||
}
|
}
|
||||||
val secLevel = metadata.keySecurityLevel
|
val secLevel = metadata.keySecurityLevel
|
||||||
|
|
||||||
val entry = KeyMintSecurityLevelInterceptor.generatedKeys.entries.find { (id, info) ->
|
val entry =
|
||||||
id.uid == callingUid && info.nspace == generatedKeyInfo.nspace
|
KeyMintSecurityLevelInterceptor.generatedKeys.entries.find { (id, info) ->
|
||||||
}
|
id.uid == callingUid && info.nspace == generatedKeyInfo.nspace
|
||||||
|
}
|
||||||
if (entry == null) {
|
if (entry == null) {
|
||||||
SystemLogger.debug("rePersist: key not found in map for uid=$callingUid nspace=${generatedKeyInfo.nspace}")
|
SystemLogger.debug(
|
||||||
|
"rePersist: key not found in map for uid=$callingUid nspace=${generatedKeyInfo.nspace}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,16 +364,20 @@ object GeneratedKeyPersistence {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val persisted = runCatching {
|
val persisted =
|
||||||
DataInputStream(BufferedInputStream(FileInputStream(existing))).use { input ->
|
runCatching {
|
||||||
val version = input.readInt()
|
DataInputStream(BufferedInputStream(FileInputStream(existing))).use { input ->
|
||||||
if (version != FORMAT_VERSION) {
|
val version = input.readInt()
|
||||||
SystemLogger.warning("rePersist: legacy format version $version for $keyId, will not re-persist (next generateKey replaces it)")
|
if (version != FORMAT_VERSION) {
|
||||||
return
|
SystemLogger.warning(
|
||||||
|
"rePersist: legacy format version $version for $keyId, will not re-persist (next generateKey replaces it)"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
readPersistedKeyData(input)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
readPersistedKeyData(input)
|
.getOrNull()
|
||||||
}
|
|
||||||
}.getOrNull()
|
|
||||||
if (persisted == null) {
|
if (persisted == null) {
|
||||||
SystemLogger.warning("rePersist: failed to read existing data for $keyId")
|
SystemLogger.warning("rePersist: failed to read existing data for $keyId")
|
||||||
return
|
return
|
||||||
@@ -392,16 +392,18 @@ object GeneratedKeyPersistence {
|
|||||||
// Serialize the live KeyMetadata (now contains the user-installed cert
|
// Serialize the live KeyMetadata (now contains the user-installed cert
|
||||||
// chain via updateSubcomponent) so the next boot restores byte-identical
|
// chain via updateSubcomponent) so the next boot restores byte-identical
|
||||||
// metadata. KeyMetadata is binder-free, so marshall() is safe here.
|
// metadata. KeyMetadata is binder-free, so marshall() is safe here.
|
||||||
val metadataBytes = runCatching {
|
val metadataBytes =
|
||||||
android.os.Parcel.obtain().let { parcel ->
|
runCatching {
|
||||||
try {
|
android.os.Parcel.obtain().let { parcel ->
|
||||||
metadata.writeToParcel(parcel, 0)
|
try {
|
||||||
parcel.marshall()
|
metadata.writeToParcel(parcel, 0)
|
||||||
} finally {
|
parcel.marshall()
|
||||||
parcel.recycle()
|
} finally {
|
||||||
|
parcel.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
.getOrNull()
|
||||||
}.getOrNull()
|
|
||||||
save(
|
save(
|
||||||
keyId = keyId,
|
keyId = keyId,
|
||||||
keyPair = keyPair,
|
keyPair = keyPair,
|
||||||
@@ -427,8 +429,8 @@ object GeneratedKeyPersistence {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun keyFileName(uid: Int, alias: String): String {
|
private fun keyFileName(uid: Int, alias: String): String {
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
val digest =
|
||||||
.digest("$uid:$alias".toByteArray(Charsets.UTF_8))
|
MessageDigest.getInstance("SHA-256").digest("$uid:$alias".toByteArray(Charsets.UTF_8))
|
||||||
return digest.joinToString("") { "%02x".format(it) } + ".bin"
|
return digest.joinToString("") { "%02x".format(it) } + ".bin"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,23 +457,20 @@ object GeneratedKeyPersistence {
|
|||||||
if (pkLen > 0) input.readFully(pkBytes)
|
if (pkLen > 0) input.readFully(pkBytes)
|
||||||
|
|
||||||
val certCount = requireBounds(input.readInt(), 10, "certCount")
|
val certCount = requireBounds(input.readInt(), 10, "certCount")
|
||||||
val certChainBytes = (0 until certCount).map {
|
val certChainBytes =
|
||||||
val certLen = requireBounds(input.readInt(), 65536, "certLen")
|
(0 until certCount).map {
|
||||||
val certBytes = ByteArray(certLen)
|
val certLen = requireBounds(input.readInt(), 65536, "certLen")
|
||||||
input.readFully(certBytes)
|
val certBytes = ByteArray(certLen)
|
||||||
certBytes
|
input.readFully(certBytes)
|
||||||
}
|
certBytes
|
||||||
|
}
|
||||||
|
|
||||||
val metaLen = requireBounds(input.readInt(), 256 * 1024, "metaLen")
|
val metaLen = requireBounds(input.readInt(), 256 * 1024, "metaLen")
|
||||||
val metadataBytes = ByteArray(metaLen).also {
|
val metadataBytes = ByteArray(metaLen).also { if (metaLen > 0) input.readFully(it) }
|
||||||
if (metaLen > 0) input.readFully(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val skAlgo = input.readUTF()
|
val skAlgo = input.readUTF()
|
||||||
val skLen = requireBounds(input.readInt(), 8192, "skLen")
|
val skLen = requireBounds(input.readInt(), 8192, "skLen")
|
||||||
val skBytes = ByteArray(skLen).also {
|
val skBytes = ByteArray(skLen).also { if (skLen > 0) input.readFully(it) }
|
||||||
if (skLen > 0) input.readFully(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
return PersistedKeyData(
|
return PersistedKeyData(
|
||||||
uid = uid,
|
uid = uid,
|
||||||
|
|||||||
+1090
-670
File diff suppressed because it is too large
Load Diff
+8
-1
@@ -28,8 +28,15 @@ class OperationInterceptor(
|
|||||||
val methodName = transactionNames[code] ?: "unknown code=$code"
|
val methodName = transactionNames[code] ?: "unknown code=$code"
|
||||||
logTransaction(txId, methodName, callingUid, callingPid, true)
|
logTransaction(txId, methodName, callingUid, callingPid, true)
|
||||||
|
|
||||||
|
// Mirror SoftwareOperation's vendor gate: a real-key op must answer non-AEAD updateAad
|
||||||
|
// exactly as the forged-key path does. Samsung and Xiaomi-MTK TEEs accept it; rejecting
|
||||||
|
// here while the forged path accepts diverges the two and fingerprints the injection.
|
||||||
if (code == UPDATE_AAD_TRANSACTION && !isAead) {
|
if (code == UPDATE_AAD_TRANSACTION && !isAead) {
|
||||||
return InterceptorUtils.createServiceSpecificErrorReply(KeystoreErrorCodes.invalidTag)
|
return if (VendorQuirks.nonAeadUpdateAadSucceeds()) {
|
||||||
|
InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||||
|
} else {
|
||||||
|
InterceptorUtils.createServiceSpecificErrorReply(KeystoreErrorCodes.invalidTag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code == FINISH_TRANSACTION || code == ABORT_TRANSACTION) {
|
if (code == FINISH_TRANSACTION || code == ABORT_TRANSACTION) {
|
||||||
|
|||||||
+262
-68
@@ -8,25 +8,61 @@ import android.hardware.security.keymint.KeyParameterValue
|
|||||||
import android.hardware.security.keymint.KeyPurpose
|
import android.hardware.security.keymint.KeyPurpose
|
||||||
import android.hardware.security.keymint.PaddingMode
|
import android.hardware.security.keymint.PaddingMode
|
||||||
import android.hardware.security.keymint.Tag
|
import android.hardware.security.keymint.Tag
|
||||||
|
import android.os.Build
|
||||||
import android.os.ServiceSpecificException
|
import android.os.ServiceSpecificException
|
||||||
import java.util.concurrent.locks.LockSupport
|
import android.os.SystemProperties
|
||||||
import android.system.keystore2.IKeystoreOperation
|
import android.system.keystore2.IKeystoreOperation
|
||||||
import android.system.keystore2.KeyParameters
|
import android.system.keystore2.KeyParameters
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.Signature
|
import java.security.Signature
|
||||||
import java.security.SignatureException
|
import java.security.SignatureException
|
||||||
|
import java.util.concurrent.locks.LockSupport
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||||
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors the per-vendor TEE quirk that Duck Detector's OperationErrorPathProbe checks: real
|
||||||
|
* Samsung and Xiaomi-MTK TrustZone return success for updateAad on a non-AEAD operation, while
|
||||||
|
* every other vendor rejects it with a service-specific INVALID_TAG. The module reads the same
|
||||||
|
* device-identity fields the probe reads, so a forged software operation answers exactly as that
|
||||||
|
* vendor's real TEE would.
|
||||||
|
*/
|
||||||
|
internal object VendorQuirks {
|
||||||
|
private val UPDATE_AAD_ALLOWS_SUCCESS = setOf("samsung")
|
||||||
|
private val XIAOMI_BRANDS = setOf("xiaomi", "redmi", "poco")
|
||||||
|
|
||||||
|
fun nonAeadUpdateAadSucceeds(): Boolean {
|
||||||
|
val manufacturer = Build.MANUFACTURER.lowercase()
|
||||||
|
val brand = Build.BRAND.lowercase()
|
||||||
|
if (manufacturer in UPDATE_AAD_ALLOWS_SUCCESS || brand in UPDATE_AAD_ALLOWS_SUCCESS) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (manufacturer != "xiaomi" && brand !in XIAOMI_BRANDS) return false
|
||||||
|
return isMediaTek()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isMediaTek(): Boolean {
|
||||||
|
val roHardware = SystemProperties.get("ro.hardware", "")
|
||||||
|
return roHardware.startsWith("mt") || Build.HARDWARE.startsWith("mt", ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed interface CryptoPrimitive {
|
private sealed interface CryptoPrimitive {
|
||||||
fun updateAad(aadInput: ByteArray?) {
|
fun updateAad(aadInput: ByteArray?) {
|
||||||
throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
|
// Real Samsung / Xiaomi-MTK TEEs accept updateAad on non-AEAD ops; others reject it.
|
||||||
|
if (!VendorQuirks.nonAeadUpdateAadSucceeds()) {
|
||||||
|
throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(data: ByteArray?): ByteArray?
|
fun update(data: ByteArray?): ByteArray?
|
||||||
|
|
||||||
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
|
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
|
||||||
|
|
||||||
fun abort()
|
fun abort()
|
||||||
|
|
||||||
fun getBeginParameters(): Array<KeyParameter>? = null
|
fun getBeginParameters(): Array<KeyParameter>? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +119,24 @@ private object JcaAlgorithmMapper {
|
|||||||
}
|
}
|
||||||
return "$keyAlgo/$blockMode/$padding"
|
return "$keyAlgo/$blockMode/$padding"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun mapOaepDigest(digest: Int?): String =
|
||||||
|
when (digest) {
|
||||||
|
Digest.SHA1 -> "SHA-1"
|
||||||
|
Digest.SHA_2_224 -> "SHA-224"
|
||||||
|
Digest.SHA_2_256 -> "SHA-256"
|
||||||
|
Digest.SHA_2_384 -> "SHA-384"
|
||||||
|
Digest.SHA_2_512 -> "SHA-512"
|
||||||
|
else -> "SHA-256"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapMacAlgorithm(params: KeyMintAttestation): String =
|
||||||
|
when (params.digest.firstOrNull()) {
|
||||||
|
Digest.SHA_2_256 -> "HmacSHA256"
|
||||||
|
Digest.SHA_2_384 -> "HmacSHA384"
|
||||||
|
Digest.SHA_2_512 -> "HmacSHA512"
|
||||||
|
else -> "HmacSHA256"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
|
private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
|
||||||
@@ -118,10 +172,16 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri
|
|||||||
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
if (data != null) update(data)
|
if (data != null) update(data)
|
||||||
if (signature == null) {
|
if (signature == null) {
|
||||||
throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature to verify is null")
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.verificationFailed,
|
||||||
|
"Signature to verify is null",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (!this.signature.verify(signature)) {
|
if (!this.signature.verify(signature)) {
|
||||||
throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature verification failed")
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.verificationFailed,
|
||||||
|
"Signature verification failed",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -133,6 +193,7 @@ private class CipherPrimitive(
|
|||||||
cryptoKey: java.security.Key,
|
cryptoKey: java.security.Key,
|
||||||
params: KeyMintAttestation,
|
params: KeyMintAttestation,
|
||||||
private val opMode: Int,
|
private val opMode: Int,
|
||||||
|
txId: Long,
|
||||||
) : CryptoPrimitive {
|
) : CryptoPrimitive {
|
||||||
private val isAead = params.blockMode.firstOrNull() == BlockMode.GCM
|
private val isAead = params.blockMode.firstOrNull() == BlockMode.GCM
|
||||||
private val cipher: Cipher =
|
private val cipher: Cipher =
|
||||||
@@ -142,13 +203,38 @@ private class CipherPrimitive(
|
|||||||
init(opMode, cryptoKey, javax.crypto.spec.GCMParameterSpec(128, nonce))
|
init(opMode, cryptoKey, javax.crypto.spec.GCMParameterSpec(128, nonce))
|
||||||
} else if (nonce != null) {
|
} else if (nonce != null) {
|
||||||
init(opMode, cryptoKey, javax.crypto.spec.IvParameterSpec(nonce))
|
init(opMode, cryptoKey, javax.crypto.spec.IvParameterSpec(nonce))
|
||||||
|
} else if (params.padding.firstOrNull() == PaddingMode.RSA_OAEP) {
|
||||||
|
val mainDigest = JcaAlgorithmMapper.mapOaepDigest(params.digest.firstOrNull())
|
||||||
|
val mgfDigest =
|
||||||
|
params.rsaOaepMgfDigest.firstOrNull()?.let {
|
||||||
|
JcaAlgorithmMapper.mapOaepDigest(it)
|
||||||
|
} ?: mainDigest
|
||||||
|
init(
|
||||||
|
opMode,
|
||||||
|
cryptoKey,
|
||||||
|
javax.crypto.spec.OAEPParameterSpec(
|
||||||
|
mainDigest,
|
||||||
|
"MGF1",
|
||||||
|
java.security.spec.MGF1ParameterSpec(mgfDigest),
|
||||||
|
javax.crypto.spec.PSource.PSpecified.DEFAULT,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
SystemLogger.debug {
|
||||||
|
"[SoftwareOp TX_ID: $txId] oaep-op main=$mainDigest mgf=$mgfDigest " +
|
||||||
|
"mode=${if (opMode == Cipher.DECRYPT_MODE) "decrypt" else "encrypt"}"
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
init(opMode, cryptoKey)
|
init(opMode, cryptoKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateAad(aadInput: ByteArray?) {
|
override fun updateAad(aadInput: ByteArray?) {
|
||||||
if (!isAead) throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
|
if (!isAead) {
|
||||||
|
if (!VendorQuirks.nonAeadUpdateAadSucceeds()) {
|
||||||
|
throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if (aadInput != null) cipher.updateAAD(aadInput)
|
if (aadInput != null) cipher.updateAAD(aadInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +279,53 @@ private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive {
|
|||||||
override fun abort() {}
|
override fun abort() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class MacPrimitive(
|
||||||
|
secretKey: javax.crypto.SecretKey,
|
||||||
|
private val params: KeyMintAttestation,
|
||||||
|
private val txId: Long,
|
||||||
|
) : CryptoPrimitive {
|
||||||
|
private val mac: javax.crypto.Mac =
|
||||||
|
javax.crypto.Mac.getInstance(JcaAlgorithmMapper.mapMacAlgorithm(params)).apply {
|
||||||
|
init(secretKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(data: ByteArray?): ByteArray? {
|
||||||
|
if (data != null) mac.update(data)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
|
if (data != null) mac.update(data)
|
||||||
|
val full = mac.doFinal()
|
||||||
|
// Tag.MAC_LENGTH is optional on the AndroidKeyStore Mac SPI; default to the
|
||||||
|
// full digest length so real Mac use keeps working when it is omitted.
|
||||||
|
val tagBytes = (params.macLength ?: (full.size * 8)) / 8
|
||||||
|
val tag = full.copyOf(tagBytes)
|
||||||
|
if (params.purpose.firstOrNull() == KeyPurpose.VERIFY) {
|
||||||
|
if (signature == null) {
|
||||||
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.verificationFailed,
|
||||||
|
"MAC to verify is null",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!java.security.MessageDigest.isEqual(tag, signature)) {
|
||||||
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.verificationFailed,
|
||||||
|
"MAC verification failed",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
SystemLogger.debug {
|
||||||
|
"[SoftwareOp TX_ID: $txId] hmac-op digest=${params.digest.firstOrNull()} " +
|
||||||
|
"macLen=${params.macLength} tag=${tag.size}B result=ok"
|
||||||
|
}
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun abort() {}
|
||||||
|
}
|
||||||
|
|
||||||
class SoftwareOperation(
|
class SoftwareOperation(
|
||||||
private val txId: Long,
|
private val txId: Long,
|
||||||
keyPair: KeyPair?,
|
keyPair: KeyPair?,
|
||||||
@@ -201,7 +334,8 @@ class SoftwareOperation(
|
|||||||
private val latencyFloorMs: Long = 0L,
|
private val latencyFloorMs: Long = 0L,
|
||||||
) {
|
) {
|
||||||
private val primitive: CryptoPrimitive
|
private val primitive: CryptoPrimitive
|
||||||
@Volatile var finalized = false
|
@Volatile
|
||||||
|
var finalized = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var onFinishCallback: (() -> Unit)? = null
|
var onFinishCallback: (() -> Unit)? = null
|
||||||
@@ -229,9 +363,9 @@ class SoftwareOperation(
|
|||||||
// silently corrupt their session.
|
// silently corrupt their session.
|
||||||
SystemLogger.warning(
|
SystemLogger.warning(
|
||||||
"[SoftwareOp TX_ID: $txId] Purpose missing on restored key " +
|
"[SoftwareOp TX_ID: $txId] Purpose missing on restored key " +
|
||||||
"(authorizations=${params.purpose}, keyPair=${if (keyPair != null) "present" else "null"}, " +
|
"(authorizations=${params.purpose}, keyPair=${if (keyPair != null) "present" else "null"}, " +
|
||||||
"secretKey=${if (secretKey != null) "present" else "null"}). " +
|
"secretKey=${if (secretKey != null) "present" else "null"}). " +
|
||||||
"Returning unsupportedPurpose."
|
"Returning unsupportedPurpose."
|
||||||
)
|
)
|
||||||
throw ServiceSpecificException(
|
throw ServiceSpecificException(
|
||||||
KeystoreErrorCodes.unsupportedPurpose,
|
KeystoreErrorCodes.unsupportedPurpose,
|
||||||
@@ -240,77 +374,113 @@ class SoftwareOperation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
primitive =
|
primitive =
|
||||||
when (purpose) {
|
if (params.algorithm == Algorithm.HMAC) {
|
||||||
KeyPurpose.SIGN -> {
|
// An HMAC key is symmetric (secretKey set, keyPair null), so it must
|
||||||
val kp = keyPair ?: throw ServiceSpecificException(
|
// not fall through to the purpose-keyed Signer/Verifier paths, which
|
||||||
KeystoreErrorCodes.invalidArgument,
|
// require a keyPair. secretKey is populated at HMAC keygen and restore,
|
||||||
"[SoftwareOp TX_ID: $txId] SIGN requested but keyPair is null",
|
// so the throw is a defensive floor, not a live path.
|
||||||
)
|
MacPrimitive(
|
||||||
Signer(kp, params)
|
secretKey
|
||||||
}
|
|
||||||
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(
|
?: throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.invalidArgument,
|
||||||
|
"[SoftwareOp TX_ID: $txId] HMAC op but secretKey null",
|
||||||
|
),
|
||||||
|
params,
|
||||||
|
txId,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
when (purpose) {
|
||||||
|
KeyPurpose.SIGN -> {
|
||||||
|
val kp =
|
||||||
|
keyPair
|
||||||
|
?: throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.invalidArgument,
|
||||||
|
"[SoftwareOp TX_ID: $txId] SIGN requested but keyPair is null",
|
||||||
|
)
|
||||||
|
Signer(kp, params)
|
||||||
|
}
|
||||||
|
KeyPurpose.VERIFY -> {
|
||||||
|
val kp =
|
||||||
|
keyPair
|
||||||
|
?: throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.invalidArgument,
|
||||||
|
"[SoftwareOp TX_ID: $txId] VERIFY requested but keyPair is null",
|
||||||
|
)
|
||||||
|
Verifier(kp, params)
|
||||||
|
}
|
||||||
|
KeyPurpose.ENCRYPT -> {
|
||||||
|
val key: java.security.Key =
|
||||||
|
secretKey
|
||||||
|
?: keyPair?.public
|
||||||
|
?: throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.unsupportedPurpose,
|
||||||
|
"[SoftwareOp TX_ID: $txId] ENCRYPT requires either secretKey or keyPair.public",
|
||||||
|
)
|
||||||
|
CipherPrimitive(key, params, Cipher.ENCRYPT_MODE, txId)
|
||||||
|
}
|
||||||
|
KeyPurpose.DECRYPT -> {
|
||||||
|
val key: java.security.Key =
|
||||||
|
secretKey
|
||||||
|
?: keyPair?.private
|
||||||
|
?: throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.unsupportedPurpose,
|
||||||
|
"[SoftwareOp TX_ID: $txId] DECRYPT requires either secretKey or keyPair.private",
|
||||||
|
)
|
||||||
|
CipherPrimitive(key, params, Cipher.DECRYPT_MODE, txId)
|
||||||
|
}
|
||||||
|
KeyPurpose.AGREE_KEY -> {
|
||||||
|
val kp =
|
||||||
|
keyPair
|
||||||
|
?: throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.invalidArgument,
|
||||||
|
"[SoftwareOp TX_ID: $txId] AGREE_KEY requested but keyPair is null",
|
||||||
|
)
|
||||||
|
KeyAgreementPrimitive(kp)
|
||||||
|
}
|
||||||
|
else ->
|
||||||
|
throw ServiceSpecificException(
|
||||||
KeystoreErrorCodes.unsupportedPurpose,
|
KeystoreErrorCodes.unsupportedPurpose,
|
||||||
"[SoftwareOp TX_ID: $txId] ENCRYPT requires either secretKey or keyPair.public",
|
"Unsupported operation purpose: $purpose",
|
||||||
)
|
)
|
||||||
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 ServiceSpecificException(
|
|
||||||
KeystoreErrorCodes.unsupportedPurpose,
|
|
||||||
"Unsupported operation purpose: $purpose",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkActive() {
|
private fun checkActive() {
|
||||||
if (finalized) {
|
if (finalized) {
|
||||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)")
|
SystemLogger.debug(
|
||||||
|
"[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)"
|
||||||
|
)
|
||||||
throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle)
|
throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkInputLength(data: ByteArray?) {
|
private fun checkInputLength(data: ByteArray?) {
|
||||||
if (data != null && data.size > MAX_RECEIVE_DATA) {
|
if (data != null && data.size > MAX_RECEIVE_DATA) {
|
||||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] Input too large: ${data.size} > $MAX_RECEIVE_DATA, throwing TOO_MUCH_DATA(${KeystoreErrorCodes.tooMuchData})")
|
SystemLogger.info(
|
||||||
|
"[SoftwareOp TX_ID: $txId] Input too large: ${data.size} > $MAX_RECEIVE_DATA, throwing TOO_MUCH_DATA(${KeystoreErrorCodes.tooMuchData})"
|
||||||
|
)
|
||||||
throw ServiceSpecificException(KeystoreErrorCodes.tooMuchData)
|
throw ServiceSpecificException(KeystoreErrorCodes.tooMuchData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateAad(aadInput: ByteArray?) {
|
fun updateAad(aadInput: ByteArray?) {
|
||||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] updateAad() ENTRY inputSize=${aadInput?.size ?: 0} primitive=${primitive::class.simpleName}")
|
SystemLogger.info(
|
||||||
|
"[SoftwareOp TX_ID: $txId] updateAad() ENTRY inputSize=${aadInput?.size ?: 0} primitive=${primitive::class.simpleName}"
|
||||||
|
)
|
||||||
checkActive()
|
checkActive()
|
||||||
checkInputLength(aadInput)
|
checkInputLength(aadInput)
|
||||||
try {
|
try {
|
||||||
primitive.updateAad(aadInput)
|
primitive.updateAad(aadInput)
|
||||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] updateAad() RETURNED_NORMALLY (unexpected for non-AEAD)")
|
SystemLogger.info(
|
||||||
|
"[SoftwareOp TX_ID: $txId] updateAad() RETURNED_NORMALLY (unexpected for non-AEAD)"
|
||||||
|
)
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
val top = throwable.stackTrace.firstOrNull()?.toString() ?: "<no-frame>"
|
val top = throwable.stackTrace.firstOrNull()?.toString() ?: "<no-frame>"
|
||||||
val code = (throwable as? ServiceSpecificException)?.errorCode
|
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")
|
SystemLogger.info(
|
||||||
|
"[SoftwareOp TX_ID: $txId] updateAad() THREW class=${throwable::class.java.name} code=$code msg=${throwable.message} top=$top"
|
||||||
|
)
|
||||||
throw throwable
|
throw throwable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,13 +528,18 @@ class SoftwareOperation(
|
|||||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
|
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapToServiceSpecificException(e: Exception): ServiceSpecificException = when (e) {
|
private fun mapToServiceSpecificException(e: Exception): ServiceSpecificException =
|
||||||
is SignatureException -> ServiceSpecificException(KeystoreErrorCodes.verificationFailed, e.message)
|
when (e) {
|
||||||
is javax.crypto.BadPaddingException -> ServiceSpecificException(KeystoreErrorCodes.invalidArgument, e.message)
|
is SignatureException ->
|
||||||
is javax.crypto.IllegalBlockSizeException -> ServiceSpecificException(KeystoreErrorCodes.invalidInputLength, e.message)
|
ServiceSpecificException(KeystoreErrorCodes.verificationFailed, e.message)
|
||||||
is java.security.InvalidKeyException -> ServiceSpecificException(KeystoreErrorCodes.incompatibleKey, e.message)
|
is javax.crypto.BadPaddingException ->
|
||||||
else -> ServiceSpecificException(KeystoreErrorCodes.unknownError, e.message)
|
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 {
|
companion object {
|
||||||
private const val MAX_RECEIVE_DATA = 0x8000
|
private const val MAX_RECEIVE_DATA = 0x8000
|
||||||
@@ -428,13 +603,28 @@ internal object KeystoreErrorCodes {
|
|||||||
resolveField("android.hardware.security.keymint.ErrorCode", "UNKNOWN_ERROR", -1000)
|
resolveField("android.hardware.security.keymint.ErrorCode", "UNKNOWN_ERROR", -1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val incompatibleBlockMode: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_BLOCK_MODE", -8)
|
||||||
|
}
|
||||||
|
|
||||||
|
val incompatiblePaddingMode: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_PADDING_MODE", -11)
|
||||||
|
}
|
||||||
|
|
||||||
|
val incompatibleDigest: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_DIGEST", -13)
|
||||||
|
}
|
||||||
|
|
||||||
|
val invalidMacLength: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_MAC_LENGTH", -57)
|
||||||
|
}
|
||||||
|
|
||||||
fun resolveField(className: String, fieldName: String, fallback: Int): Int =
|
fun resolveField(className: String, fieldName: String, fallback: Int): Int =
|
||||||
runCatching {
|
runCatching { Class.forName(className).getField(fieldName).getInt(null) }
|
||||||
Class.forName(className).getField(fieldName).getInt(null)
|
.getOrElse {
|
||||||
}.getOrElse {
|
SystemLogger.debug("Resolved $className.$fieldName via fallback: $fallback")
|
||||||
SystemLogger.debug("Resolved $className.$fieldName via fallback: $fallback")
|
fallback
|
||||||
fallback
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
||||||
@@ -442,13 +632,17 @@ class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun updateAad(aadInput: ByteArray?) {
|
override fun updateAad(aadInput: ByteArray?) {
|
||||||
SystemLogger.info("[SoftwareOpBinder] updateAad() ENTRY callingUid=${android.os.Binder.getCallingUid()} size=${aadInput?.size ?: 0}")
|
SystemLogger.info(
|
||||||
|
"[SoftwareOpBinder] updateAad() ENTRY callingUid=${android.os.Binder.getCallingUid()} size=${aadInput?.size ?: 0}"
|
||||||
|
)
|
||||||
try {
|
try {
|
||||||
operation.updateAad(aadInput)
|
operation.updateAad(aadInput)
|
||||||
SystemLogger.info("[SoftwareOpBinder] updateAad() RETURNED_NORMALLY")
|
SystemLogger.info("[SoftwareOpBinder] updateAad() RETURNED_NORMALLY")
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
val code = (throwable as? ServiceSpecificException)?.errorCode
|
val code = (throwable as? ServiceSpecificException)?.errorCode
|
||||||
SystemLogger.info("[SoftwareOpBinder] updateAad() PROPAGATING class=${throwable::class.java.name} code=$code msg=${throwable.message}")
|
SystemLogger.info(
|
||||||
|
"[SoftwareOpBinder] updateAad() PROPAGATING class=${throwable::class.java.name} code=$code msg=${throwable.message}"
|
||||||
|
)
|
||||||
throw throwable
|
throw throwable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+170
@@ -0,0 +1,170 @@
|
|||||||
|
package org.matrix.TEESimulator.interception.soter
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.HandlerThread
|
||||||
|
import android.os.IBinder
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||||
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps [SoterServiceInterceptor] mounted on the on-demand, restartable
|
||||||
|
* `com.tencent.soter.soterserver` process.
|
||||||
|
*
|
||||||
|
* `AbstractKeystoreInterceptor` injects `keystore2` exactly once: it is always alive and
|
||||||
|
* servicemanager-published, so the daemon gets its binder from `ServiceManager` and may
|
||||||
|
* `exitProcess` on failure. soterserver inverts both — it is Intent-bound (NOT in
|
||||||
|
* `ServiceManager`) and may die and respawn. This supervisor therefore *binds* the SOTER
|
||||||
|
* service, which both triggers its on-demand start AND yields the `ISoterService` binder
|
||||||
|
* (the target the native MITM registry keys on); injects `libTEESimulator.so` on every
|
||||||
|
* (re)start; confirms the landing with the `0xdeadbeef` backdoor handshake; then registers
|
||||||
|
* the forge. It re-binds — re-poking, re-injecting, re-registering — whenever the process
|
||||||
|
* dies, never exiting.
|
||||||
|
*
|
||||||
|
* The bind recipe (action = the interface descriptor, package, `BIND_AUTO_CREATE`) and the
|
||||||
|
* rebind-on-death lifecycle mirror the SOTER SDK's own `SoterCoreTreble`, so the daemon
|
||||||
|
* connects exactly as a real client would. Everything runs on a dedicated [HandlerThread]
|
||||||
|
* so it never stalls keystore init or `Looper.loop()` in [org.matrix.TEESimulator.App].
|
||||||
|
*
|
||||||
|
* Observability (the checkpoint's mandatory gate): every lifecycle event — bind, connect,
|
||||||
|
* inject ok/fail, handshake, respawn — is logged via [SystemLogger], debug-gated. It never
|
||||||
|
* gates the forge.
|
||||||
|
*/
|
||||||
|
object SoterProcessSupervisor {
|
||||||
|
|
||||||
|
/** soterserver hosts the package's own process (recon 2026-06-26: process == package). */
|
||||||
|
private const val SOTER_PACKAGE = "com.tencent.soter.soterserver"
|
||||||
|
|
||||||
|
/** Reuses the daemon's native injector + `entry`, PID-resolved by the target package. */
|
||||||
|
private const val INJECTION_COMMAND =
|
||||||
|
"exec ./inject `pidof $SOTER_PACKAGE` libTEESimulator.so entry"
|
||||||
|
|
||||||
|
private const val REBIND_DELAY_MS = 1000L
|
||||||
|
private const val REBIND_MAX_MS = 30_000L
|
||||||
|
|
||||||
|
private val started = AtomicBoolean(false)
|
||||||
|
|
||||||
|
/** Re-bind backoff; doubles each failed (re)bind up to [REBIND_MAX_MS], resets on a clean mount. Handler-thread-confined. */
|
||||||
|
private var rebindDelay = REBIND_DELAY_MS
|
||||||
|
|
||||||
|
private lateinit var context: Context
|
||||||
|
private lateinit var handler: Handler
|
||||||
|
|
||||||
|
/** Delivers bind callbacks onto the supervisor thread so nothing touches the main looper. */
|
||||||
|
private val executor = Executor { command -> handler.post(command) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts supervising on a dedicated thread and returns immediately. Idempotent. [context]
|
||||||
|
* must be able to bind services (the daemon's system context); supplied by the App wiring.
|
||||||
|
*/
|
||||||
|
fun start(context: Context) {
|
||||||
|
if (!started.compareAndSet(false, true)) return
|
||||||
|
this.context = context
|
||||||
|
handler = Handler(HandlerThread("soter-supervisor").apply { start() }.looper)
|
||||||
|
handler.post { bind() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val connection =
|
||||||
|
object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
|
SystemLogger.debug("SOTER service connected; mounting forge")
|
||||||
|
service?.let(::mount)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
SystemLogger.debug("SOTER service disconnected (process died); rebinding")
|
||||||
|
scheduleRetry()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindingDied(name: ComponentName?) {
|
||||||
|
SystemLogger.debug("SOTER binding died; rebinding")
|
||||||
|
scheduleRetry()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNullBinding(name: ComponentName?) {
|
||||||
|
SystemLogger.debug("SOTER onBind returned null; rebinding")
|
||||||
|
scheduleRetry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bind() {
|
||||||
|
val intent = Intent(SoterServiceInterceptor.DESCRIPTOR).setPackage(SOTER_PACKAGE)
|
||||||
|
val bound =
|
||||||
|
runCatching {
|
||||||
|
context.bindService(intent, Context.BIND_AUTO_CREATE, executor, connection)
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.debug { "SOTER bindService threw: $it" }
|
||||||
|
false
|
||||||
|
}
|
||||||
|
if (bound) {
|
||||||
|
SystemLogger.debug("SOTER bind requested (on-demand poke)")
|
||||||
|
} else {
|
||||||
|
SystemLogger.debug("SOTER bindService returned false; retrying")
|
||||||
|
scheduleRetry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rebind() {
|
||||||
|
runCatching { context.unbindService(connection) }
|
||||||
|
bind()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-attempts the bind after the current backoff, then widens it (capped at [REBIND_MAX_MS]).
|
||||||
|
* Every path that fails to leave the forge mounted routes here, so a live-but-uninjected
|
||||||
|
* binding is re-attempted instead of stranding the forge. A clean [mount] resets the backoff.
|
||||||
|
*/
|
||||||
|
private fun scheduleRetry() {
|
||||||
|
val delay = rebindDelay
|
||||||
|
rebindDelay = (rebindDelay * 2).coerceAtMost(REBIND_MAX_MS)
|
||||||
|
handler.postDelayed({ rebind() }, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Confirms injection via the `0xdeadbeef` handshake, injecting first if absent, then registers. */
|
||||||
|
private fun mount(soterBinder: IBinder) {
|
||||||
|
var backdoor = BinderInterceptor.getBackdoor(soterBinder)
|
||||||
|
if (backdoor == null) {
|
||||||
|
SystemLogger.debug("SOTER backdoor absent; injecting libTEESimulator.so")
|
||||||
|
if (!injectLibrary()) {
|
||||||
|
SystemLogger.debug("SOTER injection failed; scheduling re-bind")
|
||||||
|
scheduleRetry()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
backdoor = BinderInterceptor.getBackdoor(soterBinder)
|
||||||
|
}
|
||||||
|
if (backdoor == null) {
|
||||||
|
SystemLogger.debug("SOTER backdoor handshake failed after injection; scheduling re-bind")
|
||||||
|
scheduleRetry()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val registered =
|
||||||
|
BinderInterceptor.register(
|
||||||
|
backdoor,
|
||||||
|
soterBinder,
|
||||||
|
SoterServiceInterceptor,
|
||||||
|
SoterServiceInterceptor.interceptedCodes,
|
||||||
|
)
|
||||||
|
if (!registered) {
|
||||||
|
SystemLogger.debug("SOTER register failed; scheduling re-bind")
|
||||||
|
scheduleRetry()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rebindDelay = REBIND_DELAY_MS
|
||||||
|
SystemLogger.debug("SOTER forge mounted; handshake ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun injectLibrary(): Boolean =
|
||||||
|
runCatching {
|
||||||
|
Runtime.getRuntime().exec(arrayOf("/system/bin/sh", "-c", INJECTION_COMMAND)).waitFor() == 0
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.debug { "SOTER inject exec failed: $it" }
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
+229
@@ -0,0 +1,229 @@
|
|||||||
|
package org.matrix.TEESimulator.interception.soter
|
||||||
|
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.util.Base64
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
|
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||||
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forges healthy `com.tencent.soter.soterserver.ISoterService` (Layer A: AIDL over
|
||||||
|
* `/dev/binder`) replies from inside the injected soterserver app process, so the SOTER
|
||||||
|
* capability probe (春秋 / DuckDetector `SoterCapabilityProbe`) reads `available = true`
|
||||||
|
* / `damaged = false` on a bootloader-unlocked device whose SOTER TA can no longer use
|
||||||
|
* its factory ATTK. Replaces the external SoterFixer loop + the Hail freeze.
|
||||||
|
*
|
||||||
|
* Unconditional by design: the forge decision never consults `ConfigurationManager` /
|
||||||
|
* `target.txt` (Phase 10 spec §Decision, gate G). It is mounted by the SOTER process
|
||||||
|
* supervisor (10.B/10.W) against the ISoterService binder, so `onPreTransact` only sees
|
||||||
|
* transactions on that binder — matching the raw transaction code is therefore enough.
|
||||||
|
*
|
||||||
|
* Diagnostics follow the module's standard three-layer capture (debug-gated, per-UID
|
||||||
|
* NDJSON via [SystemLogger]; see `logging/SystemLogger.kt`): a `tx` line for every
|
||||||
|
* transaction ([logTransaction]), the raw inbound request parcel, and the raw forged
|
||||||
|
* reply wire. Capture is scoped to targeted UIDs (`isUidLogged`) exactly like the
|
||||||
|
* keystore lane — it does NOT make the forge conditional; the forge still fires for all.
|
||||||
|
*
|
||||||
|
* Transaction codes are HARDCODED 1..13 in AIDL declaration order, NOT resolved via
|
||||||
|
* [org.matrix.TEESimulator.interception.keystore.InterceptorUtils.getTransactCode]: the
|
||||||
|
* shipped soterserver build is R8/ProGuard obfuscated — there is no `ISoterService$Stub`
|
||||||
|
* class and no `TRANSACTION_*` fields (recon 2026-06-26, `a$a.smali` packed-switch). The
|
||||||
|
* codes are fixed by Tencent's `ISoterService.aidl` and are obfuscation-independent.
|
||||||
|
*
|
||||||
|
* Scope boundary (10.A vs 10.M): the seven primitive-returning methods are fully forged
|
||||||
|
* here. The six parcelable-returning methods emit the correct AIDL envelope + the
|
||||||
|
* recon-verified `writeToParcel` field order; 10.M fills the payloads with
|
||||||
|
* detector-satisfying values — a framed SOTER pubkey envelope the SDK's
|
||||||
|
* `retrieveJsonFromExportedData` parses to a non-null `SoterPubKeyModel`, a non-zero sign
|
||||||
|
* session, and a 256-byte signature.
|
||||||
|
*/
|
||||||
|
object SoterServiceInterceptor : BinderInterceptor() {
|
||||||
|
|
||||||
|
/** The surviving, obfuscation-stable interface identifier (used by the 10.B/10.W mount). */
|
||||||
|
const val DESCRIPTOR = "com.tencent.soter.soterserver.ISoterService"
|
||||||
|
|
||||||
|
// AIDL transaction codes = FIRST_CALL_TRANSACTION (1) + declaration index, verified
|
||||||
|
// against the obfuscated `a$a.smali` packed-switch (recon 2026-06-26). NOTE the 5/6
|
||||||
|
// order: removeAuthKey precedes getAuthKey in the real .aidl (the spec prose had it
|
||||||
|
// reversed). Comments record each method's return shape.
|
||||||
|
private const val TX_GENERATE_APP_SECURE_KEY = 1 // int
|
||||||
|
private const val TX_GET_APP_SECURE_KEY = 2 // SoterExportResult
|
||||||
|
private const val TX_HAS_ASK_ALREADY = 3 // boolean
|
||||||
|
private const val TX_GENERATE_AUTH_KEY = 4 // int
|
||||||
|
private const val TX_REMOVE_AUTH_KEY = 5 // int (NOT getAuthKey)
|
||||||
|
private const val TX_GET_AUTH_KEY = 6 // SoterExportResult (NOT removeAuthKey)
|
||||||
|
private const val TX_REMOVE_ALL_AUTH_KEY = 7 // int
|
||||||
|
private const val TX_HAS_AUTH_KEY = 8 // boolean
|
||||||
|
private const val TX_INIT_SIGH = 9 // SoterSessionResult (sic: Tencent's spelling)
|
||||||
|
private const val TX_FINISH_SIGN = 10 // SoterSignResult
|
||||||
|
private const val TX_GET_DEVICE_ID = 11 // SoterDeviceResult
|
||||||
|
private const val TX_GET_VERSION = 12 // int (real service returns 1)
|
||||||
|
private const val TX_GET_EXTRA_PARAM = 13 // SoterExtraParam
|
||||||
|
|
||||||
|
/** SOTER success result code (`SoterCoreResult` ERR_OK). */
|
||||||
|
private const val SOTER_OK = 0
|
||||||
|
|
||||||
|
/** finishSign signature length the probe expects. */
|
||||||
|
private const val SIGNATURE_LEN = 256
|
||||||
|
|
||||||
|
/** `cpu_id` placeholder in the export envelope; the local probe never reads its value
|
||||||
|
* (the backend pins the real per-`cpu_id` ATTK, which the forge cannot satisfy). */
|
||||||
|
private const val CPU_ID = "0000000000000000"
|
||||||
|
|
||||||
|
/** Code -> Tencent method name, for the `tx` diagnostic line. Names from the recon decompile. */
|
||||||
|
private val methodNames =
|
||||||
|
mapOf(
|
||||||
|
TX_GENERATE_APP_SECURE_KEY to "generateAppSecureKey",
|
||||||
|
TX_GET_APP_SECURE_KEY to "getAppSecureKey",
|
||||||
|
TX_HAS_ASK_ALREADY to "hasAskAlready",
|
||||||
|
TX_GENERATE_AUTH_KEY to "generateAuthKey",
|
||||||
|
TX_REMOVE_AUTH_KEY to "removeAuthKey",
|
||||||
|
TX_GET_AUTH_KEY to "getAuthKey",
|
||||||
|
TX_REMOVE_ALL_AUTH_KEY to "removeAllAuthKey",
|
||||||
|
TX_HAS_AUTH_KEY to "hasAuthKey",
|
||||||
|
TX_INIT_SIGH to "initSigh",
|
||||||
|
TX_FINISH_SIGN to "finishSign",
|
||||||
|
TX_GET_DEVICE_ID to "getDeviceId",
|
||||||
|
TX_GET_VERSION to "getVersion",
|
||||||
|
TX_GET_EXTRA_PARAM to "getExtraParam",
|
||||||
|
)
|
||||||
|
|
||||||
|
/** The codes this interceptor forges; consumed by the supervisor's registration (10.B/10.W). */
|
||||||
|
val interceptedCodes: IntArray = methodNames.keys.toIntArray()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload of [SoterExportResult.exportData] for getAppSecureKey (txn 2) and getAuthKey
|
||||||
|
* (txn 6). The detector's capability probe gates `damaged=false` on
|
||||||
|
* `SoterCore.getApp/AuthKeyModel() != null`, and the SDK's `retrieveJsonFromExportedData`
|
||||||
|
* (`SoterCoreBase`) returns a non-null `SoterPubKeyModel` only when this exact framing
|
||||||
|
* parses: `[4-byte LITTLE-ENDIAN json length][UTF-8 json][signature bytes]`. A
|
||||||
|
* non-empty-but-unframed blob throws inside the SDK and is read as `damaged` silently.
|
||||||
|
* The JSON parser swallows every exception, so only the framing is load-bearing; the
|
||||||
|
* `pub_key` is a genuine RSA-2048 SubjectPublicKeyInfo so a probe that base64/X.509-parses
|
||||||
|
* the field locally still succeeds. Lazily built — keygen runs once, off the mount path.
|
||||||
|
*/
|
||||||
|
private val exportBlob: ByteArray by lazy { buildExportBlob() }
|
||||||
|
|
||||||
|
/** getDeviceId (txn 11) payload — well-formed, non-empty; the probe never parses it. */
|
||||||
|
private val deviceIdBlob = "TEESIM-SOTER-0001".toByteArray(Charsets.UTF_8)
|
||||||
|
|
||||||
|
/** finishSign (txn 10) signature payload — [SIGNATURE_LEN] bytes. */
|
||||||
|
private val signatureBlob = ByteArray(SIGNATURE_LEN)
|
||||||
|
|
||||||
|
private fun buildExportBlob(): ByteArray {
|
||||||
|
val pubKey =
|
||||||
|
runCatching {
|
||||||
|
val generator = KeyPairGenerator.getInstance("RSA").apply { initialize(2048) }
|
||||||
|
Base64.encodeToString(generator.generateKeyPair().public.encoded, Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
.getOrDefault("")
|
||||||
|
val json =
|
||||||
|
"""{"pub_key":"$pubKey","counter":0,"cpu_id":"$CPU_ID","uid":0}"""
|
||||||
|
.toByteArray(Charsets.UTF_8)
|
||||||
|
val lengthPrefix = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(json.size).array()
|
||||||
|
return lengthPrefix + json + signatureBlob
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPreTransact(
|
||||||
|
txId: Long,
|
||||||
|
target: IBinder,
|
||||||
|
code: Int,
|
||||||
|
flags: Int,
|
||||||
|
callingUid: Int,
|
||||||
|
callingPid: Int,
|
||||||
|
data: Parcel,
|
||||||
|
): TransactionResult {
|
||||||
|
val method = methodNames[code]
|
||||||
|
if (method == null) {
|
||||||
|
// Not an ISoterService method we forge — record it as observed, then pass through.
|
||||||
|
logTransaction(txId, "code=$code", callingUid, callingPid, skipPost = true)
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
logTransaction(txId, method, callingUid, callingPid)
|
||||||
|
captureRequest(callingUid, txId, method, data)
|
||||||
|
|
||||||
|
return when (code) {
|
||||||
|
// Primitive returns — fully forged here.
|
||||||
|
TX_GENERATE_APP_SECURE_KEY,
|
||||||
|
TX_GENERATE_AUTH_KEY,
|
||||||
|
TX_REMOVE_AUTH_KEY,
|
||||||
|
TX_REMOVE_ALL_AUTH_KEY -> forgedReply(callingUid, txId, method) { writeInt(SOTER_OK) }
|
||||||
|
TX_GET_VERSION -> forgedReply(callingUid, txId, method) { writeInt(1) }
|
||||||
|
TX_HAS_ASK_ALREADY,
|
||||||
|
TX_HAS_AUTH_KEY -> forgedReply(callingUid, txId, method) { writeInt(1) } // boolean true
|
||||||
|
|
||||||
|
// Parcelable returns — correct envelope + recon field order, payloads filled (10.M).
|
||||||
|
TX_GET_APP_SECURE_KEY,
|
||||||
|
TX_GET_AUTH_KEY ->
|
||||||
|
forgedReply(callingUid, txId, method) {
|
||||||
|
writeInt(1) // non-null marker
|
||||||
|
writeInt(SOTER_OK) // resultCode
|
||||||
|
writeByteArray(exportBlob) // exportData — framed SOTER pubkey envelope
|
||||||
|
writeInt(exportBlob.size) // exportDataLength
|
||||||
|
}
|
||||||
|
TX_INIT_SIGH ->
|
||||||
|
forgedReply(callingUid, txId, method) {
|
||||||
|
writeInt(1)
|
||||||
|
writeLong(1L) // session — any non-zero satisfies the probe
|
||||||
|
writeInt(SOTER_OK) // resultCode — probe requires == 0 (SoterCapabilityProbe.kt:107)
|
||||||
|
}
|
||||||
|
TX_FINISH_SIGN ->
|
||||||
|
forgedReply(callingUid, txId, method) {
|
||||||
|
writeInt(1)
|
||||||
|
writeInt(SOTER_OK) // resultCode — finishSign throws on != 0
|
||||||
|
writeByteArray(signatureBlob) // exportData = signature
|
||||||
|
writeInt(signatureBlob.size) // exportDataLength
|
||||||
|
}
|
||||||
|
TX_GET_DEVICE_ID ->
|
||||||
|
forgedReply(callingUid, txId, method) {
|
||||||
|
writeInt(1)
|
||||||
|
writeInt(SOTER_OK) // resultCode
|
||||||
|
writeByteArray(deviceIdBlob) // exportData = device id
|
||||||
|
writeInt(deviceIdBlob.size) // exportDataLength
|
||||||
|
}
|
||||||
|
TX_GET_EXTRA_PARAM ->
|
||||||
|
forgedReply(callingUid, txId, method) {
|
||||||
|
writeInt(1)
|
||||||
|
writeValue("optical") // SoterExtraParam.result = fingerprint sensor type
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable: method != null means code is one of the 13 above.
|
||||||
|
else -> TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Snapshots the inbound request parcel to the per-UID NDJSON plane (debug + targeted only). */
|
||||||
|
private fun captureRequest(uid: Int, txId: Long, method: String, data: Parcel) {
|
||||||
|
if (!SystemLogger.isUidLogged(uid)) return
|
||||||
|
runCatching { data.marshall() }
|
||||||
|
.onSuccess { raw ->
|
||||||
|
SystemLogger.uidLogRaw(uid, txId, "$method-request", "len=${raw.size}", raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an AIDL reply (`writeNoException()` then [body]) and snapshots its wire bytes to the
|
||||||
|
* per-UID NDJSON plane before handing it to the native hook. Parcelable bodies write their own
|
||||||
|
* `writeInt(1)` non-null marker; the native hook recycles the parcel after use.
|
||||||
|
*/
|
||||||
|
private fun forgedReply(
|
||||||
|
uid: Int,
|
||||||
|
txId: Long,
|
||||||
|
method: String,
|
||||||
|
body: Parcel.() -> Unit,
|
||||||
|
): TransactionResult.OverrideReply {
|
||||||
|
val reply = Parcel.obtain()
|
||||||
|
reply.writeNoException()
|
||||||
|
reply.body()
|
||||||
|
if (SystemLogger.isUidLogged(uid)) {
|
||||||
|
runCatching { reply.marshall() }
|
||||||
|
.onSuccess { raw ->
|
||||||
|
SystemLogger.uidLogRaw(uid, txId, "$method-reply", "len=${raw.size}", raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TransactionResult.OverrideReply(reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package org.matrix.TEESimulator.logging
|
||||||
|
|
||||||
|
import android.hardware.security.keymint.Tag
|
||||||
|
import android.system.keystore2.Authorization
|
||||||
|
import java.security.cert.Certificate
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
||||||
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
|
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assembles the per-UID "attestation dossier": for a targeted app, the full decoded attestation we
|
||||||
|
* actually hand it, the identity of every certificate in the returned chain, and the source of each
|
||||||
|
* device value that fed that attestation. Emitting all three where a chain is produced turns "the
|
||||||
|
* app rejects us" into a field-by-field record that can be diffed against a genuine TEE.
|
||||||
|
*/
|
||||||
|
object AttestationDossier {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the dossier for [chain] under [uid], tagged with the [path] that produced it
|
||||||
|
* (`FORGE-rust`, `FORGE-bouncycastle`, or `PATCH`). No-op for untargeted UIDs and release
|
||||||
|
* builds; the expensive decoding is skipped entirely when the UID is out of scope.
|
||||||
|
*/
|
||||||
|
fun log(uid: Int, txId: Long, path: String, chain: List<Certificate>) {
|
||||||
|
if (!SystemLogger.isUidLogged(uid)) return
|
||||||
|
val leaf = chain.firstOrNull() as? X509Certificate
|
||||||
|
val extension =
|
||||||
|
leaf?.let { AttestationPatcher.formatAttestationExtension(it) }
|
||||||
|
?: "<no attestation extension>"
|
||||||
|
SystemLogger.uidLog(uid, txId, "attest", "path=$path depth=${chain.size} $extension")
|
||||||
|
SystemLogger.uidLog(uid, txId, "keybox", "file=${ConfigurationManager.getKeyboxFileForUid(uid)}")
|
||||||
|
SystemLogger.uidLog(uid, txId, "chain", AttestationPatcher.formatCertChain(chain))
|
||||||
|
SystemLogger.uidLog(uid, txId, "chain-verify", AttestationPatcher.formatChainVerification(chain))
|
||||||
|
SystemLogger.uidLog(uid, txId, "props", AndroidDeviceUtils.describeSources(uid))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the *shape* of the emitted authorization list — count, ordered tags, and per-auth
|
||||||
|
* securityLevel. This is the exact surface the duck detector's generate-mode parcel fingerprint
|
||||||
|
* stride-walks, so logging it readably lets a "fingerprint" detection be compared against the
|
||||||
|
* known genuine-TEE shape without decoding the marshalled reply offline.
|
||||||
|
*/
|
||||||
|
fun logAuthShape(uid: Int, txId: Long, authorizations: Array<Authorization>?) {
|
||||||
|
if (!SystemLogger.isUidLogged(uid)) return
|
||||||
|
val auths = authorizations ?: return
|
||||||
|
val shape = auths.joinToString(",") { "${tagName(it.keyParameter.tag)}/${it.securityLevel}" }
|
||||||
|
SystemLogger.uidLog(uid, txId, "auth-shape", "n=${auths.size} [$shape]")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Names the authorization tags that occur in generate-mode replies; others render as numbers. */
|
||||||
|
private fun tagName(tag: Int): String =
|
||||||
|
when (tag) {
|
||||||
|
Tag.PURPOSE -> "PURPOSE"
|
||||||
|
Tag.ALGORITHM -> "ALGORITHM"
|
||||||
|
Tag.KEY_SIZE -> "KEY_SIZE"
|
||||||
|
Tag.DIGEST -> "DIGEST"
|
||||||
|
Tag.PADDING -> "PADDING"
|
||||||
|
Tag.EC_CURVE -> "EC_CURVE"
|
||||||
|
Tag.RSA_PUBLIC_EXPONENT -> "RSA_PUBLIC_EXPONENT"
|
||||||
|
Tag.NO_AUTH_REQUIRED -> "NO_AUTH_REQUIRED"
|
||||||
|
Tag.ORIGIN -> "ORIGIN"
|
||||||
|
Tag.OS_VERSION -> "OS_VERSION"
|
||||||
|
Tag.OS_PATCHLEVEL -> "OS_PATCHLEVEL"
|
||||||
|
Tag.VENDOR_PATCHLEVEL -> "VENDOR_PATCHLEVEL"
|
||||||
|
Tag.BOOT_PATCHLEVEL -> "BOOT_PATCHLEVEL"
|
||||||
|
Tag.CREATION_DATETIME -> "CREATION_DATETIME"
|
||||||
|
Tag.ROOT_OF_TRUST -> "ROOT_OF_TRUST"
|
||||||
|
Tag.USER_ID -> "USER_ID"
|
||||||
|
Tag.USAGE_COUNT_LIMIT -> "USAGE_COUNT_LIMIT"
|
||||||
|
Tag.UNLOCKED_DEVICE_REQUIRED -> "UNLOCKED_DEVICE_REQUIRED"
|
||||||
|
Tag.ACTIVE_DATETIME -> "ACTIVE_DATETIME"
|
||||||
|
else -> "tag${tag and 0x0FFFFFFF}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,12 +69,23 @@ object KeyMintParameterLogger {
|
|||||||
.associate { field -> (field.get(null) as Int) to field.name }
|
.associate { field -> (field.get(null) as Int) to field.name }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Logs a single KeyParameter to the shared debug stream (used for un-scoped param dumps). */
|
||||||
* Logs a single KeyParameter in a formatted, readable way.
|
|
||||||
*
|
|
||||||
* @param param The KeyParameter to log.
|
|
||||||
*/
|
|
||||||
fun logParameter(param: KeyParameter) {
|
fun logParameter(param: KeyParameter) {
|
||||||
|
SystemLogger.debug("KeyParam: ${describe(param)}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Logs a single KeyParameter onto a targeted UID's diagnostic plane as a `param` record. */
|
||||||
|
fun logParameter(uid: Int, txId: Long, param: KeyParameter) {
|
||||||
|
SystemLogger.uidLog(uid, txId, "param", describe(param))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a single KeyParameter into a readable `tag | Value` string. Shared by both
|
||||||
|
* [logParameter] overloads so the two logging planes render parameters identically.
|
||||||
|
*
|
||||||
|
* @param param The KeyParameter to format.
|
||||||
|
*/
|
||||||
|
private fun describe(param: KeyParameter): String {
|
||||||
val tagName = tagNames[param.tag] ?: "UNKNOWN_TAG"
|
val tagName = tagNames[param.tag] ?: "UNKNOWN_TAG"
|
||||||
val value = param.value
|
val value = param.value
|
||||||
val formattedValue: String =
|
val formattedValue: String =
|
||||||
@@ -110,7 +121,7 @@ object KeyMintParameterLogger {
|
|||||||
else -> "<raw>"
|
else -> "<raw>"
|
||||||
} ?: "Unknown Value"
|
} ?: "Unknown Value"
|
||||||
|
|
||||||
SystemLogger.debug("KeyParam: %-25s | Value: %s".format(tagName, formattedValue))
|
return "%-25s | Value: %s".format(tagName, formattedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ByteArray.toReadableString(): String {
|
private fun ByteArray.toReadableString(): String {
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
package org.matrix.TEESimulator.logging
|
package org.matrix.TEESimulator.logging
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import java.io.BufferedWriter
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileWriter
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
import org.json.JSONObject
|
||||||
import org.matrix.TEESimulator.BuildConfig
|
import org.matrix.TEESimulator.BuildConfig
|
||||||
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A centralized logging utility for the TEESimulator application. This object provides a consistent
|
* A centralized logging utility for the TEESimulator application. This object provides a consistent
|
||||||
@@ -26,10 +36,11 @@ object SystemLogger {
|
|||||||
private val suppressedCount = AtomicInteger(0)
|
private val suppressedCount = AtomicInteger(0)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if this message should be emitted. Resets the window if expired
|
* Returns true if this message should be emitted. Resets the window if expired and emits a
|
||||||
* and emits a suppression summary for the previous window.
|
* suppression summary for the previous window.
|
||||||
*/
|
*/
|
||||||
@PublishedApi internal fun acquireLogPermit(): Boolean {
|
@PublishedApi
|
||||||
|
internal fun acquireLogPermit(): Boolean {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val start = windowStart.get()
|
val start = windowStart.get()
|
||||||
if (now - start > RATE_LIMIT_WINDOW_MS) {
|
if (now - start > RATE_LIMIT_WINDOW_MS) {
|
||||||
@@ -38,7 +49,10 @@ object SystemLogger {
|
|||||||
val suppressed = suppressedCount.getAndSet(0)
|
val suppressed = suppressedCount.getAndSet(0)
|
||||||
windowCount.set(1) // this call counts as #1 in the new window
|
windowCount.set(1) // this call counts as #1 in the new window
|
||||||
if (suppressed > 0) {
|
if (suppressed > 0) {
|
||||||
Log.i(TAG, "[rate-limit] suppressed $suppressed log messages in previous window")
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"[rate-limit] suppressed $suppressed log messages in previous window",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -49,9 +63,7 @@ object SystemLogger {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Logs a debug message. Use this for fine-grained information that is useful for debugging. */
|
||||||
* Logs a debug message. Use this for fine-grained information that is useful for debugging.
|
|
||||||
*/
|
|
||||||
fun debug(message: String) {
|
fun debug(message: String) {
|
||||||
if (!isDebugBuild) return
|
if (!isDebugBuild) return
|
||||||
if (!acquireLogPermit()) return
|
if (!acquireLogPermit()) return
|
||||||
@@ -65,9 +77,7 @@ object SystemLogger {
|
|||||||
Log.d(TAG, message())
|
Log.d(TAG, message())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Logs an informational message. Use this to report major application lifecycle events. */
|
||||||
* Logs an informational message. Use this to report major application lifecycle events.
|
|
||||||
*/
|
|
||||||
fun info(message: String) {
|
fun info(message: String) {
|
||||||
if (!acquireLogPermit()) return
|
if (!acquireLogPermit()) return
|
||||||
Log.i(TAG, message)
|
Log.i(TAG, message)
|
||||||
@@ -79,9 +89,7 @@ object SystemLogger {
|
|||||||
Log.i(TAG, message())
|
Log.i(TAG, message())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Logs a warning message. Warnings are never rate-limited. */
|
||||||
* Logs a warning message. Warnings are never rate-limited.
|
|
||||||
*/
|
|
||||||
fun warning(message: String, throwable: Throwable? = null) {
|
fun warning(message: String, throwable: Throwable? = null) {
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
Log.w(TAG, message, throwable)
|
Log.w(TAG, message, throwable)
|
||||||
@@ -90,9 +98,7 @@ object SystemLogger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Logs an error message. Errors are never rate-limited. */
|
||||||
* Logs an error message. Errors are never rate-limited.
|
|
||||||
*/
|
|
||||||
fun error(message: String, throwable: Throwable? = null) {
|
fun error(message: String, throwable: Throwable? = null) {
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
Log.e(TAG, message, throwable)
|
Log.e(TAG, message, throwable)
|
||||||
@@ -122,4 +128,145 @@ object SystemLogger {
|
|||||||
if (!isDebugBuild) return
|
if (!isDebugBuild) return
|
||||||
Log.w(TAG, message())
|
Log.w(TAG, message())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- UID-keyed diagnostic plane (debug builds only) -------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when [uid] should receive deep, per-UID diagnostic logging: a debug build AND the UID is
|
||||||
|
* targeted in `target.txt`. This is the single scope gate for the diagnostic plane; it reuses
|
||||||
|
* the existing activation set, so no new configuration surface is introduced.
|
||||||
|
*/
|
||||||
|
fun isUidLogged(uid: Int): Boolean = isDebugBuild && !ConfigurationManager.shouldSkipUid(uid)
|
||||||
|
|
||||||
|
/** Resolves a UID to its primary package name for log labelling, falling back to `uid:N`. */
|
||||||
|
private fun label(uid: Int): String =
|
||||||
|
ConfigurationManager.getPackagesForUid(uid).firstOrNull() ?: "uid:$uid"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits one structured diagnostic record for a targeted [uid]. The human form
|
||||||
|
* `[<pkg> tx=<txId>] <event>: <detail>` goes to logcat; the file sink receives one NDJSON object
|
||||||
|
* per line under that UID's own file. In-scope records bypass the global rate limiter: a
|
||||||
|
* targeted app's traffic is already volume-bounded, and dropping a line mid-probe would corrupt
|
||||||
|
* the very trace we are trying to read. No-op for untargeted UIDs and in release builds.
|
||||||
|
*/
|
||||||
|
fun uidLog(uid: Int, txId: Long?, event: String, detail: String) {
|
||||||
|
if (!isUidLogged(uid)) return
|
||||||
|
val correlation = txId?.let { " tx=$it" } ?: ""
|
||||||
|
Log.d(TAG, "[${label(uid)}$correlation] $event: $detail")
|
||||||
|
runCatching { uidWriter(uid).append(jsonRecord(uid, txId, event, detail, null)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lazy [uidLog]: [detail] is only built for targeted UIDs in debug builds. */
|
||||||
|
inline fun uidLog(uid: Int, txId: Long?, event: String, detail: () -> String) {
|
||||||
|
if (!isUidLogged(uid)) return
|
||||||
|
uidLog(uid, txId, event, detail())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [uidLog] plus the exact wire bytes that produced the event, base64 (NO_WRAP) in a `raw_b64`
|
||||||
|
* field. This is the structured replacement for the per-call `.bin` parcel dumps: one NDJSON
|
||||||
|
* line on the per-UID file instead of a fresh undecodable file per transaction, with the raw
|
||||||
|
* parcel still recoverable for offline parsers.
|
||||||
|
*/
|
||||||
|
fun uidLogRaw(uid: Int, txId: Long?, event: String, detail: String, raw: ByteArray) {
|
||||||
|
if (!isUidLogged(uid)) return
|
||||||
|
val correlation = txId?.let { " tx=$it" } ?: ""
|
||||||
|
Log.d(TAG, "[${label(uid)}$correlation] $event: $detail (raw ${raw.size}B)")
|
||||||
|
runCatching {
|
||||||
|
val encoded = Base64.encodeToString(raw, Base64.NO_WRAP)
|
||||||
|
uidWriter(uid).append(jsonRecord(uid, txId, event, detail, encoded))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* External-storage root for every debug diagnostic. `/data/media/0/TEESimulator` is the
|
||||||
|
* in-namespace backing path the keystore domain can reach; a normal file manager sees the same
|
||||||
|
* files at `/sdcard/TEESimulator`. Release builds never write here and purge it on boot
|
||||||
|
* (App.purgeDebugDiagnostics). The domain reaches it via a debug-only media_rw_data_file
|
||||||
|
* sepolicy grant, and service.sh pre-creates the directory.
|
||||||
|
*/
|
||||||
|
const val DIAGNOSTIC_DIR = "/data/media/0/TEESimulator"
|
||||||
|
|
||||||
|
private val uidLogDir = File(DIAGNOSTIC_DIR)
|
||||||
|
private const val UID_LOG_MAX_BYTES = 4L * 1024 * 1024
|
||||||
|
private val uidWriters = ConcurrentHashMap<Int, UidLogFile>()
|
||||||
|
|
||||||
|
private val recordClock =
|
||||||
|
DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS").withZone(ZoneId.systemDefault())
|
||||||
|
|
||||||
|
private fun jsonRecord(
|
||||||
|
uid: Int,
|
||||||
|
txId: Long?,
|
||||||
|
event: String,
|
||||||
|
detail: String,
|
||||||
|
rawB64: String?,
|
||||||
|
): String =
|
||||||
|
JSONObject()
|
||||||
|
.apply {
|
||||||
|
put("ts", recordClock.format(Instant.now()))
|
||||||
|
put("uid", uid)
|
||||||
|
put("pkg", label(uid))
|
||||||
|
txId?.let { put("tx", it) }
|
||||||
|
put("event", event)
|
||||||
|
put("detail", detail)
|
||||||
|
rawB64?.let { put("raw_b64", it) }
|
||||||
|
}
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
private fun uidWriter(uid: Int): UidLogFile =
|
||||||
|
uidWriters.computeIfAbsent(uid) { key ->
|
||||||
|
UidLogFile(key, uidLogDir).also { file ->
|
||||||
|
val packages =
|
||||||
|
ConfigurationManager.getPackagesForUid(key).joinToString().ifEmpty { "<unresolved>" }
|
||||||
|
runCatching {
|
||||||
|
file.append(jsonRecord(key, null, "session", "packages=[$packages]", null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append-only NDJSON sink for a single UID at `<logDir>/teesim-uid-<uid>.ndjson`, rotated once
|
||||||
|
* to `.ndjson.1` at [UID_LOG_MAX_BYTES]; one JSON object per line. Writes are synchronised
|
||||||
|
* because the keystore binder pool is multi-threaded, and every operation is wrapped so a
|
||||||
|
* logging fault can never propagate into the daemon. Created only on the debug-gated path.
|
||||||
|
*/
|
||||||
|
private class UidLogFile(uid: Int, private val logDir: File) {
|
||||||
|
private val primary = File(logDir, "teesim-uid-$uid.ndjson")
|
||||||
|
private val rotated = File(logDir, "teesim-uid-$uid.ndjson.1")
|
||||||
|
private var writer: BufferedWriter? = null
|
||||||
|
private var size = 0L
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun append(jsonLine: String) {
|
||||||
|
runCatching {
|
||||||
|
val out = writer ?: open()
|
||||||
|
out.write(jsonLine)
|
||||||
|
out.write("\n")
|
||||||
|
out.flush()
|
||||||
|
size += jsonLine.length + 1
|
||||||
|
if (size >= UID_LOG_MAX_BYTES) rotate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun open(): BufferedWriter {
|
||||||
|
logDir.mkdirs()
|
||||||
|
val out = BufferedWriter(FileWriter(primary, /* append = */ true))
|
||||||
|
writer = out
|
||||||
|
size = primary.length()
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rotate() {
|
||||||
|
runCatching {
|
||||||
|
writer?.flush()
|
||||||
|
writer?.close()
|
||||||
|
}
|
||||||
|
writer = null
|
||||||
|
runCatching {
|
||||||
|
if (rotated.exists()) rotated.delete()
|
||||||
|
primary.renameTo(rotated)
|
||||||
|
}
|
||||||
|
size = 0L
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,37 +93,53 @@ object CertificateGenerator {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
// AOSP ta/src/keys.rs:451-478: no challenge + no attestKey = self-signed, depth 1
|
// AOSP ta/src/keys.rs:451-478: no challenge + no attestKey = self-signed, depth 1
|
||||||
if (challenge == null && attestKeyAlias == null) {
|
if (challenge == null && attestKeyAlias == null) {
|
||||||
SystemLogger.trace { "[certgen] no-challenge key: self-signed, depth=1, purposes=${params.purpose}" }
|
SystemLogger.trace {
|
||||||
return listOf(buildSelfSignedCertificate(subjectKeyPair, params))
|
"[certgen] no-challenge key: self-signed, depth=1, purposes=${params.purpose}"
|
||||||
}
|
}
|
||||||
|
return listOf(buildSelfSignedCertificate(subjectKeyPair, params))
|
||||||
|
}
|
||||||
|
|
||||||
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
|
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
|
||||||
|
|
||||||
val attestKeyInfo =
|
val wantsAttestKey =
|
||||||
if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||||
getAttestationKeyInfo(uid, attestKeyAlias)
|
val attestKeyInfo =
|
||||||
} else null
|
if (wantsAttestKey) getAttestationKeyInfo(uid, attestKeyAlias) else null
|
||||||
|
|
||||||
val (signingKey, issuer) = attestKeyInfo
|
// When the caller designates an attest key, the leaf MUST be signed by it and returned
|
||||||
?.let { it.first to it.second }
|
// alone (the caller appends the attest key's own chain). Re-rooting under the keybox
|
||||||
|
// here instead yields a self-rooted leaf that, concatenated with the attest key chain,
|
||||||
|
// double-roots and fails verification (WRONG_PUBLIC_KEY_TYPE). Refuse rather than emit
|
||||||
|
// a
|
||||||
|
// broken chain.
|
||||||
|
if (wantsAttestKey && attestKeyInfo == null) {
|
||||||
|
SystemLogger.error(
|
||||||
|
"Designated attest key '$attestKeyAlias' not found for uid $uid; refusing to " +
|
||||||
|
"emit a keybox-rooted leaf that would break the caller's chain."
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val (signingKey, issuer) =
|
||||||
|
attestKeyInfo?.let { it.first to it.second }
|
||||||
?: (keybox.keyPair to getIssuerFromKeybox(keybox))
|
?: (keybox.keyPair to getIssuerFromKeybox(keybox))
|
||||||
|
|
||||||
val leafCert =
|
val leafCert =
|
||||||
buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel)
|
buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel)
|
||||||
|
|
||||||
if (attestKeyInfo != null) {
|
if (attestKeyInfo != null) {
|
||||||
listOf(leafCert)
|
listOf(leafCert)
|
||||||
} else {
|
} else {
|
||||||
listOf(leafCert) + keybox.certificates
|
listOf(leafCert) + keybox.certificates
|
||||||
}
|
|
||||||
} catch (e: android.os.ServiceSpecificException) {
|
|
||||||
throw e
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SystemLogger.error("Failed to generate certificate chain.", e)
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
} catch (e: android.os.ServiceSpecificException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("Failed to generate certificate chain.", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,27 +154,23 @@ object CertificateGenerator {
|
|||||||
securityLevel: Int,
|
securityLevel: Int,
|
||||||
): Pair<KeyPair, List<Certificate>>? {
|
): Pair<KeyPair, List<Certificate>>? {
|
||||||
return try {
|
return try {
|
||||||
SystemLogger.info(
|
SystemLogger.info("Generating new attested key pair for alias: '$alias' (UID: $uid)")
|
||||||
"Generating new attested key pair for alias: '$alias' (UID: $uid)"
|
val newKeyPair =
|
||||||
)
|
generateSoftwareKeyPair(params)
|
||||||
val newKeyPair =
|
?: throw Exception("Failed to generate underlying software key pair.")
|
||||||
generateSoftwareKeyPair(params)
|
|
||||||
?: throw Exception("Failed to generate underlying software key pair.")
|
|
||||||
|
|
||||||
val chain =
|
val chain =
|
||||||
generateCertificateChain(uid, newKeyPair, attestKeyAlias, params, securityLevel)
|
generateCertificateChain(uid, newKeyPair, attestKeyAlias, params, securityLevel)
|
||||||
?: throw Exception("Failed to generate certificate chain for new key pair.")
|
?: throw Exception("Failed to generate certificate chain for new key pair.")
|
||||||
|
|
||||||
SystemLogger.info(
|
SystemLogger.info("Successfully generated new certificate chain for alias: '$alias'.")
|
||||||
"Successfully generated new certificate chain for alias: '$alias'."
|
Pair(newKeyPair, chain)
|
||||||
)
|
} catch (e: android.os.ServiceSpecificException) {
|
||||||
Pair(newKeyPair, chain)
|
throw e
|
||||||
} catch (e: android.os.ServiceSpecificException) {
|
} catch (e: Exception) {
|
||||||
throw e
|
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", e)
|
||||||
} catch (e: Exception) {
|
null
|
||||||
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", e)
|
}
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getIssuerFromKeybox(keybox: KeyBox) =
|
fun getIssuerFromKeybox(keybox: KeyBox) =
|
||||||
@@ -172,11 +184,28 @@ object CertificateGenerator {
|
|||||||
Algorithm.RSA -> "RSA"
|
Algorithm.RSA -> "RSA"
|
||||||
else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm")
|
else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm")
|
||||||
}
|
}
|
||||||
return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName)
|
// Prefer the algorithm-matching keybox, but fall back to any usable key (EC preferred) when
|
||||||
?: throw android.os.ServiceSpecificException(
|
// none exists. An EC attestation key validly ECDSA-signs a leaf carrying an RSA subject key,
|
||||||
-75, // ATTESTATION_KEYS_NOT_PROVISIONED
|
// so an EC-only keybox can still root an RSA forge. Without this fallback an RSA ATTEST_KEY
|
||||||
"No attestation key for algorithm $algorithmName in $keyboxFile",
|
// request on an EC-only keybox throws -75 and the caller's chain never roots ("unknown
|
||||||
)
|
// certificate"). Mirrors the patch path's fail-safe
|
||||||
|
// (AttestationPatcher.getKeyboxForUidAndAlgorithm) and the RSA-leaf-under-EC-keybox handling
|
||||||
|
// in commit e6d5e4d.
|
||||||
|
val matched = KeyBoxManager.getAttestationKey(keyboxFile, algorithmName)
|
||||||
|
val keybox =
|
||||||
|
matched
|
||||||
|
?: KeyBoxManager.getAnyAttestationKey(keyboxFile)
|
||||||
|
?: throw android.os.ServiceSpecificException(
|
||||||
|
-75, // ATTESTATION_KEYS_NOT_PROVISIONED
|
||||||
|
"No usable attestation key in $keyboxFile",
|
||||||
|
)
|
||||||
|
// Surface which keybox actually signs the forge, so an EC-only-keybox fallback (an RSA leaf
|
||||||
|
// rooted under the EC key) is visible on the per-UID plane instead of silent.
|
||||||
|
SystemLogger.uidLog(uid, null, "keybox-pick") {
|
||||||
|
"req=$algorithmName ${if (matched != null) "matched" else "fellback-to-any"} " +
|
||||||
|
"signer=${getIssuerFromKeybox(keybox)}"
|
||||||
|
}
|
||||||
|
return keybox
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Retrieves the key pair and issuer name for a given attestation key alias. */
|
/** Retrieves the key pair and issuer name for a given attestation key alias. */
|
||||||
@@ -189,6 +218,15 @@ object CertificateGenerator {
|
|||||||
val certChain = CertificateHelper.getCertificateChain(keyInfo.response)
|
val certChain = CertificateHelper.getCertificateChain(keyInfo.response)
|
||||||
if (!certChain.isNullOrEmpty()) {
|
if (!certChain.isNullOrEmpty()) {
|
||||||
val issuer = X509CertificateHolder(certChain[0].encoded).subject
|
val issuer = X509CertificateHolder(certChain[0].encoded).subject
|
||||||
|
// The leaf is signed by keyInfo.keyPair, but the caller verifies it against the
|
||||||
|
// public key of the chain getCertChain(attestKeyAlias) serves. A two-rooted EC chain
|
||||||
|
// (DATA_TOO_LARGE_FOR_MODULUS) is exactly those two disagreeing on algorithm; log
|
||||||
|
// both at the signing instant so an EC attest-key run pins the mismatched edge.
|
||||||
|
SystemLogger.uidLog(uid, null, "attest-sign") {
|
||||||
|
"alias=$attestKeyAlias signerKey=${keyInfo.keyPair?.public?.algorithm} " +
|
||||||
|
"servedLeafKey=${certChain[0].publicKey.algorithm} " +
|
||||||
|
"depth=${certChain.size} issuer=$issuer"
|
||||||
|
}
|
||||||
Pair(keyInfo.keyPair, issuer)
|
Pair(keyInfo.keyPair, issuer)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -205,14 +243,16 @@ object CertificateGenerator {
|
|||||||
private fun buildKeyUsageFromPurposes(purposes: List<Int>): Int {
|
private fun buildKeyUsageFromPurposes(purposes: List<Int>): Int {
|
||||||
var bits = 0
|
var bits = 0
|
||||||
for (purpose in purposes) {
|
for (purpose in purposes) {
|
||||||
bits = bits or when (purpose) {
|
bits =
|
||||||
KeyPurpose.SIGN -> KeyUsage.digitalSignature
|
bits or
|
||||||
KeyPurpose.DECRYPT -> KeyUsage.dataEncipherment
|
when (purpose) {
|
||||||
KeyPurpose.WRAP_KEY -> KeyUsage.keyEncipherment
|
KeyPurpose.SIGN -> KeyUsage.digitalSignature
|
||||||
KeyPurpose.AGREE_KEY -> KeyUsage.keyAgreement
|
KeyPurpose.DECRYPT -> KeyUsage.dataEncipherment
|
||||||
KeyPurpose.ATTEST_KEY -> KeyUsage.keyCertSign
|
KeyPurpose.WRAP_KEY -> KeyUsage.keyEncipherment
|
||||||
else -> 0
|
KeyPurpose.AGREE_KEY -> KeyUsage.keyAgreement
|
||||||
}
|
KeyPurpose.ATTEST_KEY -> KeyUsage.keyCertSign
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return bits
|
return bits
|
||||||
}
|
}
|
||||||
@@ -253,9 +293,13 @@ object CertificateGenerator {
|
|||||||
|
|
||||||
val signerAlgorithm =
|
val signerAlgorithm =
|
||||||
when (signingKeyPair.private.algorithm) {
|
when (signingKeyPair.private.algorithm) {
|
||||||
"EC", "ECDSA" -> "SHA256withECDSA"
|
"EC",
|
||||||
|
"ECDSA" -> "SHA256withECDSA"
|
||||||
"RSA" -> "SHA256withRSA"
|
"RSA" -> "SHA256withRSA"
|
||||||
else -> throw IllegalArgumentException("Unsupported signing key: ${signingKeyPair.private.algorithm}")
|
else ->
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Unsupported signing key: ${signingKeyPair.private.algorithm}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val contentSigner =
|
val contentSigner =
|
||||||
JcaContentSignerBuilder(signerAlgorithm)
|
JcaContentSignerBuilder(signerAlgorithm)
|
||||||
@@ -274,28 +318,33 @@ object CertificateGenerator {
|
|||||||
val notBefore = params.certificateNotBefore ?: Date(0)
|
val notBefore = params.certificateNotBefore ?: Date(0)
|
||||||
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
|
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
|
||||||
|
|
||||||
val builder = JcaX509v3CertificateBuilder(
|
val builder =
|
||||||
subject,
|
JcaX509v3CertificateBuilder(
|
||||||
params.certificateSerial ?: BigInteger.ONE,
|
subject,
|
||||||
notBefore,
|
params.certificateSerial ?: BigInteger.ONE,
|
||||||
notAfter,
|
notBefore,
|
||||||
subject,
|
notAfter,
|
||||||
keyPair.public,
|
subject,
|
||||||
)
|
keyPair.public,
|
||||||
|
)
|
||||||
|
|
||||||
val keyUsageBits = buildKeyUsageFromPurposes(params.purpose)
|
val keyUsageBits = buildKeyUsageFromPurposes(params.purpose)
|
||||||
if (keyUsageBits != 0) {
|
if (keyUsageBits != 0) {
|
||||||
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
|
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
|
||||||
}
|
}
|
||||||
|
|
||||||
val signerAlgorithm = when (keyPair.private.algorithm) {
|
val signerAlgorithm =
|
||||||
"EC", "ECDSA" -> "SHA256withECDSA"
|
when (keyPair.private.algorithm) {
|
||||||
"RSA" -> "SHA256withRSA"
|
"EC",
|
||||||
else -> throw IllegalArgumentException("Unsupported key: ${keyPair.private.algorithm}")
|
"ECDSA" -> "SHA256withECDSA"
|
||||||
}
|
"RSA" -> "SHA256withRSA"
|
||||||
val contentSigner = JcaContentSignerBuilder(signerAlgorithm)
|
else ->
|
||||||
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
throw IllegalArgumentException("Unsupported key: ${keyPair.private.algorithm}")
|
||||||
.build(keyPair.private)
|
}
|
||||||
|
val contentSigner =
|
||||||
|
JcaContentSignerBuilder(signerAlgorithm)
|
||||||
|
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
.build(keyPair.private)
|
||||||
|
|
||||||
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.matrix.TEESimulator.pki
|
|||||||
import android.security.keystore.KeyProperties
|
import android.security.keystore.KeyProperties
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.StringReader
|
import java.io.StringReader
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
import java.security.interfaces.ECPrivateKey
|
import java.security.interfaces.ECPrivateKey
|
||||||
import java.security.interfaces.RSAPrivateKey
|
import java.security.interfaces.RSAPrivateKey
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
@@ -53,10 +54,38 @@ object KeyBoxManager {
|
|||||||
// If it's not in the cache, the `getOrPut` block is executed to parse and store it.
|
// If it's not in the cache, the `getOrPut` block is executed to parse and store it.
|
||||||
val keyMap =
|
val keyMap =
|
||||||
keyStoreCache.getOrPut(keyStoreFileName) { parseKeyStoreFile(keyStoreFileName) }
|
keyStoreCache.getOrPut(keyStoreFileName) { parseKeyStoreFile(keyStoreFileName) }
|
||||||
SystemLogger.verbose(
|
val keyBox = keyMap[algorithm]
|
||||||
"Fetching attestation key in $keyStoreFileName with $algorithm algorithm."
|
if (keyBox != null) {
|
||||||
)
|
// Surface attestation cert serials on every fetch so a revoked/leaked keybox is
|
||||||
return keyMap[algorithm]
|
// obvious from logcat alone -- Google's CRL and Duck's "mass abuse" check both match
|
||||||
|
// by certificate serial (lowercase hex). Logged here rather than at parse time because
|
||||||
|
// the parse is cached and would emit at most once per boot.
|
||||||
|
val serials =
|
||||||
|
keyBox.certificates.joinToString(", ") { cert ->
|
||||||
|
(cert as? X509Certificate)?.serialNumber?.toString(16) ?: "?"
|
||||||
|
}
|
||||||
|
SystemLogger.info(
|
||||||
|
"Using $algorithm keybox $keyStoreFileName; attestation cert serials (hex): $serials"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return keyBox
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves any usable attestation key from a key store file, preferring EC.
|
||||||
|
*
|
||||||
|
* EC is the modern device-attestation key type and validly signs a leaf carrying either an EC
|
||||||
|
* or an RSA subject key. This is the fail-safe used when no algorithm-matching key exists, so
|
||||||
|
* patching can still re-root the chain under the keybox instead of leaking the device's real
|
||||||
|
* attestation.
|
||||||
|
*
|
||||||
|
* @param keyStoreFileName The name of the XML file (e.g., "keybox.xml").
|
||||||
|
* @return The preferred [KeyBox], or `null` if the file contains no usable key.
|
||||||
|
*/
|
||||||
|
fun getAnyAttestationKey(keyStoreFileName: String): KeyBox? {
|
||||||
|
val keyMap =
|
||||||
|
keyStoreCache.getOrPut(keyStoreFileName) { parseKeyStoreFile(keyStoreFileName) }
|
||||||
|
return keyMap[KeyProperties.KEY_ALGORITHM_EC] ?: keyMap.values.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ data class CertGenConfig(
|
|||||||
val callerNonce: Boolean = false,
|
val callerNonce: Boolean = false,
|
||||||
val unlockedDeviceRequired: Boolean = false,
|
val unlockedDeviceRequired: Boolean = false,
|
||||||
val noAuthRequired: Boolean = true,
|
val noAuthRequired: Boolean = true,
|
||||||
|
// Diagnostic plane: the calling app UID keys the native log lines, and debugLogging mirrors the
|
||||||
|
// APK debug variant so the native extension dump is silent in release.
|
||||||
|
val uid: Int,
|
||||||
|
val debugLogging: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
object NativeCertGen {
|
object NativeCertGen {
|
||||||
@@ -69,7 +73,10 @@ object NativeCertGen {
|
|||||||
isAvailable = true
|
isAvailable = true
|
||||||
SystemLogger.info("NativeCertGen: loaded libcertgen.so successfully")
|
SystemLogger.info("NativeCertGen: loaded libcertgen.so successfully")
|
||||||
} catch (e: UnsatisfiedLinkError) {
|
} catch (e: UnsatisfiedLinkError) {
|
||||||
SystemLogger.error("NativeCertGen: failed to load libcertgen.so, falling back to BouncyCastle", e)
|
SystemLogger.error(
|
||||||
|
"NativeCertGen: failed to load libcertgen.so, falling back to BouncyCastle",
|
||||||
|
e,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,10 +84,6 @@ object NativeCertGen {
|
|||||||
|
|
||||||
private external fun initLogging(verbose: Boolean, logDir: String): Boolean
|
private external fun initLogging(verbose: Boolean, logDir: String): Boolean
|
||||||
|
|
||||||
private external fun dumpLogs(): String?
|
|
||||||
|
|
||||||
fun dump(): String? = if (isAvailable) dumpLogs() else null
|
|
||||||
|
|
||||||
fun parseNativeResult(bytes: ByteArray): Pair<KeyPair, List<Certificate>> {
|
fun parseNativeResult(bytes: ByteArray): Pair<KeyPair, List<Certificate>> {
|
||||||
val buf = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN)
|
val buf = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN)
|
||||||
|
|
||||||
@@ -111,11 +114,13 @@ object NativeCertGen {
|
|||||||
throw IllegalStateException("No certificates in native result")
|
throw IllegalStateException("No certificates in native result")
|
||||||
}
|
}
|
||||||
|
|
||||||
val algorithmName = when (certs[0].publicKey.algorithm) {
|
val algorithmName =
|
||||||
"EC", "ECDSA" -> "EC"
|
when (certs[0].publicKey.algorithm) {
|
||||||
"RSA" -> "RSA"
|
"EC",
|
||||||
else -> certs[0].publicKey.algorithm
|
"ECDSA" -> "EC"
|
||||||
}
|
"RSA" -> "RSA"
|
||||||
|
else -> certs[0].publicKey.algorithm
|
||||||
|
}
|
||||||
val keyFactory = KeyFactory.getInstance(algorithmName)
|
val keyFactory = KeyFactory.getInstance(algorithmName)
|
||||||
val privateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(pkBytes))
|
val privateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(pkBytes))
|
||||||
val publicKey = certs[0].publicKey
|
val publicKey = certs[0].publicKey
|
||||||
|
|||||||
@@ -8,13 +8,16 @@ import java.io.FileInputStream
|
|||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
import org.bouncycastle.asn1.ASN1EncodableVector
|
import org.bouncycastle.asn1.ASN1EncodableVector
|
||||||
import org.bouncycastle.asn1.ASN1Integer
|
import org.bouncycastle.asn1.ASN1Integer
|
||||||
import org.bouncycastle.asn1.DEROctetString
|
import org.bouncycastle.asn1.DEROctetString
|
||||||
import org.bouncycastle.asn1.DERSequence
|
import org.bouncycastle.asn1.DERSequence
|
||||||
import org.matrix.TEESimulator.attestation.DeviceAttestationService
|
import org.matrix.TEESimulator.attestation.DeviceAttestationService
|
||||||
|
import org.matrix.TEESimulator.config.BootStateManager
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides utility functions for accessing Android system properties and device-specific
|
* Provides utility functions for accessing Android system properties and device-specific
|
||||||
@@ -43,6 +46,7 @@ object AndroidDeviceUtils {
|
|||||||
DeviceAttestationService.CachedAttestationData?.verifiedBootKey
|
DeviceAttestationService.CachedAttestationData?.verifiedBootKey
|
||||||
},
|
},
|
||||||
expectedSize = 32,
|
expectedSize = 32,
|
||||||
|
recordSource = { bootKeySource = it },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,9 +64,16 @@ object AndroidDeviceUtils {
|
|||||||
DeviceAttestationService.CachedAttestationData?.verifiedBootHash
|
DeviceAttestationService.CachedAttestationData?.verifiedBootHash
|
||||||
},
|
},
|
||||||
expectedSize = 32,
|
expectedSize = 32,
|
||||||
|
recordSource = { bootHashSource = it },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Records which fallback tier supplied bootKey/bootHash so the diagnostic dossier can flag a
|
||||||
|
// random-fallback value — a real verifiedBootKey that resolves to random bytes is a textbook
|
||||||
|
// simulated-TEE tell. Populated by initializeBootProperty on first access.
|
||||||
|
@Volatile private var bootKeySource: String = "uninitialized"
|
||||||
|
@Volatile private var bootHashSource: String = "uninitialized"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public function to explicitly trigger the initialization of the boot key and hash. Accessing
|
* Public function to explicitly trigger the initialization of the boot key and hash. Accessing
|
||||||
* these properties here ensures they are set up before they might be needed elsewhere.
|
* these properties here ensures they are set up before they might be needed elsewhere.
|
||||||
@@ -89,9 +100,11 @@ object AndroidDeviceUtils {
|
|||||||
propertyName: String,
|
propertyName: String,
|
||||||
attestationValueProvider: () -> ByteArray?,
|
attestationValueProvider: () -> ByteArray?,
|
||||||
expectedSize: Int,
|
expectedSize: Int,
|
||||||
|
recordSource: (String) -> Unit,
|
||||||
): ByteArray {
|
): ByteArray {
|
||||||
getProperty(propertyName, expectedSize)?.let {
|
getProperty(propertyName, expectedSize)?.let {
|
||||||
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
|
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
|
||||||
|
recordSource("system-prop")
|
||||||
persistToFile(propertyName, it)
|
persistToFile(propertyName, it)
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
@@ -99,7 +112,8 @@ object AndroidDeviceUtils {
|
|||||||
try {
|
try {
|
||||||
attestationValueProvider()?.let {
|
attestationValueProvider()?.let {
|
||||||
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
|
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
|
||||||
setProperty(propertyName, it)
|
recordSource("tee-attestation")
|
||||||
|
setBootProperty(propertyName, it)
|
||||||
persistToFile(propertyName, it)
|
persistToFile(propertyName, it)
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
@@ -109,13 +123,15 @@ object AndroidDeviceUtils {
|
|||||||
|
|
||||||
readFromFile(propertyName, expectedSize)?.let {
|
readFromFile(propertyName, expectedSize)?.let {
|
||||||
SystemLogger.debug("Using $propertyName from persistent file: ${it.toHex()}")
|
SystemLogger.debug("Using $propertyName from persistent file: ${it.toHex()}")
|
||||||
setProperty(propertyName, it)
|
recordSource("persistent-file")
|
||||||
|
setBootProperty(propertyName, it)
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
|
|
||||||
return generateRandomBytes(expectedSize).also {
|
return generateRandomBytes(expectedSize).also {
|
||||||
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
|
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
|
||||||
setProperty(propertyName, it)
|
recordSource("random-fallback")
|
||||||
|
setBootProperty(propertyName, it)
|
||||||
persistToFile(propertyName, it)
|
persistToFile(propertyName, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,6 +179,14 @@ object AndroidDeviceUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setBootProperty(name: String, bytes: ByteArray) {
|
||||||
|
if (!BootStateManager.shouldSpoofBootProps()) {
|
||||||
|
SystemLogger.info("Skipping system property '$name' because boot prop spoofing is disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setProperty(name, bytes)
|
||||||
|
}
|
||||||
|
|
||||||
internal fun setProperty(name: String, value: String) {
|
internal fun setProperty(name: String, value: String) {
|
||||||
try {
|
try {
|
||||||
SystemLogger.debug("Setting system property '$name' to: $value")
|
SystemLogger.debug("Setting system property '$name' to: $value")
|
||||||
@@ -186,11 +210,12 @@ object AndroidDeviceUtils {
|
|||||||
|
|
||||||
private val PERSIST_DIR = File("/data/adb/tricky_store")
|
private val PERSIST_DIR = File("/data/adb/tricky_store")
|
||||||
|
|
||||||
private fun fileForProperty(propertyName: String): File = when (propertyName) {
|
private fun fileForProperty(propertyName: String): File =
|
||||||
"ro.boot.vbmeta.digest" -> File(PERSIST_DIR, "boot_hash.bin")
|
when (propertyName) {
|
||||||
"ro.boot.vbmeta.public_key_digest" -> File(PERSIST_DIR, "boot_key.bin")
|
"ro.boot.vbmeta.digest" -> File(PERSIST_DIR, "boot_hash.bin")
|
||||||
else -> File(PERSIST_DIR, "${propertyName.replace('.', '_')}.bin")
|
"ro.boot.vbmeta.public_key_digest" -> File(PERSIST_DIR, "boot_key.bin")
|
||||||
}
|
else -> File(PERSIST_DIR, "${propertyName.replace('.', '_')}.bin")
|
||||||
|
}
|
||||||
|
|
||||||
private fun persistToFile(propertyName: String, bytes: ByteArray) {
|
private fun persistToFile(propertyName: String, bytes: ByteArray) {
|
||||||
try {
|
try {
|
||||||
@@ -229,6 +254,23 @@ object AndroidDeviceUtils {
|
|||||||
return custom ?: getRealDevicePatchLevelInt("boot", isLong = true)
|
return custom ?: getRealDevicePatchLevelInt("boot", isLong = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summarises, for a targeted [uid], the device values that feed attestation and where each came
|
||||||
|
* from. This is what exposes attested-versus-live mismatches: a random-fallback verifiedBootKey,
|
||||||
|
* a patch level overridden away from the live prop, or an OS version pulled from a stale cache.
|
||||||
|
*/
|
||||||
|
fun describeSources(uid: Int): String {
|
||||||
|
val customPatchLevel = ConfigurationManager.getPatchLevelForUid(uid) != null
|
||||||
|
val osVersionSource =
|
||||||
|
if (DeviceAttestationService.CachedAttestationData?.osVersion != null) "cache" else "map"
|
||||||
|
return "osVersion=$osVersion(src=$osVersionSource) " +
|
||||||
|
"osPatch=${getPatchLevel(uid)} vendorPatch=${getVendorPatchLevelLong(uid)} " +
|
||||||
|
"bootPatch=${getBootPatchLevelLong(uid)} customPatchLevel=$customPatchLevel " +
|
||||||
|
"bootKey=${bootKey.toHex()}(src=$bootKeySource) " +
|
||||||
|
"bootHash=${bootHash.toHex()}(src=$bootHashSource) " +
|
||||||
|
"teeCacheData=${DeviceAttestationService.CachedAttestationData != null}"
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the definitive device patch level integer for a given component. This function
|
* Retrieves the definitive device patch level integer for a given component. This function
|
||||||
* encapsulates the entire fallback chain and guarantees a non-null return.
|
* encapsulates the entire fallback chain and guarantees a non-null return.
|
||||||
@@ -294,7 +336,10 @@ object AndroidDeviceUtils {
|
|||||||
// Resolve from live system prop — matches what detectors see via getprop,
|
// Resolve from live system prop — matches what detectors see via getprop,
|
||||||
// even when PIF has spoofed ro.build.version.security_patch via resetprop
|
// even when PIF has spoofed ro.build.version.security_patch via resetprop
|
||||||
resolvedValue.equals("prop", ignoreCase = true) ->
|
resolvedValue.equals("prop", ignoreCase = true) ->
|
||||||
parsePatchLevelValue(SystemProperties.get("ro.build.version.security_patch", ""), isLong)
|
parsePatchLevelValue(
|
||||||
|
SystemProperties.get("ro.build.version.security_patch", ""),
|
||||||
|
isLong,
|
||||||
|
)
|
||||||
resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
|
resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
|
||||||
else -> parsePatchLevelValue(resolvedValue, isLong)
|
else -> parsePatchLevelValue(resolvedValue, isLong)
|
||||||
}
|
}
|
||||||
@@ -394,36 +439,253 @@ object AndroidDeviceUtils {
|
|||||||
Build.VERSION_CODES.BAKLAVA to 400, // KeyMint 4.0
|
Build.VERSION_CODES.BAKLAVA to 400, // KeyMint 4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** AOSP-mandated attestation version for the running OS, or null when the SDK is unmapped. */
|
||||||
|
internal val aospAttestVersion: Int?
|
||||||
|
get() = attestVersionMap[Build.VERSION.SDK_INT]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the attestation version for the given security level. The value follows the device
|
* Retrieves the attestation version for the given security level. A readable KeyMint VINTF
|
||||||
* OS: cached attestation data wins, then attestVersionMap[SDK_INT], then 400 as last resort.
|
* declaration wins first, so local probes that compare the attested version against the
|
||||||
* A static StrongBox=300 floor would force a major-version mismatch with the TEE chain on
|
* device's manifest see a coherent pair. Otherwise the legacy chain applies: cached attestation
|
||||||
* Android 16 devices that report keymaster 400 across both security levels.
|
* data, then attestVersionMap[SDK_INT], then 400 as last resort.
|
||||||
*
|
*
|
||||||
* @param securityLevel The security level of the attestation (1 for TEE, 2 for StrongBox).
|
* @param securityLevel The security level of the attestation (1 for TEE, 2 for StrongBox).
|
||||||
* @return The appropriate attestation version number.
|
* @return The appropriate attestation version number.
|
||||||
*/
|
*/
|
||||||
fun getAttestVersion(securityLevel: Int): Int {
|
fun getAttestVersion(securityLevel: Int): Int {
|
||||||
val cached = DeviceAttestationService.CachedAttestationData?.attestVersion
|
vintfKeyMintVersion?.let { version ->
|
||||||
val version = cached
|
SystemLogger.debug(
|
||||||
?: attestVersionMap[Build.VERSION.SDK_INT]
|
"attestVersion=${version.attestationVersion} source=vintf securityLevel=$securityLevel"
|
||||||
?: 400 // Default to a recent version
|
)
|
||||||
val source = when {
|
return version.attestationVersion
|
||||||
cached != null -> "cache"
|
|
||||||
attestVersionMap.containsKey(Build.VERSION.SDK_INT) -> "map"
|
|
||||||
else -> "default"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val cached = DeviceAttestationService.CachedAttestationData?.attestVersion
|
||||||
|
val version =
|
||||||
|
cached ?: attestVersionMap[Build.VERSION.SDK_INT] ?: 400 // Default to a recent version
|
||||||
|
val source =
|
||||||
|
when {
|
||||||
|
cached != null -> "cache"
|
||||||
|
attestVersionMap.containsKey(Build.VERSION.SDK_INT) -> "map"
|
||||||
|
else -> "default"
|
||||||
|
}
|
||||||
SystemLogger.debug("attestVersion=$version source=$source securityLevel=$securityLevel")
|
SystemLogger.debug("attestVersion=$version source=$source securityLevel=$securityLevel")
|
||||||
|
SystemLogger.debug(
|
||||||
|
"vintf-version attest=$version keymaster=$version source=$source securityLevel=$securityLevel"
|
||||||
|
)
|
||||||
return version
|
return version
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the Keymaster/KeyMint version based on the attestation version.
|
* Retrieves the Keymaster/KeyMint version. A readable KeyMint VINTF declaration is
|
||||||
|
* authoritative because recent local detectors compare the attested version directly against
|
||||||
|
* the manifest declaration.
|
||||||
*
|
*
|
||||||
* @param securityLevel The security level, used to determine the correct attestation version.
|
* @param securityLevel The security level, used to determine the correct attestation version.
|
||||||
* @return The appropriate Keymaster or KeyMint version number.
|
* @return The appropriate Keymaster or KeyMint version number.
|
||||||
*/
|
*/
|
||||||
fun getKeymasterVersion(securityLevel: Int): Int = getAttestVersion(securityLevel)
|
fun getKeymasterVersion(securityLevel: Int): Int {
|
||||||
|
vintfKeyMintVersion?.let { version ->
|
||||||
|
SystemLogger.debug(
|
||||||
|
"keymasterVersion=${version.keymasterVersion} source=vintf securityLevel=$securityLevel"
|
||||||
|
)
|
||||||
|
return version.keymasterVersion
|
||||||
|
}
|
||||||
|
return getAttestVersion(securityLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyMint/Keymaster version pair resolved from a device VINTF manifest. [attestationVersion] is
|
||||||
|
* the value written into the attestation record; [keymasterVersion] is the HAL version field.
|
||||||
|
* They coincide for AIDL KeyMint and diverge only for legacy HIDL Keymaster.
|
||||||
|
*/
|
||||||
|
private data class VintfKeyMintVersion(
|
||||||
|
val attestationVersion: Int,
|
||||||
|
val keymasterVersion: Int,
|
||||||
|
val sourcePath: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyMint version derived from the device's VINTF manifests, or null when none is readable.
|
||||||
|
* Resolved lazily so the manifest scan happens once, off the attestation hot path.
|
||||||
|
*/
|
||||||
|
private val vintfKeyMintVersion: VintfKeyMintVersion? by lazy {
|
||||||
|
readVintfKeyMintVersion().also { version ->
|
||||||
|
if (version != null) {
|
||||||
|
SystemLogger.info(
|
||||||
|
"Using KeyMint version from VINTF: attestation=${version.attestationVersion}, " +
|
||||||
|
"keymaster=${version.keymasterVersion}, source=${version.sourcePath}"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SystemLogger.debug(
|
||||||
|
"No usable KeyMint VINTF declaration found; using attestation fallback"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readVintfKeyMintVersion(): VintfKeyMintVersion? {
|
||||||
|
val files = linkedMapOf<String, File>()
|
||||||
|
|
||||||
|
VINTF_MANIFEST_DIRS.forEach { path ->
|
||||||
|
val dir = File(path)
|
||||||
|
if (!dir.exists() || !dir.isDirectory) return@forEach
|
||||||
|
val listed =
|
||||||
|
runCatching {
|
||||||
|
dir.listFiles { file ->
|
||||||
|
file.isFile && file.name.endsWith(".xml", ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.getOrElse { throwable ->
|
||||||
|
SystemLogger.debug("Unable to list VINTF dir $path: ${throwable.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
listed?.forEach { file -> files[file.absolutePath] = file }
|
||||||
|
}
|
||||||
|
|
||||||
|
VINTF_MANIFEST_FILES.forEach { path ->
|
||||||
|
val file = File(path)
|
||||||
|
if (file.exists() && file.isFile) {
|
||||||
|
files[file.absolutePath] = file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files.values
|
||||||
|
.flatMap { file ->
|
||||||
|
runCatching { parseKeyMintVersions(file) }
|
||||||
|
.getOrElse { throwable ->
|
||||||
|
SystemLogger.debug(
|
||||||
|
"Unable to parse KeyMint VINTF ${file.absolutePath}: ${throwable.message}"
|
||||||
|
)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.maxByOrNull { it.attestationVersion }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseKeyMintVersions(file: File): List<VintfKeyMintVersion> {
|
||||||
|
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file)
|
||||||
|
val root = document.documentElement ?: return emptyList()
|
||||||
|
|
||||||
|
return directChildElements(root, "hal").flatMap { hal ->
|
||||||
|
val halName = directChildTexts(hal, "name").firstOrNull().orEmpty()
|
||||||
|
val versions = directChildTexts(hal, "version")
|
||||||
|
val fqnames = directChildTexts(hal, "fqname")
|
||||||
|
val interfaces =
|
||||||
|
directChildElements(hal, "interface").associate { interfaceElement ->
|
||||||
|
val name = directChildTexts(interfaceElement, "name").firstOrNull().orEmpty()
|
||||||
|
val instances = directChildTexts(interfaceElement, "instance").toSet()
|
||||||
|
name to instances
|
||||||
|
}
|
||||||
|
|
||||||
|
when (halName) {
|
||||||
|
KEYMINT_HAL_NAME ->
|
||||||
|
if (hasDefaultInstance(fqnames, interfaces, KEYMINT_INTERFACE_NAME)) {
|
||||||
|
versions.mapNotNull { version ->
|
||||||
|
version
|
||||||
|
.toIntOrNull()
|
||||||
|
?.takeIf { it > 0 }
|
||||||
|
?.let { aidlVersion ->
|
||||||
|
val attestationVersion = aidlVersion * 100
|
||||||
|
VintfKeyMintVersion(
|
||||||
|
attestationVersion = attestationVersion,
|
||||||
|
keymasterVersion = attestationVersion,
|
||||||
|
sourcePath = file.absolutePath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
KEYMASTER_HAL_NAME ->
|
||||||
|
if (hasDefaultInstance(fqnames, interfaces, KEYMASTER_INTERFACE_NAME)) {
|
||||||
|
(versions.flatMap(::expandHidlVersions) +
|
||||||
|
fqnames.mapNotNull(::versionFromFqname))
|
||||||
|
.distinct()
|
||||||
|
.mapNotNull { version ->
|
||||||
|
expectedLegacyVersions(version)?.let { expected ->
|
||||||
|
VintfKeyMintVersion(
|
||||||
|
attestationVersion = expected.second,
|
||||||
|
keymasterVersion = expected.first,
|
||||||
|
sourcePath = file.absolutePath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun directChildElements(parent: Element, tagName: String): List<Element> = buildList {
|
||||||
|
val children = parent.childNodes
|
||||||
|
for (index in 0 until children.length) {
|
||||||
|
val child = children.item(index)
|
||||||
|
if (child is Element && child.tagName == tagName) {
|
||||||
|
add(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun directChildTexts(parent: Element, tagName: String): List<String> =
|
||||||
|
directChildElements(parent, tagName)
|
||||||
|
.map { it.textContent.trim() }
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
|
|
||||||
|
private fun hasDefaultInstance(
|
||||||
|
fqnames: List<String>,
|
||||||
|
interfaces: Map<String, Set<String>>,
|
||||||
|
interfaceName: String,
|
||||||
|
): Boolean =
|
||||||
|
fqnames.any { fqname ->
|
||||||
|
fqname.substringAfter("::", fqname).substringBefore("/") == interfaceName &&
|
||||||
|
fqname.substringAfter("/", "") == DEFAULT_INSTANCE
|
||||||
|
} || interfaces[interfaceName]?.contains(DEFAULT_INSTANCE) == true
|
||||||
|
|
||||||
|
private fun versionFromFqname(fqname: String): String? =
|
||||||
|
FQNAME_VERSION_REGEX.find(fqname)?.groupValues?.getOrNull(1)
|
||||||
|
|
||||||
|
private fun expectedLegacyVersions(version: String): Pair<Int, Int>? =
|
||||||
|
when (version) {
|
||||||
|
"3.0" -> 3 to 2
|
||||||
|
"4.0" -> 4 to 3
|
||||||
|
"4.1" -> 41 to 4
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun expandHidlVersions(version: String): List<String> {
|
||||||
|
val range = HIDL_VERSION_RANGE_REGEX.matchEntire(version) ?: return listOf(version)
|
||||||
|
val major = range.groupValues[1]
|
||||||
|
val firstMinor = range.groupValues[2].toInt()
|
||||||
|
val lastMinor = range.groupValues[3].toInt()
|
||||||
|
return (firstMinor..lastMinor).map { minor -> "$major.$minor" }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val VINTF_MANIFEST_DIRS =
|
||||||
|
listOf(
|
||||||
|
"/system/etc/vintf/manifest",
|
||||||
|
"/system_ext/etc/vintf/manifest",
|
||||||
|
"/product/etc/vintf/manifest",
|
||||||
|
"/vendor/etc/vintf/manifest",
|
||||||
|
"/odm/etc/vintf/manifest",
|
||||||
|
)
|
||||||
|
private val VINTF_MANIFEST_FILES =
|
||||||
|
listOf(
|
||||||
|
"/system/etc/vintf/manifest.xml",
|
||||||
|
"/system_ext/etc/vintf/manifest.xml",
|
||||||
|
"/product/etc/vintf/manifest.xml",
|
||||||
|
"/vendor/etc/vintf/manifest.xml",
|
||||||
|
"/odm/etc/vintf/manifest.xml",
|
||||||
|
)
|
||||||
|
private const val KEYMINT_HAL_NAME = "android.hardware.security.keymint"
|
||||||
|
private const val KEYMASTER_HAL_NAME = "android.hardware.keymaster"
|
||||||
|
private const val KEYMINT_INTERFACE_NAME = "IKeyMintDevice"
|
||||||
|
private const val KEYMASTER_INTERFACE_NAME = "IKeymasterDevice"
|
||||||
|
private const val DEFAULT_INSTANCE = "default"
|
||||||
|
private val FQNAME_VERSION_REGEX = Regex("^@([0-9]+(?:\\.[0-9]+)?)::")
|
||||||
|
private val HIDL_VERSION_RANGE_REGEX = Regex("^([0-9]+)\\.([0-9]+)-([0-9]+)$")
|
||||||
|
|
||||||
// --- APEX and Module Hash Properties ---
|
// --- APEX and Module Hash Properties ---
|
||||||
|
|
||||||
@@ -518,11 +780,12 @@ object AndroidDeviceUtils {
|
|||||||
|
|
||||||
val moduleHash: ByteArray by lazy {
|
val moduleHash: ByteArray by lazy {
|
||||||
DeviceAttestationService.CachedAttestationData?.moduleHash
|
DeviceAttestationService.CachedAttestationData?.moduleHash
|
||||||
|
?.also { SystemLogger.debug { "module-hash source=cache hash=${it.toHex().take(8)}" } }
|
||||||
|
?: supplementaryModuleHash()?.also {
|
||||||
|
SystemLogger.debug { "module-hash source=framework-api hash=${it.toHex().take(8)}" }
|
||||||
|
}
|
||||||
?: runCatching {
|
?: runCatching {
|
||||||
data class ModuleEntry(
|
data class ModuleEntry(val nameEncoded: ByteArray, val fullEncoded: ByteArray)
|
||||||
val nameEncoded: ByteArray,
|
|
||||||
val fullEncoded: ByteArray,
|
|
||||||
)
|
|
||||||
|
|
||||||
val modules =
|
val modules =
|
||||||
apexInfos.map { (packageName, versionCode) ->
|
apexInfos.map { (packageName, versionCode) ->
|
||||||
@@ -555,12 +818,38 @@ object AndroidDeviceUtils {
|
|||||||
|
|
||||||
MessageDigest.getInstance("SHA-256").digest(finalDerSet)
|
MessageDigest.getInstance("SHA-256").digest(finalDerSet)
|
||||||
}
|
}
|
||||||
|
.onSuccess {
|
||||||
|
SystemLogger.debug { "module-hash source=rederive hash=${it.toHex().take(8)}" }
|
||||||
|
}
|
||||||
|
.onFailure { SystemLogger.debug { "module-hash source=zero hash=00000000" } }
|
||||||
.getOrElse {
|
.getOrElse {
|
||||||
SystemLogger.error("Failed to compute module hash.", it)
|
SystemLogger.error("Failed to compute module hash.", it)
|
||||||
ByteArray(32)
|
ByteArray(32)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the module-hash DER pre-image straight from the framework's own KeyStoreManager and
|
||||||
|
* SHA-256's it, so the value byte-matches what a verifier derives from the same
|
||||||
|
* getSupplementaryAttestationInfo call. Returns null on any failure (e.g. the API is
|
||||||
|
* unreachable from this process) so the caller falls back to local re-derivation.
|
||||||
|
*/
|
||||||
|
private fun supplementaryModuleHash(): ByteArray? =
|
||||||
|
runCatching {
|
||||||
|
// @SystemApi surface added in Android 16, absent from the compile SDK, so reflect.
|
||||||
|
val managerClass = Class.forName("android.security.keystore.KeyStoreManager")
|
||||||
|
val manager = managerClass.getMethod("getInstance").invoke(null)
|
||||||
|
// MODULE_HASH is the KeyMint tag (TagType.BYTES | 724 = 0x900002D4), read from the
|
||||||
|
// framework so it matches the verifier's argument exactly.
|
||||||
|
val moduleHashTag = managerClass.getField("MODULE_HASH").getInt(null)
|
||||||
|
val derPreImage =
|
||||||
|
managerClass
|
||||||
|
.getMethod("getSupplementaryAttestationInfo", Int::class.java)
|
||||||
|
.invoke(manager, moduleHashTag) as ByteArray
|
||||||
|
MessageDigest.getInstance("SHA-256").digest(derPreImage)
|
||||||
|
}
|
||||||
|
.getOrNull()
|
||||||
|
|
||||||
private fun compareByteArrays(a: ByteArray, b: ByteArray): Int {
|
private fun compareByteArrays(a: ByteArray, b: ByteArray): Int {
|
||||||
val length = minOf(a.size, b.size)
|
val length = minOf(a.size, b.size)
|
||||||
for (i in 0 until length) {
|
for (i in 0 until length) {
|
||||||
|
|||||||
@@ -14,12 +14,15 @@ object AndroidPermissionUtils {
|
|||||||
val activityThreadClass = Class.forName("android.app.ActivityThread")
|
val activityThreadClass = Class.forName("android.app.ActivityThread")
|
||||||
|
|
||||||
// 2. Invoke the static currentActivityThread() method
|
// 2. Invoke the static currentActivityThread() method
|
||||||
val currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread")
|
val currentActivityThreadMethod =
|
||||||
|
activityThreadClass.getDeclaredMethod("currentActivityThread")
|
||||||
currentActivityThreadMethod.isAccessible = true
|
currentActivityThreadMethod.isAccessible = true
|
||||||
val activityThread = currentActivityThreadMethod.invoke(null)
|
val activityThread = currentActivityThreadMethod.invoke(null)
|
||||||
|
|
||||||
if (activityThread == null) {
|
if (activityThread == null) {
|
||||||
SystemLogger.warning("Reflection: ActivityThread.currentActivityThread() returned null")
|
SystemLogger.warning(
|
||||||
|
"Reflection: ActivityThread.currentActivityThread() returned null"
|
||||||
|
)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,25 +33,27 @@ object AndroidPermissionUtils {
|
|||||||
|
|
||||||
if (application != null) return application
|
if (application != null) return application
|
||||||
|
|
||||||
// 4. Fallback to getSystemContext() if application is null (often happens in system_server)
|
// 4. Fallback to getSystemContext() if application is null (often happens in
|
||||||
|
// system_server)
|
||||||
val getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext")
|
val getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext")
|
||||||
getSystemContextMethod.isAccessible = true
|
getSystemContextMethod.isAccessible = true
|
||||||
getSystemContextMethod.invoke(activityThread) as? Context
|
getSystemContextMethod.invoke(activityThread) as? Context
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("Reflection failed to get global context for permission check", e)
|
SystemLogger.error("Reflection failed to get global context for permission check", e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Core permission check. */
|
||||||
* Core permission check.
|
|
||||||
*/
|
|
||||||
fun hasPermission(uid: Int, permission: String): Boolean {
|
fun hasPermission(uid: Int, permission: String): Boolean {
|
||||||
val context = getGlobalContext() ?: run {
|
val context =
|
||||||
SystemLogger.warning("AndroidPermissionUtils: Context is null, failing permission check safely.")
|
getGlobalContext()
|
||||||
return false
|
?: run {
|
||||||
}
|
SystemLogger.warning(
|
||||||
|
"AndroidPermissionUtils: Context is null, failing permission check safely."
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
val result = context.checkPermission(permission, -1, uid)
|
val result = context.checkPermission(permission, -1, uid)
|
||||||
return result == PackageManager.PERMISSION_GRANTED
|
return result == PackageManager.PERMISSION_GRANTED
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ package org.matrix.TEESimulator.util
|
|||||||
* @return A new string with each line individually trimmed.
|
* @return A new string with each line individually trimmed.
|
||||||
*/
|
*/
|
||||||
fun String.trimLines(): String =
|
fun String.trimLines(): String =
|
||||||
this.trim()
|
this.trim().lines().filter { !it.trim().startsWith("<!--") }.joinToString("\n") { it.trim() }
|
||||||
.lines()
|
|
||||||
.filter { !it.trim().startsWith("<!--") }
|
|
||||||
.joinToString("\n") { it.trim() }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a ByteArray to its hexadecimal string representation.
|
* Converts a ByteArray to its hexadecimal string representation.
|
||||||
|
|||||||
+25
-11
@@ -4,17 +4,6 @@ CONFIG_DIR=/data/adb/tricky_store
|
|||||||
|
|
||||||
. "$MODDIR/action_i18n.sh"
|
. "$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() {
|
confirm() {
|
||||||
# Sample getevent in 1s bursts; a piped stream block-buffers and misses
|
# Sample getevent in 1s bursts; a piped stream block-buffers and misses
|
||||||
# a single key-press before the timeout.
|
# a single key-press before the timeout.
|
||||||
@@ -29,6 +18,31 @@ confirm() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Debug builds ship diag.sh, adding a one-tap log export before the destructive clear-keys action.
|
||||||
|
if [ -f "$MODDIR/diag.sh" ]; then
|
||||||
|
. "$MODDIR/diag.sh"
|
||||||
|
echo " "
|
||||||
|
echo " 📦 Export diagnostic logs to /sdcard/Download?"
|
||||||
|
echo " 🔊 Vol-Up = export logs"
|
||||||
|
echo " 🔉 Vol-Down = skip to clear keys"
|
||||||
|
echo " "
|
||||||
|
if confirm; then
|
||||||
|
diag_export
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo " ⚠️ $(_msg confirm_header)"
|
||||||
|
echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo " "
|
||||||
|
echo " $(_msg confirm_warning_1)"
|
||||||
|
echo " $(_msg confirm_warning_2)"
|
||||||
|
echo " "
|
||||||
|
echo " 🔊 $(_msg confirm_vol_up)"
|
||||||
|
echo " 🔉 $(_msg confirm_vol_down)"
|
||||||
|
echo " "
|
||||||
|
|
||||||
if ! confirm; then
|
if ! confirm; then
|
||||||
echo " "
|
echo " "
|
||||||
echo " ❌ $(_msg confirm_cancelled)"
|
echo " ❌ $(_msg confirm_cancelled)"
|
||||||
|
|||||||
+124
-46
@@ -1,3 +1,81 @@
|
|||||||
|
> [!NOTE]
|
||||||
|
> The project is going through a heavy refactor at the moment, so public commits may lag behind for a while.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v6.0.1-307
|
||||||
|
|
||||||
|
Fixes five gaps in the module's TEE key-operation and attestation emulation. Two of them fix crashes in real app crypto on a broken-TEE device: any app using an AndroidKeyStore HMAC key or an RSA-OAEP-SHA256 key was throwing. This is a beta; the confirmation logs are in the debug build only, and nothing is field-verified yet.
|
||||||
|
|
||||||
|
### App crypto correctness
|
||||||
|
- HMAC operations now run instead of throwing. An AndroidKeyStore HMAC key is symmetric, so an HMAC SIGN fell into the asymmetric SIGN path and failed on a null key pair, and no MAC primitive existed. SIGN and VERIFY now work: the tag is computed with Mac (HmacSHA256/384/512), truncated to the requested MAC_LENGTH (full digest when unspecified), and checked with a constant-time compare.
|
||||||
|
- RSA-OAEP-SHA256 decrypt no longer fails with BadPaddingException. The cipher ran with no OAEPParameterSpec, so JCA fell back to SHA-1 and rejected SHA-256 ciphertext. It now applies the correct main and MGF1 digests. A key that authorizes several MGF1 digests uses the one the operation requested, not the key's first.
|
||||||
|
- Grant-domain attestation keys now resolve. A self-granted PURPOSE_ATTEST_KEY (a Domain.GRANT descriptor) could not resolve its signer alias, so generateKey failed and the subject key was never stored, returning KEY_NOT_FOUND on readback. The grant now resolves to the owner key's alias, and the subject key stays readable under Domain.APP.
|
||||||
|
|
||||||
|
### Supplementary attestation
|
||||||
|
- MODULE_HASH now comes from the framework's own getSupplementaryAttestationInfo, so it matches the value a verifier computes. It falls back to local re-derivation when that API is unreachable.
|
||||||
|
|
||||||
|
### Diagnostics (debug builds only)
|
||||||
|
- New per-operation (oaep-op, hmac-op, attest-grant) and device-level (module-hash, vintf-version) source logs. R8 strips them from release builds.
|
||||||
|
|
||||||
|
### 中文说明
|
||||||
|
|
||||||
|
修复本模块在 TEE 密钥操作与证明模拟中的五处缺陷。其中两处修复的是真实应用在 TEE 损坏设备上的加密崩溃:任何使用 AndroidKeyStore HMAC 密钥或 RSA-OAEP-SHA256 密钥的应用此前都会抛出异常。本版本为测试版;确认日志仅存在于 debug 构建中,且尚未经过真机验证。
|
||||||
|
|
||||||
|
**应用加密正确性**
|
||||||
|
- HMAC 操作现在可正常执行,不再抛出异常。AndroidKeyStore 的 HMAC 密钥是对称密钥,因此 HMAC SIGN 此前落入了非对称的 SIGN 分支,并因 key pair 为空而失败,当时也没有 MAC 原语。现在 SIGN 与 VERIFY 均可工作:用 Mac (HmacSHA256/384/512) 计算标签,按请求的 MAC_LENGTH 截断(未指定时取完整摘要长度),并用恒定时间比较进行校验。
|
||||||
|
- RSA-OAEP-SHA256 解密不再抛出 BadPaddingException。此前 cipher 未传入 OAEPParameterSpec,JCA 因而回退到 SHA-1 并拒绝 SHA-256 密文。现在会应用正确的主摘要与 MGF1 摘要。若密钥授权了多个 MGF1 摘要,将使用本次操作请求的那个,而非密钥的第一个。
|
||||||
|
- Grant 域证明密钥现在可以解析。自授权的 PURPOSE_ATTEST_KEY(Domain.GRANT 描述符)此前无法解析其签名者别名,导致 generateKey 失败且从不存储主体密钥,读取时返回 KEY_NOT_FOUND。现在该 grant 会解析为属主密钥的别名,主体密钥在 Domain.APP 下仍可读取。
|
||||||
|
|
||||||
|
**补充证明**
|
||||||
|
- MODULE_HASH 现在取自框架自带的 getSupplementaryAttestationInfo,因而与验证方计算出的值一致。当该 API 不可用时,回退到本地重新推导。
|
||||||
|
|
||||||
|
**诊断(仅 debug 构建)**
|
||||||
|
- 新增按操作 (oaep-op、hmac-op、attest-grant) 与设备级 (module-hash、vintf-version) 的来源日志。R8 会在 release 构建中将其剥离。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v6.0.1-282
|
||||||
|
|
||||||
|
AUTO-mode key attestation now forges plain attestation from the keybox instead of deferring to the real TEE.
|
||||||
|
|
||||||
|
### Detection coverage
|
||||||
|
- AUTO dispatch probed the device with `checkTeeFunctionality`, which only proves the TEE can mint one EC key. It says nothing about RSA attestation, device-ID attestation, or whether a patched chain survives RSA verify. Plain attestation requests (attest-key OFF, challenge present) were routed to PATCH and deferred to hardware, so devices that can't back that surfaced KeyAttestation reds: `ATTESTATION_KEYS_NOT_PROVISIONED` (-49) and `BLOCK_TYPE_IS_NOT_01`.
|
||||||
|
- AUTO targets carrying an attestation challenge now take the FORGE path, the same one attest-key-ON already used: a synthetic chain built from the keybox and rooted under the Google root key. Requests with no challenge still pass through to real hardware, so KeyDetector's hardware-backed checks are unaffected.
|
||||||
|
|
||||||
|
### Verified
|
||||||
|
- Offline conformance against real FORGE captures: uid10389 and uid10154 chains are GREEN; the root SPKI byte-matches `GOOGLE_ROOT_PUBLIC_KEY`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v6.0.1-280
|
||||||
|
|
||||||
|
Clears the Duck Detector generate-mode parcel fingerprint that real Android 16 hardware also trips, fixes RSA attestation under an EC-only keybox, and restores device-property attestation for Play Integrity hardware apps such as BHIM and UPI. Generate-mode fix field-confirmed on Android 16.
|
||||||
|
|
||||||
|
### Detection coverage
|
||||||
|
- Generate-mode fingerprint: Duck reads the reply at a flat 12-byte stride and flags the sentinel tuple at positions 12 and 13 that the device's native ALGORITHM-first authorization order lands on. Real A16 silicon trips the same probe, so faithful mirroring stayed flagged. `normalizeAuthorizationLayout` marshals the auth array, runs Duck's exact predicate, and applies a minimal deterministic reorder only when it would match. Count, values, security levels, and the cert chain are untouched, and the reorder keys on the byte condition, never on a package. Applied on both the patch and forge reply paths. (#33)
|
||||||
|
- `updateAad` on a non-AEAD operation now answers per vendor: Samsung and Xiaomi-MTK TEEs return success, others return INVALID_TAG, matching Duck's OperationErrorPathProbe on both the sign/verify and cipher paths.
|
||||||
|
|
||||||
|
### Attestation correctness (Android 16, EC and RSA)
|
||||||
|
- RSA leaf under an EC-only keybox: patching used to catch the no-RSA-key throw and return the chain untouched, leaking the device's real unlocked Root of Trust for RSA keys while EC keys patched cleanly. It now falls back to any keybox key (EC preferred) and signs the patched leaf with the keybox key's own algorithm, so the RSA leaf re-roots to the Google keybox under a forged locked RoT.
|
||||||
|
- RSA attest-key forge on an EC-only keybox: the forge path matched the algorithm exactly and threw -75 ATTESTATION_KEYS_NOT_PROVISIONED on a miss, so an RSA ATTEST_KEY request never rooted and verifiers reported an unknown certificate. It now falls back to any attestation key, since an EC key validly ECDSA-signs an RSA-subject leaf. No-op on a dual keybox.
|
||||||
|
- A16 attestVersion: the device's KeyMint reports version 100 and the lazy cache shadowed the BAKLAVA-to-400 map, so the forge presented 100. It now caches the AOSP value per SDK and presents the correct 400.
|
||||||
|
- Algorithm-split key on restore: a persisted record holding an EC private key under an RSA leaf failed every signature as DATA_TOO_LARGE_FOR_MODULUS. Restore now drops the record when the private key and served leaf disagree, so the next generateKey rebuilds a coherent key.
|
||||||
|
- Stale chain on regenerate: reusing an alias in generateKey now evicts the cached chain, matching keystore2, so getKeyEntry serves the current key instead of a stale forge from an earlier generation.
|
||||||
|
|
||||||
|
### App compatibility
|
||||||
|
- Device-property attestation (BRAND, MODEL, and the rest) now forges unconditionally. The old gate probed the live TEE, which is dead on every device the module serves, so it rejected GMS Play Integrity's hardware path and broke BHIM and other UPI and Play-Integrity apps. Device-ID attestation (IMEI, serial) stays governed by the real KeyMint caller-permission rule: privileged callers get it, ordinary apps do not.
|
||||||
|
- getKeyEntry now reaches the owned-key lookup for skipped privileged UIDs, so framework attestKeyAlias resolution no longer returns "Invalid attestKeyAlias" for Key Attestation over Shizuku. Non-owned keys still skip post-processing, so a real app's key is never patched.
|
||||||
|
- Device-ID attestation over Shizuku (a privileged UID absent from target.txt) now takes the forge path instead of hitting the real TEE's CANNOT_ATTEST_IDS (-66). "Use attest key" no longer double-roots: a reused persistent attest key is resolved by KEY_ID as well as alias, and an unresolved designated attest key refuses to emit a leaf rather than silently re-rooting under the keybox.
|
||||||
|
|
||||||
|
### Diagnostics (debug builds only)
|
||||||
|
- Per-UID attestation dossier for targeted UIDs at /data/local/tmp/teesim/, recording the decoded chain on both forge and patch paths, key params, the keybox pick (including EC fail-safe), prop sources, forge failures, the emitted authorization shape, and served-versus-verified chains. Release builds strip this through R8 and stay silent. Keybox certificate serials log on every fetch for revocation triage.
|
||||||
|
|
||||||
|
### Verified
|
||||||
|
- Android 16: generate-mode fingerprint signal gone, confirmed on device 2026-06-19.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## TEESimulator-RS v6.0.1-251
|
## 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.
|
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.
|
||||||
@@ -5,10 +83,10 @@
|
|||||||
### Detection coverage
|
### Detection coverage
|
||||||
- Grant plane virtualized: owner read and cross-app `Domain.GRANT` read return one identical chain. 6 RED rows cleared. (28 → 18)
|
- 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)
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- Device-ID attestation mirrors the real TEE: returns CANNOT_ATTEST_IDS where silicon can't attest, instead of forging it.
|
||||||
|
|
||||||
### App compatibility
|
### App compatibility
|
||||||
@@ -16,8 +94,8 @@
|
|||||||
- Fingerprint / vendor keys: KEY_ID miss skips the post-handler, so real HAL operations are no longer wrapped and broken. (PR #26)
|
- Fingerprint / vendor keys: KEY_ID miss skips the post-handler, so real HAL operations are no longer wrapped and broken. (PR #26)
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- PatchLevelManager — auto-resolved the security-patch date from an installed PlayIntegrityFix module (with hot-reload) and applied it to props.
|
- 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.
|
- BulletinPoller, scheduled security-bulletin refresh.
|
||||||
|
|
||||||
### Other
|
### Other
|
||||||
- Release builds purge stale `teesim-*.bin` diagnostics from `/data/local/tmp` at boot.
|
- Release builds purge stale `teesim-*.bin` diagnostics from `/data/local/tmp` at boot.
|
||||||
@@ -25,7 +103,7 @@
|
|||||||
|
|
||||||
### Verified
|
### Verified
|
||||||
- SDK 35, Xiaomi 23106RN0DA: tamper 28 → 8; generate-mode signal gone; 4 grant rows UNAVAILABLE (correct for Android 15); no regressions.
|
- 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.
|
- Android 16 grant fix built but unconfirmed on SDK 36, needs an affected OnePlus user to confirm the grant rows clear.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -176,7 +254,7 @@ Major release integrating 30+ AOSP compliance improvements from upstream PR #157
|
|||||||
|
|
||||||
## TEESimulator-RS v4.8.1: StrongBox Op Rejection Fix
|
## 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.
|
- **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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -184,13 +262,13 @@ Major release integrating 30+ AOSP compliance improvements from upstream PR #157
|
|||||||
|
|
||||||
Tested against DuckDetector on OnePlus (Android 16, KSU). Tamper score dropped from 32 to 8.
|
Tested against DuckDetector on OnePlus (Android 16, KSU). Tamper score dropped from 32 to 8.
|
||||||
|
|
||||||
- **LRU operation pruning** — Concurrent software operations capped at 15 per UID (TEE) and 4 per UID (StrongBox), with oldest-first eviction. Pruned operations return `INVALID_OPERATION_HANDLE (-28)`, matching AOSP keystore2 malus-based pruning.
|
- **LRU operation pruning**, Concurrent software operations capped at 15 per UID (TEE) and 4 per UID (StrongBox), with oldest-first eviction. Pruned operations return `INVALID_OPERATION_HANDLE (-28)`, matching AOSP keystore2 malus-based pruning.
|
||||||
- **StrongBox param guard** — Unsupported StrongBox params (RSA >2048-bit, non-P256 EC curves) forwarded to real HAL for proper rejection instead of generating in software.
|
- **StrongBox param guard**, Unsupported StrongBox params (RSA >2048-bit, non-P256 EC curves) forwarded to real HAL for proper rejection instead of generating in software.
|
||||||
- **StrongBox timing** — Key generation floors at 250ms, signing at 80ms on StrongBox security level to match real secure element latency.
|
- **StrongBox timing**, Key generation floors at 250ms, signing at 80ms on StrongBox security level to match real secure element latency.
|
||||||
- **StrongBox op limit** — Sliding-window enforcer caps concurrent StrongBox operations for both software and hardware key paths, returning `TOO_MANY_OPERATIONS (-29)` when exceeded.
|
- **StrongBox op limit**, Sliding-window enforcer caps concurrent StrongBox operations for both software and hardware key paths, returning `TOO_MANY_OPERATIONS (-29)` when exceeded.
|
||||||
- **ECDSA algorithm alias** — Accept "ECDSA" in addition to "EC" as JCA private key algorithm name. Fixes SIGSEGV crash on Android 10 devices where the provider reports EC keys as "ECDSA". Closes #4.
|
- **ECDSA algorithm alias**, Accept "ECDSA" in addition to "EC" as JCA private key algorithm name. Fixes SIGSEGV crash on Android 10 devices where the provider reports EC keys as "ECDSA". Closes #4.
|
||||||
- **createOperation domain handling** — Software-generated keys now found via both `Domain.APP` (alias) and `Domain.KEY_ID` (nspace) lookup paths.
|
- **createOperation domain handling**, Software-generated keys now found via both `Domain.APP` (alias) and `Domain.KEY_ID` (nspace) lookup paths.
|
||||||
- **Permission guards** — Device ID attestation tags (IMEI, MEID, serial) require caller permission checks.
|
- **Permission guards**, Device ID attestation tags (IMEI, MEID, serial) require caller permission checks.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -198,20 +276,20 @@ Tested against DuckDetector on OnePlus (Android 16, KSU). Tamper score dropped f
|
|||||||
|
|
||||||
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) and [Key Attestation](https://github.com/nickel-lang/nickel) on OnePlus (Android 16) and Xiaomi Redmi 14C (Android 14).
|
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) and [Key Attestation](https://github.com/nickel-lang/nickel) on OnePlus (Android 16) and Xiaomi Redmi 14C (Android 14).
|
||||||
|
|
||||||
- **PADDING encoding** — Fixed ASN.1 encoding of PADDING tag in attestation extension from individual `[6] INTEGER` entries to `[6] SET OF INTEGER`, matching AOSP `attestation_record.h` schema. Broke all RSA key attestation since v4.6.
|
- **PADDING encoding**, Fixed ASN.1 encoding of PADDING tag in attestation extension from individual `[6] INTEGER` entries to `[6] SET OF INTEGER`, matching AOSP `attestation_record.h` schema. Broke all RSA key attestation since v4.6.
|
||||||
- **Operation error-path conformance** — Software operations now track finalized state and return `INVALID_OPERATION_HANDLE (-28)` on post-abort calls. Input length guard (32KB) returns `TOO_MUCH_DATA` matching AOSP `operation.rs`. Passes KeyDetector's OperationErrorPathChecker.
|
- **Operation error-path conformance**, Software operations now track finalized state and return `INVALID_OPERATION_HANDLE (-28)` on post-abort calls. Input length guard (32KB) returns `TOO_MUCH_DATA` matching AOSP `operation.rs`. Passes KeyDetector's OperationErrorPathChecker.
|
||||||
- **updateAad support** — Added `updateAad` to `SoftwareOperationBinder`, fixing `AbstractMethodError` on Android 16 where the runtime Stub declares it abstract.
|
- **updateAad support**, Added `updateAad` to `SoftwareOperationBinder`, fixing `AbstractMethodError` on Android 16 where the runtime Stub declares it abstract.
|
||||||
- **Algorithm inference** — `createOperation` now infers algorithm from the stored key pair when operation params omit the ALGORITHM tag, matching AOSP behavior.
|
- **Algorithm inference**, `createOperation` now infers algorithm from the stored key pair when operation params omit the ALGORITHM tag, matching AOSP behavior.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## TEESimulator-RS v4.6: Rebrand & Detection Fix
|
## TEESimulator-RS v4.6: Rebrand & Detection Fix
|
||||||
|
|
||||||
- **RTT normalization rework** — Replaced Gaussian sleep (mean=55ms) with a 15ms floor fence. The old approach triggered Chunqiu Native Check 2.8 timing analysis; the floor-only approach satisfies the minimum RTT threshold without creating a detectable delay pattern.
|
- **RTT normalization rework**, Replaced Gaussian sleep (mean=55ms) with a 15ms floor fence. The old approach triggered Chunqiu Native Check 2.8 timing analysis; the floor-only approach satisfies the minimum RTT threshold without creating a detectable delay pattern.
|
||||||
- **Cross-algorithm attestation** — Signing algorithm now derived from the attestation key's actual type, not the generated key's algorithm. Fixes BouncyCastle crash when signing RSA keys with EC attestation keys (Shizuku attestation flow).
|
- **Cross-algorithm attestation**, Signing algorithm now derived from the attestation key's actual type, not the generated key's algorithm. Fixes BouncyCastle crash when signing RSA keys with EC attestation keys (Shizuku attestation flow).
|
||||||
- **Device ID attestation** — Serial/IMEI/MEID/secondImei tags now flow through to software cert gen instead of blanket rejection. Only DEVICE_UNIQUE_ATTESTATION is rejected, matching AOSP keystore2 policy.
|
- **Device ID attestation**, Serial/IMEI/MEID/secondImei tags now flow through to software cert gen instead of blanket rejection. Only DEVICE_UNIQUE_ATTESTATION is rejected, matching AOSP keystore2 policy.
|
||||||
- **Rebrand to TEESimulator-RS** — Distinguishes this fork from upstream. Version scheme simplified to v{major}.{minor}-{commitCount}.
|
- **Rebrand to TEESimulator-RS**, Distinguishes this fork from upstream. Version scheme simplified to v{major}.{minor}-{commitCount}.
|
||||||
- **CI streamlined** — Release pipeline uses Gradle-generated filenames directly, eliminating the rename step.
|
- **CI streamlined**, Release pipeline uses Gradle-generated filenames directly, eliminating the rename step.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -219,28 +297,28 @@ Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) and [K
|
|||||||
|
|
||||||
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) (23-check attestation validator). All keystore-level checks now pass.
|
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) (23-check attestation validator). All keystore-level checks now pass.
|
||||||
|
|
||||||
- **Key deletion consistency** — After deleting a software-generated key, `getKeyEntry` now correctly returns `KEY_NOT_FOUND` instead of falling through to a stale live-patch fallback. Fixes binder consistency checks that detect ghost key responses.
|
- **Key deletion consistency**, After deleting a software-generated key, `getKeyEntry` now correctly returns `KEY_NOT_FOUND` instead of falling through to a stale live-patch fallback. Fixes binder consistency checks that detect ghost key responses.
|
||||||
- **generateKey timing normalization** — Software key generation RTT now matches real TEE latency profile (Gaussian distribution, mean=55ms, floor=15ms). Previously completed in ~4ms, which is an immediate timing side-channel.
|
- **generateKey timing normalization**, Software key generation RTT now matches real TEE latency profile (Gaussian distribution, mean=55ms, floor=15ms). Previously completed in ~4ms, which is an immediate timing side-channel.
|
||||||
- **Delete cleanup scope** — `deleteKey` now clears all cached state (patched chains, attestation keys) regardless of whether the key was software or hardware-generated.
|
- **Delete cleanup scope**, `deleteKey` now clears all cached state (patched chains, attestation keys) regardless of whether the key was software or hardware-generated.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## TEESimulator v4.4: AOSP Conformance
|
## TEESimulator v4.4: AOSP Conformance
|
||||||
|
|
||||||
- **Binder error reply format** — Aligned EX_SERVICE_SPECIFIC wire layout with AOSP Status.cpp, including the remote stack trace header field.
|
- **Binder error reply format**, Aligned EX_SERVICE_SPECIFIC wire layout with AOSP Status.cpp, including the remote stack trace header field.
|
||||||
- **Key enumeration** — Corrected list_past_alias pagination order to match AOSP database.rs semantics.
|
- **Key enumeration**, Corrected list_past_alias pagination order to match AOSP database.rs semantics.
|
||||||
- **KeyMetadata fields** — Generated key responses now include modificationTimeMs, Tag.ORIGIN, and normalized KeyDescriptor fields per AOSP Keystore2.
|
- **KeyMetadata fields**, Generated key responses now include modificationTimeMs, Tag.ORIGIN, and normalized KeyDescriptor fields per AOSP Keystore2.
|
||||||
- **Parcel handling** — hasException() preserves reply position for downstream consumers.
|
- **Parcel handling**, hasException() preserves reply position for downstream consumers.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## TEESimulator v4.3: Performance & Reliability
|
## TEESimulator v4.3: Performance & Reliability
|
||||||
|
|
||||||
- **Debug log gating** — `SystemLogger.debug()` now skipped entirely in release builds, eliminating unnecessary logcat syscalls on every intercepted transaction.
|
- **Debug log gating**, `SystemLogger.debug()` now skipped entirely in release builds, eliminating unnecessary logcat syscalls on every intercepted transaction.
|
||||||
- **Supervisor backoff** — Exponential restart delay (500ms → 30s cap) prevents CPU spin if the daemon crashes repeatedly. Resets automatically once stable.
|
- **Supervisor backoff**, Exponential restart delay (500ms → 30s cap) prevents CPU spin if the daemon crashes repeatedly. Resets automatically once stable.
|
||||||
- **Process priority** — Daemon runs at nice=10, yielding CPU to foreground apps on constrained devices.
|
- **Process priority**, Daemon runs at nice=10, yielding CPU to foreground apps on constrained devices.
|
||||||
- **Map eviction** — Rate limiter and file lock maps now evict stale entries instead of growing unbounded.
|
- **Map eviction**, Rate limiter and file lock maps now evict stale entries instead of growing unbounded.
|
||||||
- **CI pipeline** — Single-trigger build→release pipeline with proper changelog extraction and correctly sized artifacts.
|
- **CI pipeline**, Single-trigger build→release pipeline with proper changelog extraction and correctly sized artifacts.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -252,9 +330,9 @@ Fixes 6 detection vectors flagged by attestation validator apps.
|
|||||||
|
|
||||||
Replicate AOSP keystore2's `add_required_parameters()` validation that our software keygen path was bypassing:
|
Replicate AOSP keystore2's `add_required_parameters()` validation that our software keygen path was bypassing:
|
||||||
|
|
||||||
- **CREATION_DATETIME** — Reject caller-provided input with `INVALID_ARGUMENT (20)`, matching `security_level.rs:424`. Our cert gen still adds its own timestamp, same as real keystore2.
|
- **CREATION_DATETIME**, Reject caller-provided input with `INVALID_ARGUMENT (20)`, matching `security_level.rs:424`. Our cert gen still adds its own timestamp, same as real keystore2.
|
||||||
- **Device ID attestation** — Reject ATTESTATION_ID_SERIAL, IMEI, MEID, SECOND_IMEI, and DEVICE_UNIQUE_ATTESTATION with `CANNOT_ATTEST_IDS (-66)`. No consumer app has READ_PRIVILEGED_PHONE_STATE.
|
- **Device ID attestation**, Reject ATTESTATION_ID_SERIAL, IMEI, MEID, SECOND_IMEI, and DEVICE_UNIQUE_ATTESTATION with `CANNOT_ATTEST_IDS (-66)`. No consumer app has READ_PRIVILEGED_PHONE_STATE.
|
||||||
- **Error reply format** — Fixed AIDL ServiceSpecificException parcel write order (was errorCode→message, now message→errorCode).
|
- **Error reply format**, Fixed AIDL ServiceSpecificException parcel write order (was errorCode→message, now message→errorCode).
|
||||||
|
|
||||||
### Certificate Fix
|
### Certificate Fix
|
||||||
|
|
||||||
@@ -282,15 +360,15 @@ Major release. Certificate chain generation rebuilt from the ground up in Rust,
|
|||||||
|
|
||||||
### Native Cert Generation
|
### Native Cert Generation
|
||||||
|
|
||||||
The headline feature. `libcertgen.so` generates X.509 certificate chains using `ring` (EC-P256/P384) and `rsa` (RSA-2048/4096) with manual DER assembly. No more BouncyCastle quirks — issuer/subject DN bytes are injected directly from the keybox, ensuring byte-perfect chain linkage. BouncyCastle remains as fallback for unsupported curves (P-224, P-521, Curve25519).
|
The headline feature. `libcertgen.so` generates X.509 certificate chains using `ring` (EC-P256/P384) and `rsa` (RSA-2048/4096) with manual DER assembly. No more BouncyCastle quirks, issuer/subject DN bytes are injected directly from the keybox, ensuring byte-perfect chain linkage. BouncyCastle remains as fallback for unsupported curves (P-224, P-521, Curve25519).
|
||||||
|
|
||||||
### Anti-Detection Hardening
|
### Anti-Detection Hardening
|
||||||
|
|
||||||
- **Challenge validation** — Oversized attestation challenges (>128 bytes) now return `INVALID_INPUT_LENGTH (-21)`, matching real KeyMint behavior. Previously accepted silently — DuckDetector exploited this.
|
- **Challenge validation**, Oversized attestation challenges (>128 bytes) now return `INVALID_INPUT_LENGTH (-21)`, matching real KeyMint behavior. Previously accepted silently, DuckDetector exploited this.
|
||||||
- **Per-UID rate limiter** — 2 hardware keygens per 30s burst, 2 concurrent max. Overflow falls back to software certs. Blocks DuckDetector-style keygen flooding that starves GMS.
|
- **Per-UID rate limiter**, 2 hardware keygens per 30s burst, 2 concurrent max. Overflow falls back to software certs. Blocks DuckDetector-style keygen flooding that starves GMS.
|
||||||
- **importKey eviction guard** — Retained patch chains prevent generate-then-import attacks that evict cached attestation data.
|
- **importKey eviction guard**, Retained patch chains prevent generate-then-import attacks that evict cached attestation data.
|
||||||
- **256KB native payload cap** — Oversized binder payloads bypass interception cleanly instead of stalling threads.
|
- **256KB native payload cap**, Oversized binder payloads bypass interception cleanly instead of stalling threads.
|
||||||
- **Alias size rejection** — Oversized key aliases rejected before they hit the binder buffer.
|
- **Alias size rejection**, Oversized key aliases rejected before they hit the binder buffer.
|
||||||
|
|
||||||
### Key Persistence
|
### Key Persistence
|
||||||
|
|
||||||
@@ -302,7 +380,7 @@ Generated keys now survive reboots. File-backed storage with file-level locking,
|
|||||||
- Correct `module_hash` field to match AOSP Keystore2 format
|
- Correct `module_hash` field to match AOSP Keystore2 format
|
||||||
- Override pre-existing attest keys instead of skipping them
|
- Override pre-existing attest keys instead of skipping them
|
||||||
- Strip HTML comments from PEM blocks in keybox parsing
|
- Strip HTML comments from PEM blocks in keybox parsing
|
||||||
- Security patch consistency — `system=prop` forces boot/vendor to match
|
- Security patch consistency, `system=prop` forces boot/vendor to match
|
||||||
|
|
||||||
### Module Lifecycle
|
### Module Lifecycle
|
||||||
|
|
||||||
@@ -313,9 +391,9 @@ Generated keys now survive reboots. File-backed storage with file-level locking,
|
|||||||
### Stability
|
### Stability
|
||||||
|
|
||||||
- FileObserver NPE on config deletion fixed
|
- FileObserver NPE on config deletion fixed
|
||||||
- Global uncaught exception handler — daemon stays alive on unexpected errors
|
- Global uncaught exception handler, daemon stays alive on unexpected errors
|
||||||
- PEM parsing hardened against malformed keybox files
|
- PEM parsing hardened against malformed keybox files
|
||||||
|
|
||||||
### Tested Against
|
### Tested Against
|
||||||
|
|
||||||
DuckDetector, Luna, Play Integrity, Key Attestation Demo — all passing on Redmi 14C (Android 14, Beanpod KeyMaster, KSU).
|
DuckDetector, Luna, Play Integrity, Key Attestation Demo, all passing on Redmi 14C (Android 14, Beanpod KeyMaster, KSU).
|
||||||
|
|||||||
@@ -76,6 +76,20 @@ mv "$MODPATH/libsupervisor.so" "$MODPATH/supervisor"
|
|||||||
chmod 755 "$MODPATH/inject"
|
chmod 755 "$MODPATH/inject"
|
||||||
chmod 755 "$MODPATH/supervisor"
|
chmod 755 "$MODPATH/supervisor"
|
||||||
|
|
||||||
|
# Debug builds carry diag.sh (the diagnostic plane); release builds do not. Extract it when
|
||||||
|
# present; otherwise sweep any external-storage diagnostics a prior debug install left behind,
|
||||||
|
# since the release keystore domain has no grant to remove them itself.
|
||||||
|
# Detect presence by the extracted FILE, not unzip's exit code: the busybox/toybox unzip in
|
||||||
|
# the install environment exits 0 even when the entry is absent, so the sweep never ran.
|
||||||
|
unzip -qqjo "$ZIPFILE" "diag.sh" -d "$MODPATH" 2>/dev/null
|
||||||
|
if [ -f "$MODPATH/diag.sh" ]; then
|
||||||
|
chmod 644 "$MODPATH/diag.sh"
|
||||||
|
ui_print "- Debug diagnostic plane enabled"
|
||||||
|
else
|
||||||
|
rm -rf /data/media/0/TEESimulator /data/local/tmp/teesim
|
||||||
|
ui_print "- Release build: swept stale diagnostics"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Configuration Files ---
|
# --- Configuration Files ---
|
||||||
if [ ! -d "$CONFIG_DIR" ]; then
|
if [ ! -d "$CONFIG_DIR" ]; then
|
||||||
ui_print "- Creating configuration directory"
|
ui_print "- Creating configuration directory"
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#!/system/bin/sh
|
||||||
|
# Debug-only diagnostic plane. Shipped solely in debug ZIPs; its presence is the gate that
|
||||||
|
# service.sh (setup) and action.sh (export) test before touching external storage.
|
||||||
|
DIAG_DIR=/data/media/0/TEESimulator
|
||||||
|
|
||||||
|
diag_setup() {
|
||||||
|
mkdir -p "$DIAG_DIR"
|
||||||
|
chmod 0777 "$DIAG_DIR"
|
||||||
|
chcon u:object_r:media_rw_data_file:s0 "$DIAG_DIR" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
diag_export() {
|
||||||
|
_ts=$(date +%Y%m%d-%H%M%S)
|
||||||
|
_dest=/sdcard/Download/teesim-logs-$_ts
|
||||||
|
mkdir -p "$_dest"
|
||||||
|
cp -f "$DIAG_DIR"/teesim-uid-* "$_dest"/ 2>/dev/null
|
||||||
|
cp -f /data/adb/tricky_store/logs/certgen.log* "$_dest"/ 2>/dev/null
|
||||||
|
logcat -d -s TEESimulator > "$_dest/logcat.txt" 2>/dev/null
|
||||||
|
echo " ✅ Saved to $_dest"
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
allow keystore {adb_data_file shell_data_file} file *
|
allow keystore {adb_data_file shell_data_file} file *
|
||||||
allow crash_dump keystore process *
|
allow crash_dump keystore process *
|
||||||
|
|
||||||
|
# SOTER Layer-A (10.C): ptrace inject into soterserver (platform_app). The debug NDJSON
|
||||||
|
# media_rw_data_file grant is debug-only — appended for debug builds in app/build.gradle.kts.
|
||||||
|
allow crash_dump platform_app process *
|
||||||
|
|
||||||
allow ksu self:tcp_socket { create connect read write getopt setopt }
|
allow ksu self:tcp_socket { create connect read write getopt setopt }
|
||||||
allow ksu node:tcp_socket node_bind
|
allow ksu node:tcp_socket node_bind
|
||||||
allow ksu port:tcp_socket name_connect
|
allow ksu port:tcp_socket name_connect
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ cd $MODDIR
|
|||||||
# Fork-based supervisor for instant restart
|
# Fork-based supervisor for instant restart
|
||||||
./supervisor ./daemon "$MODDIR" &
|
./supervisor ./daemon "$MODDIR" &
|
||||||
|
|
||||||
|
# Debug builds ship diag.sh; its presence enables the external-storage diagnostic plane.
|
||||||
|
if [ -f "$MODDIR/diag.sh" ]; then
|
||||||
|
. "$MODDIR/diag.sh"
|
||||||
|
diag_setup
|
||||||
|
fi
|
||||||
|
|
||||||
# Clear logd size persist properties once boot completes
|
# Clear logd size persist properties once boot completes
|
||||||
(
|
(
|
||||||
until [ "$(getprop sys.boot_completed)" = "1" ]; do
|
until [ "$(getprop sys.boot_completed)" = "1" ]; do
|
||||||
|
|||||||
@@ -11,3 +11,6 @@ rm -rf "$CONFIG_DIR/persistent_keys"
|
|||||||
rm -f "$CONFIG_DIR/tee_status.txt"
|
rm -f "$CONFIG_DIR/tee_status.txt"
|
||||||
rm -f "$CONFIG_DIR/boot_hash.bin" "$CONFIG_DIR/boot_key.bin"
|
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"
|
rm -f "$CONFIG_DIR/security_patch.txt" "$CONFIG_DIR/security_patch.txt.next" "$CONFIG_DIR/last_bulletin_fetch.json"
|
||||||
|
|
||||||
|
# Debug diagnostics live on external storage; remove them on uninstall.
|
||||||
|
rm -rf /data/media/0/TEESimulator
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "v6.0.1-251",
|
"version": "v6.0.1-307",
|
||||||
"versionCode": 251,
|
"versionCode": 307,
|
||||||
"zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/download/v6.0.1-251/TEESimulator-RS-v6.0.1-251-Release.zip",
|
"zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/download/v6.0.1-307/TEESimulator-RS-v6.0.1-307-Release.zip",
|
||||||
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/changelog.md"
|
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/changelog.md"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2
-159
@@ -2,12 +2,6 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "adler2"
|
|
||||||
version = "2.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@@ -23,15 +17,6 @@ version = "1.0.102"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arbitrary"
|
|
||||||
version = "1.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
|
||||||
dependencies = [
|
|
||||||
"derive_arbitrary",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -53,12 +38,6 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bumpalo"
|
|
||||||
version = "3.20.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@@ -83,7 +62,6 @@ dependencies = [
|
|||||||
"const-oid",
|
"const-oid",
|
||||||
"der",
|
"der",
|
||||||
"jni",
|
"jni",
|
||||||
"libc",
|
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
"rand",
|
"rand",
|
||||||
"ring",
|
"ring",
|
||||||
@@ -93,7 +71,6 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"x509-cert",
|
"x509-cert",
|
||||||
"zip",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -133,21 +110,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crc32fast"
|
|
||||||
version = "1.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-utils"
|
|
||||||
version = "0.8.21"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -191,17 +153,6 @@ dependencies = [
|
|||||||
"powerfmt",
|
"powerfmt",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "derive_arbitrary"
|
|
||||||
version = "1.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -213,23 +164,6 @@ dependencies = [
|
|||||||
"crypto-common",
|
"crypto-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "displaydoc"
|
|
||||||
version = "0.2.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "equivalent"
|
|
||||||
version = "1.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@@ -242,16 +176,6 @@ version = "0.4.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe"
|
checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "flate2"
|
|
||||||
version = "1.1.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
|
||||||
dependencies = [
|
|
||||||
"crc32fast",
|
|
||||||
"miniz_oxide",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@@ -273,22 +197,6 @@ dependencies = [
|
|||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.16.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indexmap"
|
|
||||||
version = "2.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
|
||||||
dependencies = [
|
|
||||||
"equivalent",
|
|
||||||
"hashbrown",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.17"
|
version = "1.0.17"
|
||||||
@@ -306,7 +214,7 @@ dependencies = [
|
|||||||
"combine",
|
"combine",
|
||||||
"jni-sys",
|
"jni-sys",
|
||||||
"log",
|
"log",
|
||||||
"thiserror 1.0.69",
|
"thiserror",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"windows-sys 0.45.0",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
@@ -359,16 +267,6 @@ version = "2.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "miniz_oxide"
|
|
||||||
version = "0.8.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
|
||||||
dependencies = [
|
|
||||||
"adler2",
|
|
||||||
"simd-adler32",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.50.3"
|
version = "0.50.3"
|
||||||
@@ -674,12 +572,6 @@ dependencies = [
|
|||||||
"rand_core",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "simd-adler32"
|
|
||||||
version = "0.3.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.15.1"
|
version = "1.15.1"
|
||||||
@@ -725,16 +617,7 @@ version = "1.0.69"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl 1.0.69",
|
"thiserror-impl",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror"
|
|
||||||
version = "2.0.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror-impl 2.0.18",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -748,17 +631,6 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror-impl"
|
|
||||||
version = "2.0.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.1.9"
|
version = "1.1.9"
|
||||||
@@ -1130,37 +1002,8 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zip"
|
|
||||||
version = "2.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
|
|
||||||
dependencies = [
|
|
||||||
"arbitrary",
|
|
||||||
"crc32fast",
|
|
||||||
"crossbeam-utils",
|
|
||||||
"displaydoc",
|
|
||||||
"flate2",
|
|
||||||
"indexmap",
|
|
||||||
"memchr",
|
|
||||||
"thiserror 2.0.18",
|
|
||||||
"zopfli",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zopfli"
|
|
||||||
version = "0.8.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
|
||||||
dependencies = [
|
|
||||||
"bumpalo",
|
|
||||||
"crc32fast",
|
|
||||||
"log",
|
|
||||||
"simd-adler32",
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ time = { version = "0.3", features = ["std"] }
|
|||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
libc = "0.2"
|
|
||||||
zip = { version = "2.2", default-features = false, features = ["deflate"] }
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
+29
-40
@@ -9,7 +9,7 @@ pub mod certbuilder;
|
|||||||
pub mod logging;
|
pub mod logging;
|
||||||
|
|
||||||
use jni::objects::{JByteArray, JClass, JIntArray, JObject, JString};
|
use jni::objects::{JByteArray, JClass, JIntArray, JObject, JString};
|
||||||
use jni::sys::{jboolean, jbyteArray, jstring};
|
use jni::sys::{jboolean, jbyteArray};
|
||||||
use jni::JNIEnv;
|
use jni::JNIEnv;
|
||||||
|
|
||||||
use crate::error::{CertGenError, Result};
|
use crate::error::{CertGenError, Result};
|
||||||
@@ -64,18 +64,41 @@ fn generate_attested_inner(env: &mut JNIEnv, config: &JObject) -> Result<jbyteAr
|
|||||||
|
|
||||||
let cert_chain = if params.attestation_challenge.is_some() {
|
let cert_chain = if params.attestation_challenge.is_some() {
|
||||||
let attest_ext = attestation::build_attestation_extension(¶ms)?;
|
let attest_ext = attestation::build_attestation_extension(¶ms)?;
|
||||||
|
// Ground truth of what the Rust forger emitted, keyed to the app. Gated on the APK debug
|
||||||
|
// variant so release builds never dump the extension.
|
||||||
|
if params.debug_logging {
|
||||||
|
tracing::info!(
|
||||||
|
uid = params.uid,
|
||||||
|
ext_hex = %hex_encode(&attest_ext),
|
||||||
|
"produced attestation extension"
|
||||||
|
);
|
||||||
|
}
|
||||||
certbuilder::build_certificate_chain(&key_pair, Some(&attest_ext), &keybox, ¶ms)?
|
certbuilder::build_certificate_chain(&key_pair, Some(&attest_ext), &keybox, ¶ms)?
|
||||||
} else {
|
} else {
|
||||||
tracing::info!("no attestation challenge, generating self-signed cert (depth 1)");
|
tracing::info!(
|
||||||
|
uid = params.uid,
|
||||||
|
"no attestation challenge, generating self-signed cert (depth 1)"
|
||||||
|
);
|
||||||
certbuilder::build_self_signed_cert(&key_pair, ¶ms)?
|
certbuilder::build_self_signed_cert(&key_pair, ¶ms)?
|
||||||
};
|
};
|
||||||
|
|
||||||
let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain);
|
let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain);
|
||||||
|
tracing::info!(uid = params.uid, certs = cert_chain.len(), "assembled native cert result");
|
||||||
|
|
||||||
let out = env.byte_array_from_slice(&blob)?;
|
let out = env.byte_array_from_slice(&blob)?;
|
||||||
Ok(out.into_raw())
|
Ok(out.into_raw())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lowercase hex of a byte slice for diagnostic dumps; the crate has no `hex` dependency.
|
||||||
|
fn hex_encode(bytes: &[u8]) -> String {
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
let mut out = String::with_capacity(bytes.len() * 2);
|
||||||
|
for b in bytes {
|
||||||
|
let _ = write!(out, "{:02x}", b);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// JNI entry: initLogging
|
// JNI entry: initLogging
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -117,44 +140,6 @@ fn init_logging_inner(env: &mut JNIEnv, verbose: jboolean, log_dir: &JString) ->
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// JNI entry: dumpLogs
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "system" fn Java_org_matrix_TEESimulator_pki_NativeCertGen_dumpLogs(
|
|
||||||
mut env: JNIEnv,
|
|
||||||
_class: JClass,
|
|
||||||
) -> jstring {
|
|
||||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
|
||||||
dump_logs_inner(&mut env)
|
|
||||||
}));
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(Ok(raw)) => raw,
|
|
||||||
Ok(Err(e)) => {
|
|
||||||
tracing::error!(%e, "dumpLogs failed");
|
|
||||||
std::ptr::null_mut()
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
tracing::error!("dumpLogs panicked");
|
|
||||||
std::ptr::null_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dump_logs_inner(env: &mut JNIEnv) -> Result<jstring> {
|
|
||||||
logging::dump::execute_dump()
|
|
||||||
.map_err(|e| CertGenError::Jni(format!("dump failed: {e}")))?;
|
|
||||||
|
|
||||||
// Read the dump path written by execute_dump
|
|
||||||
let path = std::fs::read_to_string("/data/adb/tricky_store/.dump_path")
|
|
||||||
.map_err(|e| CertGenError::Jni(format!("read dump path: {e}")))?;
|
|
||||||
|
|
||||||
let jpath = env.new_string(&path)?;
|
|
||||||
Ok(jpath.into_raw())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Config extraction from Java CertGenConfig object
|
// Config extraction from Java CertGenConfig object
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -205,6 +190,8 @@ fn extract_config(env: &mut JNIEnv, config: &JObject) -> Result<CertGenParams> {
|
|||||||
let caller_nonce = get_boolean(env, config, "callerNonce")?;
|
let caller_nonce = get_boolean(env, config, "callerNonce")?;
|
||||||
let unlocked_device_required = get_boolean(env, config, "unlockedDeviceRequired")?;
|
let unlocked_device_required = get_boolean(env, config, "unlockedDeviceRequired")?;
|
||||||
let no_auth_required = get_boolean(env, config, "noAuthRequired")?;
|
let no_auth_required = get_boolean(env, config, "noAuthRequired")?;
|
||||||
|
let uid = get_int(env, config, "uid")?;
|
||||||
|
let debug_logging = get_boolean(env, config, "debugLogging")?;
|
||||||
|
|
||||||
Ok(CertGenParams {
|
Ok(CertGenParams {
|
||||||
algorithm: Algorithm::try_from(algorithm)?,
|
algorithm: Algorithm::try_from(algorithm)?,
|
||||||
@@ -252,6 +239,8 @@ fn extract_config(env: &mut JNIEnv, config: &JObject) -> Result<CertGenParams> {
|
|||||||
caller_nonce,
|
caller_nonce,
|
||||||
unlocked_device_required,
|
unlocked_device_required,
|
||||||
no_auth_required,
|
no_auth_required,
|
||||||
|
uid,
|
||||||
|
debug_logging,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
use std::fs::{self, File};
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
use std::path::Path;
|
|
||||||
use std::process::Command;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
const DUMP_DIR: &str = "/sdcard/Download";
|
|
||||||
const LOCK_PATH: &str = "/data/adb/tricky_store/.dump_lock";
|
|
||||||
const DUMP_PATH_FILE: &str = "/data/adb/tricky_store/.dump_path";
|
|
||||||
const LOG_DIR: &str = "/data/adb/tricky_store/logs";
|
|
||||||
const BASE_DIR: &str = "/data/adb/tricky_store";
|
|
||||||
const LOGCAT_SIZE_LIMIT: usize = 2 * 1024 * 1024;
|
|
||||||
|
|
||||||
struct FlockGuard {
|
|
||||||
_file: File,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FlockGuard {
|
|
||||||
fn acquire() -> Result<Self, Box<dyn std::error::Error>> {
|
|
||||||
if let Some(parent) = Path::new(LOCK_PATH).parent() {
|
|
||||||
fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
let file = File::create(LOCK_PATH)?;
|
|
||||||
let fd = {
|
|
||||||
use std::os::unix::io::AsRawFd;
|
|
||||||
file.as_raw_fd()
|
|
||||||
};
|
|
||||||
let ret = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
|
|
||||||
if ret != 0 {
|
|
||||||
return Err("dump already in progress".into());
|
|
||||||
}
|
|
||||||
Ok(Self { _file: file })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for FlockGuard {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
// flock released automatically when file descriptor closes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn random_name(len: usize) -> String {
|
|
||||||
use rand::Rng;
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
(0..len)
|
|
||||||
.map(|_| {
|
|
||||||
let idx = rng.gen_range(0..36u8);
|
|
||||||
if idx < 10 {
|
|
||||||
(b'0' + idx) as char
|
|
||||||
} else {
|
|
||||||
(b'a' + idx - 10) as char
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_logcat(tag: &str) -> Vec<u8> {
|
|
||||||
let output = Command::new("logcat")
|
|
||||||
.args(["-d", "-s", tag])
|
|
||||||
.output();
|
|
||||||
|
|
||||||
match output {
|
|
||||||
Ok(o) => {
|
|
||||||
let mut data = o.stdout;
|
|
||||||
data.truncate(LOGCAT_SIZE_LIMIT);
|
|
||||||
data
|
|
||||||
}
|
|
||||||
Err(_) => Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_device_info() -> String {
|
|
||||||
let mut info = String::new();
|
|
||||||
|
|
||||||
if let Ok(output) = Command::new("uname").arg("-a").output() {
|
|
||||||
info.push_str(&format!(
|
|
||||||
"uname={}\n",
|
|
||||||
String::from_utf8_lossy(&output.stdout).trim()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (key, prop) in [
|
|
||||||
("device", "ro.product.device"),
|
|
||||||
("build", "ro.build.display.id"),
|
|
||||||
("android", "ro.build.version.release"),
|
|
||||||
] {
|
|
||||||
if let Ok(output) = Command::new("getprop").arg(prop).output() {
|
|
||||||
info.push_str(&format!(
|
|
||||||
"{}={}\n",
|
|
||||||
key,
|
|
||||||
String::from_utf8_lossy(&output.stdout).trim()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// KSU version
|
|
||||||
if let Ok(ver) = fs::read_to_string("/data/adb/ksu/version") {
|
|
||||||
info.push_str(&format!("ksu={}\n", ver.trim()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Module version from module.prop
|
|
||||||
if let Ok(prop) = fs::read_to_string("/data/adb/modules/tricky_store/module.prop") {
|
|
||||||
for line in prop.lines() {
|
|
||||||
if let Some(ver) = line.strip_prefix("version=") {
|
|
||||||
info.push_str(&format!("module={}\n", ver.trim()));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_file_bytes(path: &str) -> Option<Vec<u8>> {
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
File::open(path).ok()?.read_to_end(&mut buf).ok()?;
|
|
||||||
Some(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn epoch_millis() -> u64 {
|
|
||||||
SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.map(|d| d.as_millis() as u64)
|
|
||||||
.unwrap_or(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn execute_dump() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let _lock = FlockGuard::acquire()?;
|
|
||||||
|
|
||||||
let _ = fs::create_dir_all(DUMP_DIR);
|
|
||||||
let zip_name = format!("{}.zip", random_name(8));
|
|
||||||
let zip_path = format!("{}/{}", DUMP_DIR, zip_name);
|
|
||||||
|
|
||||||
let zip_file = File::create(&zip_path)?;
|
|
||||||
let mut zip = zip::ZipWriter::new(zip_file);
|
|
||||||
let options =
|
|
||||||
zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
|
|
||||||
|
|
||||||
let mut file_count = 0u32;
|
|
||||||
|
|
||||||
// Log files
|
|
||||||
let log_files = [
|
|
||||||
"certgen.log",
|
|
||||||
"certgen.log.1",
|
|
||||||
"certgen.log.2",
|
|
||||||
"certgen.log.3",
|
|
||||||
"certgen.log.4",
|
|
||||||
];
|
|
||||||
for name in &log_files {
|
|
||||||
let path = format!("{}/{}", LOG_DIR, name);
|
|
||||||
if let Some(data) = read_file_bytes(&path) {
|
|
||||||
zip.start_file(*name, options)?;
|
|
||||||
zip.write_all(&data)?;
|
|
||||||
file_count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logcat
|
|
||||||
let logcat = collect_logcat("TEESimulator");
|
|
||||||
if !logcat.is_empty() {
|
|
||||||
zip.start_file("logcat-teesimulator.log", options)?;
|
|
||||||
zip.write_all(&logcat)?;
|
|
||||||
file_count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config files
|
|
||||||
for name in ["tee_status.txt", "security_patch.txt"] {
|
|
||||||
let path = format!("{}/{}", BASE_DIR, name);
|
|
||||||
if let Some(data) = read_file_bytes(&path) {
|
|
||||||
zip.start_file(name, options)?;
|
|
||||||
zip.write_all(&data)?;
|
|
||||||
file_count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Device info
|
|
||||||
let device_info = collect_device_info();
|
|
||||||
if !device_info.is_empty() {
|
|
||||||
zip.start_file("device-info.txt", options)?;
|
|
||||||
zip.write_all(device_info.as_bytes())?;
|
|
||||||
file_count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manifest
|
|
||||||
let manifest = serde_json::json!({
|
|
||||||
"timestamp": epoch_millis(),
|
|
||||||
"version": env!("CARGO_PKG_VERSION"),
|
|
||||||
"files": file_count,
|
|
||||||
});
|
|
||||||
zip.start_file("manifest.json", options)?;
|
|
||||||
zip.write_all(manifest.to_string().as_bytes())?;
|
|
||||||
|
|
||||||
zip.finish()?;
|
|
||||||
|
|
||||||
let zip_size = fs::metadata(&zip_path).map(|m| m.len()).unwrap_or(0);
|
|
||||||
fs::write(DUMP_PATH_FILE, &zip_path)?;
|
|
||||||
|
|
||||||
let result = serde_json::json!({
|
|
||||||
"zip": zip_path,
|
|
||||||
"size": zip_size,
|
|
||||||
"files": file_count + 1, // +1 for manifest
|
|
||||||
});
|
|
||||||
println!("{}", result);
|
|
||||||
|
|
||||||
tracing::info!(path = %zip_path, size = zip_size, "diagnostic dump created");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
mod kmsg;
|
mod kmsg;
|
||||||
mod rotating;
|
mod rotating;
|
||||||
pub mod sysfs;
|
|
||||||
pub mod dump;
|
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
use std::fs;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
const VERBOSE_MARKER: &str = "/data/adb/tricky_store/.verbose";
|
|
||||||
|
|
||||||
pub fn is_verbose() -> bool {
|
|
||||||
Path::new(VERBOSE_MARKER).exists()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_verbose_marker(enabled: bool) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
if enabled {
|
|
||||||
if let Some(parent) = Path::new(VERBOSE_MARKER).parent() {
|
|
||||||
fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
fs::write(VERBOSE_MARKER, "")?;
|
|
||||||
} else if Path::new(VERBOSE_MARKER).exists() {
|
|
||||||
fs::remove_file(VERBOSE_MARKER)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enable() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
set_verbose_marker(true)?;
|
|
||||||
tracing::info!("verbose logging enabled via marker file");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn disable() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
set_verbose_marker(false)?;
|
|
||||||
tracing::info!("verbose logging disabled, marker file removed");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn status() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let state = if is_verbose() { "enabled" } else { "disabled" };
|
|
||||||
tracing::info!(verbose = state, "verbose marker status");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -93,6 +93,11 @@ pub struct CertGenParams {
|
|||||||
pub caller_nonce: bool,
|
pub caller_nonce: bool,
|
||||||
pub unlocked_device_required: bool,
|
pub unlocked_device_required: bool,
|
||||||
pub no_auth_required: bool,
|
pub no_auth_required: bool,
|
||||||
|
|
||||||
|
/// Calling app UID, used only to key diagnostic log lines to the requesting app.
|
||||||
|
pub uid: i32,
|
||||||
|
/// Mirrors the APK debug variant; gates the produced-extension dump so release stays quiet.
|
||||||
|
pub debug_logging: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GeneratedKeyPair {
|
pub struct GeneratedKeyPair {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ REBOOT=false
|
|||||||
VERIFY=false
|
VERIFY=false
|
||||||
BUILD_RUST=false
|
BUILD_RUST=false
|
||||||
CLEAR_KEYS=false
|
CLEAR_KEYS=false
|
||||||
|
CLEAR_LOGS=false
|
||||||
TRACE=false
|
TRACE=false
|
||||||
ROOT_PROVIDER="ksu"
|
ROOT_PROVIDER="ksu"
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ Deploy options:
|
|||||||
--deploy Push ZIP to device and install
|
--deploy Push ZIP to device and install
|
||||||
--reboot Reboot device after install
|
--reboot Reboot device after install
|
||||||
--clear-keys Clear persistent_keys before deploy
|
--clear-keys Clear persistent_keys before deploy
|
||||||
|
--clear-logs Clear per-UID diagnostic logs before deploy
|
||||||
--verify Run logcat verification after deploy
|
--verify Run logcat verification after deploy
|
||||||
--root PROVIDER Root provider: ksu (default), magisk, apatch
|
--root PROVIDER Root provider: ksu (default), magisk, apatch
|
||||||
|
|
||||||
@@ -73,6 +75,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
--verify) VERIFY=true; shift ;;
|
--verify) VERIFY=true; shift ;;
|
||||||
--rust) BUILD_RUST=true; shift ;;
|
--rust) BUILD_RUST=true; shift ;;
|
||||||
--clear-keys) CLEAR_KEYS=true; shift ;;
|
--clear-keys) CLEAR_KEYS=true; shift ;;
|
||||||
|
--clear-logs) CLEAR_LOGS=true; shift ;;
|
||||||
-v|--verbose) TRACE=true; shift ;;
|
-v|--verbose) TRACE=true; shift ;;
|
||||||
--root) ROOT_PROVIDER="$2"; shift 2 ;;
|
--root) ROOT_PROVIDER="$2"; shift 2 ;;
|
||||||
--help|-h) usage ;;
|
--help|-h) usage ;;
|
||||||
@@ -153,6 +156,11 @@ deploy_zip() {
|
|||||||
adb shell "rm -rf /data/adb/tricky_store/persistent_keys/*" 2>/dev/null || true
|
adb shell "rm -rf /data/adb/tricky_store/persistent_keys/*" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "$CLEAR_LOGS" == true ]]; then
|
||||||
|
bold "==> Clearing per-UID diagnostic logs"
|
||||||
|
adb shell "rm -rf /data/media/0/TEESimulator /data/local/tmp/teesim" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
bold "==> Deploying $name"
|
bold "==> Deploying $name"
|
||||||
adb push "$zip" /data/local/tmp/module.zip
|
adb push "$zip" /data/local/tmp/module.zip
|
||||||
adb shell "su -c '$INSTALL_CMD /data/local/tmp/module.zip'"
|
adb shell "su -c '$INSTALL_CMD /data/local/tmp/module.zip'"
|
||||||
|
|||||||
Reference in New Issue
Block a user