Compare commits
37
Commits
v4.7-121
...
v6.0.0-158
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18724cf40d | ||
|
|
1b7800345d | ||
|
|
54c12a9fd5 | ||
|
|
1bc47840d5 | ||
|
|
c8fadb07ae | ||
|
|
c0b14eeeb1 | ||
|
|
47ab0225e1 | ||
|
|
ebb6336281 | ||
|
|
784373c8b5 | ||
|
|
2241bfb13d | ||
|
|
954478b89b | ||
|
|
191085087b | ||
|
|
f870598e77 | ||
|
|
8fdc59a142 | ||
|
|
b6f9d7b486 | ||
|
|
99e6b0b5ea | ||
|
|
397bb6338b | ||
|
|
cb81116701 | ||
|
|
45477a7898 | ||
|
|
634a1293c1 | ||
|
|
f115eda2dc | ||
|
|
217edf61fe | ||
|
|
ee5bf2e1a7 | ||
|
|
2181157cb6 | ||
|
|
aa4917e623 | ||
|
|
f06cb30b40 | ||
|
|
03c71bd202 | ||
|
|
7e2fc0b288 | ||
|
|
258a65ba59 | ||
|
|
d2b8a92fbd | ||
|
|
0723865eab | ||
|
|
7cb44b9999 | ||
|
|
eddd9908af | ||
|
|
36ccd22cdc | ||
|
|
81e6fbf97e | ||
|
|
7f63713f07 | ||
|
|
5df76eacd1 |
@@ -1 +1,7 @@
|
|||||||
out
|
out
|
||||||
|
.gradle
|
||||||
|
.kotlin
|
||||||
|
app/build
|
||||||
|
build
|
||||||
|
native-certgen/target
|
||||||
|
app/src/main/jniLibs
|
||||||
|
|||||||
@@ -1,256 +1,139 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<h1 align="center">🔐 TEESimulator</h1>
|
<h1 align="center">TEESimulator-RS</h1>
|
||||||
<p align="center"><b>Full TEE Emulation for Rooted Android</b></p>
|
<p align="center"><b>Full TEE Emulation for Rooted Android</b></p>
|
||||||
<p align="center">Hardware attestation. Software keys. Zero detection.</p>
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml"><img src="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml/badge.svg" alt="Build"></a>
|
<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/version-v4.2-blue?style=for-the-badge" alt="v4.2">
|
<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?style=for-the-badge&logo=android" alt="Android 10+">
|
<a href="https://t.me/superpowers9"><img src="https://img.shields.io/badge/Telegram-community-blue?logo=telegram" alt="Telegram"></a>
|
||||||
<img src="https://img.shields.io/badge/Telegram-community-blue?style=for-the-badge&logo=telegram" alt="Telegram">
|
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> **This is a personal fork of [JingMatrix/TEESimulator](https://github.com/JingMatrix/TEESimulator)** with additional hardening, native Rust certificate generation, key persistence, and anti-detection features. For the upstream project, see the original repo.
|
> Fork of [JingMatrix/TEESimulator](https://github.com/JingMatrix/TEESimulator) with native Rust certificate generation, key persistence, and AOSP-compliant attestation behavior. For the upstream project, see the original repo.
|
||||||
|
|
||||||
---
|
## What It Does
|
||||||
|
|
||||||
## 🧬 What is TEESimulator?
|
TEESimulator intercepts Binder IPC at the `ioctl` level inside the `keystore2` process and generates entire certificate chains from scratch, signed by your keybox, with correct attestation extensions. Apps that verify hardware attestation see a legitimate device.
|
||||||
|
|
||||||
TEESimulator is a **complete software simulation** of Android's hardware-backed [Trusted Execution Environment](https://source.android.com/docs/security/features/trusty) for [Key Attestation](https://developer.android.com/privacy-and-security/security-key-attestation). Instead of patching certificates from the real TEE after the fact, TEESimulator intercepts Binder IPC at the `ioctl` level and generates entire certificate chains from scratch — signed by your keybox, with correct attestation extensions, indistinguishable from hardware-generated keys.
|
This is not TrickyStore. TEESimulator replaces TrickyStore and its forks entirely. It shares the same config paths for drop-in compatibility, but the internals are different: native Rust cert generation, binder-level interception via `lsplt`, per-UID rate limiting, key persistence, and AOSP-spec attestation behavior.
|
||||||
|
|
||||||
The result: **apps that verify hardware attestation see a legitimate, unmodified device** — even on rooted hardware with an unlocked bootloader.
|
## Requirements
|
||||||
|
|
||||||
> **This is not TrickyStore.** TEESimulator replaces TrickyStore and its forks entirely. It shares the same config paths for drop-in compatibility, but the architecture is fundamentally different: native Rust certificate generation, binder-level interception via `lsplt`, per-UID rate limiting, key persistence, and a multi-layer defense against detector apps.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔥 Why TEESimulator?
|
|
||||||
|
|
||||||
🔐 **Native Cert Generation** — v4.0 generates X.509 certificate chains in Rust with `ring` and manual DER encoding. No BouncyCastle overhead, no Java crypto quirks, byte-perfect issuer chain linkage.
|
|
||||||
|
|
||||||
🎯 **Binder-Level Interception** — Hooks `ioctl()` on `libc.so` via `lsplt` inside the `keystore2` process. Intercepts `generateKey`, `importKey`, and `getKeyEntry` transactions before the HAL ever sees them.
|
|
||||||
|
|
||||||
🛡️ **Detector Resistant** — Per-UID rate limiting blocks DuckDetector-style keygen flooding. Oversized challenges rejected with real KeyMint error codes. Chain consistency verified byte-for-byte.
|
|
||||||
|
|
||||||
💾 **Key Persistence** — Generated keys survive reboots. Apps that store attestation keys (banking, biometrics) don't break after a restart.
|
|
||||||
|
|
||||||
🔧 **Drop-In Replacement** — Same config paths as TrickyStore (`/data/adb/tricky_store/`). Swap the module ZIP, keep your keybox and target list.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
**Core Attestation Engine**
|
|
||||||
- [x] **Full certificate chain generation** — leaf + intermediates + root, signed by your keybox
|
|
||||||
- [x] **Native Rust certgen** — `libcertgen.so` built with `ring`, `rsa`, and manual DER assembly
|
|
||||||
- [x] **BouncyCastle fallback** — unsupported curves (P-224, P-521, Curve25519) fall back to Java
|
|
||||||
- [x] **ASN.1 attestation extensions** — OID 1.3.6.1.4.1.11129.2.1.17 with all AOSP-specified tags
|
|
||||||
- [x] **Multi-keybox support** — different keybox files per app group via `target.txt`
|
|
||||||
|
|
||||||
**Interception Layer**
|
|
||||||
- [x] **Binder ioctl hook** — `lsplt` PLT hook on `libc.so` inside `keystore2` process
|
|
||||||
- [x] **generateKey / importKey / getKeyEntry** — all three transaction types intercepted
|
|
||||||
- [x] **256KB native payload cap** — oversized binder payloads bypass interception cleanly
|
|
||||||
- [x] **Challenge validation** — rejects >128-byte attestation challenges with `INVALID_INPUT_LENGTH`
|
|
||||||
|
|
||||||
**Hardening**
|
|
||||||
- [x] **Per-UID rate limiter** — 2 hardware keygens per 30s burst window, software fallback on overflow
|
|
||||||
- [x] **importKey eviction guard** — retained patch chains prevent generate-then-import cache attacks
|
|
||||||
- [x] **Key persistence** — file-backed storage with file-level locking, survives reboots and keybox rotations
|
|
||||||
- [x] **Global exception handler** — uncaught exceptions logged, daemon stays alive
|
|
||||||
|
|
||||||
**Configuration**
|
|
||||||
- [x] **Live config reload** — `FileObserver` watches all config files, changes apply immediately
|
|
||||||
- [x] **Security patch spoofing** — per-package `system`, `vendor`, `boot` patch levels with dynamic templates
|
|
||||||
- [x] **Lifecycle scripts** — KSU Action button clears key cache, uninstall removes all traces
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Requirements
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> TEESimulator requires root access and a valid `keybox.xml` for hardware-level attestation results. Without a keybox, the module generates software-level certificates that won't pass strict hardware attestation checks.
|
> A valid `keybox.xml` is required for hardware-level attestation. Without one, the module generates software-level certificates that won't pass strict hardware checks.
|
||||||
|
|
||||||
**You need:**
|
1. Android 10+
|
||||||
1. Android 10 or above
|
2. Root manager: KernelSU, Magisk, or APatch
|
||||||
2. A supported root manager (KernelSU, Magisk, or APatch)
|
3. `keybox.xml` at `/data/adb/tricky_store/keybox.xml`
|
||||||
3. A hardware-backed `keybox.xml` placed at `/data/adb/tricky_store/keybox.xml`
|
|
||||||
|
|
||||||
---
|
## Quick Start
|
||||||
|
|
||||||
## 📱 Compatibility
|
1. Download the latest ZIP from [Releases](https://github.com/Enginex0/TEESimulator-RS/releases)
|
||||||
|
2. Install via your root manager and reboot
|
||||||
|
3. Place your keybox at `/data/adb/tricky_store/keybox.xml`
|
||||||
|
4. Configure targets in `/data/adb/tricky_store/target.txt`
|
||||||
|
5. Verify with Play Integrity or Key Attestation Demo
|
||||||
|
|
||||||
### Root Managers
|
## Architecture
|
||||||
|
|
||||||
| Manager | Status | Notes |
|
**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).
|
||||||
|---|---|---|
|
|
||||||
| KernelSU | ✅ Tested | Full support including Action button and lifecycle scripts |
|
|
||||||
| Magisk | ✅ Supported | Standard module install |
|
|
||||||
| APatch | ✅ Supported | Standard module install |
|
|
||||||
|
|
||||||
### Tested Devices
|
**Binder Interception** — PLT hook on `ioctl()` in `libc.so` via `lsplt` inside `keystore2`. Intercepts `generateKey`, `importKey`, and `getKeyEntry` transactions.
|
||||||
|
|
||||||
| Device | Android | TEE | Status |
|
**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.
|
||||||
|---|---|---|---|
|
|
||||||
| Redmi 14C (2409BRN2CA) | 14 (SDK 34) | Beanpod KeyMaster | ✅ Daily driver |
|
|
||||||
|
|
||||||
> Tested against DuckDetector, Luna, Play Integrity, and Key Attestation Demo. If you test on a different device, [open an issue](https://github.com/Enginex0/TEESimulator/issues) with your results.
|
**Key Persistence** — Generated keys survive reboots. File-backed with file-level locking.
|
||||||
|
|
||||||
---
|
**Rate Limiting** — Per-UID hardware keygen cap (2/30s window, 2 concurrent). Overflow falls to software certs.
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## Configuration
|
||||||
|
|
||||||
1. **Download** the latest release ZIP from [Releases](https://github.com/Enginex0/TEESimulator/releases)
|
All config files live at `/data/adb/tricky_store/` and are hot-reloaded via `FileObserver`.
|
||||||
2. **Install** via your root manager (KSU / Magisk / APatch) and reboot
|
|
||||||
3. **Place your keybox** at `/data/adb/tricky_store/keybox.xml`
|
|
||||||
4. **Configure targets** in `/data/adb/tricky_store/target.txt`
|
|
||||||
5. **Verify** — check Play Integrity or run Key Attestation Demo
|
|
||||||
|
|
||||||
TEESimulator replaces TrickyStore, TrickyStoreOSS, and their forks. Existing config files are compatible.
|
### target.txt
|
||||||
|
|
||||||
---
|
Controls which apps get intercepted and the simulation mode.
|
||||||
|
|
||||||
## 🔨 Building from Source
|
| Suffix | Mode |
|
||||||
|
|--------|------|
|
||||||
|
| `!` | Force software key generation |
|
||||||
|
| `?` | Force leaf certificate patching (real TEE key, patched cert) |
|
||||||
|
| *(none)* | Automatic selection |
|
||||||
|
|
||||||
The CI workflow builds on every push to `main`. You can also build locally or trigger a build from your own fork.
|
Multi-keybox support via `[filename.xml]` headers:
|
||||||
|
|
||||||
**Prerequisites:** JDK 21, Android SDK/NDK 27, Rust stable with `aarch64-linux-android` target, `cargo-ndk`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/Enginex0/TEESimulator.git
|
|
||||||
cd TEESimulator
|
|
||||||
./gradlew zipRelease zipDebug
|
|
||||||
```
|
|
||||||
|
|
||||||
Output ZIPs land in `out/`. The Gradle build automatically invokes `cargo ndk` to cross-compile `libcertgen.so` before packaging.
|
|
||||||
|
|
||||||
To rebuild from a fork, push to `main` or use **Actions → Build → Run workflow**. The workflow installs all toolchains (Java, Rust, cargo-ndk, ccache) and uploads Release + Debug ZIPs as artifacts.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ Configuration
|
|
||||||
|
|
||||||
All configuration files live at `/data/adb/tricky_store/` and are monitored by `FileObserver` — changes take effect immediately without rebooting.
|
|
||||||
|
|
||||||
### The `keybox.xml` Root of Trust
|
|
||||||
|
|
||||||
This file provides the master cryptographic identity. It contains a private key and a hardware-backed certificate chain from a real device. TEESimulator signs all generated certificates with this key, making them appear legitimate to verifiers.
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<?xml version="1.0"?>
|
|
||||||
<AndroidAttestation>
|
|
||||||
<Keybox DeviceID="...">
|
|
||||||
<Key algorithm="ecdsa|rsa">
|
|
||||||
<PrivateKey format="pem">...</PrivateKey>
|
|
||||||
<CertificateChain>...</CertificateChain>
|
|
||||||
</Key>
|
|
||||||
</Keybox>
|
|
||||||
</AndroidAttestation>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Target Packages (`target.txt`)
|
|
||||||
|
|
||||||
Controls which apps get intercepted and what simulation mode to use.
|
|
||||||
|
|
||||||
#### Mode Suffixes
|
|
||||||
|
|
||||||
* **`!` → Force Generation** — Creates a complete software-based virtual key. Full TEE simulation.
|
|
||||||
* **`?` → Force Leaf Hacking** — Real TEE key generated, but its attestation certificate is intercepted and patched.
|
|
||||||
* **No symbol → Automatic** — Module selects the best mode for your device.
|
|
||||||
|
|
||||||
#### Multi-Keybox
|
|
||||||
|
|
||||||
Specify different keybox files for different app groups. Apps listed after a `[filename.xml]` line use that keybox. Apps before any declaration use the default `keybox.xml`.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
# Default keybox
|
|
||||||
com.google.android.gms!
|
com.google.android.gms!
|
||||||
io.github.vvb2060.keyattestation?
|
io.github.vvb2060.keyattestation?
|
||||||
|
|
||||||
# Switch to a different keybox for the following apps
|
|
||||||
[aosp_keybox.xml]
|
[aosp_keybox.xml]
|
||||||
com.google.android.gsf
|
com.google.android.gsf
|
||||||
|
|
||||||
# Another keybox
|
|
||||||
[demo_keybox.xml]
|
|
||||||
org.matrix.demo
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Security Patch Level (`security_patch.txt`)
|
### security_patch.txt
|
||||||
|
|
||||||
Configure the `osPatchLevel`, `vendorPatchLevel`, and `bootPatchLevel` reported in attestation certificates. This only affects attestation data — it does not change actual system properties.
|
Override patch levels reported in attestation certificates. Global defaults at top, per-package overrides with `[package.name]`.
|
||||||
|
|
||||||
#### Global and Per-Package
|
|
||||||
|
|
||||||
Settings at the top of the file are global defaults. Add `[package.name]` to override for specific apps.
|
|
||||||
|
|
||||||
#### Keys
|
|
||||||
|
|
||||||
| Key | Scope |
|
| Key | Scope |
|
||||||
|---|---|
|
|-----|-------|
|
||||||
| `system` | OS patch level |
|
| `system` | OS patch level |
|
||||||
| `vendor` | Vendor patch level |
|
| `vendor` | Vendor patch level |
|
||||||
| `boot` | Boot/kernel patch level |
|
| `boot` | Boot/kernel patch level |
|
||||||
| `all` | Shorthand — sets all three at once |
|
| `all` | Sets all three |
|
||||||
|
|
||||||
#### Special Keywords
|
Special values: `today`, `YYYY-MM-DD` templates, `no` (omit tag), `device_default`, `prop` (read from system property).
|
||||||
|
|
||||||
| Keyword | Effect |
|
|
||||||
|---|---|
|
|
||||||
| `today` | Current date, dynamically resolved on each attestation |
|
|
||||||
| `YYYY-MM-DD` templates | Semi-dynamic — `YYYY-MM-05` resolves to the 5th of the current month |
|
|
||||||
| `no` | Omit this patch level tag entirely from the attestation |
|
|
||||||
| `device_default` | Use the device's real hardware value |
|
|
||||||
| `prop` | Read from `ro.build.version.security_patch` (matches what detectors see via getprop) |
|
|
||||||
|
|
||||||
#### Example
|
|
||||||
|
|
||||||
```
|
```
|
||||||
# Global — default for all apps
|
|
||||||
system=YYYY-MM-05
|
system=YYYY-MM-05
|
||||||
vendor=device_default
|
vendor=device_default
|
||||||
boot=no
|
boot=no
|
||||||
|
|
||||||
# Override for GMS
|
|
||||||
[com.google.android.gms]
|
[com.google.android.gms]
|
||||||
system=2024-10-01
|
system=2025-10-01
|
||||||
|
|
||||||
# Custom config for a demo app
|
|
||||||
[org.matrix.demo]
|
|
||||||
all=2025-09-15
|
|
||||||
boot=device_default
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Building from Source
|
||||||
|
|
||||||
## 💬 Community
|
Prerequisites: JDK 21, Android SDK/NDK 27, Rust stable with `aarch64-linux-android` target, `cargo-ndk`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --recursive https://github.com/Enginex0/TEESimulator-RS.git
|
||||||
|
cd TEESimulator-RS
|
||||||
|
./gradlew zipRelease zipDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
Output ZIPs in `out/`. Gradle invokes `cargo ndk` automatically to cross-compile `libcertgen.so`.
|
||||||
|
|
||||||
|
Push to `main` or use **Actions > Build > Run workflow** to trigger CI.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
| Root Manager | Status |
|
||||||
|
|---|---|
|
||||||
|
| KernelSU | Tested (Action button + lifecycle scripts) |
|
||||||
|
| Magisk | Supported |
|
||||||
|
| APatch | Supported |
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://t.me/superpowers9">
|
<a href="https://t.me/superpowers9">
|
||||||
<img src="https://img.shields.io/badge/⚡_JOIN_THE_GRID-SuperPowers_Telegram-black?style=for-the-badge&logo=telegram&logoColor=cyan&labelColor=0d1117&color=00d4ff" alt="Telegram">
|
<img src="https://img.shields.io/badge/SuperPowers_Telegram-Join-blue?style=for-the-badge&logo=telegram" alt="Telegram">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
## Credits
|
||||||
|
|
||||||
## 🙏 Credits
|
- [JingMatrix](https://github.com/JingMatrix/TEESimulator) — original TEESimulator and interception architecture
|
||||||
|
- [5ec1cff](https://github.com/5ec1cff/TrickyStore) — TrickyStore, the project that pioneered keystore interception
|
||||||
|
- [LSPlt](https://github.com/LSPosed/LSPlt) — PLT hook library
|
||||||
|
- [ring](https://github.com/briansmith/ring) — Rust cryptography library
|
||||||
|
- [MhmRdd](https://github.com/MhmRdd) — AOSP compliance work via upstream [PR #157](https://github.com/JingMatrix/TEESimulator/pull/157)
|
||||||
|
- [fatalcoder524](https://github.com/fatalcoder524) — contributor and collaborator
|
||||||
|
- [huguangares](https://github.com/huguangares) — collaborator and tester
|
||||||
|
|
||||||
- **[JingMatrix](https://github.com/JingMatrix/TEESimulator)** — original author of TEESimulator and the interception architecture
|
## License
|
||||||
- **[5ec1cff](https://github.com/5ec1cff/TrickyStore)** — TrickyStore, the project that pioneered keystore interception on Android
|
|
||||||
- **[LSPlt](https://github.com/LSPosed/LSPlt)** — PLT hook library used for binder interception
|
|
||||||
- **[ring](https://github.com/briansmith/ring)** — Rust cryptography library powering native cert generation
|
|
||||||
|
|
||||||
---
|
[GNU General Public License v3.0](LICENSE)
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
This project is licensed under the [GNU General Public License v3.0](LICENSE).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<b>🔐 Because the best attestation is the one the TEE never generated.</b>
|
|
||||||
</p>
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java)
|
|||||||
|
|
||||||
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
|
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
|
||||||
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
|
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
|
||||||
val verName = "v4.7"
|
val verName = "v6.0.0"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "org.matrix.TEESimulator"
|
namespace = "org.matrix.TEESimulator"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ set(CMAKE_CXX_STANDARD 23)
|
|||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti")
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions")
|
||||||
|
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -DNDEBUG")
|
||||||
|
|
||||||
# LSPlt configuration
|
# LSPlt configuration
|
||||||
OPTION(LSPLT_BUILD_SHARED OFF)
|
OPTION(LSPLT_BUILD_SHARED OFF)
|
||||||
|
|||||||
@@ -235,19 +235,21 @@ class BinderInterceptor : public BBinder {
|
|||||||
struct RegistrationEntry {
|
struct RegistrationEntry {
|
||||||
wp<IBinder> target;
|
wp<IBinder> target;
|
||||||
sp<IBinder> callback_interface;
|
sp<IBinder> callback_interface;
|
||||||
|
std::vector<uint32_t> filtered_codes;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reader-Writer lock for the registry to allow concurrent reads (lookups)
|
|
||||||
mutable std::shared_mutex registry_mutex_;
|
mutable std::shared_mutex registry_mutex_;
|
||||||
std::map<wp<IBinder>, RegistrationEntry> registry_;
|
std::map<wp<IBinder>, RegistrationEntry> registry_;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
BinderInterceptor() = default;
|
BinderInterceptor() = default;
|
||||||
|
|
||||||
// Checks if a specific Binder instance is currently registered for interception
|
bool shouldIntercept(const wp<BBinder> &target, uint32_t code) const {
|
||||||
bool isBinderIntercepted(const wp<BBinder> &target) const {
|
|
||||||
std::shared_lock lock(registry_mutex_);
|
std::shared_lock lock(registry_mutex_);
|
||||||
return registry_.find(target) != registry_.end();
|
auto it = registry_.find(target);
|
||||||
|
if (it == registry_.end()) return false;
|
||||||
|
const auto &codes = it->second.filtered_codes;
|
||||||
|
return codes.empty() || std::find(codes.begin(), codes.end(), code) != codes.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main entry point for processing the "Man-in-the-Middle" logic
|
// Main entry point for processing the "Man-in-the-Middle" logic
|
||||||
@@ -348,16 +350,10 @@ static sp<BinderStub> g_stub_instance = nullptr;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr binder_size_t kMaxInterceptableDataSize = 256 * 1024;
|
|
||||||
|
|
||||||
void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
||||||
if (!txn_data || txn_data->target.ptr == 0)
|
if (!txn_data || txn_data->target.ptr == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Bypass interception for oversized payloads to prevent thread starvation from flood attacks
|
|
||||||
if (txn_data->data_size > kMaxInterceptableDataSize)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// AIDL methods use codes in [FIRST_CALL_TRANSACTION, LAST_CALL_TRANSACTION] (1..0x00ffffff).
|
// AIDL methods use codes in [FIRST_CALL_TRANSACTION, LAST_CALL_TRANSACTION] (1..0x00ffffff).
|
||||||
// System transactions (PING, INTERFACE, DUMP, SHELL_COMMAND) use codes above that range.
|
// System transactions (PING, INTERFACE, DUMP, SHELL_COMMAND) use codes above that range.
|
||||||
// Skip those — intercepting a ping adds measurable latency that timing detectors flag.
|
// Skip those — intercepting a ping adds measurable latency that timing detectors flag.
|
||||||
@@ -393,7 +389,7 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
|||||||
// This is safe because we are holding a strong reference.
|
// This is safe because we are holding a strong reference.
|
||||||
wp<BBinder> wp_target = target_binder_ptr;
|
wp<BBinder> wp_target = target_binder_ptr;
|
||||||
|
|
||||||
if (g_interceptor_instance->isBinderIntercepted(wp_target)) {
|
if (g_interceptor_instance->shouldIntercept(wp_target, txn_data->code)) {
|
||||||
info.transaction_code = txn_data->code;
|
info.transaction_code = txn_data->code;
|
||||||
info.target_binder = wp_target; // Assign the valid weak pointer
|
info.target_binder = wp_target; // Assign the valid weak pointer
|
||||||
hijack = true;
|
hijack = true;
|
||||||
@@ -432,44 +428,29 @@ void processBinderReadBuffer(const binder_write_read &bwr) {
|
|||||||
uintptr_t ptr = bwr.read_buffer;
|
uintptr_t ptr = bwr.read_buffer;
|
||||||
uintptr_t end = ptr + bwr.read_consumed;
|
uintptr_t end = ptr + bwr.read_consumed;
|
||||||
|
|
||||||
LOGV("[Hook] Processing Read Buffer: Size=%llu, Consumed=%llu", bwr.read_size, bwr.read_consumed);
|
|
||||||
|
|
||||||
while (ptr < end) {
|
while (ptr < end) {
|
||||||
// Ensure we can read at least the command header
|
|
||||||
if (end - ptr < sizeof(uint32_t))
|
if (end - ptr < sizeof(uint32_t))
|
||||||
break;
|
break;
|
||||||
|
|
||||||
uint32_t cmd = *reinterpret_cast<const uint32_t *>(ptr);
|
uint32_t cmd = *reinterpret_cast<const uint32_t *>(ptr);
|
||||||
ptr += sizeof(uint32_t);
|
ptr += sizeof(uint32_t);
|
||||||
|
|
||||||
// Calculate payload size from the ioctl command code
|
|
||||||
size_t cmd_size = _IOC_SIZE(cmd);
|
size_t cmd_size = _IOC_SIZE(cmd);
|
||||||
|
|
||||||
// Log the command using our generated to-string function
|
|
||||||
LOGV("[Driver -> User] Command: %s (0x%x), DataSize: %zu", getBinderReturnCommandName(cmd), cmd, cmd_size);
|
|
||||||
|
|
||||||
// Safety check: ensure the command's data does not exceed the buffer
|
|
||||||
if (ptr + cmd_size > end) {
|
if (ptr + cmd_size > end) {
|
||||||
LOGE("[Hook] Buffer overflow detected while parsing command %s", getBinderReturnCommandName(cmd));
|
LOGE("[Hook] Buffer overrun parsing command 0x%x", cmd);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We are primarily interested in BR_TRANSACTION commands to intercept
|
if (__builtin_expect(cmd == BR_TRANSACTION || cmd == BR_TRANSACTION_SEC_CTX, 0)) {
|
||||||
if (cmd == BR_TRANSACTION || cmd == BR_TRANSACTION_SEC_CTX) {
|
binder_transaction_data *txn;
|
||||||
binder_transaction_data *txn = nullptr;
|
|
||||||
|
|
||||||
if (cmd == BR_TRANSACTION_SEC_CTX) {
|
if (cmd == BR_TRANSACTION_SEC_CTX) {
|
||||||
// The data is wrapped in a secctx struct
|
txn = &reinterpret_cast<binder_transaction_data_secctx *>(ptr)->transaction_data;
|
||||||
auto *wrapper = reinterpret_cast<binder_transaction_data_secctx *>(ptr);
|
|
||||||
txn = &wrapper->transaction_data;
|
|
||||||
} else {
|
} else {
|
||||||
txn = reinterpret_cast<binder_transaction_data *>(ptr);
|
txn = reinterpret_cast<binder_transaction_data *>(ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
inspectAndRewriteTransaction(txn);
|
inspectAndRewriteTransaction(txn);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance pointer to the next command
|
|
||||||
ptr += cmd_size;
|
ptr += cmd_size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -489,13 +470,17 @@ int intercepted_ioctl(int fd, int request, ...) {
|
|||||||
// 1. Call original kernel ioctl to let the driver do its work
|
// 1. Call original kernel ioctl to let the driver do its work
|
||||||
int result = g_original_ioctl(fd, request, arg);
|
int result = g_original_ioctl(fd, request, arg);
|
||||||
|
|
||||||
// 2. After the call returns, check if it was a BINDER_WRITE_READ and if it succeeded
|
|
||||||
if (result >= 0 && request == BINDER_WRITE_READ && arg != nullptr) {
|
if (result >= 0 && request == BINDER_WRITE_READ && arg != nullptr) {
|
||||||
const auto *bwr = static_cast<const binder_write_read *>(arg);
|
const auto *bwr = static_cast<const binder_write_read *>(arg);
|
||||||
|
// Fast reject: only enter the parser if the buffer could contain a BR_TRANSACTION.
|
||||||
// We only care about data read FROM the driver (i.e., incoming commands)
|
// Pings, ref ops, and looper management never produce BR_TRANSACTION, so scanning
|
||||||
if (bwr->read_consumed > 0) {
|
// their buffers is pure overhead (~2-5us per ioctl in debug builds).
|
||||||
processBinderReadBuffer(*bwr);
|
if (bwr->read_consumed >= sizeof(uint32_t)) {
|
||||||
|
uint32_t first_cmd = *reinterpret_cast<const uint32_t *>(bwr->read_buffer);
|
||||||
|
if (first_cmd == BR_TRANSACTION || first_cmd == BR_TRANSACTION_SEC_CTX
|
||||||
|
|| bwr->read_consumed > sizeof(uint32_t) + _IOC_SIZE(first_cmd)) {
|
||||||
|
processBinderReadBuffer(*bwr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,18 +523,29 @@ status_t BinderInterceptor::handleRegister(const Parcel &data) {
|
|||||||
if (data.readStrongBinder(&callback) != OK || !callback)
|
if (data.readStrongBinder(&callback) != OK || !callback)
|
||||||
return BAD_VALUE;
|
return BAD_VALUE;
|
||||||
|
|
||||||
// We can only intercept local Binders (BBinder), not remote proxies (BpBinder)
|
|
||||||
if (target->localBinder() == nullptr) {
|
if (target->localBinder() == nullptr) {
|
||||||
LOGE("Cannot intercept remote binder proxies.");
|
LOGE("Cannot intercept remote binder proxies.");
|
||||||
return BAD_TYPE;
|
return BAD_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<uint32_t> codes;
|
||||||
|
int32_t code_count = 0;
|
||||||
|
if (data.dataAvail() >= sizeof(int32_t) && data.readInt32(&code_count) == OK && code_count > 0) {
|
||||||
|
codes.reserve(code_count);
|
||||||
|
for (int32_t i = 0; i < code_count; i++) {
|
||||||
|
uint32_t c = 0;
|
||||||
|
if (data.readUint32(&c) == OK) codes.push_back(c);
|
||||||
|
}
|
||||||
|
LOGI("Interceptor registered for binder %p with %zu filtered codes", target.get(), codes.size());
|
||||||
|
} else {
|
||||||
|
LOGI("Interceptor registered for binder %p (all codes)", target.get());
|
||||||
|
}
|
||||||
|
|
||||||
wp<IBinder> weak_target = target;
|
wp<IBinder> weak_target = target;
|
||||||
|
|
||||||
std::unique_lock lock(registry_mutex_);
|
std::unique_lock lock(registry_mutex_);
|
||||||
registry_[weak_target] = {weak_target, callback};
|
registry_[weak_target] = {weak_target, callback, std::move(codes)};
|
||||||
|
|
||||||
LOGI("Interceptor registered for binder %p", target.get());
|
|
||||||
return OK;
|
return OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,8 +33,11 @@ object App {
|
|||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
SystemLogger.info("Welcome to TEESimulator!")
|
SystemLogger.info("Welcome to TEESimulator!")
|
||||||
|
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||||
|
SystemLogger.error("Uncaught exception on ${thread.name}", throwable)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Initialize the Android framework environment
|
|
||||||
prepareEnvironment()
|
prepareEnvironment()
|
||||||
// Initialize and start the appropriate keystore interceptors.
|
// Initialize and start the appropriate keystore interceptors.
|
||||||
initializeInterceptors()
|
initializeInterceptors()
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ package org.matrix.TEESimulator.attestation
|
|||||||
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import java.nio.ByteBuffer
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
import org.bouncycastle.asn1.ASN1Boolean
|
import org.bouncycastle.asn1.ASN1Boolean
|
||||||
import org.bouncycastle.asn1.ASN1Encodable
|
import org.bouncycastle.asn1.ASN1Encodable
|
||||||
import org.bouncycastle.asn1.ASN1Enumerated
|
import org.bouncycastle.asn1.ASN1Enumerated
|
||||||
@@ -40,11 +43,12 @@ object AttestationBuilder {
|
|||||||
securityLevel: Int,
|
securityLevel: Int,
|
||||||
): Extension {
|
): Extension {
|
||||||
val keyDescription = buildKeyDescription(params, uid, securityLevel)
|
val keyDescription = buildKeyDescription(params, uid, securityLevel)
|
||||||
var formattedString =
|
SystemLogger.verbose {
|
||||||
keyDescription.joinToString(separator = ", ") {
|
val formattedString = keyDescription.joinToString(separator = ", ") {
|
||||||
AttestationPatcher.formatAsn1Primitive(it)
|
AttestationPatcher.formatAsn1Primitive(it)
|
||||||
}
|
}
|
||||||
SystemLogger.verbose("Forged attestation data: ${formattedString}")
|
"Forged attestation data: $formattedString"
|
||||||
|
}
|
||||||
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
|
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,33 +131,59 @@ object AttestationBuilder {
|
|||||||
return properties
|
return properties
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */
|
|
||||||
private fun buildKeyDescription(
|
private fun buildKeyDescription(
|
||||||
params: KeyMintAttestation,
|
params: KeyMintAttestation,
|
||||||
uid: Int,
|
uid: Int,
|
||||||
securityLevel: Int,
|
securityLevel: Int,
|
||||||
): ASN1Sequence {
|
): ASN1Sequence {
|
||||||
|
val creationTime = System.currentTimeMillis()
|
||||||
val teeEnforced = buildTeeEnforcedList(params, uid, securityLevel)
|
val teeEnforced = buildTeeEnforcedList(params, uid, securityLevel)
|
||||||
val softwareEnforced = buildSoftwareEnforcedList(uid, securityLevel)
|
val softwareEnforced = buildSoftwareEnforcedList(params, uid, securityLevel, creationTime)
|
||||||
|
|
||||||
|
val uniqueId =
|
||||||
|
if (params.includeUniqueId == true && params.attestationChallenge != null) {
|
||||||
|
computeUniqueId(creationTime, createApplicationId(uid).octets)
|
||||||
|
} else {
|
||||||
|
ByteArray(0)
|
||||||
|
}
|
||||||
|
|
||||||
val fields =
|
val fields =
|
||||||
arrayOf(
|
arrayOf(
|
||||||
ASN1Integer(
|
ASN1Integer(AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()),
|
||||||
AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()
|
ASN1Enumerated(securityLevel),
|
||||||
), // attestationVersion
|
ASN1Integer(AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()),
|
||||||
ASN1Enumerated(securityLevel), // attestationSecurityLevel
|
ASN1Enumerated(securityLevel),
|
||||||
ASN1Integer(
|
DEROctetString(params.attestationChallenge ?: ByteArray(0)),
|
||||||
AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()
|
DEROctetString(uniqueId),
|
||||||
), // keymasterVersion
|
|
||||||
ASN1Enumerated(securityLevel), // keymasterSecurityLevel
|
|
||||||
DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge
|
|
||||||
DEROctetString(ByteArray(0)), // uniqueId
|
|
||||||
softwareEnforced,
|
softwareEnforced,
|
||||||
teeEnforced,
|
teeEnforced,
|
||||||
)
|
)
|
||||||
return DERSequence(fields)
|
return DERSequence(fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun computeUniqueId(creationTimeMs: Long, aaidDer: ByteArray): ByteArray {
|
||||||
|
val temporalCounter = creationTimeMs / 2592000000L
|
||||||
|
val message =
|
||||||
|
ByteBuffer.allocate(8 + aaidDer.size + 1)
|
||||||
|
.putLong(temporalCounter)
|
||||||
|
.put(aaidDer)
|
||||||
|
.put(0x00)
|
||||||
|
.array()
|
||||||
|
val mac = Mac.getInstance("HmacSHA256")
|
||||||
|
mac.init(SecretKeySpec(hbk, "HmacSHA256"))
|
||||||
|
return mac.doFinal(message).copyOf(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val hbk: ByteArray by lazy {
|
||||||
|
val file = java.io.File(ConfigurationManager.CONFIG_PATH, "hbk")
|
||||||
|
if (file.exists() && file.length() == 32L) {
|
||||||
|
file.readBytes()
|
||||||
|
} else {
|
||||||
|
SystemLogger.warning("hbk not found, generating ephemeral HBK.")
|
||||||
|
ByteArray(32).also { java.security.SecureRandom().nextBytes(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */
|
/** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */
|
||||||
private fun buildTeeEnforcedList(
|
private fun buildTeeEnforcedList(
|
||||||
params: KeyMintAttestation,
|
params: KeyMintAttestation,
|
||||||
@@ -194,6 +224,16 @@ object AttestationBuilder {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.blockMode.isNotEmpty()) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_BLOCK_MODE,
|
||||||
|
DERSet(params.blockMode.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (params.padding.isNotEmpty()) {
|
if (params.padding.isNotEmpty()) {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
@@ -214,14 +254,61 @@ object AttestationBuilder {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel)
|
||||||
|
|
||||||
|
if (params.rsaOaepMgfDigest.isNotEmpty() && attestVersion >= 100) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_RSA_OAEP_MGF_DIGEST,
|
||||||
|
DERSet(params.rsaOaepMgfDigest.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.rollbackResistance == true && attestVersion >= 3) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_ROLLBACK_RESISTANCE, DERNull.INSTANCE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.earlyBootOnly == true && attestVersion >= 4) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_EARLY_BOOT_ONLY, DERNull.INSTANCE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.noAuthRequired == true) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.allowWhileOnBody == true) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_ALLOW_WHILE_ON_BODY, DERNull.INSTANCE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.trustedUserPresenceRequired == true && attestVersion >= 3) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_TRUSTED_USER_PRESENCE_REQUIRED, DERNull.INSTANCE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.trustedConfirmationRequired == true && attestVersion >= 3) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_TRUSTED_CONFIRMATION_REQUIRED, DERNull.INSTANCE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
list.addAll(
|
list.addAll(
|
||||||
listOf(
|
listOf(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE),
|
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
true,
|
true,
|
||||||
AttestationConstants.TAG_ORIGIN,
|
AttestationConstants.TAG_ORIGIN,
|
||||||
ASN1Integer(0L),
|
ASN1Integer((params.origin ?: 0).toLong()),
|
||||||
), // KeyOrigin.GENERATED
|
),
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
true,
|
true,
|
||||||
AttestationConstants.TAG_ROOT_OF_TRUST,
|
AttestationConstants.TAG_ROOT_OF_TRUST,
|
||||||
@@ -325,20 +412,32 @@ object AttestationBuilder {
|
|||||||
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
|
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
|
||||||
* Keystore.
|
* Keystore.
|
||||||
*/
|
*/
|
||||||
private fun buildSoftwareEnforcedList(uid: Int, securityLevel: Int): DERSequence {
|
private fun buildSoftwareEnforcedList(
|
||||||
val list =
|
params: KeyMintAttestation,
|
||||||
mutableListOf<ASN1Encodable>(
|
uid: Int,
|
||||||
DERTaggedObject(
|
securityLevel: Int,
|
||||||
true,
|
creationTimeMs: Long = System.currentTimeMillis(),
|
||||||
AttestationConstants.TAG_CREATION_DATETIME,
|
): DERSequence {
|
||||||
ASN1Integer(System.currentTimeMillis()),
|
val list = mutableListOf<ASN1Encodable>()
|
||||||
),
|
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_CREATION_DATETIME,
|
||||||
|
ASN1Integer(creationTimeMs),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (params.attestationChallenge != null) {
|
||||||
|
list.add(
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
true,
|
true,
|
||||||
AttestationConstants.TAG_ATTESTATION_APPLICATION_ID,
|
AttestationConstants.TAG_ATTESTATION_APPLICATION_ID,
|
||||||
createApplicationId(uid),
|
createApplicationId(uid),
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) {
|
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
@@ -348,7 +447,39 @@ object AttestationBuilder {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return DERSequence(list.toTypedArray())
|
|
||||||
|
if (params.callerNonce == true) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_CALLER_NONCE, DERNull.INSTANCE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
params.activeDateTime?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
params.originationExpireDateTime?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_ORIGINATION_EXPIRE_DATETIME, ASN1Integer(it.time))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
params.usageExpireDateTime?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_USAGE_EXPIRE_DATETIME, ASN1Integer(it.time))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
params.usageCountLimit?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_USAGE_COUNT_LIMIT, ASN1Integer(it.toLong()))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (params.unlockedDeviceRequired == true) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_UNLOCKED_DEVICE_REQUIRED, DERNull.INSTANCE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -376,6 +507,11 @@ object AttestationBuilder {
|
|||||||
*/
|
*/
|
||||||
@Throws(Throwable::class)
|
@Throws(Throwable::class)
|
||||||
internal fun createApplicationId(uid: Int): DEROctetString {
|
internal fun createApplicationId(uid: Int): DEROctetString {
|
||||||
|
val appUid = uid % 100000
|
||||||
|
if (appUid == 0 || appUid == 1000) {
|
||||||
|
return buildApplicationIdDer(listOf("AndroidSystem" to 1L), emptySet())
|
||||||
|
}
|
||||||
|
|
||||||
val pm =
|
val pm =
|
||||||
ConfigurationManager.getPackageManager()
|
ConfigurationManager.getPackageManager()
|
||||||
?: throw IllegalStateException("PackageManager not found!")
|
?: throw IllegalStateException("PackageManager not found!")
|
||||||
@@ -383,12 +519,11 @@ object AttestationBuilder {
|
|||||||
pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid")
|
pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid")
|
||||||
|
|
||||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||||
val packageInfoList = mutableListOf<DERSequence>()
|
val packageInfoList = mutableListOf<Pair<String, Long>>()
|
||||||
val signatureDigests = mutableSetOf<Digest>()
|
val signatureDigests = mutableSetOf<Digest>()
|
||||||
|
|
||||||
// Process all packages associated with the UID in a single loop.
|
val userId = uid / 100000
|
||||||
packages.forEach { packageName ->
|
packages.forEach { packageName ->
|
||||||
val userId = uid / 100000
|
|
||||||
val packageInfo =
|
val packageInfo =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
pm.getPackageInfo(
|
pm.getPackageInfo(
|
||||||
@@ -401,34 +536,36 @@ object AttestationBuilder {
|
|||||||
pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId)
|
pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add package information (name and version code) to our list.
|
packageInfoList.add(packageInfo.packageName to packageInfo.longVersionCode)
|
||||||
packageInfoList.add(
|
|
||||||
DERSequence(
|
|
||||||
arrayOf(
|
|
||||||
DEROctetString(packageInfo.packageName.toByteArray(StandardCharsets.UTF_8)),
|
|
||||||
ASN1Integer(packageInfo.longVersionCode),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Collect unique signature digests from the signing history.
|
|
||||||
packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature ->
|
packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature ->
|
||||||
val digest = sha256.digest(signature.toByteArray())
|
signatureDigests.add(Digest(sha256.digest(signature.toByteArray())))
|
||||||
signatureDigests.add(Digest(digest))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The application ID is a sequence of two sets:
|
return buildApplicationIdDer(packageInfoList, signatureDigests)
|
||||||
// 1. A set of package information (name and version).
|
}
|
||||||
// 2. A set of SHA-256 digests of the signing certificates.
|
|
||||||
|
private fun buildApplicationIdDer(
|
||||||
|
packages: List<Pair<String, Long>>,
|
||||||
|
digests: Set<Digest>,
|
||||||
|
): DEROctetString {
|
||||||
|
val packageInfoList =
|
||||||
|
packages.map { (name, version) ->
|
||||||
|
DERSequence(
|
||||||
|
arrayOf(
|
||||||
|
DEROctetString(name.toByteArray(StandardCharsets.UTF_8)),
|
||||||
|
ASN1Integer(version),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
val applicationIdSequence =
|
val applicationIdSequence =
|
||||||
DERSequence(
|
DERSequence(
|
||||||
arrayOf(
|
arrayOf(
|
||||||
DERSet(packageInfoList.toTypedArray()),
|
DERSet(packageInfoList.toTypedArray()),
|
||||||
DERSet(signatureDigests.map { DEROctetString(it.digest) }.toTypedArray()),
|
DERSet(digests.map { DEROctetString(it.digest) }.toTypedArray()),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return DEROctetString(applicationIdSequence.encoded)
|
return DEROctetString(applicationIdSequence.encoded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,9 +44,11 @@ object AttestationConstants {
|
|||||||
|
|
||||||
// --- Key Lifetime and Usage Control ---
|
// --- Key Lifetime and Usage Control ---
|
||||||
const val TAG_ROLLBACK_RESISTANCE = 303
|
const val TAG_ROLLBACK_RESISTANCE = 303
|
||||||
|
const val TAG_EARLY_BOOT_ONLY = 305
|
||||||
const val TAG_ACTIVE_DATETIME = 400
|
const val TAG_ACTIVE_DATETIME = 400
|
||||||
const val TAG_ORIGINATION_EXPIRE_DATETIME = 401
|
const val TAG_ORIGINATION_EXPIRE_DATETIME = 401
|
||||||
const val TAG_USAGE_EXPIRE_DATETIME = 402
|
const val TAG_USAGE_EXPIRE_DATETIME = 402
|
||||||
|
const val TAG_MAX_BOOT_LEVEL = 403
|
||||||
const val TAG_MAX_USES_PER_BOOT = 404
|
const val TAG_MAX_USES_PER_BOOT = 404
|
||||||
const val TAG_USAGE_COUNT_LIMIT = 405
|
const val TAG_USAGE_COUNT_LIMIT = 405
|
||||||
|
|
||||||
@@ -56,6 +58,10 @@ object AttestationConstants {
|
|||||||
const val TAG_NO_AUTH_REQUIRED = 503
|
const val TAG_NO_AUTH_REQUIRED = 503
|
||||||
const val TAG_USER_AUTH_TYPE = 504
|
const val TAG_USER_AUTH_TYPE = 504
|
||||||
const val TAG_AUTH_TIMEOUT = 505
|
const val TAG_AUTH_TIMEOUT = 505
|
||||||
|
const val TAG_ALLOW_WHILE_ON_BODY = 506
|
||||||
|
const val TAG_TRUSTED_USER_PRESENCE_REQUIRED = 507
|
||||||
|
const val TAG_TRUSTED_CONFIRMATION_REQUIRED = 508
|
||||||
|
const val TAG_UNLOCKED_DEVICE_REQUIRED = 509
|
||||||
|
|
||||||
// --- Attestation and Application Info ---
|
// --- Attestation and Application Info ---
|
||||||
const val TAG_APPLICATION_ID = 601
|
const val TAG_APPLICATION_ID = 601
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
|||||||
import org.matrix.TEESimulator.pki.KeyBox
|
import org.matrix.TEESimulator.pki.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.
|
||||||
@@ -36,7 +37,12 @@ object AttestationPatcher {
|
|||||||
* @return A new, cryptographically valid, patched certificate chain. Returns the original chain
|
* @return A new, cryptographically valid, patched certificate chain. Returns the original chain
|
||||||
* on any failure.
|
* on any failure.
|
||||||
*/
|
*/
|
||||||
fun patchCertificateChain(originalChain: Array<Certificate>?, uid: Int): Array<Certificate> {
|
fun patchCertificateChain(
|
||||||
|
originalChain: Array<Certificate>?,
|
||||||
|
uid: Int,
|
||||||
|
notBefore: Date? = null,
|
||||||
|
notAfter: Date? = null,
|
||||||
|
): Array<Certificate> {
|
||||||
if (originalChain.isNullOrEmpty()) {
|
if (originalChain.isNullOrEmpty()) {
|
||||||
SystemLogger.error("Attempted to patch a null or empty certificate chain for UID $uid.")
|
SystemLogger.error("Attempted to patch a null or empty certificate chain for UID $uid.")
|
||||||
return originalChain ?: emptyArray()
|
return originalChain ?: emptyArray()
|
||||||
@@ -63,6 +69,8 @@ object AttestationPatcher {
|
|||||||
keybox,
|
keybox,
|
||||||
originalLeaf.sigAlgName,
|
originalLeaf.sigAlgName,
|
||||||
uid,
|
uid,
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
)
|
)
|
||||||
|
|
||||||
// 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's
|
// 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's
|
||||||
@@ -111,17 +119,27 @@ object AttestationPatcher {
|
|||||||
keybox: KeyBox,
|
keybox: KeyBox,
|
||||||
sigAlgName: String,
|
sigAlgName: String,
|
||||||
uid: Int,
|
uid: Int,
|
||||||
|
notBefore: Date? = null,
|
||||||
|
notAfter: Date? = null,
|
||||||
): Certificate {
|
): Certificate {
|
||||||
// The issuer of our new leaf is the subject of the first certificate in our custom keybox
|
// The issuer of our new leaf is the subject of the first certificate in our custom keybox
|
||||||
// chain.
|
// chain.
|
||||||
val newIssuer = X509CertificateHolder(keybox.certificates[0].encoded).subject
|
val newIssuer = X509CertificateHolder(keybox.certificates[0].encoded).subject
|
||||||
|
|
||||||
|
val effectiveNotBefore = notBefore ?: originalLeafHolder.notBefore
|
||||||
|
val effectiveNotAfter = notAfter ?: originalLeafHolder.notAfter
|
||||||
|
if (notBefore != null || notAfter != null) {
|
||||||
|
SystemLogger.debug(
|
||||||
|
"Overriding cert dates: notBefore=${effectiveNotBefore} (was ${originalLeafHolder.notBefore}), notAfter=${effectiveNotAfter} (was ${originalLeafHolder.notAfter})"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val builder =
|
val builder =
|
||||||
X509v3CertificateBuilder(
|
X509v3CertificateBuilder(
|
||||||
newIssuer,
|
newIssuer,
|
||||||
originalLeafHolder.serialNumber,
|
originalLeafHolder.serialNumber,
|
||||||
originalLeafHolder.notBefore,
|
effectiveNotBefore,
|
||||||
originalLeafHolder.notAfter,
|
effectiveNotAfter,
|
||||||
originalLeafHolder.subject,
|
originalLeafHolder.subject,
|
||||||
originalLeafHolder.subjectPublicKeyInfo,
|
originalLeafHolder.subjectPublicKeyInfo,
|
||||||
)
|
)
|
||||||
@@ -146,7 +164,7 @@ object AttestationPatcher {
|
|||||||
// Log the signature of the newly created certificate to observe its non-deterministic
|
// Log the signature of the newly created certificate to observe its non-deterministic
|
||||||
// nature.
|
// nature.
|
||||||
val signatureBytes = (newCertificate as X509Certificate).signature
|
val signatureBytes = (newCertificate as X509Certificate).signature
|
||||||
SystemLogger.verbose("Signature of patched leaf cert: ${signatureBytes.toHex()}")
|
SystemLogger.verbose { "Signature of patched leaf cert: ${signatureBytes.toHex()}" }
|
||||||
|
|
||||||
return newCertificate
|
return newCertificate
|
||||||
}
|
}
|
||||||
@@ -268,8 +286,10 @@ object AttestationPatcher {
|
|||||||
private fun createPatchedAttestationExtension(parsed: ParsedAttestation, uid: Int): Extension {
|
private fun createPatchedAttestationExtension(parsed: ParsedAttestation, uid: Int): Extension {
|
||||||
val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed
|
val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed
|
||||||
|
|
||||||
var formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
SystemLogger.verbose {
|
||||||
SystemLogger.verbose("Original attestation data: ${formattedString}")
|
val formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
||||||
|
"Original attestation data: $formattedString"
|
||||||
|
}
|
||||||
|
|
||||||
// Build the new Root of Trust and add/replace it in the map.
|
// Build the new Root of Trust and add/replace it in the map.
|
||||||
val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust)
|
val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust)
|
||||||
@@ -296,8 +316,10 @@ 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)
|
||||||
formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
SystemLogger.verbose {
|
||||||
SystemLogger.verbose("Patched attestation data: ${formattedString}")
|
val formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
||||||
|
"Patched attestation data: $formattedString"
|
||||||
|
}
|
||||||
val patchedOctets = DEROctetString(patchedSequence)
|
val patchedOctets = DEROctetString(patchedSequence)
|
||||||
|
|
||||||
return Extension(ATTESTATION_OID, false, patchedOctets)
|
return Extension(ATTESTATION_OID, false, patchedOctets)
|
||||||
|
|||||||
@@ -148,11 +148,12 @@ 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)
|
||||||
var formattedString =
|
SystemLogger.verbose {
|
||||||
keyDescriptionSeq.joinToString(separator = ", ") {
|
val formattedString = keyDescriptionSeq.joinToString(separator = ", ") {
|
||||||
AttestationPatcher.formatAsn1Primitive(it)
|
AttestationPatcher.formatAsn1Primitive(it)
|
||||||
}
|
}
|
||||||
SystemLogger.verbose("Cached attestation data: ${formattedString}")
|
"Cached attestation data: $formattedString"
|
||||||
|
}
|
||||||
val fields = keyDescriptionSeq.toArray()
|
val fields = keyDescriptionSeq.toArray()
|
||||||
|
|
||||||
val attestVersion =
|
val attestVersion =
|
||||||
|
|||||||
@@ -41,13 +41,30 @@ data class KeyMintAttestation(
|
|||||||
val manufacturer: ByteArray?,
|
val manufacturer: ByteArray?,
|
||||||
val model: ByteArray?,
|
val model: ByteArray?,
|
||||||
val secondImei: ByteArray?,
|
val secondImei: ByteArray?,
|
||||||
|
val activeDateTime: Date?,
|
||||||
|
val originationExpireDateTime: Date?,
|
||||||
|
val usageExpireDateTime: Date?,
|
||||||
|
val usageCountLimit: Int?,
|
||||||
|
val callerNonce: Boolean?,
|
||||||
|
val nonce: ByteArray?,
|
||||||
|
val unlockedDeviceRequired: Boolean?,
|
||||||
|
val includeUniqueId: Boolean?,
|
||||||
|
val rollbackResistance: Boolean?,
|
||||||
|
val earlyBootOnly: Boolean?,
|
||||||
|
val allowWhileOnBody: Boolean?,
|
||||||
|
val trustedUserPresenceRequired: Boolean?,
|
||||||
|
val trustedConfirmationRequired: Boolean?,
|
||||||
|
val noAuthRequired: Boolean?,
|
||||||
|
val maxUsesPerBoot: Int?,
|
||||||
|
val maxBootLevel: Int?,
|
||||||
|
val minMacLength: Int?,
|
||||||
|
val rsaOaepMgfDigest: List<Int>,
|
||||||
) {
|
) {
|
||||||
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */
|
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */
|
||||||
constructor(
|
constructor(
|
||||||
params: Array<KeyParameter>
|
params: Array<KeyParameter>
|
||||||
) : this(
|
) : this(
|
||||||
// AOSP: [key_param(tag = KEY_SIZE, field = Integer)]
|
keySize = params.findInteger(Tag.KEY_SIZE) ?: params.deriveKeySizeFromCurve(),
|
||||||
keySize = params.findInteger(Tag.KEY_SIZE) ?: 0,
|
|
||||||
|
|
||||||
// AOSP: [key_param(tag = ALGORITHM, field = Algorithm)]
|
// AOSP: [key_param(tag = ALGORITHM, field = Algorithm)]
|
||||||
algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0,
|
algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0,
|
||||||
@@ -100,6 +117,24 @@ data class KeyMintAttestation(
|
|||||||
manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER),
|
manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER),
|
||||||
model = params.findBlob(Tag.ATTESTATION_ID_MODEL),
|
model = params.findBlob(Tag.ATTESTATION_ID_MODEL),
|
||||||
secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI),
|
secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI),
|
||||||
|
activeDateTime = params.findDate(Tag.ACTIVE_DATETIME),
|
||||||
|
originationExpireDateTime = params.findDate(Tag.ORIGINATION_EXPIRE_DATETIME),
|
||||||
|
usageExpireDateTime = params.findDate(Tag.USAGE_EXPIRE_DATETIME),
|
||||||
|
usageCountLimit = params.findInteger(Tag.USAGE_COUNT_LIMIT),
|
||||||
|
callerNonce = params.findBoolean(Tag.CALLER_NONCE),
|
||||||
|
nonce = params.findBlob(Tag.NONCE),
|
||||||
|
unlockedDeviceRequired = params.findBoolean(Tag.UNLOCKED_DEVICE_REQUIRED),
|
||||||
|
includeUniqueId = params.findBoolean(Tag.INCLUDE_UNIQUE_ID),
|
||||||
|
rollbackResistance = params.findBoolean(Tag.ROLLBACK_RESISTANCE),
|
||||||
|
earlyBootOnly = params.findBoolean(Tag.EARLY_BOOT_ONLY),
|
||||||
|
allowWhileOnBody = params.findBoolean(Tag.ALLOW_WHILE_ON_BODY),
|
||||||
|
trustedUserPresenceRequired = params.findBoolean(Tag.TRUSTED_USER_PRESENCE_REQUIRED),
|
||||||
|
trustedConfirmationRequired = params.findBoolean(Tag.TRUSTED_CONFIRMATION_REQUIRED),
|
||||||
|
noAuthRequired = params.findBoolean(Tag.NO_AUTH_REQUIRED),
|
||||||
|
maxUsesPerBoot = params.findInteger(Tag.MAX_USES_PER_BOOT),
|
||||||
|
maxBootLevel = params.findInteger(Tag.MAX_BOOT_LEVEL),
|
||||||
|
minMacLength = params.findInteger(Tag.MIN_MAC_LENGTH),
|
||||||
|
rsaOaepMgfDigest = params.findAllDigests(Tag.RSA_OAEP_MGF_DIGEST),
|
||||||
) {
|
) {
|
||||||
// Log all parsed parameters for debugging purposes.
|
// Log all parsed parameters for debugging purposes.
|
||||||
params.forEach { KeyMintParameterLogger.logParameter(it) }
|
params.forEach { KeyMintParameterLogger.logParameter(it) }
|
||||||
@@ -156,6 +191,21 @@ private fun Array<KeyParameter>.findAllKeyPurpose(tag: Int): List<Int> =
|
|||||||
private fun Array<KeyParameter>.findAllDigests(tag: Int): List<Int> =
|
private fun Array<KeyParameter>.findAllDigests(tag: Int): List<Int> =
|
||||||
this.filter { it.tag == tag }.map { it.value.digest }
|
this.filter { it.tag == tag }.map { it.value.digest }
|
||||||
|
|
||||||
|
private fun Array<KeyParameter>.findBoolean(tag: Int): Boolean? =
|
||||||
|
if (this.any { it.tag == tag }) true else null
|
||||||
|
|
||||||
|
private fun Array<KeyParameter>.deriveKeySizeFromCurve(): Int {
|
||||||
|
val curveId = this.find { it.tag == Tag.EC_CURVE }?.value?.ecCurve ?: return 0
|
||||||
|
return when (curveId) {
|
||||||
|
EcCurve.P_224 -> 224
|
||||||
|
EcCurve.P_256 -> 256
|
||||||
|
EcCurve.P_384 -> 384
|
||||||
|
EcCurve.P_521 -> 521
|
||||||
|
EcCurve.CURVE_25519 -> 256
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derives the EC Curve name. Logic: Checks specific EC_CURVE tag first (field=EcCurve), falls back
|
* Derives the EC Curve name. Logic: Checks specific EC_CURVE tag first (field=EcCurve), falls back
|
||||||
* to KEY_SIZE (field=Integer).
|
* to KEY_SIZE (field=Integer).
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ object ConfigurationManager {
|
|||||||
// --- Configuration Paths ---
|
// --- Configuration Paths ---
|
||||||
const val CONFIG_PATH = "/data/adb/tricky_store"
|
const val CONFIG_PATH = "/data/adb/tricky_store"
|
||||||
private const val TARGET_PACKAGES_FILE = "target.txt"
|
private const val TARGET_PACKAGES_FILE = "target.txt"
|
||||||
private const val TEE_STATUS_FILE = "tee_status.txt"
|
|
||||||
private const val PATCH_LEVEL_FILE = "security_patch.txt"
|
private const val PATCH_LEVEL_FILE = "security_patch.txt"
|
||||||
private const val DEFAULT_KEYBOX_FILE = "keybox.xml"
|
private const val DEFAULT_KEYBOX_FILE = "keybox.xml"
|
||||||
private val configRoot = File(CONFIG_PATH)
|
private val configRoot = File(CONFIG_PATH)
|
||||||
@@ -39,7 +38,6 @@ object ConfigurationManager {
|
|||||||
// --- In-Memory Configuration State ---
|
// --- In-Memory Configuration State ---
|
||||||
@Volatile private var packageModes = mapOf<String, Mode>()
|
@Volatile private var packageModes = mapOf<String, Mode>()
|
||||||
@Volatile private var packageKeyboxes = mapOf<String, String>()
|
@Volatile private var packageKeyboxes = mapOf<String, String>()
|
||||||
@Volatile private var isTeeBroken: Boolean? = null
|
|
||||||
@Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null
|
@Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null
|
||||||
@Volatile private var packagePatchLevels = mapOf<String, CustomPatchLevel>()
|
@Volatile private var packagePatchLevels = mapOf<String, CustomPatchLevel>()
|
||||||
|
|
||||||
@@ -68,8 +66,6 @@ object ConfigurationManager {
|
|||||||
// Initial load of all configuration files.
|
// Initial load of all configuration files.
|
||||||
loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE))
|
loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE))
|
||||||
loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE))
|
loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE))
|
||||||
storeTeeStatus() // Check and store the current TEE status.
|
|
||||||
|
|
||||||
// Start watching for any subsequent file changes.
|
// Start watching for any subsequent file changes.
|
||||||
ConfigObserver.startWatching()
|
ConfigObserver.startWatching()
|
||||||
SystemLogger.info("Configuration initialized and file observer started.")
|
SystemLogger.info("Configuration initialized and file observer started.")
|
||||||
@@ -87,33 +83,31 @@ object ConfigurationManager {
|
|||||||
return packages.firstNotNullOfOrNull { pkg -> packageKeyboxes[pkg] } ?: DEFAULT_KEYBOX_FILE
|
return packages.firstNotNullOfOrNull { pkg -> packageKeyboxes[pkg] } ?: DEFAULT_KEYBOX_FILE
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Determines if the certificate for a given UID needs to be patched. */
|
fun shouldPatch(uid: Int): Boolean {
|
||||||
fun shouldPatch(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.PATCH
|
val mode = getPackageModeForUid(uid)
|
||||||
|
return mode == Mode.PATCH || mode == Mode.AUTO
|
||||||
|
}
|
||||||
|
|
||||||
/** Determines if a new certificate needs to be generated for a given UID. */
|
/** Determines if a new certificate needs to be generated for a given UID. */
|
||||||
fun shouldGenerate(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.GENERATE
|
fun shouldGenerate(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.GENERATE
|
||||||
|
|
||||||
/** Determines if no operation is needed for a given UID. */
|
|
||||||
fun shouldSkipUid(uid: Int): Boolean = getPackageModeForUid(uid) == null
|
fun shouldSkipUid(uid: Int): Boolean = getPackageModeForUid(uid) == null
|
||||||
|
|
||||||
/** Resolves the operating mode for a given UID based on its packages and the TEE status. */
|
fun isAutoMode(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.AUTO
|
||||||
|
|
||||||
private fun getPackageModeForUid(uid: Int): Mode? {
|
private fun getPackageModeForUid(uid: Int): Mode? {
|
||||||
val packages = getPackagesForUid(uid)
|
val packages = getPackagesForUid(uid)
|
||||||
if (packages.isEmpty()) return null
|
if (packages.isEmpty()) return null
|
||||||
|
|
||||||
// Lazily load TEE status if it hasn't been checked yet.
|
|
||||||
if (isTeeBroken == null) loadTeeStatus()
|
|
||||||
|
|
||||||
// Find the first configured mode for any of the UID's packages.
|
|
||||||
for (pkg in packages) {
|
for (pkg in packages) {
|
||||||
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 (isTeeBroken == true) Mode.GENERATE else Mode.PATCH
|
Mode.AUTO -> return if (DeviceAttestationService.isTeeFunctional) Mode.PATCH else Mode.GENERATE
|
||||||
null -> continue // No config for this package, check the next one.
|
null -> continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null // No configuration found for this UID.
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -171,7 +165,6 @@ object ConfigurationManager {
|
|||||||
newModes[pkg] = Mode.PATCH
|
newModes[pkg] = Mode.PATCH
|
||||||
newKeyboxes[pkg] = currentKeybox
|
newKeyboxes[pkg] = currentKeybox
|
||||||
}
|
}
|
||||||
// No suffix means AUTO mode.
|
|
||||||
else -> {
|
else -> {
|
||||||
newModes[trimmedLine] = Mode.AUTO
|
newModes[trimmedLine] = Mode.AUTO
|
||||||
newKeyboxes[trimmedLine] = currentKeybox
|
newKeyboxes[trimmedLine] = currentKeybox
|
||||||
@@ -280,29 +273,6 @@ object ConfigurationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Checks the device's TEE status and writes the result to a file for persistence. */
|
|
||||||
private fun storeTeeStatus() {
|
|
||||||
val statusFile = File(configRoot, TEE_STATUS_FILE)
|
|
||||||
isTeeBroken = !DeviceAttestationService.isTeeFunctional
|
|
||||||
try {
|
|
||||||
statusFile.writeText("tee_broken=$isTeeBroken")
|
|
||||||
SystemLogger.info("TEE status stored: isTeeBroken=$isTeeBroken")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SystemLogger.error("Failed to write TEE status to file.", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Loads the TEE status from the file. */
|
|
||||||
private fun loadTeeStatus() {
|
|
||||||
val statusFile = File(configRoot, TEE_STATUS_FILE)
|
|
||||||
isTeeBroken =
|
|
||||||
if (statusFile.exists()) {
|
|
||||||
statusFile.readText().trim() == "tee_broken=true"
|
|
||||||
} else {
|
|
||||||
null // Status is unknown.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A FileObserver that monitors the configuration directory for changes and triggers reloads of
|
* A FileObserver that monitors the configuration directory for changes and triggers reloads of
|
||||||
* the relevant settings.
|
* the relevant settings.
|
||||||
@@ -360,7 +330,29 @@ object ConfigurationManager {
|
|||||||
return iPackageManager
|
return iPackageManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Retrieves the package names associated with a UID. */
|
fun checkSELinuxPermission(callingPid: Int, tclass: String, perm: String): Boolean {
|
||||||
|
return try {
|
||||||
|
val callerCtx =
|
||||||
|
java.io.File("/proc/$callingPid/attr/current").readText().trim('\u0000', ' ', '\n')
|
||||||
|
val selfCtx =
|
||||||
|
java.io.File("/proc/self/attr/current").readText().trim('\u0000', ' ', '\n')
|
||||||
|
android.os.SELinux.checkSELinuxAccess(callerCtx, selfCtx, tclass, perm)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasPermissionForUid(uid: Int, permission: String): Boolean {
|
||||||
|
val userId = uid / 100000
|
||||||
|
return getPackagesForUid(uid).any { pkg ->
|
||||||
|
try {
|
||||||
|
getPackageManager()?.checkPermission(permission, pkg, userId) == 0
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getPackagesForUid(uid: Int): Array<String> {
|
fun getPackagesForUid(uid: Int): Array<String> {
|
||||||
return uidToPackagesCache.getOrPut(uid) {
|
return uidToPackagesCache.getOrPut(uid) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -109,17 +109,17 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
* `handlePostTransact`).
|
* `handlePostTransact`).
|
||||||
*/
|
*/
|
||||||
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 {
|
||||||
// The native hook prepends a transaction ID to the data parcel.
|
|
||||||
val txId = data.readLong()
|
val txId = data.readLong()
|
||||||
val result =
|
val result = try {
|
||||||
when (code) {
|
when (code) {
|
||||||
// These codes are defined in the native layer to distinguish hook types.
|
|
||||||
PRE_TRANSACT_CODE -> handlePreTransact(txId, data)
|
PRE_TRANSACT_CODE -> handlePreTransact(txId, data)
|
||||||
POST_TRANSACT_CODE -> handlePostTransact(txId, data)
|
POST_TRANSACT_CODE -> handlePostTransact(txId, data)
|
||||||
else -> return super.onTransact(code, data, reply, flags)
|
else -> return super.onTransact(code, data, reply, flags)
|
||||||
}
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
// The reply parcel is guaranteed to be non-null for our custom transactions.
|
SystemLogger.error("[TX_ID: $txId] Interceptor exception, falling through to HAL", e)
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
writeResultToReply(result, reply!!)
|
writeResultToReply(result, reply!!)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -293,15 +293,21 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Uses the backdoor binder to register an interceptor for a specific target service. */
|
fun register(
|
||||||
fun register(backdoor: IBinder, target: IBinder, interceptor: BinderInterceptor) {
|
backdoor: IBinder,
|
||||||
|
target: IBinder,
|
||||||
|
interceptor: BinderInterceptor,
|
||||||
|
filteredCodes: IntArray = intArrayOf(),
|
||||||
|
) {
|
||||||
val data = Parcel.obtain()
|
val data = Parcel.obtain()
|
||||||
val reply = Parcel.obtain()
|
val reply = Parcel.obtain()
|
||||||
try {
|
try {
|
||||||
data.writeStrongBinder(target)
|
data.writeStrongBinder(target)
|
||||||
data.writeStrongBinder(interceptor)
|
data.writeStrongBinder(interceptor)
|
||||||
|
data.writeInt(filteredCodes.size)
|
||||||
|
for (code in filteredCodes) data.writeInt(code)
|
||||||
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
|
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
|
||||||
SystemLogger.info("Registered interceptor for target: $target")
|
SystemLogger.info("Registered interceptor for target: $target (${filteredCodes.size} filtered codes)")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("Failed to register binder interceptor.", e)
|
SystemLogger.error("Failed to register binder interceptor.", e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
+3
-2
@@ -68,11 +68,12 @@ abstract class AbstractKeystoreInterceptor : BinderInterceptor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Registers this interceptor with the native hook layer and sets up a death recipient. */
|
protected open val interceptedCodes: IntArray = intArrayOf()
|
||||||
|
|
||||||
private fun setupInterceptor(service: IBinder, backdoor: IBinder) {
|
private fun setupInterceptor(service: IBinder, backdoor: IBinder) {
|
||||||
keystoreService = service
|
keystoreService = service
|
||||||
SystemLogger.info("Registering interceptor for service: $serviceName")
|
SystemLogger.info("Registering interceptor for service: $serviceName")
|
||||||
register(backdoor, service, this)
|
register(backdoor, service, this, interceptedCodes)
|
||||||
service.linkToDeath(createDeathRecipient(), 0)
|
service.linkToDeath(createDeathRecipient(), 0)
|
||||||
onInterceptorReady(service, backdoor)
|
onInterceptorReady(service, backdoor)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
package org.matrix.TEESimulator.interception.keystore
|
package org.matrix.TEESimulator.interception.keystore
|
||||||
|
|
||||||
|
import android.hardware.security.keymint.KeyParameter
|
||||||
|
import android.hardware.security.keymint.KeyParameterValue
|
||||||
|
import android.hardware.security.keymint.Tag
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
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 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
|
||||||
|
|
||||||
data class KeyIdentifier(val uid: Int, val alias: String)
|
data class KeyIdentifier(val uid: Int, val alias: String)
|
||||||
|
|
||||||
@@ -124,4 +129,53 @@ object InterceptorUtils {
|
|||||||
if (exception != null) reply.setDataPosition(0)
|
if (exception != null) reply.setDataPosition(0)
|
||||||
return exception != null
|
return exception != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createServiceSpecificErrorReply(
|
||||||
|
errorCode: Int
|
||||||
|
): BinderInterceptor.TransactionResult.OverrideReply {
|
||||||
|
val parcel =
|
||||||
|
Parcel.obtain().apply {
|
||||||
|
writeException(android.os.ServiceSpecificException(errorCode))
|
||||||
|
}
|
||||||
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun patchAuthorizations(
|
||||||
|
authorizations: Array<Authorization>?,
|
||||||
|
callingUid: Int,
|
||||||
|
): Array<Authorization>? {
|
||||||
|
if (authorizations == null) return null
|
||||||
|
|
||||||
|
val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid)
|
||||||
|
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
|
||||||
|
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
|
||||||
|
|
||||||
|
return authorizations
|
||||||
|
.map { auth ->
|
||||||
|
val replacement =
|
||||||
|
when (auth.keyParameter.tag) {
|
||||||
|
Tag.OS_PATCHLEVEL ->
|
||||||
|
if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) osPatch else null
|
||||||
|
Tag.VENDOR_PATCHLEVEL ->
|
||||||
|
if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) vendorPatch
|
||||||
|
else null
|
||||||
|
Tag.BOOT_PATCHLEVEL ->
|
||||||
|
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) bootPatch else null
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (replacement != null) {
|
||||||
|
Authorization().apply {
|
||||||
|
keyParameter =
|
||||||
|
KeyParameter().apply {
|
||||||
|
tag = auth.keyParameter.tag
|
||||||
|
value = KeyParameterValue.integer(replacement)
|
||||||
|
}
|
||||||
|
securityLevel = auth.securityLevel
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
auth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toTypedArray()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+142
-27
@@ -5,6 +5,7 @@ import android.hardware.security.keymint.SecurityLevel
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
|
import android.system.keystore2.Domain
|
||||||
import android.system.keystore2.IKeystoreService
|
import android.system.keystore2.IKeystoreService
|
||||||
import android.system.keystore2.KeyDescriptor
|
import android.system.keystore2.KeyDescriptor
|
||||||
import android.system.keystore2.KeyEntryResponse
|
import android.system.keystore2.KeyEntryResponse
|
||||||
@@ -45,6 +46,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
if (Build.VERSION.SDK_INT >= 34)
|
if (Build.VERSION.SDK_INT >= 34)
|
||||||
InterceptorUtils.getTransactCode(stubBinderClass, "listEntriesBatched")
|
InterceptorUtils.getTransactCode(stubBinderClass, "listEntriesBatched")
|
||||||
else null
|
else null
|
||||||
|
private val GET_NUMBER_OF_ENTRIES_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(stubBinderClass, "getNumberOfEntries")
|
||||||
|
|
||||||
private val transactionNames: Map<Int, String> by lazy {
|
private val transactionNames: Map<Int, String> by lazy {
|
||||||
stubBinderClass.declaredFields
|
stubBinderClass.declaredFields
|
||||||
@@ -57,11 +60,24 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
|
|
||||||
private const val RESPONSE_KEY_NOT_FOUND = 7
|
private const val RESPONSE_KEY_NOT_FOUND = 7
|
||||||
private val deletedSoftwareKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
private val deletedSoftwareKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
||||||
|
private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
|
||||||
|
|
||||||
override val serviceName = "android.system.keystore2.IKeystoreService/default"
|
override val serviceName = "android.system.keystore2.IKeystoreService/default"
|
||||||
override val processName = "keystore2"
|
override val processName = "keystore2"
|
||||||
override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry"
|
override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry"
|
||||||
|
|
||||||
|
override val interceptedCodes: IntArray by lazy {
|
||||||
|
listOfNotNull(
|
||||||
|
GET_KEY_ENTRY_TRANSACTION,
|
||||||
|
DELETE_KEY_TRANSACTION,
|
||||||
|
UPDATE_SUBCOMPONENT_TRANSACTION,
|
||||||
|
LIST_ENTRIES_TRANSACTION,
|
||||||
|
LIST_ENTRIES_BATCHED_TRANSACTION,
|
||||||
|
GET_NUMBER_OF_ENTRIES_TRANSACTION,
|
||||||
|
)
|
||||||
|
.toIntArray()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is called once the main service is hooked. It proceeds to find and hook the
|
* This method is called once the main service is hooked. It proceeds to find and hook the
|
||||||
* security level sub-services (e.g., TEE, StrongBox).
|
* security level sub-services (e.g., TEE, StrongBox).
|
||||||
@@ -78,7 +94,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...")
|
SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...")
|
||||||
val interceptor =
|
val interceptor =
|
||||||
KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
|
KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
|
||||||
register(backdoor, tee.asBinder(), interceptor)
|
register(
|
||||||
|
backdoor,
|
||||||
|
tee.asBinder(),
|
||||||
|
interceptor,
|
||||||
|
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
|
||||||
|
)
|
||||||
interceptor.loadPersistedKeys()
|
interceptor.loadPersistedKeys()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +111,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
|
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
|
||||||
val interceptor =
|
val interceptor =
|
||||||
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
|
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
|
||||||
register(backdoor, strongbox.asBinder(), interceptor)
|
register(
|
||||||
|
backdoor,
|
||||||
|
strongbox.asBinder(),
|
||||||
|
interceptor,
|
||||||
|
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
|
||||||
|
)
|
||||||
interceptor.loadPersistedKeys()
|
interceptor.loadPersistedKeys()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,7 +132,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
callingPid: Int,
|
callingPid: Int,
|
||||||
data: Parcel,
|
data: Parcel,
|
||||||
): TransactionResult {
|
): TransactionResult {
|
||||||
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) {
|
||||||
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
|
||||||
|
return if (ConfigurationManager.shouldSkipUid(callingUid))
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
else TransactionResult.Continue
|
||||||
|
} else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
|
||||||
|
|
||||||
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
|
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
|
||||||
@@ -149,29 +180,40 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
data.readTypedObject(KeyDescriptor.CREATOR)
|
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
?: return TransactionResult.ContinueAndSkipPost
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
|
|
||||||
if (descriptor.alias != null) {
|
|
||||||
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
|
|
||||||
} else {
|
|
||||||
SystemLogger.info(
|
|
||||||
"Skip ${transactionNames[code]!!} for key [alias, blob, domain, nspace]: [${descriptor.alias}, ${descriptor.blob}, ${descriptor.domain}, ${descriptor.nspace}]"
|
|
||||||
)
|
|
||||||
return TransactionResult.ContinueAndSkipPost
|
|
||||||
}
|
|
||||||
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
|
||||||
|
|
||||||
if (code == DELETE_KEY_TRANSACTION) {
|
if (code == DELETE_KEY_TRANSACTION) {
|
||||||
val wasSoftwareKey = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null
|
val keyId =
|
||||||
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
if (descriptor.alias != null) {
|
||||||
if (wasSoftwareKey) {
|
KeyIdentifier(callingUid, descriptor.alias)
|
||||||
deletedSoftwareKeys.add(keyId)
|
} else if (descriptor.domain == Domain.KEY_ID) {
|
||||||
SystemLogger.info(
|
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||||
"[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response."
|
callingUid, descriptor.nspace
|
||||||
)
|
)?.let { info ->
|
||||||
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
KeyMintSecurityLevelInterceptor.generatedKeys.entries
|
||||||
|
.find { it.value.nspace == info.nspace && it.key.uid == callingUid }
|
||||||
|
?.key
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
|
||||||
|
if (keyId != null) {
|
||||||
|
val isSoftwareKey =
|
||||||
|
KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(keyId)
|
||||||
|
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
||||||
|
if (isSoftwareKey) {
|
||||||
|
deletedSoftwareKeys.add(keyId)
|
||||||
|
SystemLogger.info(
|
||||||
|
"[TX_ID: $txId] Deleted cached keypair ${keyId.alias}, replying with empty response."
|
||||||
|
)
|
||||||
|
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (descriptor.alias == null) {
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
||||||
|
|
||||||
val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
if (deletedSoftwareKeys.remove(keyId)) {
|
if (deletedSoftwareKeys.remove(keyId)) {
|
||||||
@@ -217,7 +259,26 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
|
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
|
||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
|
|
||||||
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) {
|
||||||
|
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||||
|
return runCatching {
|
||||||
|
val hardwareCount = reply.readInt()
|
||||||
|
val softwareCount =
|
||||||
|
KeyMintSecurityLevelInterceptor.generatedKeys.keys.count {
|
||||||
|
it.uid == callingUid
|
||||||
|
}
|
||||||
|
val totalCount = hardwareCount + softwareCount
|
||||||
|
val parcel = Parcel.obtain().apply {
|
||||||
|
writeNoException()
|
||||||
|
writeInt(totalCount)
|
||||||
|
}
|
||||||
|
TransactionResult.OverrideReply(parcel)
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.error("[TX_ID: $txId] Failed to modify getNumberOfEntries.", it)
|
||||||
|
TransactionResult.SkipTransaction
|
||||||
|
}
|
||||||
|
} else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||||
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||||
|
|
||||||
return runCatching {
|
return runCatching {
|
||||||
@@ -252,23 +313,44 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
val response = reply.readTypedObject(KeyEntryResponse.CREATOR)!!
|
val response = reply.readTypedObject(KeyEntryResponse.CREATOR)!!
|
||||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||||
|
|
||||||
|
if (userUpdatedKeys.remove(keyId)) {
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: userUpdated=true, skipping patch" }
|
||||||
|
SystemLogger.debug("[TX_ID: $txId] Skipping cert patch for user-updated key $keyId.")
|
||||||
|
return TransactionResult.SkipTransaction
|
||||||
|
}
|
||||||
|
|
||||||
val authorizations = response.metadata.authorizations
|
val authorizations = response.metadata.authorizations
|
||||||
val parsedParameters =
|
val parsedParameters =
|
||||||
KeyMintAttestation(
|
KeyMintAttestation(
|
||||||
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()}" }
|
||||||
|
|
||||||
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.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
|
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.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
|
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
|
||||||
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
|
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
|
||||||
|
response.metadata.authorizations =
|
||||||
|
InterceptorUtils.patchAuthorizations(
|
||||||
|
response.metadata.authorizations,
|
||||||
|
callingUid,
|
||||||
|
)
|
||||||
return InterceptorUtils.createTypedObjectReply(response)
|
return InterceptorUtils.createTypedObjectReply(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)) {
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: in importedKeys set, skip" }
|
||||||
|
SystemLogger.debug("[TX_ID: $txId] Skipping attest-key override for imported key $keyId")
|
||||||
|
return TransactionResult.SkipTransaction
|
||||||
|
}
|
||||||
|
|
||||||
if (parsedParameters.isAttestKey()) {
|
if (parsedParameters.isAttestKey()) {
|
||||||
SystemLogger.warning(
|
SystemLogger.warning(
|
||||||
"[TX_ID: $txId] Found hardware attest key ${keyId.alias} in the reply."
|
"[TX_ID: $txId] Found hardware attest key ${keyId.alias} in the reply."
|
||||||
@@ -287,20 +369,28 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
keyData.second.toTypedArray(),
|
keyData.second.toTypedArray(),
|
||||||
)
|
)
|
||||||
.getOrThrow()
|
.getOrThrow()
|
||||||
|
response.metadata.authorizations =
|
||||||
|
InterceptorUtils.patchAuthorizations(
|
||||||
|
response.metadata.authorizations,
|
||||||
|
callingUid,
|
||||||
|
)
|
||||||
|
|
||||||
keyDescriptor.nspace = SecureRandom().nextLong()
|
val newNspace = SecureRandom().nextLong()
|
||||||
|
response.metadata.key?.let { it.nspace = newNspace }
|
||||||
KeyMintSecurityLevelInterceptor.generatedKeys[keyId] =
|
KeyMintSecurityLevelInterceptor.generatedKeys[keyId] =
|
||||||
KeyMintSecurityLevelInterceptor.GeneratedKeyInfo(
|
KeyMintSecurityLevelInterceptor.GeneratedKeyInfo(
|
||||||
keyData.first,
|
keyData.first,
|
||||||
keyDescriptor.nspace,
|
null,
|
||||||
|
newNspace,
|
||||||
response,
|
response,
|
||||||
|
parsedParameters,
|
||||||
)
|
)
|
||||||
KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId)
|
KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId)
|
||||||
|
|
||||||
GeneratedKeyPersistence.save(
|
GeneratedKeyPersistence.save(
|
||||||
keyId = keyId,
|
keyId = keyId,
|
||||||
keyPair = keyData.first,
|
keyPair = keyData.first,
|
||||||
nspace = keyDescriptor.nspace,
|
nspace = newNspace,
|
||||||
securityLevel = response.metadata.keySecurityLevel,
|
securityLevel = response.metadata.keySecurityLevel,
|
||||||
certChain = keyData.second,
|
certChain = keyData.second,
|
||||||
algorithm = parsedParameters.algorithm,
|
algorithm = parsedParameters.algorithm,
|
||||||
@@ -342,6 +432,11 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
|
|
||||||
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
|
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
|
||||||
.getOrThrow()
|
.getOrThrow()
|
||||||
|
response.metadata.authorizations =
|
||||||
|
InterceptorUtils.patchAuthorizations(
|
||||||
|
response.metadata.authorizations,
|
||||||
|
callingUid,
|
||||||
|
)
|
||||||
|
|
||||||
return InterceptorUtils.createTypedObjectReply(response)
|
return InterceptorUtils.createTypedObjectReply(response)
|
||||||
}
|
}
|
||||||
@@ -359,9 +454,29 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
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 = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
|
|
||||||
val generatedKeyInfo =
|
val generatedKeyInfo =
|
||||||
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(callingUid, descriptor?.nspace)
|
when (descriptor.domain) {
|
||||||
?: return TransactionResult.ContinueAndSkipPost
|
Domain.KEY_ID ->
|
||||||
|
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||||
|
callingUid, descriptor.nspace
|
||||||
|
)
|
||||||
|
Domain.APP ->
|
||||||
|
descriptor.alias?.let {
|
||||||
|
KeyMintSecurityLevelInterceptor.generatedKeys[KeyIdentifier(callingUid, it)]
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generatedKeyInfo == null) {
|
||||||
|
descriptor.alias?.let {
|
||||||
|
val kid = KeyIdentifier(callingUid, it)
|
||||||
|
userUpdatedKeys.add(kid)
|
||||||
|
SystemLogger.trace { "[TRACE] updateSubcomponent $kid: not generated key, added to userUpdatedKeys" }
|
||||||
|
}
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
|
||||||
SystemLogger.info("Updating sub-component with key[${generatedKeyInfo.nspace}]")
|
SystemLogger.info("Updating sub-component with key[${generatedKeyInfo.nspace}]")
|
||||||
val metadata = generatedKeyInfo.response.metadata
|
val metadata = generatedKeyInfo.response.metadata
|
||||||
|
|||||||
+18
@@ -431,6 +431,24 @@ private data class LegacyKeygenParameters(
|
|||||||
manufacturer = null,
|
manufacturer = null,
|
||||||
model = null,
|
model = null,
|
||||||
secondImei = null,
|
secondImei = null,
|
||||||
|
activeDateTime = null,
|
||||||
|
originationExpireDateTime = null,
|
||||||
|
usageExpireDateTime = null,
|
||||||
|
usageCountLimit = null,
|
||||||
|
callerNonce = null,
|
||||||
|
nonce = null,
|
||||||
|
unlockedDeviceRequired = null,
|
||||||
|
includeUniqueId = null,
|
||||||
|
rollbackResistance = null,
|
||||||
|
earlyBootOnly = null,
|
||||||
|
allowWhileOnBody = null,
|
||||||
|
trustedUserPresenceRequired = null,
|
||||||
|
trustedConfirmationRequired = null,
|
||||||
|
noAuthRequired = null,
|
||||||
|
maxUsesPerBoot = null,
|
||||||
|
maxBootLevel = null,
|
||||||
|
minMacLength = null,
|
||||||
|
rsaOaepMgfDigest = emptyList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
package org.matrix.TEESimulator.interception.keystore.shim
|
||||||
|
|
||||||
|
import android.hardware.security.keymint.Algorithm
|
||||||
|
import android.hardware.security.keymint.KeyPurpose
|
||||||
|
import android.hardware.security.keymint.KeyParameter
|
||||||
|
import android.hardware.security.keymint.Tag
|
||||||
|
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||||
|
|
||||||
|
object AuthorizeCreate {
|
||||||
|
|
||||||
|
fun check(
|
||||||
|
keyParams: KeyMintAttestation?,
|
||||||
|
opParams: KeyMintAttestation,
|
||||||
|
rawOpParams: Array<KeyParameter>? = null,
|
||||||
|
): Int? {
|
||||||
|
if (keyParams == null) return null
|
||||||
|
val purpose = opParams.purpose.firstOrNull() ?: return null
|
||||||
|
// Algorithm-level rejection runs before purpose-list check (AOSP HAL behavior)
|
||||||
|
return checkAlgorithmPurpose(keyParams, purpose)
|
||||||
|
?: checkPurpose(keyParams, purpose)
|
||||||
|
?: checkTemporalValidity(keyParams, purpose)
|
||||||
|
?: checkCallerNonce(keyParams, purpose, rawOpParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
|
||||||
|
val algo = keyParams.algorithm
|
||||||
|
if ((algo == Algorithm.EC || algo == Algorithm.RSA) &&
|
||||||
|
(purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.ENCRYPT)
|
||||||
|
) {
|
||||||
|
return KeystoreErrorCodes.unsupportedPurpose
|
||||||
|
}
|
||||||
|
if (algo == Algorithm.RSA && purpose == KeyPurpose.AGREE_KEY)
|
||||||
|
return KeystoreErrorCodes.unsupportedPurpose
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
|
||||||
|
if (purpose == KeyPurpose.WRAP_KEY)
|
||||||
|
return KeystoreErrorCodes.incompatiblePurpose
|
||||||
|
if (purpose !in keyParams.purpose)
|
||||||
|
return KeystoreErrorCodes.incompatiblePurpose
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkTemporalValidity(keyParams: KeyMintAttestation, purpose: Int): Int? {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
|
||||||
|
keyParams.activeDateTime?.let { activeDate ->
|
||||||
|
if (now < activeDate.time) return KeystoreErrorCodes.keyNotYetValid
|
||||||
|
}
|
||||||
|
|
||||||
|
keyParams.originationExpireDateTime?.let { expireDate ->
|
||||||
|
if (purpose == KeyPurpose.SIGN || purpose == KeyPurpose.ENCRYPT) {
|
||||||
|
if (now > expireDate.time) return KeystoreErrorCodes.keyExpired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyParams.usageExpireDateTime?.let { expireDate ->
|
||||||
|
if (purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.DECRYPT) {
|
||||||
|
if (now > expireDate.time) return KeystoreErrorCodes.keyExpired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkCallerNonce(keyParams: KeyMintAttestation, purpose: Int, rawOpParams: Array<KeyParameter>?): Int? {
|
||||||
|
if (purpose != KeyPurpose.SIGN && purpose != KeyPurpose.ENCRYPT) return null
|
||||||
|
if (keyParams.callerNonce == true) return null
|
||||||
|
if (rawOpParams?.any { it.tag == Tag.NONCE } == true)
|
||||||
|
return KeystoreErrorCodes.callerNonceProhibited
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -303,9 +303,10 @@ object GeneratedKeyPersistence {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val keyPair = generatedKeyInfo.keyPair ?: return
|
||||||
save(
|
save(
|
||||||
keyId = keyId,
|
keyId = keyId,
|
||||||
keyPair = generatedKeyInfo.keyPair,
|
keyPair = keyPair,
|
||||||
nspace = generatedKeyInfo.nspace,
|
nspace = generatedKeyInfo.nspace,
|
||||||
securityLevel = secLevel,
|
securityLevel = secLevel,
|
||||||
certChain = newChain.toList(),
|
certChain = newChain.toList(),
|
||||||
|
|||||||
+561
-125
@@ -1,9 +1,13 @@
|
|||||||
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.BlockMode
|
||||||
|
import android.hardware.security.keymint.EcCurve
|
||||||
import android.hardware.security.keymint.KeyParameter
|
import android.hardware.security.keymint.KeyParameter
|
||||||
|
import android.hardware.security.keymint.KeyPurpose
|
||||||
import android.hardware.security.keymint.KeyParameterValue
|
import android.hardware.security.keymint.KeyParameterValue
|
||||||
import android.hardware.security.keymint.KeyOrigin
|
import android.hardware.security.keymint.KeyOrigin
|
||||||
|
import android.hardware.security.keymint.SecurityLevel
|
||||||
import android.hardware.security.keymint.Tag
|
import android.hardware.security.keymint.Tag
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
@@ -16,8 +20,14 @@ import java.security.SecureRandom
|
|||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
import java.security.cert.CertificateFactory
|
import java.security.cert.CertificateFactory
|
||||||
import java.security.spec.PKCS8EncodedKeySpec
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque
|
||||||
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
import java.util.concurrent.locks.LockSupport
|
||||||
import org.matrix.TEESimulator.attestation.AttestationBuilder
|
import org.matrix.TEESimulator.attestation.AttestationBuilder
|
||||||
import org.matrix.TEESimulator.attestation.AttestationConstants
|
import org.matrix.TEESimulator.attestation.AttestationConstants
|
||||||
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
||||||
@@ -33,6 +43,8 @@ import org.matrix.TEESimulator.pki.CertificateHelper
|
|||||||
import org.matrix.TEESimulator.pki.KeyBoxManager
|
import org.matrix.TEESimulator.pki.KeyBoxManager
|
||||||
import org.matrix.TEESimulator.pki.NativeCertGen
|
import org.matrix.TEESimulator.pki.NativeCertGen
|
||||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||||
|
import org.matrix.TEESimulator.util.AndroidPermissionUtils
|
||||||
|
import org.matrix.TEESimulator.util.TeeLatencySimulator
|
||||||
|
|
||||||
class KeyMintSecurityLevelInterceptor(
|
class KeyMintSecurityLevelInterceptor(
|
||||||
private val original: IKeystoreSecurityLevel,
|
private val original: IKeystoreSecurityLevel,
|
||||||
@@ -40,11 +52,20 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
) : BinderInterceptor() {
|
) : BinderInterceptor() {
|
||||||
|
|
||||||
data class GeneratedKeyInfo(
|
data class GeneratedKeyInfo(
|
||||||
val keyPair: KeyPair,
|
val keyPair: KeyPair?,
|
||||||
|
val secretKey: javax.crypto.SecretKey?,
|
||||||
val nspace: Long,
|
val nspace: Long,
|
||||||
val response: KeyEntryResponse,
|
val response: KeyEntryResponse,
|
||||||
|
val keyParams: KeyMintAttestation? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// null = undecided, true = TEE works (use PATCH), false = TEE broken (use GENERATE)
|
||||||
|
// Instance field so TRUSTED_ENVIRONMENT and STRONGBOX decide independently
|
||||||
|
val teePathDecision = AtomicReference<Boolean?>(null)
|
||||||
|
|
||||||
|
private val activeOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<SoftwareOperation>>()
|
||||||
|
private val recentOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<Long>>()
|
||||||
|
|
||||||
override fun onPreTransact(
|
override fun onPreTransact(
|
||||||
txId: Long,
|
txId: Long,
|
||||||
target: IBinder,
|
target: IBinder,
|
||||||
@@ -60,7 +81,7 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
GENERATE_KEY_TRANSACTION -> {
|
GENERATE_KEY_TRANSACTION -> {
|
||||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||||
|
|
||||||
if (!shouldSkip) return handleGenerateKey(txId, callingUid, data)
|
if (!shouldSkip) return handleGenerateKey(txId, callingUid, callingPid, data)
|
||||||
}
|
}
|
||||||
CREATE_OPERATION_TRANSACTION -> {
|
CREATE_OPERATION_TRANSACTION -> {
|
||||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||||
@@ -71,7 +92,8 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||||
|
|
||||||
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||||
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
|
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"[TX_ID: $txId] Forward to post-importKey hook for ${keyDescriptor.alias}[${keyDescriptor.nspace}]"
|
"[TX_ID: $txId] Forward to post-importKey hook for ${keyDescriptor.alias}[${keyDescriptor.nspace}]"
|
||||||
)
|
)
|
||||||
@@ -101,12 +123,6 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
reply: Parcel?,
|
reply: Parcel?,
|
||||||
resultCode: Int,
|
resultCode: Int,
|
||||||
): TransactionResult {
|
): TransactionResult {
|
||||||
if (code == GENERATE_KEY_TRANSACTION && hardwareKeygenTxIds.remove(txId)) {
|
|
||||||
val remaining = hardwareKeygenCount(callingUid).decrementAndGet()
|
|
||||||
SystemLogger.info("[TX_ID: $txId] PERMIT_RELEASED uid=$callingUid concurrent_remaining=$remaining result=${if (resultCode == 0) "OK" else "ERROR($resultCode)"}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// We only care about successful transactions.
|
|
||||||
if (resultCode != 0 || reply == null || InterceptorUtils.hasException(reply))
|
if (resultCode != 0 || reply == null || InterceptorUtils.hasException(reply))
|
||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
|
|
||||||
@@ -125,12 +141,38 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
GeneratedKeyPersistence.delete(keyId)
|
GeneratedKeyPersistence.delete(keyId)
|
||||||
}
|
}
|
||||||
attestationKeys.remove(keyId)
|
attestationKeys.remove(keyId)
|
||||||
|
importedKeys.add(keyId)
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] post-importKey $keyId: added to importedKeys, skipUid=${ConfigurationManager.shouldSkipUid(callingUid)}" }
|
||||||
|
|
||||||
|
if (!ConfigurationManager.shouldSkipUid(callingUid)) {
|
||||||
|
val metadata: KeyMetadata =
|
||||||
|
reply.readTypedObject(KeyMetadata.CREATOR)
|
||||||
|
?: return TransactionResult.SkipTransaction
|
||||||
|
val originalChain = CertificateHelper.getCertificateChain(metadata)
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] post-importKey $keyId: chainSize=${originalChain?.size ?: 0}" }
|
||||||
|
if (originalChain != null && originalChain.size > 1) {
|
||||||
|
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
||||||
|
CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow()
|
||||||
|
metadata.authorizations =
|
||||||
|
InterceptorUtils.patchAuthorizations(metadata.authorizations, callingUid)
|
||||||
|
patchedChains[keyId] = newChain
|
||||||
|
teeResponses[keyId] = KeyEntryResponse().apply {
|
||||||
|
this.metadata = metadata
|
||||||
|
iSecurityLevel = original
|
||||||
|
}
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] post-importKey $keyId: PATCHED chain (chainSize=${newChain.size})" }
|
||||||
|
SystemLogger.debug("Cached patched certificate chain for imported key $keyId.")
|
||||||
|
return InterceptorUtils.createTypedObjectReply(metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (code == CREATE_OPERATION_TRANSACTION) {
|
} else if (code == CREATE_OPERATION_TRANSACTION) {
|
||||||
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||||
|
|
||||||
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||||
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
|
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
?: return TransactionResult.SkipTransaction
|
||||||
|
val params = data.createTypedArray(KeyParameter.CREATOR)
|
||||||
|
?: return TransactionResult.SkipTransaction
|
||||||
val parsedParams = KeyMintAttestation(params)
|
val parsedParams = KeyMintAttestation(params)
|
||||||
val forced = data.readBoolean()
|
val forced = data.readBoolean()
|
||||||
if (forced)
|
if (forced)
|
||||||
@@ -138,7 +180,8 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
"[TX_ID: $txId] Current operation has a very high pruning power."
|
"[TX_ID: $txId] Current operation has a very high pruning power."
|
||||||
)
|
)
|
||||||
val response: CreateOperationResponse =
|
val response: CreateOperationResponse =
|
||||||
reply.readTypedObject(CreateOperationResponse.CREATOR)!!
|
reply.readTypedObject(CreateOperationResponse.CREATOR)
|
||||||
|
?: return TransactionResult.SkipTransaction
|
||||||
SystemLogger.verbose(
|
SystemLogger.verbose(
|
||||||
"[TX_ID: $txId] CreateOperationResponse: ${response.iOperation} ${response.operationChallenge}"
|
"[TX_ID: $txId] CreateOperationResponse: ${response.iOperation} ${response.operationChallenge}"
|
||||||
)
|
)
|
||||||
@@ -151,7 +194,7 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
val backdoor = getBackdoor(target)
|
val backdoor = getBackdoor(target)
|
||||||
if (backdoor != null) {
|
if (backdoor != null) {
|
||||||
val interceptor = OperationInterceptor(operation, backdoor)
|
val interceptor = OperationInterceptor(operation, backdoor)
|
||||||
register(backdoor, operationBinder, interceptor)
|
register(backdoor, operationBinder, interceptor, OperationInterceptor.INTERCEPTED_CODES)
|
||||||
interceptedOperations[operationBinder] = interceptor
|
interceptedOperations[operationBinder] = interceptor
|
||||||
} else {
|
} else {
|
||||||
SystemLogger.error(
|
SystemLogger.error(
|
||||||
@@ -170,18 +213,32 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
CertificateHelper.getCertificateChain(metadata)
|
CertificateHelper.getCertificateChain(metadata)
|
||||||
?: return TransactionResult.SkipTransaction
|
?: return TransactionResult.SkipTransaction
|
||||||
if (originalChain.size > 1) {
|
if (originalChain.size > 1) {
|
||||||
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
// Read the request parcel to extract keyDescriptor and cert date params.
|
||||||
|
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||||
|
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
|
?: return TransactionResult.SkipTransaction
|
||||||
|
data.readTypedObject(KeyDescriptor.CREATOR) // skip attestationKey
|
||||||
|
val keyParams = data.createTypedArray(KeyParameter.CREATOR)
|
||||||
|
val certNotBefore = keyParams?.find { it.tag == Tag.CERTIFICATE_NOT_BEFORE }?.value?.dateTime?.let { Date(it) }
|
||||||
|
val certNotAfter = keyParams?.find { it.tag == Tag.CERTIFICATE_NOT_AFTER }?.value?.dateTime?.let { Date(it) }
|
||||||
|
|
||||||
|
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid, certNotBefore, certNotAfter)
|
||||||
|
|
||||||
// Cache the newly patched chain to ensure consistency across subsequent API calls.
|
// Cache the newly patched chain to ensure consistency across subsequent API calls.
|
||||||
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
val key = metadata.key
|
||||||
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
|
?: return TransactionResult.SkipTransaction
|
||||||
val key = metadata.key!!
|
|
||||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||||
CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow()
|
CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow()
|
||||||
|
metadata.authorizations =
|
||||||
|
InterceptorUtils.patchAuthorizations(metadata.authorizations, callingUid)
|
||||||
|
|
||||||
// We must clean up cached generated keys before storing the patched chain
|
// We must clean up cached generated keys before storing the patched chain
|
||||||
cleanupKeyData(keyId)
|
cleanupKeyData(keyId)
|
||||||
patchedChains[keyId] = newChain
|
patchedChains[keyId] = newChain
|
||||||
|
teeResponses[keyId] = KeyEntryResponse().apply {
|
||||||
|
this.metadata = metadata
|
||||||
|
iSecurityLevel = original
|
||||||
|
}
|
||||||
SystemLogger.debug(
|
SystemLogger.debug(
|
||||||
"Cached patched certificate chain for $keyId. (${key.alias} [${key.domain}, ${key.nspace}])"
|
"Cached patched certificate chain for $keyId. (${key.alias} [${key.domain}, ${key.nspace}])"
|
||||||
)
|
)
|
||||||
@@ -192,58 +249,168 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun pruneOpsForUid(uid: Int, newOp: SoftwareOperation, maxOps: Int = MAX_CONCURRENT_OPS_PER_UID) {
|
||||||
|
val ops = activeOps.computeIfAbsent(uid) { ConcurrentLinkedDeque() }
|
||||||
|
val before = ops.size
|
||||||
|
ops.removeIf { it.finalized }
|
||||||
|
val afterClean = ops.size
|
||||||
|
while (ops.size >= maxOps) {
|
||||||
|
val oldest = ops.pollFirst() ?: break
|
||||||
|
if (!oldest.finalized) {
|
||||||
|
SystemLogger.info("[LRU] Pruning operation for uid=$uid (active=${ops.size}/$maxOps)")
|
||||||
|
oldest.abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ops.addLast(newOp)
|
||||||
|
SystemLogger.debug("[LRU] uid=$uid ops: before=$before cleaned=${before - afterClean} active=${ops.size}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun trackAndEnforceOpLimit(callingUid: Int, txId: Long): TransactionResult? {
|
||||||
|
if (securityLevel != SecurityLevel.STRONGBOX) return null
|
||||||
|
val timestamps = recentOps.computeIfAbsent(callingUid) { ConcurrentLinkedDeque() }
|
||||||
|
val cutoff = System.nanoTime() - STRONGBOX_OP_WINDOW_NS
|
||||||
|
timestamps.removeIf { it < cutoff }
|
||||||
|
val swOps = activeOps[callingUid]?.count { !it.finalized } ?: 0
|
||||||
|
if (timestamps.size + swOps >= STRONGBOX_MAX_CONCURRENT_OPS) {
|
||||||
|
SystemLogger.info("[TX_ID: $txId] StrongBox op limit reached for uid=$callingUid (hw=${timestamps.size} sw=$swOps max=$STRONGBOX_MAX_CONCURRENT_OPS)")
|
||||||
|
return InterceptorUtils.createErrorReply(KEYMINT_TOO_MANY_OPERATIONS)
|
||||||
|
}
|
||||||
|
timestamps.addLast(System.nanoTime())
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleCreateOperation(
|
private fun handleCreateOperation(
|
||||||
txId: Long,
|
txId: Long,
|
||||||
callingUid: Int,
|
callingUid: Int,
|
||||||
data: Parcel,
|
data: Parcel,
|
||||||
): TransactionResult {
|
): TransactionResult = runCatching {
|
||||||
|
SystemLogger.debug("[TX_ID: $txId] createOperation parcel: dataSize=${data.dataSize()} dataAvail=${data.dataAvail()} dataPos=${data.dataPosition()}")
|
||||||
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||||
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
|
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
|
||||||
|
|
||||||
// An operation must use the KEY_ID domain.
|
SystemLogger.debug("[TX_ID: $txId] createOperation descriptor: domain=${keyDescriptor.domain} nspace=${keyDescriptor.nspace} alias=${keyDescriptor.alias}")
|
||||||
if (keyDescriptor.domain != Domain.KEY_ID) {
|
|
||||||
return TransactionResult.ContinueAndSkipPost
|
|
||||||
}
|
|
||||||
|
|
||||||
val nspace = keyDescriptor.nspace
|
// Android framework calls createOperation with domain=APP+alias;
|
||||||
val generatedKeyInfo = findGeneratedKeyByKeyId(callingUid, nspace)
|
// keystore2 internally resolves to KEY_ID — but software keys never
|
||||||
|
// reach keystore2's database, so we must handle both lookup paths.
|
||||||
|
val resolvedEntry: Map.Entry<KeyIdentifier, GeneratedKeyInfo> =
|
||||||
|
when (keyDescriptor.domain) {
|
||||||
|
Domain.APP -> {
|
||||||
|
val alias = keyDescriptor.alias ?: run {
|
||||||
|
SystemLogger.info("[TX_ID: $txId] createOperation domain=APP with null alias, forwarding to HAL")
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
val key = KeyIdentifier(callingUid, alias)
|
||||||
|
generatedKeys[key]?.let { java.util.AbstractMap.SimpleEntry(key, it) } ?: run {
|
||||||
|
SystemLogger.info("[TX_ID: $txId] createOperation alias=$alias not in generatedKeys, forwarding to HAL")
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Domain.KEY_ID -> {
|
||||||
|
val nspace = keyDescriptor.nspace
|
||||||
|
val entry = if (nspace == null || nspace == 0L) null
|
||||||
|
else generatedKeys.entries
|
||||||
|
.filter { it.key.uid == callingUid }
|
||||||
|
.find { it.value.nspace == nspace }
|
||||||
|
entry ?: run {
|
||||||
|
trackAndEnforceOpLimit(callingUid, txId)?.let { return it }
|
||||||
|
SystemLogger.info("[TX_ID: $txId] createOperation KeyId(${keyDescriptor.nspace}) NOT FOUND for uid=$callingUid. Forwarding to HAL.")
|
||||||
|
return TransactionResult.Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
SystemLogger.info("[TX_ID: $txId] createOperation domain=${keyDescriptor.domain}, forwarding to HAL")
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val generatedKeyInfo = resolvedEntry.value
|
||||||
|
val resolvedKeyId = resolvedEntry.key
|
||||||
|
|
||||||
if (generatedKeyInfo == null) {
|
trackAndEnforceOpLimit(callingUid, txId)?.let { return it }
|
||||||
SystemLogger.debug(
|
|
||||||
"[TX_ID: $txId] Operation for unknown/hardware KeyId ($nspace). Forwarding."
|
|
||||||
)
|
|
||||||
return TransactionResult.Continue
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for KeyId $nspace.")
|
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for uid=$callingUid.")
|
||||||
|
|
||||||
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
||||||
val parsedParams = KeyMintAttestation(params).let { p ->
|
val parsedParams = KeyMintAttestation(params).let { p ->
|
||||||
if (p.algorithm != 0) p
|
if (p.algorithm != 0) p
|
||||||
else p.copy(algorithm = when (generatedKeyInfo.keyPair.private.algorithm) {
|
else {
|
||||||
"EC" -> Algorithm.EC
|
val keyAlgo = generatedKeyInfo.keyPair?.private?.algorithm
|
||||||
"RSA" -> Algorithm.RSA
|
p.copy(algorithm = when (keyAlgo) {
|
||||||
else -> p.algorithm
|
"EC", "ECDSA" -> Algorithm.EC
|
||||||
})
|
"RSA" -> Algorithm.RSA
|
||||||
|
else -> generatedKeyInfo.keyParams?.algorithm ?: p.algorithm
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val forced = data.readBoolean()
|
||||||
|
|
||||||
|
val requestedPurpose = parsedParams.purpose.firstOrNull()
|
||||||
|
if (requestedPurpose == null) {
|
||||||
|
return InterceptorUtils.createServiceSpecificErrorReply(KEYMINT_INVALID_ARGUMENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams)
|
if (forced) {
|
||||||
|
return InterceptorUtils.createServiceSpecificErrorReply(RESPONSE_PERMISSION_DENIED)
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthorizeCreate.check(generatedKeyInfo.keyParams, parsedParams, params)?.let { errorCode ->
|
||||||
|
SystemLogger.info("[TX_ID: $txId] authorize_create rejected: errorCode=$errorCode")
|
||||||
|
return InterceptorUtils.createServiceSpecificErrorReply(errorCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyParams = generatedKeyInfo.keyParams
|
||||||
|
val effectiveParams = if (keyParams != null) {
|
||||||
|
keyParams.copy(
|
||||||
|
purpose = parsedParams.purpose,
|
||||||
|
digest = parsedParams.digest.ifEmpty { keyParams.digest },
|
||||||
|
blockMode = parsedParams.blockMode.ifEmpty { keyParams.blockMode },
|
||||||
|
padding = parsedParams.padding.ifEmpty { keyParams.padding },
|
||||||
|
nonce = parsedParams.nonce,
|
||||||
|
minMacLength = parsedParams.minMacLength ?: keyParams.minMacLength,
|
||||||
|
)
|
||||||
|
} else parsedParams
|
||||||
|
|
||||||
|
val opLatency = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_OP_LATENCY_FLOOR_MS else 0L
|
||||||
|
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, generatedKeyInfo.secretKey, effectiveParams, opLatency)
|
||||||
|
|
||||||
|
if (keyParams?.usageCountLimit != null) {
|
||||||
|
val limit = keyParams.usageCountLimit
|
||||||
|
val remaining = usageCounters.getOrPut(resolvedKeyId) {
|
||||||
|
java.util.concurrent.atomic.AtomicInteger(limit)
|
||||||
|
}
|
||||||
|
if (remaining.get() <= 0) {
|
||||||
|
cleanupKeyData(resolvedKeyId)
|
||||||
|
usageCounters.remove(resolvedKeyId)
|
||||||
|
return InterceptorUtils.createServiceSpecificErrorReply(RESPONSE_KEY_NOT_FOUND)
|
||||||
|
}
|
||||||
|
softwareOperation.onFinishCallback = {
|
||||||
|
if (remaining.decrementAndGet() <= 0) {
|
||||||
|
cleanupKeyData(resolvedKeyId)
|
||||||
|
usageCounters.remove(resolvedKeyId)
|
||||||
|
SystemLogger.info("Key $resolvedKeyId exhausted (USAGE_COUNT_LIMIT=$limit).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val maxOps = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_MAX_CONCURRENT_OPS else MAX_CONCURRENT_OPS_PER_UID
|
||||||
|
pruneOpsForUid(callingUid, softwareOperation, maxOps)
|
||||||
val operationBinder = SoftwareOperationBinder(softwareOperation)
|
val operationBinder = SoftwareOperationBinder(softwareOperation)
|
||||||
|
|
||||||
val response =
|
val response =
|
||||||
CreateOperationResponse().apply {
|
CreateOperationResponse().apply {
|
||||||
iOperation = operationBinder
|
iOperation = operationBinder
|
||||||
operationChallenge = null
|
operationChallenge = null
|
||||||
|
parameters = softwareOperation.beginParameters
|
||||||
}
|
}
|
||||||
|
|
||||||
return InterceptorUtils.createTypedObjectReply(response)
|
InterceptorUtils.createTypedObjectReply(response)
|
||||||
|
}.getOrElse {
|
||||||
|
SystemLogger.error("Error during createOperation for UID $callingUid.", it)
|
||||||
|
InterceptorUtils.createServiceSpecificErrorReply(KEYMINT_UNKNOWN_ERROR)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleGenerateKey(txId: Long, callingUid: Int, data: Parcel): TransactionResult {
|
private fun handleGenerateKey(txId: Long, callingUid: Int, callingPid: Int, data: Parcel): TransactionResult {
|
||||||
if (data.dataSize() > MAX_ALIAS_LENGTH) {
|
val oversized = data.dataSize() > MAX_ALIAS_LENGTH
|
||||||
SystemLogger.warning("Skipping oversized transaction: ${data.dataSize()} bytes")
|
|
||||||
return TransactionResult.ContinueAndSkipPost
|
|
||||||
}
|
|
||||||
|
|
||||||
return runCatching {
|
return runCatching {
|
||||||
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||||
@@ -256,6 +423,11 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
||||||
val parsedParams = KeyMintAttestation(params)
|
val parsedParams = KeyMintAttestation(params)
|
||||||
|
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] generateKey alias=${keyDescriptor.alias} algo=${parsedParams.algorithm} challenge=${parsedParams.attestationChallenge?.size ?: "null"} serial=${parsedParams.serial != null} imei=${parsedParams.imei != null} noAuth=${parsedParams.noAuthRequired} purposes=${parsedParams.purpose}" }
|
||||||
|
if (SystemLogger.isDebugBuild) params.forEach { p ->
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] tag=${p.tag} value=${p.value}" }
|
||||||
|
}
|
||||||
|
|
||||||
val challenge = parsedParams.attestationChallenge
|
val challenge = parsedParams.attestationChallenge
|
||||||
if (challenge != null && challenge.size > AttestationConstants.CHALLENGE_LENGTH_LIMIT) {
|
if (challenge != null && challenge.size > AttestationConstants.CHALLENGE_LENGTH_LIMIT) {
|
||||||
SystemLogger.warning("[TX_ID: $txId] Rejecting oversized attestation challenge: ${challenge.size} bytes (max ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})")
|
SystemLogger.warning("[TX_ID: $txId] Rejecting oversized attestation challenge: ${challenge.size} bytes (max ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})")
|
||||||
@@ -267,50 +439,78 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
return InterceptorUtils.createErrorReply(RESPONSE_INVALID_ARGUMENT)
|
return InterceptorUtils.createErrorReply(RESPONSE_INVALID_ARGUMENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.any { it.tag == Tag.DEVICE_UNIQUE_ATTESTATION }) {
|
if (params.any { it.tag == Tag.DEVICE_UNIQUE_ATTESTATION } && !AndroidPermissionUtils.hasUniqueIdAttestationPermission(callingUid)) {
|
||||||
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_UNIQUE_ATTESTATION for uid=$callingUid")
|
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_UNIQUE_ATTESTATION for uid=$callingUid")
|
||||||
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
|
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val hasDeviceIdAttestation = params.any {
|
||||||
|
it.tag == Tag.ATTESTATION_ID_IMEI ||
|
||||||
|
it.tag == Tag.ATTESTATION_ID_MEID ||
|
||||||
|
it.tag == Tag.ATTESTATION_ID_SERIAL ||
|
||||||
|
it.tag == Tag.DEVICE_UNIQUE_ATTESTATION ||
|
||||||
|
it.tag == Tag.ATTESTATION_ID_SECOND_IMEI
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hasDeviceIdAttestation && !AndroidPermissionUtils.hasDeviceAttestationPermission(callingUid)) {
|
||||||
|
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_ID_ATTESTATION for uid=$callingUid")
|
||||||
|
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AOSP security_level.rs:478-485: INCLUDE_UNIQUE_ID requires
|
||||||
|
// SELinux gen_unique_id OR Android REQUEST_UNIQUE_ID_ATTESTATION
|
||||||
|
if (params.any { it.tag == Tag.INCLUDE_UNIQUE_ID }) {
|
||||||
|
val hasSELinux = ConfigurationManager.checkSELinuxPermission(
|
||||||
|
callingPid, "keystore_key", "gen_unique_id",
|
||||||
|
)
|
||||||
|
val hasAndroid = ConfigurationManager.hasPermissionForUid(
|
||||||
|
callingUid, "android.permission.REQUEST_UNIQUE_ID_ATTESTATION",
|
||||||
|
)
|
||||||
|
if (!hasSELinux && !hasAndroid) {
|
||||||
|
SystemLogger.warning("[TX_ID: $txId] Rejecting INCLUDE_UNIQUE_ID for uid=$callingUid pid=$callingPid")
|
||||||
|
return InterceptorUtils.createServiceSpecificErrorReply(RESPONSE_PERMISSION_DENIED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isSymmetric = parsedParams.algorithm == Algorithm.AES ||
|
||||||
|
parsedParams.algorithm == Algorithm.HMAC ||
|
||||||
|
parsedParams.algorithm == Algorithm.TRIPLE_DES
|
||||||
|
|
||||||
|
if (securityLevel == SecurityLevel.STRONGBOX && !isStrongBoxCapable(parsedParams)) {
|
||||||
|
SystemLogger.info("[TX_ID: $txId] StrongBox-unsupported params (algo=${parsedParams.algorithm} size=${parsedParams.keySize}) → forwarding to HAL for rejection")
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
|
||||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||||
val isAttestKeyRequest = parsedParams.isAttestKey()
|
val isAttestKeyRequest = parsedParams.isAttestKey()
|
||||||
|
|
||||||
val needsSoftwareGeneration =
|
val forceGenerate =
|
||||||
ConfigurationManager.shouldGenerate(callingUid) ||
|
oversized ||
|
||||||
|
ConfigurationManager.shouldGenerate(callingUid) ||
|
||||||
(ConfigurationManager.shouldPatch(callingUid) && isAttestKeyRequest) ||
|
(ConfigurationManager.shouldPatch(callingUid) && isAttestKeyRequest) ||
|
||||||
(attestationKey != null &&
|
(attestationKey != null &&
|
||||||
isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias)))
|
isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias)))
|
||||||
|
|
||||||
if (needsSoftwareGeneration) {
|
val isAuto = ConfigurationManager.isAutoMode(callingUid)
|
||||||
return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
|
|
||||||
} else if (parsedParams.attestationChallenge != null) {
|
|
||||||
val windowUsed = hardwareKeygenWindowCount(callingUid)
|
|
||||||
val concurrentUsed = hardwareKeygenCount(callingUid).get()
|
|
||||||
|
|
||||||
// Sliding window rate limit
|
if (isAuto) SystemLogger.debug("AUTO dispatch: teePathDecision=${teePathDecision.get()} for ${keyDescriptor.alias}")
|
||||||
if (windowUsed >= MAX_HW_KEYGEN_PER_WINDOW) {
|
|
||||||
SystemLogger.info("[TX_ID: $txId] RATE_LIMITED uid=$callingUid window=$windowUsed/$MAX_HW_KEYGEN_PER_WINDOW concurrent=$concurrentUsed → software fallback")
|
SystemLogger.trace { "[TRACE-$txId] dispatch: forceGen=$forceGenerate isAuto=$isAuto teePath=${teePathDecision.get()} hasChallenge=${challenge != null} isSymmetric=$isSymmetric isAttestKey=$isAttestKeyRequest" }
|
||||||
return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
|
|
||||||
|
when {
|
||||||
|
forceGenerate -> doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
|
||||||
|
isAuto && teePathDecision.get() == null -> raceTeePatch(callingUid, keyDescriptor, attestationKey, params, parsedParams, keyId, isAttestKeyRequest)
|
||||||
|
isAuto && teePathDecision.get() == false -> doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
|
||||||
|
parsedParams.attestationChallenge != null -> TransactionResult.Continue
|
||||||
|
else -> {
|
||||||
|
cleanupKeyData(keyId)
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
// Concurrent cap
|
|
||||||
if (hardwareKeygenCount(callingUid).incrementAndGet() > MAX_CONCURRENT_HW_KEYGEN_PER_UID) {
|
|
||||||
hardwareKeygenCount(callingUid).decrementAndGet()
|
|
||||||
SystemLogger.info("[TX_ID: $txId] CONCURRENT_LIMITED uid=$callingUid window=$windowUsed/$MAX_HW_KEYGEN_PER_WINDOW concurrent=${concurrentUsed + 1}/$MAX_CONCURRENT_HW_KEYGEN_PER_UID → software fallback")
|
|
||||||
return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
|
|
||||||
}
|
|
||||||
// Both checks passed — commit the window permit and forward to hardware TEE
|
|
||||||
recordHardwareKeygen(callingUid)
|
|
||||||
hardwareKeygenTxIds.add(txId)
|
|
||||||
SystemLogger.info("[TX_ID: $txId] HARDWARE_KEYGEN uid=$callingUid window=${windowUsed + 1}/$MAX_HW_KEYGEN_PER_WINDOW concurrent=${concurrentUsed + 1}/$MAX_CONCURRENT_HW_KEYGEN_PER_UID → forwarding to TEE")
|
|
||||||
return TransactionResult.Continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupKeyData(keyId)
|
|
||||||
TransactionResult.ContinueAndSkipPost
|
|
||||||
}
|
}
|
||||||
.getOrElse {
|
.getOrElse {
|
||||||
SystemLogger.error("Error during generateKey handling for UID $callingUid.", it)
|
SystemLogger.error("Error during generateKey handling for UID $callingUid.", it)
|
||||||
TransactionResult.ContinueAndSkipPost
|
InterceptorUtils.createServiceSpecificErrorReply(SECURE_HW_COMMUNICATION_FAILED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,10 +522,57 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
keyId: KeyIdentifier,
|
keyId: KeyIdentifier,
|
||||||
isAttestKeyRequest: Boolean,
|
isAttestKeyRequest: Boolean,
|
||||||
): TransactionResult {
|
): TransactionResult {
|
||||||
val startNs = System.nanoTime()
|
val genStartNanos = System.nanoTime()
|
||||||
keyDescriptor.nspace = secureRandom.nextLong()
|
keyDescriptor.nspace = secureRandom.nextLong()
|
||||||
SystemLogger.info("Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}].")
|
SystemLogger.info("Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}].")
|
||||||
|
|
||||||
|
cleanupKeyData(keyId)
|
||||||
|
|
||||||
|
val isSymmetric = parsedParams.algorithm != Algorithm.EC &&
|
||||||
|
parsedParams.algorithm != Algorithm.RSA
|
||||||
|
|
||||||
|
if (isSymmetric) {
|
||||||
|
val algoName = when (parsedParams.algorithm) {
|
||||||
|
Algorithm.AES -> "AES"
|
||||||
|
Algorithm.HMAC -> "HmacSHA256"
|
||||||
|
else -> throw android.os.ServiceSpecificException(
|
||||||
|
SECURE_HW_COMMUNICATION_FAILED,
|
||||||
|
"Unsupported symmetric algorithm: ${parsedParams.algorithm}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val keyGen = javax.crypto.KeyGenerator.getInstance(algoName)
|
||||||
|
keyGen.init(parsedParams.keySize)
|
||||||
|
val secretKey = keyGen.generateKey()
|
||||||
|
|
||||||
|
val metadata = KeyMetadata().apply {
|
||||||
|
keySecurityLevel = securityLevel
|
||||||
|
key = KeyDescriptor().apply {
|
||||||
|
domain = Domain.KEY_ID
|
||||||
|
nspace = keyDescriptor.nspace
|
||||||
|
alias = null
|
||||||
|
blob = null
|
||||||
|
}
|
||||||
|
certificate = null
|
||||||
|
certificateChain = null
|
||||||
|
authorizations = parsedParams.toAuthorizations(callingUid, securityLevel)
|
||||||
|
modificationTimeMs = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
val response = KeyEntryResponse().apply {
|
||||||
|
this.metadata = metadata
|
||||||
|
iSecurityLevel = original
|
||||||
|
}
|
||||||
|
generatedKeys[keyId] = GeneratedKeyInfo(null, secretKey, keyDescriptor.nspace, response, parsedParams)
|
||||||
|
|
||||||
|
if (securityLevel == SecurityLevel.STRONGBOX) {
|
||||||
|
val delayMs = STRONGBOX_KEYGEN_LATENCY_FLOOR_MS - (System.nanoTime() - genStartNanos) / 1_000_000
|
||||||
|
if (delayMs > 0) LockSupport.parkNanos(delayMs * 1_000_000)
|
||||||
|
} else {
|
||||||
|
TeeLatencySimulator.simulateGenerateKeyDelay(parsedParams.algorithm, System.nanoTime() - genStartNanos)
|
||||||
|
}
|
||||||
|
|
||||||
|
return InterceptorUtils.createTypedObjectReply(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
val keyData = if (NativeCertGen.isAvailable && attestationKey == null) {
|
val keyData = if (NativeCertGen.isAvailable && attestationKey == null) {
|
||||||
generateAttestedKeyPairNative(callingUid, parsedParams)
|
generateAttestedKeyPairNative(callingUid, parsedParams)
|
||||||
?: CertificateGenerator.generateAttestedKeyPair(
|
?: CertificateGenerator.generateAttestedKeyPair(
|
||||||
@@ -337,32 +584,135 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
)
|
)
|
||||||
} ?: throw Exception("Both native and BouncyCastle cert gen failed.")
|
} ?: throw Exception("Both native and BouncyCastle cert gen failed.")
|
||||||
|
|
||||||
cleanupKeyData(keyId)
|
|
||||||
val response = buildKeyEntryResponse(callingUid, keyData.second, parsedParams, keyDescriptor)
|
val response = buildKeyEntryResponse(callingUid, keyData.second, parsedParams, keyDescriptor)
|
||||||
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response)
|
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, null, keyDescriptor.nspace, response, parsedParams)
|
||||||
if (isAttestKeyRequest) attestationKeys.add(keyId)
|
if (isAttestKeyRequest) attestationKeys.add(keyId)
|
||||||
|
|
||||||
GeneratedKeyPersistence.save(
|
if (SystemLogger.isDebugBuild) {
|
||||||
keyId = keyId,
|
val chain = keyData.second
|
||||||
keyPair = keyData.first,
|
val leaf = chain.firstOrNull() as? java.security.cert.X509Certificate
|
||||||
nspace = keyDescriptor.nspace,
|
SystemLogger.trace {
|
||||||
securityLevel = securityLevel,
|
"[certchain] ${keyDescriptor.alias}: depth=${chain.size} " +
|
||||||
certChain = keyData.second.toList(),
|
"issuer=${leaf?.issuerX500Principal?.name} " +
|
||||||
algorithm = parsedParams.algorithm,
|
"subject=${leaf?.subjectX500Principal?.name} " +
|
||||||
keySize = parsedParams.keySize,
|
"hasAttest=${leaf?.getExtensionValue("1.3.6.1.4.1.11129.2.1.17") != null}"
|
||||||
ecCurve = parsedParams.ecCurve ?: 0,
|
}
|
||||||
purposes = parsedParams.purpose,
|
}
|
||||||
digests = parsedParams.digest,
|
|
||||||
isAttestationKey = isAttestKeyRequest,
|
|
||||||
)
|
|
||||||
|
|
||||||
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
|
val certChainCopy = keyData.second.toList()
|
||||||
val delayMs = TEE_LATENCY_FLOOR_MS - elapsedMs
|
persistExecutor.execute {
|
||||||
if (delayMs > 0) Thread.sleep(delayMs)
|
GeneratedKeyPersistence.save(
|
||||||
|
keyId = keyId,
|
||||||
|
keyPair = keyData.first,
|
||||||
|
nspace = keyDescriptor.nspace,
|
||||||
|
securityLevel = securityLevel,
|
||||||
|
certChain = certChainCopy,
|
||||||
|
algorithm = parsedParams.algorithm,
|
||||||
|
keySize = parsedParams.keySize,
|
||||||
|
ecCurve = parsedParams.ecCurve ?: 0,
|
||||||
|
purposes = parsedParams.purpose,
|
||||||
|
digests = parsedParams.digest,
|
||||||
|
isAttestationKey = isAttestKeyRequest,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (securityLevel == SecurityLevel.STRONGBOX) {
|
||||||
|
val delayMs = STRONGBOX_KEYGEN_LATENCY_FLOOR_MS - (System.nanoTime() - genStartNanos) / 1_000_000
|
||||||
|
if (delayMs > 0) LockSupport.parkNanos(delayMs * 1_000_000)
|
||||||
|
} else {
|
||||||
|
TeeLatencySimulator.simulateGenerateKeyDelay(parsedParams.algorithm, System.nanoTime() - genStartNanos)
|
||||||
|
}
|
||||||
|
|
||||||
return InterceptorUtils.createTypedObjectReply(response.metadata)
|
return InterceptorUtils.createTypedObjectReply(response.metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun raceTeePatch(
|
||||||
|
callingUid: Int,
|
||||||
|
keyDescriptor: KeyDescriptor,
|
||||||
|
attestationKey: KeyDescriptor?,
|
||||||
|
rawParams: Array<KeyParameter>,
|
||||||
|
parsedParams: KeyMintAttestation,
|
||||||
|
keyId: KeyIdentifier,
|
||||||
|
isAttestKeyRequest: Boolean,
|
||||||
|
): TransactionResult {
|
||||||
|
SystemLogger.info("AUTO: racing TEE vs software for ${keyDescriptor.alias}")
|
||||||
|
|
||||||
|
val teeDescriptor = KeyDescriptor().apply {
|
||||||
|
domain = keyDescriptor.domain
|
||||||
|
nspace = keyDescriptor.nspace
|
||||||
|
alias = keyDescriptor.alias
|
||||||
|
blob = keyDescriptor.blob
|
||||||
|
}
|
||||||
|
val teeAttestKey = attestationKey?.let {
|
||||||
|
KeyDescriptor().apply {
|
||||||
|
domain = it.domain
|
||||||
|
nspace = it.nspace
|
||||||
|
alias = it.alias
|
||||||
|
blob = it.blob
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val threadA = CompletableFuture.supplyAsync {
|
||||||
|
original.generateKey(teeDescriptor, teeAttestKey, rawParams, 0, byteArrayOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
val swDescriptor = KeyDescriptor().apply {
|
||||||
|
domain = keyDescriptor.domain
|
||||||
|
nspace = secureRandom.nextLong()
|
||||||
|
alias = keyDescriptor.alias
|
||||||
|
blob = keyDescriptor.blob
|
||||||
|
}
|
||||||
|
val swKeyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||||
|
|
||||||
|
val threadB = CompletableFuture.supplyAsync {
|
||||||
|
doSoftwareKeyGen(callingUid, swDescriptor, attestationKey, parsedParams, swKeyId, isAttestKeyRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val teeMetadata = threadA.join()
|
||||||
|
threadB.cancel(true)
|
||||||
|
teePathDecision.compareAndSet(null, true)
|
||||||
|
SystemLogger.info("AUTO: TEE succeeded, path locked to PATCH for ${keyDescriptor.alias}")
|
||||||
|
|
||||||
|
val originalChain = CertificateHelper.getCertificateChain(teeMetadata)
|
||||||
|
if (originalChain != null && originalChain.size > 1) {
|
||||||
|
val newChain = AttestationPatcher.patchCertificateChain(
|
||||||
|
originalChain, callingUid, parsedParams.certificateNotBefore, parsedParams.certificateNotAfter
|
||||||
|
)
|
||||||
|
CertificateHelper.updateCertificateChain(teeMetadata, newChain).getOrThrow()
|
||||||
|
teeMetadata.authorizations =
|
||||||
|
InterceptorUtils.patchAuthorizations(teeMetadata.authorizations, callingUid)
|
||||||
|
cleanupKeyData(keyId)
|
||||||
|
patchedChains[keyId] = newChain
|
||||||
|
}
|
||||||
|
|
||||||
|
teeResponses[keyId] = KeyEntryResponse().apply {
|
||||||
|
this.metadata = teeMetadata
|
||||||
|
iSecurityLevel = original
|
||||||
|
}
|
||||||
|
|
||||||
|
InterceptorUtils.createTypedObjectReply(teeMetadata)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
if (teePathDecision.get() == true) {
|
||||||
|
threadB.cancel(true)
|
||||||
|
SystemLogger.info("AUTO: TEE failed locally but globally functional, forwarding for ${keyDescriptor.alias}")
|
||||||
|
return TransactionResult.Continue
|
||||||
|
}
|
||||||
|
teePathDecision.compareAndSet(null, false)
|
||||||
|
SystemLogger.info("AUTO: TEE failed, path locked to GENERATE for ${keyDescriptor.alias}")
|
||||||
|
try {
|
||||||
|
threadB.join()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("AUTO: both paths failed for ${keyDescriptor.alias}.", e)
|
||||||
|
val code =
|
||||||
|
if (e.cause is android.os.ServiceSpecificException)
|
||||||
|
(e.cause as android.os.ServiceSpecificException).errorCode
|
||||||
|
else SECURE_HW_COMMUNICATION_FAILED
|
||||||
|
InterceptorUtils.createServiceSpecificErrorReply(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun generateAttestedKeyPairNative(
|
private fun generateAttestedKeyPairNative(
|
||||||
callingUid: Int,
|
callingUid: Int,
|
||||||
params: KeyMintAttestation,
|
params: KeyMintAttestation,
|
||||||
@@ -383,7 +733,8 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
|
|
||||||
val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel)
|
val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel)
|
||||||
val keymasterVersion = AndroidDeviceUtils.getKeymasterVersion(securityLevel)
|
val keymasterVersion = AndroidDeviceUtils.getKeymasterVersion(securityLevel)
|
||||||
val appId = AttestationBuilder.createApplicationId(callingUid)
|
val hasChallenge = params.attestationChallenge != null
|
||||||
|
val appId = if (hasChallenge) AttestationBuilder.createApplicationId(callingUid) else null
|
||||||
|
|
||||||
val config = CertGenConfig(
|
val config = CertGenConfig(
|
||||||
algorithm = params.algorithm,
|
algorithm = params.algorithm,
|
||||||
@@ -409,7 +760,7 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
bootKey = AndroidDeviceUtils.bootKey,
|
bootKey = AndroidDeviceUtils.bootKey,
|
||||||
bootHash = AndroidDeviceUtils.bootHash,
|
bootHash = AndroidDeviceUtils.bootHash,
|
||||||
creationDatetime = System.currentTimeMillis(),
|
creationDatetime = System.currentTimeMillis(),
|
||||||
attestationApplicationId = appId.octets,
|
attestationApplicationId = appId?.octets ?: ByteArray(0),
|
||||||
moduleHash = if (attestVersion >= 400) AndroidDeviceUtils.moduleHash else null,
|
moduleHash = if (attestVersion >= 400) AndroidDeviceUtils.moduleHash else null,
|
||||||
idBrand = params.brand,
|
idBrand = params.brand,
|
||||||
idDevice = params.device,
|
idDevice = params.device,
|
||||||
@@ -420,6 +771,13 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
idManufacturer = params.manufacturer,
|
idManufacturer = params.manufacturer,
|
||||||
idModel = params.model,
|
idModel = params.model,
|
||||||
idSecondImei = if (attestVersion >= 300) params.secondImei else null,
|
idSecondImei = if (attestVersion >= 300) params.secondImei else null,
|
||||||
|
activeDatetime = params.activeDateTime?.time ?: -1L,
|
||||||
|
originationExpireDatetime = params.originationExpireDateTime?.time ?: -1L,
|
||||||
|
usageExpireDatetime = params.usageExpireDateTime?.time ?: -1L,
|
||||||
|
usageCountLimit = params.usageCountLimit ?: -1,
|
||||||
|
callerNonce = params.callerNonce == true,
|
||||||
|
unlockedDeviceRequired = params.unlockedDeviceRequired == true,
|
||||||
|
noAuthRequired = params.noAuthRequired != false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val resultBytes = NativeCertGen.generateAttestedKeyPair(config) ?: return null
|
val resultBytes = NativeCertGen.generateAttestedKeyPair(config) ?: return null
|
||||||
@@ -525,10 +883,28 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
manufacturer = null,
|
manufacturer = null,
|
||||||
model = null,
|
model = null,
|
||||||
secondImei = null,
|
secondImei = null,
|
||||||
|
activeDateTime = null,
|
||||||
|
originationExpireDateTime = null,
|
||||||
|
usageExpireDateTime = null,
|
||||||
|
usageCountLimit = null,
|
||||||
|
callerNonce = null,
|
||||||
|
nonce = null,
|
||||||
|
unlockedDeviceRequired = null,
|
||||||
|
includeUniqueId = null,
|
||||||
|
rollbackResistance = null,
|
||||||
|
earlyBootOnly = null,
|
||||||
|
allowWhileOnBody = null,
|
||||||
|
trustedUserPresenceRequired = null,
|
||||||
|
trustedConfirmationRequired = null,
|
||||||
|
noAuthRequired = null,
|
||||||
|
maxUsesPerBoot = null,
|
||||||
|
maxBootLevel = null,
|
||||||
|
minMacLength = null,
|
||||||
|
rsaOaepMgfDigest = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
val response = buildKeyEntryResponse(record.uid, certChain, attestation, descriptor)
|
val response = buildKeyEntryResponse(record.uid, certChain, attestation, descriptor)
|
||||||
generatedKeys[keyId] = GeneratedKeyInfo(keyPair, record.nspace, response)
|
generatedKeys[keyId] = GeneratedKeyInfo(keyPair, null, record.nspace, response, attestation)
|
||||||
if (record.isAttestationKey) attestationKeys.add(keyId)
|
if (record.isAttestationKey) attestationKeys.add(keyId)
|
||||||
|
|
||||||
SystemLogger.debug("Restored persisted key: $keyId")
|
SystemLogger.debug("Restored persisted key: $keyId")
|
||||||
@@ -547,38 +923,24 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
// Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead
|
// Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead
|
||||||
private const val MAX_ALIAS_LENGTH = 256 * 1024
|
private const val MAX_ALIAS_LENGTH = 256 * 1024
|
||||||
private const val KEYMINT_INVALID_INPUT_LENGTH = -21
|
private const val KEYMINT_INVALID_INPUT_LENGTH = -21
|
||||||
|
private const val KEYMINT_INVALID_ARGUMENT = -38
|
||||||
private const val RESPONSE_INVALID_ARGUMENT = 20
|
private const val RESPONSE_INVALID_ARGUMENT = 20
|
||||||
|
private const val RESPONSE_PERMISSION_DENIED = 6
|
||||||
|
private const val RESPONSE_KEY_NOT_FOUND = 7
|
||||||
private const val TEE_LATENCY_FLOOR_MS = 15L
|
private const val TEE_LATENCY_FLOOR_MS = 15L
|
||||||
|
private const val STRONGBOX_KEYGEN_LATENCY_FLOOR_MS = 250L
|
||||||
|
private const val STRONGBOX_OP_LATENCY_FLOOR_MS = 80L
|
||||||
|
private const val KEYMINT_TOO_MANY_OPERATIONS = -29
|
||||||
private const val KEYMINT_CANNOT_ATTEST_IDS = -66
|
private const val KEYMINT_CANNOT_ATTEST_IDS = -66
|
||||||
private const val MAX_CONCURRENT_HW_KEYGEN_PER_UID = 2
|
private const val KEYMINT_UNKNOWN_ERROR = -1000
|
||||||
// Sliding window: max hardware keygen permits per UID within the burst window
|
private const val SECURE_HW_COMMUNICATION_FAILED = -49
|
||||||
private const val MAX_HW_KEYGEN_PER_WINDOW = 2
|
private const val MAX_CONCURRENT_OPS_PER_UID = 15
|
||||||
private const val BURST_WINDOW_MS = 30_000L
|
private const val STRONGBOX_MAX_CONCURRENT_OPS = 4
|
||||||
private val uidHardwareKeygenCount = ConcurrentHashMap<Int, AtomicInteger>()
|
private const val STRONGBOX_OP_WINDOW_NS = 10_000_000_000L // 10s
|
||||||
private val hardwareKeygenTxIds = ConcurrentHashMap.newKeySet<Long>()
|
private fun isStrongBoxCapable(params: KeyMintAttestation): Boolean = when (params.algorithm) {
|
||||||
private val uidKeygenTimestamps = ConcurrentHashMap<Int, MutableList<Long>>()
|
Algorithm.RSA -> params.keySize <= 2048
|
||||||
|
Algorithm.EC -> params.ecCurve == null || params.ecCurve == EcCurve.P_256
|
||||||
private fun hardwareKeygenCount(uid: Int): AtomicInteger =
|
else -> true
|
||||||
uidHardwareKeygenCount.computeIfAbsent(uid) { AtomicInteger(0) }
|
|
||||||
|
|
||||||
private fun hardwareKeygenWindowCount(uid: Int): Int {
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() }
|
|
||||||
synchronized(timestamps) {
|
|
||||||
timestamps.removeAll { now - it > BURST_WINDOW_MS }
|
|
||||||
if (timestamps.isEmpty()) {
|
|
||||||
uidKeygenTimestamps.remove(uid, timestamps)
|
|
||||||
uidHardwareKeygenCount.remove(uid)
|
|
||||||
}
|
|
||||||
return timestamps.size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun recordHardwareKeygen(uid: Int) {
|
|
||||||
val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() }
|
|
||||||
synchronized(timestamps) {
|
|
||||||
timestamps.add(System.currentTimeMillis())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val GENERATE_KEY_TRANSACTION =
|
private val GENERATE_KEY_TRANSACTION =
|
||||||
@@ -591,6 +953,9 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
"createOperation",
|
"createOperation",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val INTERCEPTED_CODES =
|
||||||
|
intArrayOf(GENERATE_KEY_TRANSACTION, IMPORT_KEY_TRANSACTION, CREATE_OPERATION_TRANSACTION)
|
||||||
|
|
||||||
private val transactionNames: Map<Int, String> by lazy {
|
private val transactionNames: Map<Int, String> by lazy {
|
||||||
IKeystoreSecurityLevel.Stub::class
|
IKeystoreSecurityLevel.Stub::class
|
||||||
.java
|
.java
|
||||||
@@ -602,13 +967,18 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
|
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val persistExecutor = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>()
|
val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>()
|
||||||
|
val teeResponses = ConcurrentHashMap<KeyIdentifier, KeyEntryResponse>()
|
||||||
val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
|
val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
|
||||||
val attestationKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
val attestationKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
||||||
|
val importedKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
||||||
|
private val usageCounters = ConcurrentHashMap<KeyIdentifier, java.util.concurrent.atomic.AtomicInteger>()
|
||||||
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
|
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
|
||||||
|
|
||||||
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
|
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
|
||||||
generatedKeys[keyId]?.response
|
generatedKeys[keyId]?.response ?: teeResponses[keyId]
|
||||||
|
|
||||||
fun findGeneratedKeyByKeyId(callingUid: Int, nspace: Long?): GeneratedKeyInfo? {
|
fun findGeneratedKeyByKeyId(callingUid: Int, nspace: Long?): GeneratedKeyInfo? {
|
||||||
if (nspace == null || nspace == 0L) return null
|
if (nspace == null || nspace == 0L) return null
|
||||||
@@ -627,12 +997,15 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
SystemLogger.debug("Remove generated key ${keyId}")
|
SystemLogger.debug("Remove generated key ${keyId}")
|
||||||
GeneratedKeyPersistence.delete(keyId)
|
GeneratedKeyPersistence.delete(keyId)
|
||||||
}
|
}
|
||||||
|
teeResponses.remove(keyId)
|
||||||
if (patchedChains.remove(keyId) != null) {
|
if (patchedChains.remove(keyId) != null) {
|
||||||
SystemLogger.debug("Remove patched chain for ${keyId}")
|
SystemLogger.debug("Remove patched chain for ${keyId}")
|
||||||
}
|
}
|
||||||
if (attestationKeys.remove(keyId)) {
|
if (attestationKeys.remove(keyId)) {
|
||||||
SystemLogger.debug("Remove cached attestaion key ${keyId}")
|
SystemLogger.debug("Remove cached attestaion key ${keyId}")
|
||||||
}
|
}
|
||||||
|
importedKeys.remove(keyId)
|
||||||
|
usageCounters.remove(keyId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) {
|
fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) {
|
||||||
@@ -655,8 +1028,11 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
val count = generatedKeys.size
|
val count = generatedKeys.size
|
||||||
val reasonMessage = reason?.let { " due to $it" } ?: ""
|
val reasonMessage = reason?.let { " due to $it" } ?: ""
|
||||||
generatedKeys.clear()
|
generatedKeys.clear()
|
||||||
|
teeResponses.clear()
|
||||||
patchedChains.clear()
|
patchedChains.clear()
|
||||||
attestationKeys.clear()
|
attestationKeys.clear()
|
||||||
|
importedKeys.clear()
|
||||||
|
usageCounters.clear()
|
||||||
GeneratedKeyPersistence.deleteAll()
|
GeneratedKeyPersistence.deleteAll()
|
||||||
SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.")
|
SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.")
|
||||||
}
|
}
|
||||||
@@ -686,13 +1062,44 @@ private fun KeyMintAttestation.toAuthorizations(
|
|||||||
authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve)))
|
authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve)))
|
||||||
}
|
}
|
||||||
this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) }
|
this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) }
|
||||||
|
this.blockMode.forEach { authList.add(createAuth(Tag.BLOCK_MODE, KeyParameterValue.blockMode(it))) }
|
||||||
this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) }
|
this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) }
|
||||||
this.padding.forEach { authList.add(createAuth(Tag.PADDING, KeyParameterValue.paddingMode(it))) }
|
this.padding.forEach { authList.add(createAuth(Tag.PADDING, KeyParameterValue.paddingMode(it))) }
|
||||||
authList.add(createAuth(Tag.KEY_SIZE, KeyParameterValue.integer(this.keySize)))
|
authList.add(createAuth(Tag.KEY_SIZE, KeyParameterValue.integer(this.keySize)))
|
||||||
if (this.rsaPublicExponent != null) {
|
if (this.rsaPublicExponent != null) {
|
||||||
authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong())))
|
authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong())))
|
||||||
}
|
}
|
||||||
authList.add(createAuth(Tag.NO_AUTH_REQUIRED, KeyParameterValue.boolValue(true)))
|
if (this.callerNonce == true) {
|
||||||
|
authList.add(createAuth(Tag.CALLER_NONCE, KeyParameterValue.boolValue(true)))
|
||||||
|
}
|
||||||
|
if (this.minMacLength != null) {
|
||||||
|
authList.add(createAuth(Tag.MIN_MAC_LENGTH, KeyParameterValue.integer(this.minMacLength)))
|
||||||
|
}
|
||||||
|
if (this.rollbackResistance == true) {
|
||||||
|
authList.add(createAuth(Tag.ROLLBACK_RESISTANCE, KeyParameterValue.boolValue(true)))
|
||||||
|
}
|
||||||
|
if (this.earlyBootOnly == true) {
|
||||||
|
authList.add(createAuth(Tag.EARLY_BOOT_ONLY, KeyParameterValue.boolValue(true)))
|
||||||
|
}
|
||||||
|
if (this.allowWhileOnBody == true) {
|
||||||
|
authList.add(createAuth(Tag.ALLOW_WHILE_ON_BODY, KeyParameterValue.boolValue(true)))
|
||||||
|
}
|
||||||
|
if (this.trustedUserPresenceRequired == true) {
|
||||||
|
authList.add(createAuth(Tag.TRUSTED_USER_PRESENCE_REQUIRED, KeyParameterValue.boolValue(true)))
|
||||||
|
}
|
||||||
|
if (this.trustedConfirmationRequired == true) {
|
||||||
|
authList.add(createAuth(Tag.TRUSTED_CONFIRMATION_REQUIRED, KeyParameterValue.boolValue(true)))
|
||||||
|
}
|
||||||
|
if (this.maxUsesPerBoot != null) {
|
||||||
|
authList.add(createAuth(Tag.MAX_USES_PER_BOOT, KeyParameterValue.integer(this.maxUsesPerBoot)))
|
||||||
|
}
|
||||||
|
if (this.maxBootLevel != null) {
|
||||||
|
authList.add(createAuth(Tag.MAX_BOOT_LEVEL, KeyParameterValue.integer(this.maxBootLevel)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.noAuthRequired != false) {
|
||||||
|
authList.add(createAuth(Tag.NO_AUTH_REQUIRED, KeyParameterValue.boolValue(true)))
|
||||||
|
}
|
||||||
authList.add(createAuth(Tag.ORIGIN, KeyParameterValue.origin(this.origin ?: KeyOrigin.GENERATED)))
|
authList.add(createAuth(Tag.ORIGIN, KeyParameterValue.origin(this.origin ?: KeyOrigin.GENERATED)))
|
||||||
authList.add(createAuth(Tag.OS_VERSION, KeyParameterValue.integer(AndroidDeviceUtils.osVersion)))
|
authList.add(createAuth(Tag.OS_VERSION, KeyParameterValue.integer(AndroidDeviceUtils.osVersion)))
|
||||||
|
|
||||||
@@ -708,8 +1115,37 @@ private fun KeyMintAttestation.toAuthorizations(
|
|||||||
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
|
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
|
||||||
authList.add(createAuth(Tag.BOOT_PATCHLEVEL, KeyParameterValue.integer(bootPatch)))
|
authList.add(createAuth(Tag.BOOT_PATCHLEVEL, KeyParameterValue.integer(bootPatch)))
|
||||||
}
|
}
|
||||||
authList.add(createAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis())))
|
|
||||||
authList.add(createAuth(Tag.USER_ID, KeyParameterValue.integer(callingUid / 100000)))
|
fun createSwAuth(tag: Int, value: KeyParameterValue): Authorization {
|
||||||
|
val param = KeyParameter().apply {
|
||||||
|
this.tag = tag
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
return Authorization().apply {
|
||||||
|
this.keyParameter = param
|
||||||
|
this.securityLevel = SecurityLevel.SOFTWARE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authList.add(createSwAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis())))
|
||||||
|
|
||||||
|
this.activeDateTime?.let {
|
||||||
|
authList.add(createSwAuth(Tag.ACTIVE_DATETIME, KeyParameterValue.dateTime(it.time)))
|
||||||
|
}
|
||||||
|
this.originationExpireDateTime?.let {
|
||||||
|
authList.add(createSwAuth(Tag.ORIGINATION_EXPIRE_DATETIME, KeyParameterValue.dateTime(it.time)))
|
||||||
|
}
|
||||||
|
this.usageExpireDateTime?.let {
|
||||||
|
authList.add(createSwAuth(Tag.USAGE_EXPIRE_DATETIME, KeyParameterValue.dateTime(it.time)))
|
||||||
|
}
|
||||||
|
this.usageCountLimit?.let {
|
||||||
|
authList.add(createSwAuth(Tag.USAGE_COUNT_LIMIT, KeyParameterValue.integer(it)))
|
||||||
|
}
|
||||||
|
if (this.unlockedDeviceRequired == true) {
|
||||||
|
authList.add(createSwAuth(Tag.UNLOCKED_DEVICE_REQUIRED, KeyParameterValue.boolValue(true)))
|
||||||
|
}
|
||||||
|
|
||||||
|
authList.add(createSwAuth(Tag.USER_ID, KeyParameterValue.integer(callingUid / 100000)))
|
||||||
|
|
||||||
return authList.toTypedArray()
|
return authList.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -44,6 +44,8 @@ class OperationInterceptor(
|
|||||||
private val ABORT_TRANSACTION =
|
private val ABORT_TRANSACTION =
|
||||||
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort")
|
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort")
|
||||||
|
|
||||||
|
val INTERCEPTED_CODES = intArrayOf(FINISH_TRANSACTION, ABORT_TRANSACTION)
|
||||||
|
|
||||||
private val transactionNames: Map<Int, String> by lazy {
|
private val transactionNames: Map<Int, String> by lazy {
|
||||||
IKeystoreOperation.Stub::class
|
IKeystoreOperation.Stub::class
|
||||||
.java
|
.java
|
||||||
|
|||||||
+192
-42
@@ -3,11 +3,15 @@ package org.matrix.TEESimulator.interception.keystore.shim
|
|||||||
import android.hardware.security.keymint.Algorithm
|
import android.hardware.security.keymint.Algorithm
|
||||||
import android.hardware.security.keymint.BlockMode
|
import android.hardware.security.keymint.BlockMode
|
||||||
import android.hardware.security.keymint.Digest
|
import android.hardware.security.keymint.Digest
|
||||||
|
import android.hardware.security.keymint.KeyParameter
|
||||||
|
import android.hardware.security.keymint.KeyParameterValue
|
||||||
import android.hardware.security.keymint.KeyPurpose
|
import android.hardware.security.keymint.KeyPurpose
|
||||||
import android.hardware.security.keymint.PaddingMode
|
import android.hardware.security.keymint.PaddingMode
|
||||||
import android.os.RemoteException
|
import android.hardware.security.keymint.Tag
|
||||||
import android.os.ServiceSpecificException
|
import android.os.ServiceSpecificException
|
||||||
|
import java.util.concurrent.locks.LockSupport
|
||||||
import android.system.keystore2.IKeystoreOperation
|
import android.system.keystore2.IKeystoreOperation
|
||||||
|
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
|
||||||
@@ -16,15 +20,16 @@ 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
|
||||||
|
|
||||||
// A sealed interface to represent the different cryptographic operations we can perform.
|
|
||||||
private sealed interface CryptoPrimitive {
|
private sealed interface CryptoPrimitive {
|
||||||
fun updateAad(aadInput: ByteArray?) {}
|
fun updateAad(aadInput: ByteArray?) {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper object to map KeyMint constants to JCA algorithm strings.
|
|
||||||
private object JcaAlgorithmMapper {
|
private object JcaAlgorithmMapper {
|
||||||
fun mapSignatureAlgorithm(params: KeyMintAttestation): String {
|
fun mapSignatureAlgorithm(params: KeyMintAttestation): String {
|
||||||
val digest =
|
val digest =
|
||||||
@@ -34,16 +39,18 @@ private object JcaAlgorithmMapper {
|
|||||||
Digest.SHA_2_512 -> "SHA512"
|
Digest.SHA_2_512 -> "SHA512"
|
||||||
else -> "NONE"
|
else -> "NONE"
|
||||||
}
|
}
|
||||||
val keyAlgo =
|
return when (params.algorithm) {
|
||||||
when (params.algorithm) {
|
Algorithm.EC -> "${digest}withECDSA"
|
||||||
Algorithm.EC -> "ECDSA"
|
Algorithm.RSA -> {
|
||||||
Algorithm.RSA -> "RSA"
|
val isPss = params.padding.firstOrNull() == PaddingMode.RSA_PSS
|
||||||
else ->
|
if (isPss) "${digest}withRSA/PSS" else "${digest}withRSA"
|
||||||
throw IllegalArgumentException(
|
|
||||||
"Unsupported signature algorithm: ${params.algorithm}"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return "${digest}with${keyAlgo}"
|
else ->
|
||||||
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.incompatibleAlgorithm,
|
||||||
|
"Unsupported signature algorithm: ${params.algorithm}",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
|
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
|
||||||
@@ -52,30 +59,32 @@ private object JcaAlgorithmMapper {
|
|||||||
Algorithm.RSA -> "RSA"
|
Algorithm.RSA -> "RSA"
|
||||||
Algorithm.AES -> "AES"
|
Algorithm.AES -> "AES"
|
||||||
else ->
|
else ->
|
||||||
throw IllegalArgumentException(
|
throw ServiceSpecificException(
|
||||||
"Unsupported cipher algorithm: ${params.algorithm}"
|
KeystoreErrorCodes.incompatibleAlgorithm,
|
||||||
|
"Unsupported cipher algorithm: ${params.algorithm}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val blockMode =
|
val blockMode =
|
||||||
when (params.blockMode.firstOrNull()) {
|
when (params.blockMode.firstOrNull()) {
|
||||||
BlockMode.ECB -> "ECB"
|
BlockMode.ECB -> "ECB"
|
||||||
BlockMode.CBC -> "CBC"
|
BlockMode.CBC -> "CBC"
|
||||||
|
BlockMode.CTR -> "CTR"
|
||||||
BlockMode.GCM -> "GCM"
|
BlockMode.GCM -> "GCM"
|
||||||
else -> "ECB" // Default for RSA
|
else -> "ECB"
|
||||||
}
|
}
|
||||||
val padding =
|
val padding =
|
||||||
when (params.padding.firstOrNull()) {
|
when (params.padding.firstOrNull()) {
|
||||||
PaddingMode.NONE -> "NoPadding"
|
PaddingMode.NONE -> "NoPadding"
|
||||||
PaddingMode.PKCS7 -> "PKCS7Padding"
|
PaddingMode.PKCS7 -> "PKCS7Padding"
|
||||||
PaddingMode.RSA_PKCS1_1_5_ENCRYPT -> "PKCS1Padding"
|
PaddingMode.RSA_PKCS1_1_5_ENCRYPT -> "PKCS1Padding"
|
||||||
|
PaddingMode.RSA_PKCS1_1_5_SIGN -> "PKCS1Padding"
|
||||||
PaddingMode.RSA_OAEP -> "OAEPPadding"
|
PaddingMode.RSA_OAEP -> "OAEPPadding"
|
||||||
else -> "NoPadding" // Default for GCM
|
else -> "NoPadding"
|
||||||
}
|
}
|
||||||
return "$keyAlgo/$blockMode/$padding"
|
return "$keyAlgo/$blockMode/$padding"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Concrete implementation for Signing.
|
|
||||||
private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
|
private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
|
||||||
private val signature: Signature =
|
private val signature: Signature =
|
||||||
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
|
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
|
||||||
@@ -95,7 +104,6 @@ private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimi
|
|||||||
override fun abort() {}
|
override fun abort() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Concrete implementation for Verification.
|
|
||||||
private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
|
private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
|
||||||
private val signature: Signature =
|
private val signature: Signature =
|
||||||
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
|
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
|
||||||
@@ -109,42 +117,101 @@ 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) throw SignatureException("Signature to verify is null")
|
if (signature == null) {
|
||||||
|
throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature to verify is null")
|
||||||
|
}
|
||||||
if (!this.signature.verify(signature)) {
|
if (!this.signature.verify(signature)) {
|
||||||
// Throwing an exception is how Keystore signals verification failure.
|
throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature verification failed")
|
||||||
throw SignatureException("Signature verification failed")
|
|
||||||
}
|
}
|
||||||
// A successful verification returns no data.
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun abort() {}
|
override fun abort() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Concrete implementation for Encryption/Decryption.
|
|
||||||
private class CipherPrimitive(
|
private class CipherPrimitive(
|
||||||
keyPair: KeyPair,
|
cryptoKey: java.security.Key,
|
||||||
params: KeyMintAttestation,
|
params: KeyMintAttestation,
|
||||||
private val opMode: Int,
|
private val opMode: Int,
|
||||||
) : CryptoPrimitive {
|
) : CryptoPrimitive {
|
||||||
|
private val isAead = params.blockMode.firstOrNull() == BlockMode.GCM
|
||||||
private val cipher: Cipher =
|
private val cipher: Cipher =
|
||||||
Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply {
|
Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply {
|
||||||
val key = if (opMode == Cipher.ENCRYPT_MODE) keyPair.public else keyPair.private
|
val nonce = params.nonce
|
||||||
init(opMode, key)
|
if (nonce != null && isAead) {
|
||||||
|
init(opMode, cryptoKey, javax.crypto.spec.GCMParameterSpec(128, nonce))
|
||||||
|
} else if (nonce != null) {
|
||||||
|
init(opMode, cryptoKey, javax.crypto.spec.IvParameterSpec(nonce))
|
||||||
|
} else {
|
||||||
|
init(opMode, cryptoKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateAad(aadInput: ByteArray?) {
|
||||||
|
if (!isAead) throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
|
||||||
|
if (aadInput != null) cipher.updateAAD(aadInput)
|
||||||
|
}
|
||||||
|
|
||||||
override fun update(data: ByteArray?): ByteArray? =
|
override fun update(data: ByteArray?): ByteArray? =
|
||||||
if (data != null) cipher.update(data) else null
|
if (data != null) cipher.update(data) else null
|
||||||
|
|
||||||
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? =
|
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? =
|
||||||
if (data != null) cipher.doFinal(data) else cipher.doFinal()
|
if (data != null) cipher.doFinal(data) else cipher.doFinal()
|
||||||
|
|
||||||
|
override fun getBeginParameters(): Array<KeyParameter>? {
|
||||||
|
val iv = cipher.iv ?: return null
|
||||||
|
return arrayOf(
|
||||||
|
KeyParameter().apply {
|
||||||
|
tag = Tag.NONCE
|
||||||
|
value = KeyParameterValue.blob(iv)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun abort() {}
|
override fun abort() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMintAttestation) {
|
private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive {
|
||||||
|
private val agreement: javax.crypto.KeyAgreement =
|
||||||
|
javax.crypto.KeyAgreement.getInstance("ECDH").apply { init(keyPair.private) }
|
||||||
|
|
||||||
|
override fun update(data: ByteArray?): ByteArray? = null
|
||||||
|
|
||||||
|
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
|
if (data == null)
|
||||||
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.invalidArgument,
|
||||||
|
"Peer public key required for key agreement",
|
||||||
|
)
|
||||||
|
val peerKey =
|
||||||
|
java.security.KeyFactory.getInstance("EC")
|
||||||
|
.generatePublic(java.security.spec.X509EncodedKeySpec(data))
|
||||||
|
agreement.doPhase(peerKey, true)
|
||||||
|
return agreement.generateSecret()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun abort() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SoftwareOperation(
|
||||||
|
private val txId: Long,
|
||||||
|
keyPair: KeyPair?,
|
||||||
|
secretKey: javax.crypto.SecretKey?,
|
||||||
|
params: KeyMintAttestation,
|
||||||
|
private val latencyFloorMs: Long = 0L,
|
||||||
|
) {
|
||||||
private val primitive: CryptoPrimitive
|
private val primitive: CryptoPrimitive
|
||||||
@Volatile private var finalized = false
|
@Volatile var finalized = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
var onFinishCallback: (() -> Unit)? = null
|
||||||
|
|
||||||
|
val beginParameters: KeyParameters?
|
||||||
|
get() {
|
||||||
|
val params = primitive.getBeginParameters() ?: return null
|
||||||
|
if (params.isEmpty()) return null
|
||||||
|
return KeyParameters().apply { keyParameter = params }
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val purpose = params.purpose.firstOrNull()
|
val purpose = params.purpose.firstOrNull()
|
||||||
@@ -153,31 +220,48 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
|
|||||||
|
|
||||||
primitive =
|
primitive =
|
||||||
when (purpose) {
|
when (purpose) {
|
||||||
KeyPurpose.SIGN -> Signer(keyPair, params)
|
KeyPurpose.SIGN -> Signer(keyPair!!, params)
|
||||||
KeyPurpose.VERIFY -> Verifier(keyPair, params)
|
KeyPurpose.VERIFY -> Verifier(keyPair!!, params)
|
||||||
KeyPurpose.ENCRYPT -> CipherPrimitive(keyPair, params, Cipher.ENCRYPT_MODE)
|
KeyPurpose.ENCRYPT -> {
|
||||||
KeyPurpose.DECRYPT -> CipherPrimitive(keyPair, params, Cipher.DECRYPT_MODE)
|
val key: java.security.Key = secretKey ?: keyPair!!.public
|
||||||
|
CipherPrimitive(key, params, Cipher.ENCRYPT_MODE)
|
||||||
|
}
|
||||||
|
KeyPurpose.DECRYPT -> {
|
||||||
|
val key: java.security.Key = secretKey ?: keyPair!!.private
|
||||||
|
CipherPrimitive(key, params, Cipher.DECRYPT_MODE)
|
||||||
|
}
|
||||||
|
KeyPurpose.AGREE_KEY -> KeyAgreementPrimitive(keyPair!!)
|
||||||
else ->
|
else ->
|
||||||
throw UnsupportedOperationException("Unsupported operation purpose: $purpose")
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.unsupportedPurpose,
|
||||||
|
"Unsupported operation purpose: $purpose",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkActive() {
|
private fun checkActive() {
|
||||||
if (finalized) throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle)
|
if (finalized) {
|
||||||
|
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)")
|
||||||
|
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})")
|
||||||
throw ServiceSpecificException(KeystoreErrorCodes.tooMuchData)
|
throw ServiceSpecificException(KeystoreErrorCodes.tooMuchData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateAad(aadInput: ByteArray?) {
|
fun updateAad(aadInput: ByteArray?) {
|
||||||
|
SystemLogger.debug("[SoftwareOp TX_ID: $txId] updateAad() inputSize=${aadInput?.size ?: 0}")
|
||||||
checkActive()
|
checkActive()
|
||||||
checkInputLength(aadInput)
|
checkInputLength(aadInput)
|
||||||
primitive.updateAad(aadInput)
|
primitive.updateAad(aadInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(data: ByteArray?): ByteArray? {
|
fun update(data: ByteArray?): ByteArray? {
|
||||||
|
SystemLogger.debug("[SoftwareOp TX_ID: $txId] update() inputSize=${data?.size ?: 0}")
|
||||||
checkActive()
|
checkActive()
|
||||||
checkInputLength(data)
|
checkInputLength(data)
|
||||||
try {
|
try {
|
||||||
@@ -186,7 +270,7 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
|
|||||||
throw e
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
|
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
|
||||||
throw e
|
throw mapToServiceSpecificException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,15 +278,22 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
|
|||||||
checkActive()
|
checkActive()
|
||||||
checkInputLength(data)
|
checkInputLength(data)
|
||||||
try {
|
try {
|
||||||
|
val startNs = if (latencyFloorMs > 0) System.nanoTime() else 0L
|
||||||
val result = primitive.finish(data, signature)
|
val result = primitive.finish(data, signature)
|
||||||
|
if (latencyFloorMs > 0) {
|
||||||
|
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
|
||||||
|
val delayMs = latencyFloorMs - elapsedMs
|
||||||
|
if (delayMs > 0) LockSupport.parkNanos(delayMs * 1_000_000)
|
||||||
|
}
|
||||||
finalized = true
|
finalized = true
|
||||||
|
onFinishCallback?.invoke()
|
||||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
|
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
|
||||||
return result
|
return result
|
||||||
} catch (e: ServiceSpecificException) {
|
} catch (e: ServiceSpecificException) {
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
|
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
|
||||||
throw e
|
throw mapToServiceSpecificException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,22 +303,77 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
|
|||||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
|
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun mapToServiceSpecificException(e: Exception): ServiceSpecificException = when (e) {
|
||||||
|
is SignatureException -> ServiceSpecificException(KeystoreErrorCodes.verificationFailed, e.message)
|
||||||
|
is javax.crypto.BadPaddingException -> ServiceSpecificException(KeystoreErrorCodes.invalidArgument, e.message)
|
||||||
|
is javax.crypto.IllegalBlockSizeException -> ServiceSpecificException(KeystoreErrorCodes.invalidInputLength, e.message)
|
||||||
|
is java.security.InvalidKeyException -> ServiceSpecificException(KeystoreErrorCodes.incompatibleKey, e.message)
|
||||||
|
else -> ServiceSpecificException(KeystoreErrorCodes.unknownError, e.message)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// AOSP keystore2 operation.rs: const MAX_RECEIVE_DATA: usize = 0x8000
|
|
||||||
private const val MAX_RECEIVE_DATA = 0x8000
|
private const val MAX_RECEIVE_DATA = 0x8000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object KeystoreErrorCodes {
|
internal object KeystoreErrorCodes {
|
||||||
val tooMuchData: Int by lazy {
|
val tooMuchData: Int by lazy {
|
||||||
resolveField("android.system.keystore2.ResponseCode", "TOO_MUCH_DATA", 29)
|
resolveField("android.system.keystore2.ResponseCode", "TOO_MUCH_DATA", 21)
|
||||||
}
|
}
|
||||||
|
|
||||||
val invalidOperationHandle: Int by lazy {
|
val invalidOperationHandle: Int by lazy {
|
||||||
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_OPERATION_HANDLE", -28)
|
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_OPERATION_HANDLE", -28)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveField(className: String, fieldName: String, fallback: Int): Int =
|
val invalidTag: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_TAG", -76)
|
||||||
|
}
|
||||||
|
|
||||||
|
val verificationFailed: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "VERIFICATION_FAILED", -30)
|
||||||
|
}
|
||||||
|
|
||||||
|
val invalidArgument: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_ARGUMENT", -38)
|
||||||
|
}
|
||||||
|
|
||||||
|
val invalidInputLength: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_INPUT_LENGTH", -21)
|
||||||
|
}
|
||||||
|
|
||||||
|
val incompatibleKey: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_KEY", -31)
|
||||||
|
}
|
||||||
|
|
||||||
|
val incompatiblePurpose: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_PURPOSE", -13)
|
||||||
|
}
|
||||||
|
|
||||||
|
val unsupportedPurpose: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "UNSUPPORTED_PURPOSE", -14)
|
||||||
|
}
|
||||||
|
|
||||||
|
val incompatibleAlgorithm: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_ALGORITHM", -18)
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyNotYetValid: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "KEY_NOT_YET_VALID", -39)
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyExpired: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "KEY_EXPIRED", -40)
|
||||||
|
}
|
||||||
|
|
||||||
|
val callerNonceProhibited: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "CALLER_NONCE_PROHIBITED", -55)
|
||||||
|
}
|
||||||
|
|
||||||
|
val unknownError: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "UNKNOWN_ERROR", -1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resolveField(className: String, fieldName: String, fallback: Int): Int =
|
||||||
runCatching {
|
runCatching {
|
||||||
Class.forName(className).getField(fieldName).getInt(null)
|
Class.forName(className).getField(fieldName).getInt(null)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
@@ -239,18 +385,22 @@ private object KeystoreErrorCodes {
|
|||||||
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
||||||
IKeystoreOperation.Stub() {
|
IKeystoreOperation.Stub() {
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
override fun updateAad(aadInput: ByteArray?) {
|
override fun updateAad(aadInput: ByteArray?) {
|
||||||
operation.updateAad(aadInput)
|
operation.updateAad(aadInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
override fun update(input: ByteArray?): ByteArray? {
|
override fun update(input: ByteArray?): ByteArray? {
|
||||||
return operation.update(input)
|
return operation.update(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
|
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
return operation.finish(input, signature)
|
return operation.finish(input, signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
override fun abort() {
|
override fun abort() {
|
||||||
operation.abort()
|
operation.abort()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,86 @@
|
|||||||
package org.matrix.TEESimulator.logging
|
package org.matrix.TEESimulator.logging
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
import org.matrix.TEESimulator.BuildConfig
|
import org.matrix.TEESimulator.BuildConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
* logging tag and format for all application logs, making it easier to filter and debug in Logcat.
|
* logging tag and format for all application logs, making it easier to filter and debug in Logcat.
|
||||||
|
*
|
||||||
|
* Includes a rate limiter that caps logd syscalls during binder stress to prevent thread pool
|
||||||
|
* contention. The first [RATE_LIMIT_BURST] messages per [RATE_LIMIT_WINDOW_MS] window are logged
|
||||||
|
* normally; subsequent messages are suppressed and a summary is emitted when the window resets.
|
||||||
*/
|
*/
|
||||||
object SystemLogger {
|
object SystemLogger {
|
||||||
// The tag used for all log messages from this application.
|
@PublishedApi internal const val TAG = "TEESimulator"
|
||||||
private const val TAG = "TEESimulator"
|
|
||||||
|
|
||||||
private val isDebugBuild = BuildConfig.DEBUG
|
@PublishedApi internal val isDebugBuild = BuildConfig.DEBUG
|
||||||
|
|
||||||
|
// Rate limiter: allow BURST messages per WINDOW, then suppress until window resets.
|
||||||
|
private const val RATE_LIMIT_BURST = 15
|
||||||
|
private const val RATE_LIMIT_WINDOW_MS = 1000L
|
||||||
|
private val windowStart = AtomicLong(System.currentTimeMillis())
|
||||||
|
private val windowCount = AtomicInteger(0)
|
||||||
|
private val suppressedCount = AtomicInteger(0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this message should be emitted. Resets the window if expired
|
||||||
|
* and emits a suppression summary for the previous window.
|
||||||
|
*/
|
||||||
|
@PublishedApi internal fun acquireLogPermit(): Boolean {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val start = windowStart.get()
|
||||||
|
if (now - start > RATE_LIMIT_WINDOW_MS) {
|
||||||
|
// Window expired: reset and emit suppression summary if needed.
|
||||||
|
if (windowStart.compareAndSet(start, now)) {
|
||||||
|
val suppressed = suppressedCount.getAndSet(0)
|
||||||
|
windowCount.set(1) // this call counts as #1 in the new window
|
||||||
|
if (suppressed > 0) {
|
||||||
|
Log.i(TAG, "[rate-limit] suppressed $suppressed log messages in previous window")
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val count = windowCount.incrementAndGet()
|
||||||
|
if (count <= RATE_LIMIT_BURST) return true
|
||||||
|
suppressedCount.incrementAndGet()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs a debug message. Use this for fine-grained information that is useful for debugging.
|
* Logs a debug message. Use this for fine-grained information that is useful for debugging.
|
||||||
*
|
|
||||||
* @param message The message to log.
|
|
||||||
*/
|
*/
|
||||||
fun debug(message: String) {
|
fun debug(message: String) {
|
||||||
if (!isDebugBuild) return
|
if (!isDebugBuild) return
|
||||||
|
if (!acquireLogPermit()) return
|
||||||
Log.d(TAG, message)
|
Log.d(TAG, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Lazy debug: lambda only evaluates if message will be logged. */
|
||||||
|
inline fun debug(message: () -> String) {
|
||||||
|
if (!isDebugBuild) return
|
||||||
|
if (!acquireLogPermit()) return
|
||||||
|
Log.d(TAG, message())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs an informational message. Use this to report major application lifecycle events.
|
* Logs an informational message. Use this to report major application lifecycle events.
|
||||||
*
|
|
||||||
* @param message The message to log.
|
|
||||||
*/
|
*/
|
||||||
fun info(message: String) {
|
fun info(message: String) {
|
||||||
|
if (!acquireLogPermit()) return
|
||||||
Log.i(TAG, message)
|
Log.i(TAG, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Lazy info: lambda only evaluates if message will be logged. */
|
||||||
|
inline fun info(message: () -> String) {
|
||||||
|
if (!acquireLogPermit()) return
|
||||||
|
Log.i(TAG, message())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs a warning message. Use this to report unexpected but non-fatal issues.
|
* Logs a warning message. Warnings are never rate-limited.
|
||||||
*
|
|
||||||
* @param message The message to log.
|
|
||||||
* @param throwable An optional exception to log with the message.
|
|
||||||
*/
|
*/
|
||||||
fun warning(message: String, throwable: Throwable? = null) {
|
fun warning(message: String, throwable: Throwable? = null) {
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
@@ -47,11 +91,7 @@ object SystemLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs an error message. Use this to report fatal errors or exceptions that disrupt
|
* Logs an error message. Errors are never rate-limited.
|
||||||
* functionality.
|
|
||||||
*
|
|
||||||
* @param message The message to log.
|
|
||||||
* @param throwable An optional exception to log with the message.
|
|
||||||
*/
|
*/
|
||||||
fun error(message: String, throwable: Throwable? = null) {
|
fun error(message: String, throwable: Throwable? = null) {
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
@@ -64,11 +104,22 @@ object SystemLogger {
|
|||||||
/**
|
/**
|
||||||
* Logs a verbose message. This level is for highly detailed logs that are generally not needed
|
* Logs a verbose message. This level is for highly detailed logs that are generally not needed
|
||||||
* unless tracking a very specific issue.
|
* unless tracking a very specific issue.
|
||||||
*
|
|
||||||
* @param message The message to log.
|
|
||||||
*/
|
*/
|
||||||
fun verbose(message: String) {
|
fun verbose(message: String) {
|
||||||
if (!isDebugBuild) return
|
if (!isDebugBuild) return
|
||||||
|
if (!acquireLogPermit()) return
|
||||||
Log.v(TAG, message)
|
Log.v(TAG, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Lazy verbose: lambda only evaluates if message will be logged. */
|
||||||
|
inline fun verbose(message: () -> String) {
|
||||||
|
if (!isDebugBuild) return
|
||||||
|
if (!acquireLogPermit()) return
|
||||||
|
Log.v(TAG, message())
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun trace(message: () -> String) {
|
||||||
|
if (!isDebugBuild) return
|
||||||
|
Log.w(TAG, message())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import java.math.BigInteger
|
|||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
import java.security.cert.X509Certificate
|
|
||||||
import java.security.spec.ECGenParameterSpec
|
import java.security.spec.ECGenParameterSpec
|
||||||
import java.security.spec.RSAKeyGenParameterSpec
|
import java.security.spec.RSAKeyGenParameterSpec
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -36,6 +35,8 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
|||||||
*/
|
*/
|
||||||
object CertificateGenerator {
|
object CertificateGenerator {
|
||||||
|
|
||||||
|
private const val UNDEFINED_NOT_AFTER = 253402300799000L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a software-based cryptographic key pair.
|
* Generates a software-based cryptographic key pair.
|
||||||
*
|
*
|
||||||
@@ -49,7 +50,10 @@ object CertificateGenerator {
|
|||||||
Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName)
|
Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName)
|
||||||
Algorithm.RSA ->
|
Algorithm.RSA ->
|
||||||
"RSA" to
|
"RSA" to
|
||||||
RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent)
|
RSAKeyGenParameterSpec(
|
||||||
|
params.keySize,
|
||||||
|
params.rsaPublicExponent ?: RSAKeyGenParameterSpec.F4,
|
||||||
|
)
|
||||||
else ->
|
else ->
|
||||||
throw IllegalArgumentException(
|
throw IllegalArgumentException(
|
||||||
"Unsupported algorithm: ${params.algorithm}"
|
"Unsupported algorithm: ${params.algorithm}"
|
||||||
@@ -88,11 +92,15 @@ object CertificateGenerator {
|
|||||||
"Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})"
|
"Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})"
|
||||||
)
|
)
|
||||||
|
|
||||||
return runCatching {
|
return try {
|
||||||
|
// AOSP ta/src/keys.rs:451-478: no challenge + no attestKey = self-signed, depth 1
|
||||||
|
if (challenge == null && attestKeyAlias == null) {
|
||||||
|
SystemLogger.trace { "[certgen] no-challenge key: self-signed, depth=1, purposes=${params.purpose}" }
|
||||||
|
return listOf(buildSelfSignedCertificate(subjectKeyPair, params))
|
||||||
|
}
|
||||||
|
|
||||||
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
|
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
|
||||||
|
|
||||||
// Determine the signing key and issuer. If an attestKey is provided, use it.
|
|
||||||
// Otherwise, fall back to the root key from the keybox.
|
|
||||||
val (signingKey, issuer) =
|
val (signingKey, issuer) =
|
||||||
if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second }
|
getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second }
|
||||||
@@ -101,20 +109,20 @@ object CertificateGenerator {
|
|||||||
keybox.keyPair to getIssuerFromKeybox(keybox)
|
keybox.keyPair to getIssuerFromKeybox(keybox)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the new leaf certificate with the simulated attestation.
|
|
||||||
val leafCert =
|
val leafCert =
|
||||||
buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel)
|
buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel)
|
||||||
|
|
||||||
// If not self-attesting, the chain is just the leaf. Otherwise, append the keybox
|
|
||||||
// chain.
|
|
||||||
if (attestKeyAlias != null) {
|
if (attestKeyAlias != 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
|
||||||
}
|
}
|
||||||
.onFailure { SystemLogger.error("Failed to generate certificate chain.", it) }
|
|
||||||
.getOrNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,7 +136,7 @@ object CertificateGenerator {
|
|||||||
params: KeyMintAttestation,
|
params: KeyMintAttestation,
|
||||||
securityLevel: Int,
|
securityLevel: Int,
|
||||||
): Pair<KeyPair, List<Certificate>>? {
|
): Pair<KeyPair, List<Certificate>>? {
|
||||||
return runCatching {
|
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)"
|
||||||
)
|
)
|
||||||
@@ -144,11 +152,12 @@ object CertificateGenerator {
|
|||||||
"Successfully generated new certificate chain for alias: '$alias'."
|
"Successfully generated new certificate chain for alias: '$alias'."
|
||||||
)
|
)
|
||||||
Pair(newKeyPair, chain)
|
Pair(newKeyPair, chain)
|
||||||
|
} catch (e: android.os.ServiceSpecificException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", e)
|
||||||
|
null
|
||||||
}
|
}
|
||||||
.onFailure {
|
|
||||||
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", it)
|
|
||||||
}
|
|
||||||
.getOrNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getIssuerFromKeybox(keybox: KeyBox) =
|
fun getIssuerFromKeybox(keybox: KeyBox) =
|
||||||
@@ -163,7 +172,10 @@ object CertificateGenerator {
|
|||||||
else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm")
|
else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm")
|
||||||
}
|
}
|
||||||
return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName)
|
return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName)
|
||||||
?: throw Exception("Could not load keybox for UID $uid and algorithm $algorithmName")
|
?: throw android.os.ServiceSpecificException(
|
||||||
|
-75, // ATTESTATION_KEYS_NOT_PROVISIONED
|
||||||
|
"No attestation key for algorithm $algorithmName in $keyboxFile",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Retrieves the key pair and issuer name for a given attestation key alias. */
|
/** Retrieves the key pair and issuer name for a given attestation key alias. */
|
||||||
@@ -214,16 +226,15 @@ object CertificateGenerator {
|
|||||||
securityLevel: Int,
|
securityLevel: Int,
|
||||||
): Certificate {
|
): Certificate {
|
||||||
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
|
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
|
||||||
val leafNotAfter =
|
val notBefore = params.certificateNotBefore ?: Date(0)
|
||||||
(signingKeyPair.public as? X509Certificate)?.notAfter
|
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
|
||||||
?: Date(System.currentTimeMillis() + 31536000000L)
|
|
||||||
|
|
||||||
val builder =
|
val builder =
|
||||||
JcaX509v3CertificateBuilder(
|
JcaX509v3CertificateBuilder(
|
||||||
issuer,
|
issuer,
|
||||||
params.certificateSerial ?: BigInteger.ONE,
|
params.certificateSerial ?: BigInteger.ONE,
|
||||||
params.certificateNotBefore ?: Date(),
|
notBefore,
|
||||||
params.certificateNotAfter ?: leafNotAfter,
|
notAfter,
|
||||||
subject,
|
subject,
|
||||||
subjectKeyPair.public,
|
subjectKeyPair.public,
|
||||||
)
|
)
|
||||||
@@ -233,14 +244,15 @@ object CertificateGenerator {
|
|||||||
if (keyUsageBits != 0) {
|
if (keyUsageBits != 0) {
|
||||||
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
|
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
|
||||||
}
|
}
|
||||||
// Add our custom, simulated attestation extension.
|
if (params.attestationChallenge != null) {
|
||||||
builder.addExtension(
|
builder.addExtension(
|
||||||
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
|
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val signerAlgorithm =
|
val signerAlgorithm =
|
||||||
when (signingKeyPair.private.algorithm) {
|
when (signingKeyPair.private.algorithm) {
|
||||||
"EC" -> "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}")
|
||||||
}
|
}
|
||||||
@@ -251,4 +263,39 @@ object CertificateGenerator {
|
|||||||
|
|
||||||
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AOSP ta/src/keys.rs:452-478, ta/src/cert.rs:111-114
|
||||||
|
private fun buildSelfSignedCertificate(
|
||||||
|
keyPair: KeyPair,
|
||||||
|
params: KeyMintAttestation,
|
||||||
|
): Certificate {
|
||||||
|
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
|
||||||
|
val notBefore = params.certificateNotBefore ?: Date(0)
|
||||||
|
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
|
||||||
|
|
||||||
|
val builder = JcaX509v3CertificateBuilder(
|
||||||
|
subject,
|
||||||
|
params.certificateSerial ?: BigInteger.ONE,
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
subject,
|
||||||
|
keyPair.public,
|
||||||
|
)
|
||||||
|
|
||||||
|
val keyUsageBits = buildKeyUsageFromPurposes(params.purpose)
|
||||||
|
if (keyUsageBits != 0) {
|
||||||
|
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
|
||||||
|
}
|
||||||
|
|
||||||
|
val signerAlgorithm = when (keyPair.private.algorithm) {
|
||||||
|
"EC", "ECDSA" -> "SHA256withECDSA"
|
||||||
|
"RSA" -> "SHA256withRSA"
|
||||||
|
else -> throw IllegalArgumentException("Unsupported key: ${keyPair.private.algorithm}")
|
||||||
|
}
|
||||||
|
val contentSigner = JcaContentSignerBuilder(signerAlgorithm)
|
||||||
|
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
.build(keyPair.private)
|
||||||
|
|
||||||
|
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,13 @@ data class CertGenConfig(
|
|||||||
val idManufacturer: ByteArray?,
|
val idManufacturer: ByteArray?,
|
||||||
val idModel: ByteArray?,
|
val idModel: ByteArray?,
|
||||||
val idSecondImei: ByteArray?,
|
val idSecondImei: ByteArray?,
|
||||||
|
val activeDatetime: Long = -1L,
|
||||||
|
val originationExpireDatetime: Long = -1L,
|
||||||
|
val usageExpireDatetime: Long = -1L,
|
||||||
|
val usageCountLimit: Int = -1,
|
||||||
|
val callerNonce: Boolean = false,
|
||||||
|
val unlockedDeviceRequired: Boolean = false,
|
||||||
|
val noAuthRequired: Boolean = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
object NativeCertGen {
|
object NativeCertGen {
|
||||||
@@ -105,7 +112,7 @@ object NativeCertGen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val algorithmName = when (certs[0].publicKey.algorithm) {
|
val algorithmName = when (certs[0].publicKey.algorithm) {
|
||||||
"EC" -> "EC"
|
"EC", "ECDSA" -> "EC"
|
||||||
"RSA" -> "RSA"
|
"RSA" -> "RSA"
|
||||||
else -> certs[0].publicKey.algorithm
|
else -> certs[0].publicKey.algorithm
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -387,9 +387,17 @@ object AndroidDeviceUtils {
|
|||||||
if (securityLevel == SecurityLevel.STRONGBOX) {
|
if (securityLevel == SecurityLevel.STRONGBOX) {
|
||||||
return 300
|
return 300
|
||||||
}
|
}
|
||||||
return DeviceAttestationService.CachedAttestationData?.attestVersion
|
val cached = DeviceAttestationService.CachedAttestationData?.attestVersion
|
||||||
|
val version = cached
|
||||||
?: attestVersionMap[Build.VERSION.SDK_INT]
|
?: attestVersionMap[Build.VERSION.SDK_INT]
|
||||||
?: 400 // Default to a recent version
|
?: 400 // Default to a recent version
|
||||||
|
val source = when {
|
||||||
|
cached != null -> "cache"
|
||||||
|
attestVersionMap.containsKey(Build.VERSION.SDK_INT) -> "map"
|
||||||
|
else -> "default"
|
||||||
|
}
|
||||||
|
SystemLogger.debug("attestVersion=$version source=$source securityLevel=$securityLevel")
|
||||||
|
return version
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -398,10 +406,7 @@ object AndroidDeviceUtils {
|
|||||||
* @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 {
|
fun getKeymasterVersion(securityLevel: Int): Int = getAttestVersion(securityLevel)
|
||||||
val attestVersion = getAttestVersion(securityLevel)
|
|
||||||
return if (attestVersion >= 100) attestVersion else 41 // Keymaster 4.1 for older versions
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- APEX and Module Hash Properties ---
|
// --- APEX and Module Hash Properties ---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package org.matrix.TEESimulator.util
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
|
||||||
|
object AndroidPermissionUtils {
|
||||||
|
|
||||||
|
@SuppressLint("PrivateApi", "DiscouragedPrivateApi")
|
||||||
|
private fun getGlobalContext(): Context? {
|
||||||
|
return try {
|
||||||
|
// 1. Get the hidden ActivityThread class via reflection
|
||||||
|
val activityThreadClass = Class.forName("android.app.ActivityThread")
|
||||||
|
|
||||||
|
// 2. Invoke the static currentActivityThread() method
|
||||||
|
val currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread")
|
||||||
|
currentActivityThreadMethod.isAccessible = true
|
||||||
|
val activityThread = currentActivityThreadMethod.invoke(null)
|
||||||
|
|
||||||
|
if (activityThread == null) {
|
||||||
|
SystemLogger.warning("Reflection: ActivityThread.currentActivityThread() returned null")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Try to get the application context
|
||||||
|
val getApplicationMethod = activityThreadClass.getDeclaredMethod("getApplication")
|
||||||
|
getApplicationMethod.isAccessible = true
|
||||||
|
val application = getApplicationMethod.invoke(activityThread) as? Context
|
||||||
|
|
||||||
|
if (application != null) return application
|
||||||
|
|
||||||
|
// 4. Fallback to getSystemContext() if application is null (often happens in system_server)
|
||||||
|
val getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext")
|
||||||
|
getSystemContextMethod.isAccessible = true
|
||||||
|
getSystemContextMethod.invoke(activityThread) as? Context
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("Reflection failed to get global context for permission check", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core permission check.
|
||||||
|
*/
|
||||||
|
fun hasPermission(uid: Int, permission: String): Boolean {
|
||||||
|
val context = getGlobalContext() ?: run {
|
||||||
|
SystemLogger.warning("AndroidPermissionUtils: Context is null, failing permission check safely.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = context.checkPermission(permission, -1, uid)
|
||||||
|
return result == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasDeviceAttestationPermission(uid: Int): Boolean {
|
||||||
|
return hasPermission(uid, "android.permission.READ_PRIVILEGED_PHONE_STATE")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasUniqueIdAttestationPermission(uid: Int): Boolean {
|
||||||
|
return hasPermission(uid, "android.permission.REQUEST_UNIQUE_ID_ATTESTATION")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasManageUsersPermission(uid: Int): Boolean {
|
||||||
|
return hasPermission(uid, "android.permission.MANAGE_USERS")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasDumpPermission(uid: Int): Boolean {
|
||||||
|
return hasPermission(uid, "android.permission.DUMP")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package org.matrix.TEESimulator.util
|
||||||
|
|
||||||
|
import android.hardware.security.keymint.Algorithm
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.util.concurrent.locks.LockSupport
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.exp
|
||||||
|
import kotlin.math.ln
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
object TeeLatencySimulator {
|
||||||
|
|
||||||
|
private val rng = SecureRandom()
|
||||||
|
|
||||||
|
private val sessionBiasMs: Double by lazy { rng.nextGaussian() * 5.0 }
|
||||||
|
private val coldPenaltyMs: Double by lazy { abs(rng.nextGaussian() * 12.0) }
|
||||||
|
|
||||||
|
@Volatile private var firstCall = true
|
||||||
|
|
||||||
|
fun simulateGenerateKeyDelay(algorithm: Int, elapsedNanos: Long) {
|
||||||
|
val elapsedMs = elapsedNanos / 1_000_000.0
|
||||||
|
val targetMs = sampleTotalDelay(algorithm)
|
||||||
|
val remainingMs = targetMs - elapsedMs
|
||||||
|
|
||||||
|
if (remainingMs > 1.0) {
|
||||||
|
LockSupport.parkNanos((remainingMs * 1_000_000).toLong())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sampleTotalDelay(algorithm: Int): Double {
|
||||||
|
val base = sampleBaseCryptoDelay(algorithm)
|
||||||
|
val transit = sampleExponential(2.5)
|
||||||
|
val jitter = (rng.nextGaussian() * 2.5).coerceIn(-8.0, 12.0)
|
||||||
|
|
||||||
|
var cold = 0.0
|
||||||
|
if (firstCall) {
|
||||||
|
firstCall = false
|
||||||
|
cold = coldPenaltyMs
|
||||||
|
}
|
||||||
|
|
||||||
|
return max(20.0, base + transit + jitter + sessionBiasMs + cold)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sampleBaseCryptoDelay(algorithm: Int): Double {
|
||||||
|
val (mu, sigma) =
|
||||||
|
when (algorithm) {
|
||||||
|
Algorithm.EC -> ln(60.0) to 0.08
|
||||||
|
Algorithm.RSA -> ln(70.0) to 0.08
|
||||||
|
Algorithm.AES -> ln(35.0) to 0.10
|
||||||
|
else -> ln(40.0) to 0.10
|
||||||
|
}
|
||||||
|
return sampleLogNormal(mu, sigma)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sampleLogNormal(mu: Double, sigma: Double): Double {
|
||||||
|
return exp(mu + sigma * rng.nextGaussian())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sampleExponential(mean: Double): Double {
|
||||||
|
var u = rng.nextDouble()
|
||||||
|
while (u == 0.0) u = rng.nextDouble()
|
||||||
|
return -mean * ln(u)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,91 @@
|
|||||||
|
## TEESimulator-RS v6.0.0
|
||||||
|
|
||||||
|
Repository consolidation release. All tee-rebuild work merged as the new main branch.
|
||||||
|
|
||||||
|
### AOSP Self-Signed Cert Compliance
|
||||||
|
- No-challenge keys now generate self-signed certs (subject == issuer, depth 1), matching AOSP `ta/src/keys.rs:451-478`
|
||||||
|
- Both Kotlin (BouncyCastle) and Rust (native-certgen) paths corrected
|
||||||
|
- Eliminates attestation behavioral probes that detect keybox issuer on non-attested keys
|
||||||
|
|
||||||
|
### Stability
|
||||||
|
- Binder stress crash hardening for concurrent generateKey calls
|
||||||
|
- AUTO mode TEE race for consistent attestation on devices with working G10
|
||||||
|
- Oversized transactions routed to software gen instead of crashing
|
||||||
|
- Operation-time params (BLOCK_MODE, PADDING, DIGEST) passed through to CipherPrimitive
|
||||||
|
|
||||||
|
### Banking App Compatibility
|
||||||
|
- Bare `target.txt` entries now default to AUTO mode, resolved at config level to PATCH (working TEE) or GENERATE (broken TEE)
|
||||||
|
- Fixes BHIM and similar banking apps that require TEE-backed attestation keys
|
||||||
|
- Restores v5.0 behavior where AUTO was resolved before the interceptor dispatch, avoiding the non-deterministic `raceTeePatch` path
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
- Version scheme changed to semver (v6.0.0)
|
||||||
|
- Repository moved to TEESimulator-RS as canonical source
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v5.0: AOSP Compliance Overhaul
|
||||||
|
|
||||||
|
Major release integrating 30+ AOSP compliance improvements from upstream PR #157 analysis, layered on top of our StrongBox hardening and native cert gen architecture.
|
||||||
|
|
||||||
|
### Attestation Extension Alignment
|
||||||
|
- 17 enforcement tags added to KeyMintAttestation (ACTIVE_DATETIME, ORIGINATION_EXPIRE, USAGE_EXPIRE, USAGE_COUNT_LIMIT, CALLER_NONCE, UNLOCKED_DEVICE_REQUIRED, INCLUDE_UNIQUE_ID, ROLLBACK_RESISTANCE, EARLY_BOOT_ONLY, ALLOW_WHILE_ON_BODY, TRUSTED_USER_PRESENCE_REQUIRED, TRUSTED_CONFIRMATION_REQUIRED, NO_AUTH_REQUIRED, MAX_USES_PER_BOOT, MAX_BOOT_LEVEL, MIN_MAC_LENGTH, RSA_OAEP_MGF_DIGEST)
|
||||||
|
- BLOCK_MODE encoded as SET OF INTEGER per AOSP attestation_record.h
|
||||||
|
- Version-guarded tags (RSA_OAEP_MGF_DIGEST >=100, ROLLBACK_RESISTANCE >=3, EARLY_BOOT_ONLY >=4)
|
||||||
|
- INCLUDE_UNIQUE_ID computed via HMAC-SHA256 per KeyMint HAL spec using device HBK
|
||||||
|
- AAID gated on attestation challenge presence
|
||||||
|
- Certificate validity defaults aligned with AOSP (epoch notBefore, 9999-12-31 notAfter)
|
||||||
|
|
||||||
|
### Binder Infrastructure
|
||||||
|
- Native transaction code filtering at C++ level, skipping JNI for non-intercepted codes
|
||||||
|
- getNumberOfEntries includes software-generated key count
|
||||||
|
- deleteKey resolves KEY_ID domain via generatedKeys lookup
|
||||||
|
- patchAuthorizations for OS/VENDOR/BOOT patch levels in authorization arrays
|
||||||
|
|
||||||
|
### Software Operation AOSP Conformance
|
||||||
|
- updateAad on non-AEAD operations returns INVALID_TAG (-76), matching AOSP operation.rs
|
||||||
|
- All crypto exceptions wrapped as ServiceSpecificException with correct KeyMint error codes
|
||||||
|
- GCM IV returned in CreateOperationResponse.parameters for encrypt operations
|
||||||
|
- SoftwareOperationBinder methods @Synchronized, matching AOSP Mutex per operation
|
||||||
|
- authorize_create enforcement: PURPOSE validation, algorithm-purpose compatibility, temporal constraints, CALLER_NONCE prohibition, WRAP_KEY rejection
|
||||||
|
|
||||||
|
### Security and Configuration
|
||||||
|
- SELinux permission checks via /proc/pid/attr/current
|
||||||
|
- Per-UID permission verification through IPackageManager.checkPermission
|
||||||
|
- Imported key tracking prevents stale attest-key overrides in getKeyEntry
|
||||||
|
- nspace consistency fix in attest-key override path
|
||||||
|
- TeeLatencySimulator with log-normal distribution matching real hardware profiles
|
||||||
|
- Device-unique HBK seed generated on install (32 bytes from /dev/random)
|
||||||
|
|
||||||
|
### Preserved from v4.8
|
||||||
|
- StrongBox op limits (4 concurrent max, TOO_MANY_OPERATIONS rejection)
|
||||||
|
- LRU operation pruning per security level
|
||||||
|
- Hardware keygen rate limiting (2/30s sliding window, 2 concurrent cap)
|
||||||
|
- Native Rust cert generation with BouncyCastle fallback
|
||||||
|
- Key persistence across reboots
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v4.8.1: StrongBox Op Rejection Fix
|
||||||
|
|
||||||
|
- **StrongBox op limit gate fix** — `trackAndEnforceOpLimit` was only called in the `Domain.KEY_ID` not-found path, so software-generated keys (found via `Domain.APP`) bypassed `STRONGBOX_MAX_CONCURRENT_OPS=4` entirely. DuckDetector's concurrent signing handles test created 24+ operations that all succeeded via LRU pruning instead of being rejected with `TOO_MANY_OPERATIONS (-29)`. Now enforced for all StrongBox createOperation paths.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v4.8: StrongBox Hardening & LRU Pruning
|
||||||
|
|
||||||
|
Tested against DuckDetector on OnePlus (Android 16, KSU). Tamper score dropped from 32 to 8.
|
||||||
|
|
||||||
|
- **LRU operation pruning** — Concurrent software operations capped at 15 per UID (TEE) and 4 per UID (StrongBox), with oldest-first eviction. Pruned operations return `INVALID_OPERATION_HANDLE (-28)`, matching AOSP keystore2 malus-based pruning.
|
||||||
|
- **StrongBox param guard** — Unsupported StrongBox params (RSA >2048-bit, non-P256 EC curves) forwarded to real HAL for proper rejection instead of generating in software.
|
||||||
|
- **StrongBox timing** — Key generation floors at 250ms, signing at 80ms on StrongBox security level to match real secure element latency.
|
||||||
|
- **StrongBox op limit** — Sliding-window enforcer caps concurrent StrongBox operations for both software and hardware key paths, returning `TOO_MANY_OPERATIONS (-29)` when exceeded.
|
||||||
|
- **ECDSA algorithm alias** — Accept "ECDSA" in addition to "EC" as JCA private key algorithm name. Fixes SIGSEGV crash on Android 10 devices where the provider reports EC keys as "ECDSA". Closes #4.
|
||||||
|
- **createOperation domain handling** — Software-generated keys now found via both `Domain.APP` (alias) and `Domain.KEY_ID` (nspace) lookup paths.
|
||||||
|
- **Permission guards** — Device ID attestation tags (IMEI, MEID, serial) require caller permission checks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## TEESimulator-RS v4.7: Operation & Attestation Fixes
|
## TEESimulator-RS v4.7: Operation & Attestation Fixes
|
||||||
|
|
||||||
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) and [Key Attestation](https://github.com/nickel-lang/nickel) on OnePlus (Android 16) and Xiaomi Redmi 14C (Android 14).
|
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).
|
||||||
|
|||||||
@@ -91,3 +91,10 @@ if [ ! -f "$CONFIG_DIR/target.txt" ]; then
|
|||||||
ui_print "- Adding default target scope"
|
ui_print "- Adding default target scope"
|
||||||
install_file "target.txt" "$CONFIG_DIR"
|
install_file "target.txt" "$CONFIG_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
rm -f "$CONFIG_DIR/tee_status.txt"
|
||||||
|
|
||||||
|
if [ ! -f "$CONFIG_DIR/hbk" ]; then
|
||||||
|
ui_print "- Generating device-unique hardware-bound key seed"
|
||||||
|
head -c 32 /dev/random > "$CONFIG_DIR/hbk"
|
||||||
|
fi
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "v4.5",
|
"version": "v6.0.0",
|
||||||
"versionCode": 111,
|
"versionCode": 155,
|
||||||
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.5/TEESimulator-v4.5-Release.zip",
|
"zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/latest/download/TEESimulator-RS-Release.zip",
|
||||||
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md"
|
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/changelog.md"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,36 @@ pub fn build_attestation_extension(params: &CertGenParams) -> Result<Vec<u8>> {
|
|||||||
fn build_software_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
|
fn build_software_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
|
||||||
let mut fields: Vec<(u32, Vec<u8>)> = Vec::new();
|
let mut fields: Vec<(u32, Vec<u8>)> = Vec::new();
|
||||||
|
|
||||||
|
// Tag 303: CALLER_NONCE — NULL (presence = true)
|
||||||
|
if params.caller_nonce {
|
||||||
|
fields.push((303, enc_null()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 400: ACTIVE_DATETIME — INTEGER (milliseconds)
|
||||||
|
if params.active_datetime >= 0 {
|
||||||
|
fields.push((400, enc_integer(params.active_datetime)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 401: ORIGINATION_EXPIRE_DATETIME — INTEGER (milliseconds)
|
||||||
|
if params.origination_expire_datetime >= 0 {
|
||||||
|
fields.push((401, enc_integer(params.origination_expire_datetime)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 402: USAGE_EXPIRE_DATETIME — INTEGER (milliseconds)
|
||||||
|
if params.usage_expire_datetime >= 0 {
|
||||||
|
fields.push((402, enc_integer(params.usage_expire_datetime)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 405: USAGE_COUNT_LIMIT — INTEGER
|
||||||
|
if params.usage_count_limit >= 0 {
|
||||||
|
fields.push((405, enc_integer(params.usage_count_limit as i64)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 509: UNLOCKED_DEVICE_REQUIRED — NULL
|
||||||
|
if params.unlocked_device_required {
|
||||||
|
fields.push((509, enc_null()));
|
||||||
|
}
|
||||||
|
|
||||||
// Tag 701: CREATION_DATETIME — INTEGER (milliseconds)
|
// Tag 701: CREATION_DATETIME — INTEGER (milliseconds)
|
||||||
fields.push((701, enc_integer(params.creation_datetime)));
|
fields.push((701, enc_integer(params.creation_datetime)));
|
||||||
|
|
||||||
@@ -77,8 +107,10 @@ fn build_tee_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
|
|||||||
fields.push((10, enc_integer(curve as i32 as i64)));
|
fields.push((10, enc_integer(curve as i32 as i64)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tag 503: NO_AUTH_REQUIRED — NULL (presence = true)
|
// Tag 503: NO_AUTH_REQUIRED — NULL (conditional)
|
||||||
fields.push((503, enc_null()));
|
if params.no_auth_required {
|
||||||
|
fields.push((503, enc_null()));
|
||||||
|
}
|
||||||
|
|
||||||
// Tag 702: ORIGIN — INTEGER 0 (GENERATED)
|
// Tag 702: ORIGIN — INTEGER 0 (GENERATED)
|
||||||
fields.push((702, enc_integer(0)));
|
fields.push((702, enc_integer(0)));
|
||||||
@@ -519,6 +551,44 @@ mod tests {
|
|||||||
assert_eq!(tags, sorted, "AuthorizationList fields must be sorted by tag number");
|
assert_eq!(tags, sorted, "AuthorizationList fields must be sorted by tag number");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enforcement_tags_in_software_enforced() {
|
||||||
|
let mut params = make_test_params();
|
||||||
|
params.usage_count_limit = 3;
|
||||||
|
params.unlocked_device_required = true;
|
||||||
|
params.caller_nonce = true;
|
||||||
|
params.active_datetime = 1709913600000;
|
||||||
|
let sw = build_software_enforced(¶ms).unwrap();
|
||||||
|
let inner = skip_tlv_header(&sw);
|
||||||
|
let tags = extract_tag_numbers(inner);
|
||||||
|
assert!(tags.contains(&303), "CALLER_NONCE (303) must be in softwareEnforced");
|
||||||
|
assert!(tags.contains(&400), "ACTIVE_DATETIME (400) must be in softwareEnforced");
|
||||||
|
assert!(tags.contains(&405), "USAGE_COUNT_LIMIT (405) must be in softwareEnforced");
|
||||||
|
assert!(tags.contains(&509), "UNLOCKED_DEVICE_REQUIRED (509) must be in softwareEnforced");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_auth_required_conditional() {
|
||||||
|
let mut params = make_test_params();
|
||||||
|
params.no_auth_required = false;
|
||||||
|
let tee = build_tee_enforced(¶ms).unwrap();
|
||||||
|
let inner = skip_tlv_header(&tee);
|
||||||
|
let tags = extract_tag_numbers(inner);
|
||||||
|
assert!(!tags.contains(&503), "NO_AUTH_REQUIRED (503) must be absent when false");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enforcement_tags_omitted_when_unset() {
|
||||||
|
let params = make_test_params();
|
||||||
|
let sw = build_software_enforced(¶ms).unwrap();
|
||||||
|
let inner = skip_tlv_header(&sw);
|
||||||
|
let tags = extract_tag_numbers(inner);
|
||||||
|
assert!(!tags.contains(&303), "CALLER_NONCE should be absent when false");
|
||||||
|
assert!(!tags.contains(&400), "ACTIVE_DATETIME should be absent when -1");
|
||||||
|
assert!(!tags.contains(&405), "USAGE_COUNT_LIMIT should be absent when -1");
|
||||||
|
assert!(!tags.contains(&509), "UNLOCKED_DEVICE_REQUIRED should be absent when false");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_full_extension_roundtrip() {
|
fn test_full_extension_roundtrip() {
|
||||||
let params = make_test_params();
|
let params = make_test_params();
|
||||||
@@ -568,6 +638,13 @@ mod tests {
|
|||||||
id_manufacturer: None,
|
id_manufacturer: None,
|
||||||
id_model: None,
|
id_model: None,
|
||||||
id_second_imei: None,
|
id_second_imei: None,
|
||||||
|
active_datetime: -1,
|
||||||
|
origination_expire_datetime: -1,
|
||||||
|
usage_expire_datetime: -1,
|
||||||
|
usage_count_limit: -1,
|
||||||
|
caller_nonce: false,
|
||||||
|
unlocked_device_required: false,
|
||||||
|
no_auth_required: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,69 @@ const OID_SHA256_WITH_RSA: &[u64] = &[1, 2, 840, 113549, 1, 1, 11];
|
|||||||
// Extension OIDs
|
// Extension OIDs
|
||||||
const OID_KEY_USAGE: &[u64] = &[2, 5, 29, 15];
|
const OID_KEY_USAGE: &[u64] = &[2, 5, 29, 15];
|
||||||
|
|
||||||
|
// AOSP ta/src/keys.rs:451-478: no challenge = self-signed leaf, chain depth 1
|
||||||
|
pub fn build_self_signed_cert(
|
||||||
|
key_pair: &GeneratedKeyPair,
|
||||||
|
params: &CertGenParams,
|
||||||
|
) -> Result<Vec<Vec<u8>>> {
|
||||||
|
let spki_der = extract_spki_from_pkcs8(&key_pair.private_key_pkcs8)?;
|
||||||
|
let sig_alg_der = signature_algorithm_for_signing_key(&key_pair.private_key_pkcs8, params.algorithm)?;
|
||||||
|
|
||||||
|
let serial_bytes = if let Some(ref serial) = params.cert_serial {
|
||||||
|
serial.clone()
|
||||||
|
} else {
|
||||||
|
vec![1u8]
|
||||||
|
};
|
||||||
|
|
||||||
|
let subject_dn_der = if let Some(ref subject) = params.cert_subject {
|
||||||
|
subject.clone()
|
||||||
|
} else {
|
||||||
|
encode_simple_cn_dn("Android Keystore Key")
|
||||||
|
};
|
||||||
|
|
||||||
|
let not_before = timestamp_to_datetime(params.cert_not_before)?;
|
||||||
|
let not_after = if params.cert_not_after == -1 {
|
||||||
|
// No keybox fallback available; use far-future (year 9999)
|
||||||
|
OffsetDateTime::from_unix_timestamp(253402300799)
|
||||||
|
.unwrap_or_else(|_| OffsetDateTime::now_utc() + time::Duration::days(365 * 30))
|
||||||
|
} else {
|
||||||
|
timestamp_to_datetime(params.cert_not_after)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let extensions_der = build_extensions(None, ¶ms.purposes)?;
|
||||||
|
|
||||||
|
let version_der = encode_der_explicit_tag(0, &encode_der_integer(&[2]));
|
||||||
|
let serial_der = encode_der_integer(&serial_bytes);
|
||||||
|
let validity_der = encode_validity(¬_before, ¬_after);
|
||||||
|
let extensions_tagged = encode_der_explicit_tag(3, &extensions_der);
|
||||||
|
|
||||||
|
// issuer == subject (self-signed, per AOSP ta/src/cert.rs:111-114)
|
||||||
|
let tbs_der = encode_der_sequence(&[
|
||||||
|
&version_der,
|
||||||
|
&serial_der,
|
||||||
|
&sig_alg_der,
|
||||||
|
&subject_dn_der,
|
||||||
|
&validity_der,
|
||||||
|
&subject_dn_der,
|
||||||
|
&spki_der,
|
||||||
|
&extensions_tagged,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let signature_bytes = sign_tbs(&tbs_der, &key_pair.private_key_pkcs8, params.algorithm)?;
|
||||||
|
let signature_bit_string = encode_der_bit_string(&signature_bytes);
|
||||||
|
|
||||||
|
let cert_der = encode_der_sequence(&[
|
||||||
|
&tbs_der,
|
||||||
|
&sig_alg_der,
|
||||||
|
&signature_bit_string,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Ok(vec![cert_der])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_certificate_chain(
|
pub fn build_certificate_chain(
|
||||||
key_pair: &GeneratedKeyPair,
|
key_pair: &GeneratedKeyPair,
|
||||||
attestation_ext_der: &[u8],
|
attestation_ext_der: Option<&[u8]>,
|
||||||
keybox: &ParsedKeybox,
|
keybox: &ParsedKeybox,
|
||||||
params: &CertGenParams,
|
params: &CertGenParams,
|
||||||
) -> Result<Vec<Vec<u8>>> {
|
) -> Result<Vec<Vec<u8>>> {
|
||||||
@@ -33,7 +93,7 @@ pub fn build_certificate_chain(
|
|||||||
|
|
||||||
fn build_leaf_cert(
|
fn build_leaf_cert(
|
||||||
key_pair: &GeneratedKeyPair,
|
key_pair: &GeneratedKeyPair,
|
||||||
attestation_ext_der: &[u8],
|
attestation_ext_der: Option<&[u8]>,
|
||||||
keybox: &ParsedKeybox,
|
keybox: &ParsedKeybox,
|
||||||
params: &CertGenParams,
|
params: &CertGenParams,
|
||||||
) -> Result<Vec<u8>> {
|
) -> Result<Vec<u8>> {
|
||||||
@@ -63,7 +123,6 @@ fn build_leaf_cert(
|
|||||||
timestamp_to_datetime(params.cert_not_after)?
|
timestamp_to_datetime(params.cert_not_after)?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extensions
|
|
||||||
let extensions_der = build_extensions(attestation_ext_der, ¶ms.purposes)?;
|
let extensions_der = build_extensions(attestation_ext_der, ¶ms.purposes)?;
|
||||||
|
|
||||||
// TBS Certificate
|
// TBS Certificate
|
||||||
@@ -256,19 +315,19 @@ fn extract_rsa_spki(pkcs8_der: &[u8]) -> Result<Vec<u8>> {
|
|||||||
Ok(encode_der_sequence(&[&alg_id, &pub_key_bits]))
|
Ok(encode_der_sequence(&[&alg_id, &pub_key_bits]))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_extensions(attestation_ext_der: &[u8], purposes: &[i32]) -> Result<Vec<u8>> {
|
fn build_extensions(attestation_ext_der: Option<&[u8]>, purposes: &[i32]) -> Result<Vec<u8>> {
|
||||||
let mut extensions: Vec<Vec<u8>> = Vec::new();
|
let mut extensions: Vec<Vec<u8>> = Vec::new();
|
||||||
|
|
||||||
// KeyUsage extension (critical)
|
|
||||||
let ku_byte = map_key_usage_byte(purposes);
|
let ku_byte = map_key_usage_byte(purposes);
|
||||||
if ku_byte != 0 {
|
if ku_byte != 0 {
|
||||||
let ku_ext = build_key_usage_extension(ku_byte);
|
let ku_ext = build_key_usage_extension(ku_byte);
|
||||||
extensions.push(ku_ext);
|
extensions.push(ku_ext);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attestation extension (non-critical)
|
if let Some(attest_der) = attestation_ext_der {
|
||||||
let attest_ext = build_extension(&encode_der_oid(ATTESTATION_OID), false, attestation_ext_der);
|
let attest_ext = build_extension(&encode_der_oid(ATTESTATION_OID), false, attest_der);
|
||||||
extensions.push(attest_ext);
|
extensions.push(attest_ext);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(encode_der_sequence_of(&extensions))
|
Ok(encode_der_sequence_of(&extensions))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,14 +62,13 @@ fn generate_attested_inner(env: &mut JNIEnv, config: &JObject) -> Result<jbyteAr
|
|||||||
|
|
||||||
let keybox = keybox::parse_keybox(¶ms.keybox_cert_chain, ¶ms.keybox_private_key)?;
|
let keybox = keybox::parse_keybox(¶ms.keybox_cert_chain, ¶ms.keybox_private_key)?;
|
||||||
|
|
||||||
let attest_ext = attestation::build_attestation_extension(¶ms)?;
|
let cert_chain = if params.attestation_challenge.is_some() {
|
||||||
|
let attest_ext = attestation::build_attestation_extension(¶ms)?;
|
||||||
let cert_chain = certbuilder::build_certificate_chain(
|
certbuilder::build_certificate_chain(&key_pair, Some(&attest_ext), &keybox, ¶ms)?
|
||||||
&key_pair,
|
} else {
|
||||||
&attest_ext,
|
tracing::info!("no attestation challenge, generating self-signed cert (depth 1)");
|
||||||
&keybox,
|
certbuilder::build_self_signed_cert(&key_pair, ¶ms)?
|
||||||
¶ms,
|
};
|
||||||
)?;
|
|
||||||
|
|
||||||
let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain);
|
let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain);
|
||||||
|
|
||||||
@@ -199,6 +198,14 @@ fn extract_config(env: &mut JNIEnv, config: &JObject) -> Result<CertGenParams> {
|
|||||||
let id_model = get_nullable_byte_array(env, config, "idModel")?;
|
let id_model = get_nullable_byte_array(env, config, "idModel")?;
|
||||||
let id_second_imei = get_nullable_byte_array(env, config, "idSecondImei")?;
|
let id_second_imei = get_nullable_byte_array(env, config, "idSecondImei")?;
|
||||||
|
|
||||||
|
let active_datetime = get_long(env, config, "activeDatetime")?;
|
||||||
|
let origination_expire_datetime = get_long(env, config, "originationExpireDatetime")?;
|
||||||
|
let usage_expire_datetime = get_long(env, config, "usageExpireDatetime")?;
|
||||||
|
let usage_count_limit = get_int(env, config, "usageCountLimit")?;
|
||||||
|
let caller_nonce = get_boolean(env, config, "callerNonce")?;
|
||||||
|
let unlocked_device_required = get_boolean(env, config, "unlockedDeviceRequired")?;
|
||||||
|
let no_auth_required = get_boolean(env, config, "noAuthRequired")?;
|
||||||
|
|
||||||
Ok(CertGenParams {
|
Ok(CertGenParams {
|
||||||
algorithm: Algorithm::try_from(algorithm)?,
|
algorithm: Algorithm::try_from(algorithm)?,
|
||||||
key_size: key_size as u32,
|
key_size: key_size as u32,
|
||||||
@@ -238,6 +245,13 @@ fn extract_config(env: &mut JNIEnv, config: &JObject) -> Result<CertGenParams> {
|
|||||||
id_manufacturer,
|
id_manufacturer,
|
||||||
id_model,
|
id_model,
|
||||||
id_second_imei,
|
id_second_imei,
|
||||||
|
active_datetime,
|
||||||
|
origination_expire_datetime,
|
||||||
|
usage_expire_datetime,
|
||||||
|
usage_count_limit,
|
||||||
|
caller_nonce,
|
||||||
|
unlocked_device_required,
|
||||||
|
no_auth_required,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +267,10 @@ fn get_long(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<i64> {
|
|||||||
Ok(env.get_field(obj, name, "J")?.j()?)
|
Ok(env.get_field(obj, name, "J")?.j()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_boolean(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<bool> {
|
||||||
|
Ok(env.get_field(obj, name, "Z")?.z()?)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_byte_array(env: &mut JNIEnv, obj: &JObject, name: &'static str) -> Result<Vec<u8>> {
|
fn get_byte_array(env: &mut JNIEnv, obj: &JObject, name: &'static str) -> Result<Vec<u8>> {
|
||||||
let field = env.get_field(obj, name, "[B")?.l()?;
|
let field = env.get_field(obj, name, "[B")?.l()?;
|
||||||
if field.is_null() {
|
if field.is_null() {
|
||||||
|
|||||||
@@ -42,35 +42,6 @@ impl TryFrom<i32> for EcCurve {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
#[repr(i32)]
|
|
||||||
pub enum KeyPurpose {
|
|
||||||
Encrypt = 0,
|
|
||||||
Decrypt = 1,
|
|
||||||
Sign = 2,
|
|
||||||
Verify = 3,
|
|
||||||
WrapKey = 5,
|
|
||||||
AgreeKey = 6,
|
|
||||||
AttestKey = 7,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
#[repr(i32)]
|
|
||||||
pub enum SecurityLevel {
|
|
||||||
Software = 0,
|
|
||||||
TrustedEnvironment = 1,
|
|
||||||
StrongBox = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
#[repr(i32)]
|
|
||||||
pub enum VerifiedBootState {
|
|
||||||
Verified = 0,
|
|
||||||
SelfSigned = 1,
|
|
||||||
Unverified = 2,
|
|
||||||
Failed = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct CertGenParams {
|
pub struct CertGenParams {
|
||||||
pub algorithm: Algorithm,
|
pub algorithm: Algorithm,
|
||||||
pub key_size: u32,
|
pub key_size: u32,
|
||||||
@@ -114,6 +85,14 @@ pub struct CertGenParams {
|
|||||||
pub id_manufacturer: Option<Vec<u8>>,
|
pub id_manufacturer: Option<Vec<u8>>,
|
||||||
pub id_model: Option<Vec<u8>>,
|
pub id_model: Option<Vec<u8>>,
|
||||||
pub id_second_imei: Option<Vec<u8>>,
|
pub id_second_imei: Option<Vec<u8>>,
|
||||||
|
|
||||||
|
pub active_datetime: i64,
|
||||||
|
pub origination_expire_datetime: i64,
|
||||||
|
pub usage_expire_datetime: i64,
|
||||||
|
pub usage_count_limit: i32,
|
||||||
|
pub caller_nonce: bool,
|
||||||
|
pub unlocked_device_required: bool,
|
||||||
|
pub no_auth_required: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GeneratedKeyPair {
|
pub struct GeneratedKeyPair {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ public interface IPackageManager {
|
|||||||
|
|
||||||
ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId);
|
ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId);
|
||||||
|
|
||||||
|
int checkPermission(String permName, String pkgName, int userId);
|
||||||
|
|
||||||
class Stub {
|
class Stub {
|
||||||
public static IPackageManager asInterface(IBinder binder) {
|
public static IPackageManager asInterface(IBinder binder) {
|
||||||
throw new UnsupportedOperationException("STUB!");
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package android.os;
|
||||||
|
|
||||||
|
public class SELinux {
|
||||||
|
public static boolean checkSELinuxAccess(
|
||||||
|
String scon, String tcon, String tclass, String perm) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,10 @@ public class ServiceManager {
|
|||||||
throw new UnsupportedOperationException("STUB!");
|
throw new UnsupportedOperationException("STUB!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isDeclared(String name) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
public static String[] listServices() {
|
public static String[] listServices() {
|
||||||
throw new UnsupportedOperationException("STUB!");
|
throw new UnsupportedOperationException("STUB!");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user