Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e83a846c9 | ||
|
|
3aff09e7dd | ||
|
|
c7b0af2d29 | ||
|
|
13a1dd7887 | ||
|
|
21a3cb1ec0 | ||
|
|
2b31d4ef47 | ||
|
|
38e9b547a5 | ||
|
|
94c8e5b182 |
@@ -0,0 +1,4 @@
|
|||||||
|
# Ensure shell scripts always have LF line endings, even on Windows.
|
||||||
|
# These get packaged into flashable zips and run on Android devices.
|
||||||
|
*.sh text eol=lf
|
||||||
|
module/daemon text eol=lf
|
||||||
@@ -149,21 +149,3 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
VER: ${{ steps.ver.outputs.version }}
|
VER: ${{ steps.ver.outputs.version }}
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Bump update.json
|
|
||||||
run: |
|
|
||||||
COUNT=$(git rev-list HEAD --count)
|
|
||||||
RELEASE_NAME=$(basename zips/*Release*.zip)
|
|
||||||
ZIP_URL="https://github.com/${{ github.repository }}/releases/download/${VER}/${RELEASE_NAME}"
|
|
||||||
jq ".versionCode = $COUNT | .zipUrl = \"$ZIP_URL\"" module/update.json > /tmp/update.json
|
|
||||||
mv /tmp/update.json module/update.json
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
git add module/update.json
|
|
||||||
git diff --cached --quiet || {
|
|
||||||
git commit -m "chore(release): bump update.json to $VER [skip ci]"
|
|
||||||
git push origin HEAD:main
|
|
||||||
}
|
|
||||||
env:
|
|
||||||
VER: ${{ steps.ver.outputs.version }}
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|||||||
@@ -1,139 +1,259 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<h1 align="center">TEESimulator-RS</h1>
|
<h1 align="center">🔐 TEESimulator</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-RS/actions/workflows/build.yml"><img src="https://github.com/Enginex0/TEESimulator-RS/actions/workflows/build.yml/badge.svg" alt="Build"></a>
|
<a href="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml"><img src="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml/badge.svg" alt="Build"></a>
|
||||||
<img src="https://img.shields.io/badge/Android-10%2B-green?logo=android" alt="Android 10+">
|
<img src="https://img.shields.io/badge/version-v4.2-blue?style=for-the-badge" alt="v4.2">
|
||||||
<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/Android-10%2B-green?style=for-the-badge&logo=android" alt="Android 10+">
|
||||||
|
<img src="https://img.shields.io/badge/Telegram-community-blue?style=for-the-badge&logo=telegram" alt="Telegram">
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Fork of [JingMatrix/TEESimulator](https://github.com/JingMatrix/TEESimulator) with native Rust certificate generation, key persistence, and AOSP-compliant attestation behavior. For the upstream project, see the original repo.
|
> **This is a 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.
|
||||||
|
|
||||||
## What It Does
|
---
|
||||||
|
|
||||||
TEESimulator intercepts Binder IPC at the `ioctl` level inside the `keystore2` process and generates entire certificate chains from scratch, signed by your keybox, with correct attestation extensions. Apps that verify hardware attestation see a legitimate device.
|
## 🧬 What is TEESimulator?
|
||||||
|
|
||||||
This is not TrickyStore. TEESimulator replaces TrickyStore and its forks entirely. It shares the same config paths for drop-in compatibility, but the internals are different: native Rust cert generation, binder-level interception via `lsplt`, per-UID rate limiting, key persistence, and AOSP-spec attestation behavior.
|
TEESimulator 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.
|
||||||
|
|
||||||
## Requirements
|
The result: **apps that verify hardware attestation see a legitimate, unmodified device** — even on rooted hardware with an unlocked bootloader.
|
||||||
|
|
||||||
|
> **This is not TrickyStore.** TEESimulator replaces TrickyStore and its forks entirely. It shares the same config paths for drop-in compatibility, but the architecture is fundamentally different: native Rust certificate generation, binder-level interception via `lsplt`, per-UID rate limiting, key persistence, and a multi-layer defense against detector apps.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔥 Why TEESimulator?
|
||||||
|
|
||||||
|
🔐 **Native Cert Generation** — v4.0 generates X.509 certificate chains in Rust with `ring` and manual DER encoding. No BouncyCastle overhead, no Java crypto quirks, byte-perfect issuer chain linkage.
|
||||||
|
|
||||||
|
🎯 **Binder-Level Interception** — Hooks `ioctl()` on `libc.so` via `lsplt` inside the `keystore2` process. Intercepts `generateKey`, `importKey`, and `getKeyEntry` transactions before the HAL ever sees them.
|
||||||
|
|
||||||
|
🛡️ **Detector Resistant** — Per-UID rate limiting blocks DuckDetector-style keygen flooding. Oversized challenges rejected with real KeyMint error codes. Chain consistency verified byte-for-byte.
|
||||||
|
|
||||||
|
💾 **Key Persistence** — Generated keys survive reboots. Apps that store attestation keys (banking, biometrics) don't break after a restart.
|
||||||
|
|
||||||
|
🔧 **Drop-In Replacement** — Same config paths as TrickyStore (`/data/adb/tricky_store/`). Swap the module ZIP, keep your keybox and target list.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
**Core Attestation Engine**
|
||||||
|
- [x] **Full certificate chain generation** — leaf + intermediates + root, signed by your keybox
|
||||||
|
- [x] **Native Rust certgen** — `libcertgen.so` built with `ring`, `rsa`, and manual DER assembly
|
||||||
|
- [x] **BouncyCastle fallback** — unsupported curves (P-224, P-521, Curve25519) fall back to Java
|
||||||
|
- [x] **ASN.1 attestation extensions** — OID 1.3.6.1.4.1.11129.2.1.17 with all AOSP-specified tags
|
||||||
|
- [x] **Multi-keybox support** — different keybox files per app group via `target.txt`
|
||||||
|
|
||||||
|
**Interception Layer**
|
||||||
|
- [x] **Binder ioctl hook** — `lsplt` PLT hook on `libc.so` inside `keystore2` process
|
||||||
|
- [x] **generateKey / importKey / getKeyEntry** — all three transaction types intercepted
|
||||||
|
- [x] **256KB native payload cap** — oversized binder payloads bypass interception cleanly
|
||||||
|
- [x] **Challenge validation** — rejects >128-byte attestation challenges with `INVALID_INPUT_LENGTH`
|
||||||
|
|
||||||
|
**Hardening**
|
||||||
|
- [x] **Per-UID rate limiter** — 2 hardware keygens per 30s burst window, software fallback on overflow
|
||||||
|
- [x] **importKey eviction guard** — retained patch chains prevent generate-then-import cache attacks
|
||||||
|
- [x] **Key persistence** — file-backed storage with file-level locking, survives reboots and keybox rotations
|
||||||
|
- [x] **Global exception handler** — uncaught exceptions logged, daemon stays alive
|
||||||
|
|
||||||
|
**Configuration**
|
||||||
|
- [x] **Live config reload** — `FileObserver` watches all config files, changes apply immediately
|
||||||
|
- [x] **Security patch spoofing** — per-package `system`, `vendor`, `boot` patch levels with dynamic templates
|
||||||
|
- [x] **Lifecycle scripts** — KSU Action button clears key cache, uninstall removes all traces
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Requirements
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> A valid `keybox.xml` is required for hardware-level attestation. Without one, the module generates software-level certificates that won't pass strict hardware checks.
|
> 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.
|
||||||
|
|
||||||
1. Android 10+
|
**You need:**
|
||||||
2. Root manager: KernelSU, Magisk, or APatch
|
1. Android 10 or above
|
||||||
3. `keybox.xml` at `/data/adb/tricky_store/keybox.xml`
|
2. A supported root manager (KernelSU, Magisk, or APatch)
|
||||||
|
3. A hardware-backed `keybox.xml` placed at `/data/adb/tricky_store/keybox.xml`
|
||||||
|
|
||||||
## Quick Start
|
---
|
||||||
|
|
||||||
1. Download the latest ZIP from [Releases](https://github.com/Enginex0/TEESimulator-RS/releases)
|
## 📱 Compatibility
|
||||||
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
|
|
||||||
|
|
||||||
## Architecture
|
### Root Managers
|
||||||
|
|
||||||
**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).
|
| Manager | Status | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| KernelSU | ✅ Tested | Full support including Action button and lifecycle scripts |
|
||||||
|
| Magisk | ✅ Supported | Standard module install |
|
||||||
|
| APatch | ✅ Supported | Standard module install |
|
||||||
|
|
||||||
**Binder Interception** — PLT hook on `ioctl()` in `libc.so` via `lsplt` inside `keystore2`. Intercepts `generateKey`, `importKey`, and `getKeyEntry` transactions.
|
### Tested Devices
|
||||||
|
|
||||||
**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.
|
| Device | Android | TEE | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Redmi 14C (2409BRN2CA) | 14 (SDK 34) | Beanpod KeyMaster | ✅ Daily driver |
|
||||||
|
|
||||||
**Key Persistence** — Generated keys survive reboots. File-backed with file-level locking.
|
> 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.
|
||||||
|
|
||||||
**Rate Limiting** — Per-UID hardware keygen cap (2/30s window, 2 concurrent). Overflow falls to software certs.
|
---
|
||||||
|
|
||||||
## Configuration
|
## 🚀 Quick Start
|
||||||
|
|
||||||
All config files live at `/data/adb/tricky_store/` and are hot-reloaded via `FileObserver`.
|
1. **Download** the latest release ZIP from [Releases](https://github.com/Enginex0/TEESimulator/releases)
|
||||||
|
2. **Install** via your root manager (KSU / Magisk / APatch) and reboot
|
||||||
|
3. **Place your keybox** at `/data/adb/tricky_store/keybox.xml`
|
||||||
|
4. **Configure targets** in `/data/adb/tricky_store/target.txt`
|
||||||
|
5. **Verify** — check Play Integrity or run Key Attestation Demo
|
||||||
|
|
||||||
### target.txt
|
TEESimulator replaces TrickyStore, TrickyStoreOSS, and their forks. Existing config files are compatible.
|
||||||
|
|
||||||
Controls which apps get intercepted and the simulation mode.
|
---
|
||||||
|
|
||||||
| Suffix | Mode |
|
## 🔨 Building from Source
|
||||||
|--------|------|
|
|
||||||
| `!` | Force software key generation |
|
|
||||||
| `?` | Force leaf certificate patching (real TEE key, patched cert) |
|
|
||||||
| *(none)* | Automatic selection |
|
|
||||||
|
|
||||||
Multi-keybox support via `[filename.xml]` headers:
|
The CI workflow builds on every push to `main`. You can also build locally or trigger a build from your own fork.
|
||||||
|
|
||||||
|
**Prerequisites:** JDK 21, Android SDK/NDK 27, Rust stable with `aarch64-linux-android` target, `cargo-ndk`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Enginex0/TEESimulator.git
|
||||||
|
cd TEESimulator
|
||||||
|
./gradlew zipRelease zipDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
Output ZIPs land in `out/`. The Gradle build automatically invokes `cargo ndk` to cross-compile `libcertgen.so` before packaging.
|
||||||
|
|
||||||
|
To rebuild from a fork, push to `main` or use **Actions → Build → Run workflow**. The workflow installs all toolchains (Java, Rust, cargo-ndk, ccache) and uploads Release + Debug ZIPs as artifacts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Configuration
|
||||||
|
|
||||||
|
All configuration files live at `/data/adb/tricky_store/` and are monitored by `FileObserver` — changes take effect immediately without rebooting.
|
||||||
|
|
||||||
|
### The `keybox.xml` Root of Trust
|
||||||
|
|
||||||
|
This file provides the master cryptographic identity. It contains a private key and a hardware-backed certificate chain from a real device. TEESimulator signs all generated certificates with this key, making them appear legitimate to verifiers.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<AndroidAttestation>
|
||||||
|
<Keybox DeviceID="...">
|
||||||
|
<Key algorithm="ecdsa|rsa">
|
||||||
|
<PrivateKey format="pem">...</PrivateKey>
|
||||||
|
<CertificateChain>...</CertificateChain>
|
||||||
|
</Key>
|
||||||
|
</Keybox>
|
||||||
|
</AndroidAttestation>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Target Packages (`target.txt`)
|
||||||
|
|
||||||
|
Controls which apps get intercepted and what simulation mode to use.
|
||||||
|
|
||||||
|
#### Mode Suffixes
|
||||||
|
|
||||||
|
* **`!` → Force Generation** — Creates a complete software-based virtual key. Full TEE simulation.
|
||||||
|
* **`?` → Force Leaf Hacking** — Real TEE key generated, but its attestation certificate is intercepted and patched.
|
||||||
|
* **No symbol → Automatic** — Module selects the best mode for your device.
|
||||||
|
|
||||||
|
#### Multi-Keybox
|
||||||
|
|
||||||
|
Specify different keybox files for different app groups. Apps listed after a `[filename.xml]` line use that keybox. Apps before any declaration use the default `keybox.xml`.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
# 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.txt
|
### Security Patch Level (`security_patch.txt`)
|
||||||
|
|
||||||
Override patch levels reported in attestation certificates. Global defaults at top, per-package overrides with `[package.name]`.
|
Configure the `osPatchLevel`, `vendorPatchLevel`, and `bootPatchLevel` reported in attestation certificates. This only affects attestation data — it does not change actual system properties.
|
||||||
|
|
||||||
|
#### Global and Per-Package
|
||||||
|
|
||||||
|
Settings at the top of the file are global defaults. Add `[package.name]` to override for specific apps.
|
||||||
|
|
||||||
|
#### Keys
|
||||||
|
|
||||||
| 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` | Sets all three |
|
| `all` | Shorthand — sets all three at once |
|
||||||
|
|
||||||
Special values: `today`, `YYYY-MM-DD` templates, `no` (omit tag), `device_default`, `prop` (read from system property).
|
#### Special Keywords
|
||||||
|
|
||||||
|
| Keyword | Effect |
|
||||||
|
|---|---|
|
||||||
|
| `today` | Current date, dynamically resolved on each attestation |
|
||||||
|
| `YYYY-MM-DD` templates | Semi-dynamic — `YYYY-MM-05` resolves to the 5th of the current month |
|
||||||
|
| `no` | Omit this patch level tag entirely from the attestation |
|
||||||
|
| `device_default` | Use the device's real hardware value |
|
||||||
|
| `prop` | Read from `ro.build.version.security_patch` (matches what detectors see via getprop) |
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
```
|
```
|
||||||
|
# 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=2025-10-01
|
system=2024-10-01
|
||||||
|
|
||||||
|
# Custom config for a demo app
|
||||||
|
[org.matrix.demo]
|
||||||
|
all=2025-09-15
|
||||||
|
boot=device_default
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building from Source
|
---
|
||||||
|
|
||||||
Prerequisites: JDK 21, Android SDK/NDK 27, Rust stable with `aarch64-linux-android` target, `cargo-ndk`.
|
## 💬 Community
|
||||||
|
|
||||||
```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/SuperPowers_Telegram-Join-blue?style=for-the-badge&logo=telegram" alt="Telegram">
|
<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">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Credits
|
---
|
||||||
|
|
||||||
- [JingMatrix](https://github.com/JingMatrix/TEESimulator) — original TEESimulator and interception architecture
|
## 🙏 Credits
|
||||||
- [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
|
|
||||||
|
|
||||||
## License
|
- **[JingMatrix](https://github.com/JingMatrix/TEESimulator)** — original author of TEESimulator and the interception architecture
|
||||||
|
- **[5ec1cff](https://github.com/5ec1cff/TrickyStore)** — TrickyStore, the project that pioneered keystore interception on Android
|
||||||
|
- **[LSPlt](https://github.com/LSPosed/LSPlt)** — PLT hook library used for binder interception
|
||||||
|
- **[ring](https://github.com/briansmith/ring)** — Rust cryptography library powering native cert generation
|
||||||
|
- **[MhmRdd](https://github.com/MhmRdd)** — AOSP compliance improvements via upstream [PR #157](https://github.com/JingMatrix/TEESimulator/pull/157), including authorize_create enforcement, attestation extension alignment, and binder transaction filtering
|
||||||
|
- **[fatalcoder524](https://github.com/fatalcoder524)** — a real contributor and collaborator on this project
|
||||||
|
- **[huguangares](https://github.com/huguangares)** — collaborator and tester
|
||||||
|
|
||||||
[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>
|
||||||
|
|||||||
+7
-45
@@ -2,7 +2,6 @@ import com.android.build.api.artifact.SingleArtifact
|
|||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.gradle.process.ExecOperations
|
import org.gradle.process.ExecOperations
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
@@ -30,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 = "v6.0.0"
|
val verName = "v5.1"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "org.matrix.TEESimulator"
|
namespace = "org.matrix.TEESimulator"
|
||||||
@@ -66,12 +65,6 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
|
||||||
compilerOptions {
|
|
||||||
jvmTarget.set(JvmTarget.JVM_21)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(project(":stub"))
|
compileOnly(project(":stub"))
|
||||||
compileOnly(libs.annotation)
|
compileOnly(libs.annotation)
|
||||||
@@ -98,7 +91,6 @@ val buildRustCertgen by tasks.registering(Exec::class) {
|
|||||||
outputs.dir(rootProject.projectDir.resolve("app/src/main/jniLibs"))
|
outputs.dir(rootProject.projectDir.resolve("app/src/main/jniLibs"))
|
||||||
|
|
||||||
environment("ANDROID_NDK_HOME", android.ndkDirectory.absolutePath)
|
environment("ANDROID_NDK_HOME", android.ndkDirectory.absolutePath)
|
||||||
environment("PATH", "${System.getProperty("user.home")}/.cargo/bin:${System.getenv("PATH") ?: ""}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AGP auto-detects jniLibs/ as an input to mergeJniLibFolders — wire the dependency
|
// AGP auto-detects jniLibs/ as an input to mergeJniLibFolders — wire the dependency
|
||||||
@@ -108,34 +100,6 @@ tasks.configureEach {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-rewrite module/update.json on every packaging build so versionCode and
|
|
||||||
// zipUrl track gitCommitCount automatically, matching module.prop.
|
|
||||||
val refreshUpdateJson by tasks.registering {
|
|
||||||
group = "TEESimulator-RS Module Packaging"
|
|
||||||
description = "Rewrite module/update.json to match current verName and gitCommitCount."
|
|
||||||
|
|
||||||
val updateJsonFile = rootProject.projectDir.resolve("module/update.json")
|
|
||||||
val capturedVerName = verName
|
|
||||||
val capturedCount = gitCommitCount
|
|
||||||
|
|
||||||
inputs.property("verName", capturedVerName)
|
|
||||||
inputs.property("gitCommitCount", capturedCount)
|
|
||||||
outputs.file(updateJsonFile)
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
val fullVer = "$capturedVerName-$capturedCount"
|
|
||||||
updateJsonFile.writeText(
|
|
||||||
"""{
|
|
||||||
"version": "$fullVer",
|
|
||||||
"versionCode": $capturedCount,
|
|
||||||
"zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/download/$fullVer/TEESimulator-RS-$fullVer-Release.zip",
|
|
||||||
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/changelog.md"
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
androidComponents {
|
androidComponents {
|
||||||
onVariants(selector().all()) { variant ->
|
onVariants(selector().all()) { variant ->
|
||||||
val capitalized = variant.name.replaceFirstChar { it.uppercase() }
|
val capitalized = variant.name.replaceFirstChar { it.uppercase() }
|
||||||
@@ -157,10 +121,9 @@ androidComponents {
|
|||||||
dependsOn("package${capitalized}")
|
dependsOn("package${capitalized}")
|
||||||
} else {
|
} else {
|
||||||
dependsOn("minify${capitalized}WithR8")
|
dependsOn("minify${capitalized}WithR8")
|
||||||
dependsOn("strip${capitalized}DebugSymbols")
|
|
||||||
}
|
}
|
||||||
|
dependsOn("strip${capitalized}DebugSymbols")
|
||||||
dependsOn(buildRustCertgen)
|
dependsOn(buildRustCertgen)
|
||||||
dependsOn(refreshUpdateJson)
|
|
||||||
|
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
from(variant.artifacts.get(SingleArtifact.APK)) {
|
from(variant.artifacts.get(SingleArtifact.APK)) {
|
||||||
@@ -177,12 +140,11 @@ androidComponents {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val nativeLibsDir = if (isDebug) {
|
from(
|
||||||
"intermediates/merged_native_libs/${variant.name}/merge${capitalized}NativeLibs/out/lib"
|
project.layout.buildDirectory.dir(
|
||||||
} else {
|
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
|
||||||
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
|
)
|
||||||
}
|
) {
|
||||||
from(project.layout.buildDirectory.dir(nativeLibsDir)) {
|
|
||||||
into("lib")
|
into("lib")
|
||||||
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so", "**/libcertgen.so")
|
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so", "**/libcertgen.so")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,15 +235,20 @@ class BinderInterceptor : public BBinder {
|
|||||||
struct RegistrationEntry {
|
struct RegistrationEntry {
|
||||||
wp<IBinder> target;
|
wp<IBinder> target;
|
||||||
sp<IBinder> callback_interface;
|
sp<IBinder> callback_interface;
|
||||||
|
// Transaction codes to intercept. Empty = intercept all (legacy behavior).
|
||||||
std::vector<uint32_t> filtered_codes;
|
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+code combination should be intercepted.
|
||||||
|
// Returns true if the binder is registered AND the code is in its filter
|
||||||
|
// (or the filter is empty, meaning intercept everything).
|
||||||
bool shouldIntercept(const wp<BBinder> &target, uint32_t code) const {
|
bool shouldIntercept(const wp<BBinder> &target, uint32_t code) const {
|
||||||
std::shared_lock lock(registry_mutex_);
|
std::shared_lock lock(registry_mutex_);
|
||||||
auto it = registry_.find(target);
|
auto it = registry_.find(target);
|
||||||
@@ -354,9 +359,7 @@ 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;
|
||||||
|
|
||||||
// AIDL methods use codes in [FIRST_CALL_TRANSACTION, LAST_CALL_TRANSACTION] (1..0x00ffffff).
|
// Skip system transactions (PING, INTERFACE, DUMP) to avoid latency detectors
|
||||||
// System transactions (PING, INTERFACE, DUMP, SHELL_COMMAND) use codes above that range.
|
|
||||||
// Skip those — intercepting a ping adds measurable latency that timing detectors flag.
|
|
||||||
if (txn_data->code > 0x00ffffffu && txn_data->code != intercept::kBackdoorCode)
|
if (txn_data->code > 0x00ffffffu && txn_data->code != intercept::kBackdoorCode)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -428,29 +431,44 @@ 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 overrun parsing command 0x%x", cmd);
|
LOGE("[Hook] Buffer overflow detected while parsing command %s", getBinderReturnCommandName(cmd));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (__builtin_expect(cmd == BR_TRANSACTION || cmd == BR_TRANSACTION_SEC_CTX, 0)) {
|
// We are primarily interested in BR_TRANSACTION commands to intercept
|
||||||
binder_transaction_data *txn;
|
if (cmd == BR_TRANSACTION || cmd == BR_TRANSACTION_SEC_CTX) {
|
||||||
|
binder_transaction_data *txn = nullptr;
|
||||||
|
|
||||||
if (cmd == BR_TRANSACTION_SEC_CTX) {
|
if (cmd == BR_TRANSACTION_SEC_CTX) {
|
||||||
txn = &reinterpret_cast<binder_transaction_data_secctx *>(ptr)->transaction_data;
|
// The data is wrapped in a secctx struct
|
||||||
|
auto *wrapper = reinterpret_cast<binder_transaction_data_secctx *>(ptr);
|
||||||
|
txn = &wrapper->transaction_data;
|
||||||
} else {
|
} 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -470,17 +488,13 @@ 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.
|
|
||||||
// Pings, ref ops, and looper management never produce BR_TRANSACTION, so scanning
|
// We only care about data read FROM the driver (i.e., incoming commands)
|
||||||
// their buffers is pure overhead (~2-5us per ioctl in debug builds).
|
if (bwr->read_consumed > 0) {
|
||||||
if (bwr->read_consumed >= sizeof(uint32_t)) {
|
processBinderReadBuffer(*bwr);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,11 +537,14 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read optional transaction code filter. If present: int32 count + count * uint32 codes.
|
||||||
|
// If absent or count <= 0: intercept all transaction codes (legacy behavior).
|
||||||
std::vector<uint32_t> codes;
|
std::vector<uint32_t> codes;
|
||||||
int32_t code_count = 0;
|
int32_t code_count = 0;
|
||||||
if (data.dataAvail() >= sizeof(int32_t) && data.readInt32(&code_count) == OK && code_count > 0) {
|
if (data.dataAvail() >= sizeof(int32_t) && data.readInt32(&code_count) == OK && code_count > 0) {
|
||||||
@@ -595,15 +612,8 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
|
|||||||
Parcel pre_req, pre_resp;
|
Parcel pre_req, pre_resp;
|
||||||
writeTransactionData(pre_req, tx_id, target, code, flags, request);
|
writeTransactionData(pre_req, tx_id, target, code, flags, request);
|
||||||
|
|
||||||
status_t pre_status = callback->transact(intercept::kPreTransact, pre_req, &pre_resp);
|
if (callback->transact(intercept::kPreTransact, pre_req, &pre_resp) != OK) {
|
||||||
if (pre_status != OK) {
|
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed. Forwarding original call.", tx_id);
|
||||||
// Block when interceptor is dead to prevent privacy leak to third-party apps
|
|
||||||
if (callback->pingBinder() != OK) {
|
|
||||||
LOGE("[TX_ID: %" PRIu64 "] Interceptor DEAD. Blocking to prevent attestation leak.", tx_id);
|
|
||||||
result = DEAD_OBJECT;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed (not dead). Forwarding.", tx_id);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,8 +667,7 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
|
|||||||
VALIDATE_STATUS(tx_id, post_req.appendFrom(reply, 0, reply_size));
|
VALIDATE_STATUS(tx_id, post_req.appendFrom(reply, 0, reply_size));
|
||||||
}
|
}
|
||||||
|
|
||||||
status_t post_status = callback->transact(intercept::kPostTransact, post_req, &post_resp);
|
if (callback->transact(intercept::kPostTransact, post_req, &post_resp) == OK) {
|
||||||
if (post_status == OK) {
|
|
||||||
int32_t post_action = post_resp.readInt32();
|
int32_t post_action = post_resp.readInt32();
|
||||||
if (post_action == intercept::kActionOverrideReply && reply) {
|
if (post_action == intercept::kActionOverrideReply && reply) {
|
||||||
result = post_resp.readInt32(); // Read new status
|
result = post_resp.readInt32(); // Read new status
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ import android.os.Build
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
import org.matrix.TEESimulator.config.BootStateManager
|
|
||||||
import org.matrix.TEESimulator.config.BulletinPoller
|
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
import org.matrix.TEESimulator.config.PatchLevelManager
|
|
||||||
import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor
|
import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor
|
||||||
import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor
|
import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor
|
||||||
import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor
|
import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor
|
||||||
@@ -26,6 +23,8 @@ import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
|||||||
object App {
|
object App {
|
||||||
// The delay in milliseconds before retrying to initialize the interceptor.
|
// The delay in milliseconds before retrying to initialize the interceptor.
|
||||||
private const val RETRY_DELAY_MS = 1000L
|
private const val RETRY_DELAY_MS = 1000L
|
||||||
|
// The sleep duration in milliseconds for the main service loop to keep the process alive.
|
||||||
|
private const val SERVICE_SLEEP_MS = 1000000L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main entry point of the TEESimulator application.
|
* The main entry point of the TEESimulator application.
|
||||||
@@ -42,19 +41,10 @@ object App {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
prepareEnvironment()
|
prepareEnvironment()
|
||||||
|
|
||||||
// Spoof boot-state and patch-level props before any hook attaches,
|
|
||||||
// so keystore2's cached snapshot reflects the spoofed values.
|
|
||||||
BootStateManager.apply()
|
|
||||||
PatchLevelManager.initialize()
|
|
||||||
|
|
||||||
// Load the package configuration.
|
|
||||||
ConfigurationManager.initialize()
|
|
||||||
|
|
||||||
// Initialize and start the appropriate keystore interceptors.
|
// Initialize and start the appropriate keystore interceptors.
|
||||||
initializeInterceptors()
|
initializeInterceptors()
|
||||||
|
|
||||||
// Set up the device's boot key and hash, which are crucial for attestation.
|
ConfigurationManager.initialize()
|
||||||
AndroidDeviceUtils.setupBootKeyAndHash()
|
AndroidDeviceUtils.setupBootKeyAndHash()
|
||||||
|
|
||||||
// Android ships with a stripped-down Bouncy Castle provider under the name "BC".
|
// Android ships with a stripped-down Bouncy Castle provider under the name "BC".
|
||||||
@@ -65,12 +55,6 @@ object App {
|
|||||||
|
|
||||||
NativeCertGen.initialize("/data/adb/modules/tricky_store/libcertgen.so")
|
NativeCertGen.initialize("/data/adb/modules/tricky_store/libcertgen.so")
|
||||||
|
|
||||||
try {
|
|
||||||
BulletinPoller.start()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
SystemLogger.error("Failed to start BulletinPoller", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This starts the message queue processing. It blocks here indefinitely
|
// This starts the message queue processing. It blocks here indefinitely
|
||||||
// processing messages until Looper.myLooper().quit() is called.
|
// processing messages until Looper.myLooper().quit() is called.
|
||||||
Looper.loop()
|
Looper.loop()
|
||||||
|
|||||||
@@ -43,12 +43,11 @@ object AttestationBuilder {
|
|||||||
securityLevel: Int,
|
securityLevel: Int,
|
||||||
): Extension {
|
): Extension {
|
||||||
val keyDescription = buildKeyDescription(params, uid, securityLevel)
|
val keyDescription = buildKeyDescription(params, uid, securityLevel)
|
||||||
SystemLogger.verbose {
|
var formattedString =
|
||||||
val formattedString = keyDescription.joinToString(separator = ", ") {
|
keyDescription.joinToString(separator = ", ") {
|
||||||
AttestationPatcher.formatAsn1Primitive(it)
|
AttestationPatcher.formatAsn1Primitive(it)
|
||||||
}
|
}
|
||||||
"Forged attestation data: $formattedString"
|
SystemLogger.verbose("Forged attestation data: ${formattedString}")
|
||||||
}
|
|
||||||
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
|
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +115,6 @@ object AttestationBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
|
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
|
||||||
SystemLogger.info("Attestation patch levels for uid=$uid: os=$osPatch, vendor=$vendorPatch, boot=$bootPatch")
|
|
||||||
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
|
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
|
||||||
if (bootPatch != DO_NOT_REPORT) {
|
if (bootPatch != DO_NOT_REPORT) {
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
@@ -131,6 +129,7 @@ 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,
|
||||||
@@ -149,11 +148,15 @@ object AttestationBuilder {
|
|||||||
|
|
||||||
val fields =
|
val fields =
|
||||||
arrayOf(
|
arrayOf(
|
||||||
ASN1Integer(AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()),
|
ASN1Integer(
|
||||||
ASN1Enumerated(securityLevel),
|
AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()
|
||||||
ASN1Integer(AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()),
|
), // attestationVersion
|
||||||
ASN1Enumerated(securityLevel),
|
ASN1Enumerated(securityLevel), // attestationSecurityLevel
|
||||||
DEROctetString(params.attestationChallenge ?: ByteArray(0)),
|
ASN1Integer(
|
||||||
|
AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()
|
||||||
|
), // keymasterVersion
|
||||||
|
ASN1Enumerated(securityLevel), // keymasterSecurityLevel
|
||||||
|
DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge
|
||||||
DEROctetString(uniqueId),
|
DEROctetString(uniqueId),
|
||||||
softwareEnforced,
|
softwareEnforced,
|
||||||
teeEnforced,
|
teeEnforced,
|
||||||
@@ -161,24 +164,37 @@ object AttestationBuilder {
|
|||||||
return DERSequence(fields)
|
return DERSequence(fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the unique ID per the KeyMint HAL spec:
|
||||||
|
* HMAC-SHA256(T || C || R, HBK) truncated to 128 bits.
|
||||||
|
*
|
||||||
|
* T = temporal counter (creationTime / 2592000000, i.e. 30-day periods since epoch)
|
||||||
|
* C = DER-encoded ATTESTATION_APPLICATION_ID
|
||||||
|
* R = 0x00 (no factory reset since ID rotation)
|
||||||
|
* HBK = device-unique secret generated once during module installation
|
||||||
|
*/
|
||||||
private fun computeUniqueId(creationTimeMs: Long, aaidDer: ByteArray): ByteArray {
|
private fun computeUniqueId(creationTimeMs: Long, aaidDer: ByteArray): ByteArray {
|
||||||
val temporalCounter = creationTimeMs / 2592000000L
|
val temporalCounter = creationTimeMs / 2592000000L
|
||||||
|
|
||||||
val message =
|
val message =
|
||||||
ByteBuffer.allocate(8 + aaidDer.size + 1)
|
ByteBuffer.allocate(8 + aaidDer.size + 1)
|
||||||
.putLong(temporalCounter)
|
.putLong(temporalCounter)
|
||||||
.put(aaidDer)
|
.put(aaidDer)
|
||||||
.put(0x00)
|
.put(0x00) // RESET_SINCE_ID_ROTATION = false
|
||||||
.array()
|
.array()
|
||||||
|
|
||||||
val mac = Mac.getInstance("HmacSHA256")
|
val mac = Mac.getInstance("HmacSHA256")
|
||||||
mac.init(SecretKeySpec(hbk, "HmacSHA256"))
|
mac.init(SecretKeySpec(hbk, "HmacSHA256"))
|
||||||
return mac.doFinal(message).copyOf(16)
|
return mac.doFinal(message).copyOf(16)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Device-unique key seed, generated once at module installation. */
|
||||||
private val hbk: ByteArray by lazy {
|
private val hbk: ByteArray by lazy {
|
||||||
val file = java.io.File(ConfigurationManager.CONFIG_PATH, "hbk")
|
val file = java.io.File(ConfigurationManager.CONFIG_PATH, "hbk")
|
||||||
if (file.exists() && file.length() == 32L) {
|
if (file.exists() && file.length() == 32L) {
|
||||||
file.readBytes()
|
file.readBytes()
|
||||||
} else {
|
} else {
|
||||||
|
// Fallback: generate in-memory (won't persist across reboots)
|
||||||
SystemLogger.warning("hbk not found, generating ephemeral HBK.")
|
SystemLogger.warning("hbk not found, generating ephemeral HBK.")
|
||||||
ByteArray(32).also { java.security.SecureRandom().nextBytes(it) }
|
ByteArray(32).also { java.security.SecureRandom().nextBytes(it) }
|
||||||
}
|
}
|
||||||
@@ -261,14 +277,20 @@ object AttestationBuilder {
|
|||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
true,
|
true,
|
||||||
AttestationConstants.TAG_RSA_OAEP_MGF_DIGEST,
|
AttestationConstants.TAG_RSA_OAEP_MGF_DIGEST,
|
||||||
DERSet(params.rsaOaepMgfDigest.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
DERSet(
|
||||||
|
params.rsaOaepMgfDigest.map { ASN1Integer(it.toLong()) }.toTypedArray()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.rollbackResistance == true && attestVersion >= 3) {
|
if (params.rollbackResistance == true && attestVersion >= 3) {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_ROLLBACK_RESISTANCE, DERNull.INSTANCE)
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ROLLBACK_RESISTANCE,
|
||||||
|
DERNull.INSTANCE,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,19 +308,31 @@ object AttestationBuilder {
|
|||||||
|
|
||||||
if (params.allowWhileOnBody == true) {
|
if (params.allowWhileOnBody == true) {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_ALLOW_WHILE_ON_BODY, DERNull.INSTANCE)
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ALLOW_WHILE_ON_BODY,
|
||||||
|
DERNull.INSTANCE,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.trustedUserPresenceRequired == true && attestVersion >= 3) {
|
if (params.trustedUserPresenceRequired == true && attestVersion >= 3) {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_TRUSTED_USER_PRESENCE_REQUIRED, DERNull.INSTANCE)
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_TRUSTED_USER_PRESENCE_REQUIRED,
|
||||||
|
DERNull.INSTANCE,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.trustedConfirmationRequired == true && attestVersion >= 3) {
|
if (params.trustedConfirmationRequired == true && attestVersion >= 3) {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_TRUSTED_CONFIRMATION_REQUIRED, DERNull.INSTANCE)
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_TRUSTED_CONFIRMATION_REQUIRED,
|
||||||
|
DERNull.INSTANCE,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,6 +462,7 @@ object AttestationBuilder {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ATTESTATION_APPLICATION_ID is only included when an attestation challenge is present.
|
||||||
if (params.attestationChallenge != null) {
|
if (params.attestationChallenge != null) {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
@@ -437,7 +472,6 @@ object AttestationBuilder {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) {
|
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
@@ -448,11 +482,8 @@ object AttestationBuilder {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.callerNonce == true) {
|
// Keystore2-enforced tags belong in softwareEnforced, not teeEnforced.
|
||||||
list.add(
|
// The HAL does not enforce these; keystore2's authorize_create handles them.
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_CALLER_NONCE, DERNull.INSTANCE)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
params.activeDateTime?.let {
|
params.activeDateTime?.let {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time))
|
DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time))
|
||||||
@@ -460,22 +491,38 @@ object AttestationBuilder {
|
|||||||
}
|
}
|
||||||
params.originationExpireDateTime?.let {
|
params.originationExpireDateTime?.let {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_ORIGINATION_EXPIRE_DATETIME, ASN1Integer(it.time))
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ORIGINATION_EXPIRE_DATETIME,
|
||||||
|
ASN1Integer(it.time),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
params.usageExpireDateTime?.let {
|
params.usageExpireDateTime?.let {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_USAGE_EXPIRE_DATETIME, ASN1Integer(it.time))
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_USAGE_EXPIRE_DATETIME,
|
||||||
|
ASN1Integer(it.time),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
params.usageCountLimit?.let {
|
params.usageCountLimit?.let {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_USAGE_COUNT_LIMIT, ASN1Integer(it.toLong()))
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_USAGE_COUNT_LIMIT,
|
||||||
|
ASN1Integer(it.toLong()),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (params.unlockedDeviceRequired == true) {
|
if (params.unlockedDeviceRequired == true) {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_UNLOCKED_DEVICE_REQUIRED, DERNull.INSTANCE)
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_UNLOCKED_DEVICE_REQUIRED,
|
||||||
|
DERNull.INSTANCE,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,9 +554,15 @@ object AttestationBuilder {
|
|||||||
*/
|
*/
|
||||||
@Throws(Throwable::class)
|
@Throws(Throwable::class)
|
||||||
internal fun createApplicationId(uid: Int): DEROctetString {
|
internal fun createApplicationId(uid: Int): DEROctetString {
|
||||||
|
// AOSP keystore_attestation_id.cpp: gather_attestation_application_id()
|
||||||
|
// uses a hardcoded identity for AID_SYSTEM (1000) and AID_ROOT (0):
|
||||||
|
// packageName = "AndroidSystem", versionCode = 1, no signing digests.
|
||||||
val appUid = uid % 100000
|
val appUid = uid % 100000
|
||||||
if (appUid == 0 || appUid == 1000) {
|
if (appUid == 0 || appUid == 1000) {
|
||||||
return buildApplicationIdDer(listOf("AndroidSystem" to 1L), emptySet())
|
return buildApplicationIdDer(
|
||||||
|
listOf("AndroidSystem" to 1L),
|
||||||
|
emptySet(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val pm =
|
val pm =
|
||||||
|
|||||||
@@ -95,5 +95,5 @@ object AttestationConstants {
|
|||||||
|
|
||||||
// --- Other Constants ---
|
// --- Other Constants ---
|
||||||
// https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp
|
// https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp
|
||||||
const val CHALLENGE_LENGTH_LIMIT = 128
|
const val CHALLENGE_LENGTH_LIMIT = 128 // kMaximumAttestationChallengeLength
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
|||||||
import org.matrix.TEESimulator.pki.KeyBox
|
import org.matrix.TEESimulator.pki.KeyBox
|
||||||
import org.matrix.TEESimulator.pki.KeyBoxManager
|
import org.matrix.TEESimulator.pki.KeyBoxManager
|
||||||
import org.matrix.TEESimulator.util.toHex
|
import org.matrix.TEESimulator.util.toHex
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the modification (patching) of Android Key Attestation extensions within certificates.
|
* Handles the modification (patching) of Android Key Attestation extensions within certificates.
|
||||||
@@ -37,12 +36,7 @@ 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(
|
fun patchCertificateChain(originalChain: Array<Certificate>?, uid: Int): Array<Certificate> {
|
||||||
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()
|
||||||
@@ -69,8 +63,6 @@ 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
|
||||||
@@ -119,27 +111,17 @@ 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,
|
||||||
effectiveNotBefore,
|
originalLeafHolder.notBefore,
|
||||||
effectiveNotAfter,
|
originalLeafHolder.notAfter,
|
||||||
originalLeafHolder.subject,
|
originalLeafHolder.subject,
|
||||||
originalLeafHolder.subjectPublicKeyInfo,
|
originalLeafHolder.subjectPublicKeyInfo,
|
||||||
)
|
)
|
||||||
@@ -164,7 +146,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
|
||||||
}
|
}
|
||||||
@@ -286,10 +268,8 @@ 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
|
||||||
|
|
||||||
SystemLogger.verbose {
|
var formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
||||||
val formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
SystemLogger.verbose("Original attestation data: ${formattedString}")
|
||||||
"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)
|
||||||
@@ -316,10 +296,8 @@ object AttestationPatcher {
|
|||||||
|
|
||||||
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
|
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
|
||||||
val patchedSequence = DERSequence(allFields)
|
val patchedSequence = DERSequence(allFields)
|
||||||
SystemLogger.verbose {
|
formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
||||||
val formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
SystemLogger.verbose("Patched attestation data: ${formattedString}")
|
||||||
"Patched attestation data: $formattedString"
|
|
||||||
}
|
|
||||||
val patchedOctets = DEROctetString(patchedSequence)
|
val patchedOctets = DEROctetString(patchedSequence)
|
||||||
|
|
||||||
return Extension(ATTESTATION_OID, false, patchedOctets)
|
return Extension(ATTESTATION_OID, false, patchedOctets)
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
package org.matrix.TEESimulator.attestation
|
package org.matrix.TEESimulator.attestation
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
|
||||||
import android.security.keystore.KeyProperties
|
|
||||||
import java.security.KeyPairGenerator
|
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.SecureRandom
|
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.security.spec.ECGenParameterSpec
|
|
||||||
import org.bouncycastle.asn1.ASN1Integer
|
import org.bouncycastle.asn1.ASN1Integer
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||||
import org.bouncycastle.asn1.ASN1OctetString
|
import org.bouncycastle.asn1.ASN1OctetString
|
||||||
@@ -57,55 +52,14 @@ object DeviceAttestationService {
|
|||||||
val bootPatchLevel: Int?,
|
val bootPatchLevel: Int?,
|
||||||
)
|
)
|
||||||
|
|
||||||
// A unique alias for the key used to perform the TEE functionality check.
|
|
||||||
private const val TEE_CHECK_KEY_ALIAS = "TEESimulator_AttestationCheck"
|
private const val TEE_CHECK_KEY_ALIAS = "TEESimulator_AttestationCheck"
|
||||||
|
|
||||||
/**
|
|
||||||
* Lazily determines if the device's TEE is functional by attempting to generate an
|
|
||||||
* attestation-backed key pair. The result is cached.
|
|
||||||
*/
|
|
||||||
val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazily fetches and parses attestation data from a genuinely generated certificate. The result
|
* Lazily fetches and parses attestation data from a genuinely generated certificate. The result
|
||||||
* is cached. Returns null if the TEE is not functional or parsing fails.
|
* is cached. Returns null if the TEE is not functional or parsing fails.
|
||||||
*/
|
*/
|
||||||
val CachedAttestationData: AttestationData? by lazy { fetchAttestationData() }
|
val CachedAttestationData: AttestationData? by lazy { fetchAttestationData() }
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the TEE is working correctly by generating a key in the Android Keystore with an
|
|
||||||
* attestation challenge.
|
|
||||||
*
|
|
||||||
* @return `true` if a key with attestation was generated successfully, `false` otherwise.
|
|
||||||
*/
|
|
||||||
private fun checkTeeFunctionality(): Boolean {
|
|
||||||
SystemLogger.info("Performing TEE functionality check...")
|
|
||||||
return try {
|
|
||||||
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
|
||||||
val keyPairGenerator =
|
|
||||||
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
|
|
||||||
|
|
||||||
// A random challenge is required for attestation.
|
|
||||||
val challenge = ByteArray(16).apply { SecureRandom().nextBytes(this) }
|
|
||||||
|
|
||||||
val spec =
|
|
||||||
KeyGenParameterSpec.Builder(TEE_CHECK_KEY_ALIAS, KeyProperties.PURPOSE_SIGN)
|
|
||||||
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
|
|
||||||
.setDigests(KeyProperties.DIGEST_SHA256)
|
|
||||||
.setAttestationChallenge(challenge)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
keyPairGenerator.initialize(spec)
|
|
||||||
keyPairGenerator.generateKeyPair()
|
|
||||||
|
|
||||||
SystemLogger.info("TEE functionality check successful.")
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SystemLogger.warning("TEE functionality check failed.", e)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the attestation certificate generated during the TEE check. The key entry is
|
* Retrieves the attestation certificate generated during the TEE check. The key entry is
|
||||||
* deleted after retrieval to clean up.
|
* deleted after retrieval to clean up.
|
||||||
@@ -113,8 +67,6 @@ object DeviceAttestationService {
|
|||||||
* @return The leaf `X509Certificate` containing the attestation, or `null` if unavailable.
|
* @return The leaf `X509Certificate` containing the attestation, or `null` if unavailable.
|
||||||
*/
|
*/
|
||||||
private fun getAttestationCertificate(): X509Certificate? {
|
private fun getAttestationCertificate(): X509Certificate? {
|
||||||
if (!isTeeFunctional) return null
|
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
||||||
val certChain = keyStore.getCertificateChain(TEE_CHECK_KEY_ALIAS)
|
val certChain = keyStore.getCertificateChain(TEE_CHECK_KEY_ALIAS)
|
||||||
@@ -148,12 +100,11 @@ object DeviceAttestationService {
|
|||||||
|
|
||||||
// The extension's value is an ASN.1 sequence.
|
// The extension's value is an ASN.1 sequence.
|
||||||
val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets)
|
val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets)
|
||||||
SystemLogger.verbose {
|
var formattedString =
|
||||||
val formattedString = keyDescriptionSeq.joinToString(separator = ", ") {
|
keyDescriptionSeq.joinToString(separator = ", ") {
|
||||||
AttestationPatcher.formatAsn1Primitive(it)
|
AttestationPatcher.formatAsn1Primitive(it)
|
||||||
}
|
}
|
||||||
"Cached attestation data: $formattedString"
|
SystemLogger.verbose("Cached attestation data: ${formattedString}")
|
||||||
}
|
|
||||||
val fields = keyDescriptionSeq.toArray()
|
val fields = keyDescriptionSeq.toArray()
|
||||||
|
|
||||||
val attestVersion =
|
val attestVersion =
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package org.matrix.TEESimulator.attestation
|
package org.matrix.TEESimulator.attestation
|
||||||
|
|
||||||
import android.hardware.security.keymint.*
|
import android.hardware.security.keymint.*
|
||||||
import android.hardware.security.keymint.KeyOrigin
|
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import javax.security.auth.x500.X500Principal
|
import javax.security.auth.x500.X500Principal
|
||||||
@@ -17,11 +16,12 @@ import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
|||||||
// Reference:
|
// Reference:
|
||||||
// https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/key_parameter.rs
|
// https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/key_parameter.rs
|
||||||
data class KeyMintAttestation(
|
data class KeyMintAttestation(
|
||||||
val keySize: Int,
|
|
||||||
val algorithm: Int,
|
val algorithm: Int,
|
||||||
val ecCurve: Int?,
|
val ecCurve: Int?,
|
||||||
val ecCurveName: String,
|
val ecCurveName: String,
|
||||||
|
val keySize: Int,
|
||||||
val origin: Int?,
|
val origin: Int?,
|
||||||
|
val noAuthRequired: Boolean?,
|
||||||
val blockMode: List<Int>,
|
val blockMode: List<Int>,
|
||||||
val padding: List<Int>,
|
val padding: List<Int>,
|
||||||
val purpose: List<Int>,
|
val purpose: List<Int>,
|
||||||
@@ -41,12 +41,12 @@ data class KeyMintAttestation(
|
|||||||
val manufacturer: ByteArray?,
|
val manufacturer: ByteArray?,
|
||||||
val model: ByteArray?,
|
val model: ByteArray?,
|
||||||
val secondImei: ByteArray?,
|
val secondImei: ByteArray?,
|
||||||
|
// Enforcement tags
|
||||||
val activeDateTime: Date?,
|
val activeDateTime: Date?,
|
||||||
val originationExpireDateTime: Date?,
|
val originationExpireDateTime: Date?,
|
||||||
val usageExpireDateTime: Date?,
|
val usageExpireDateTime: Date?,
|
||||||
val usageCountLimit: Int?,
|
val usageCountLimit: Int?,
|
||||||
val callerNonce: Boolean?,
|
val callerNonce: Boolean?,
|
||||||
val nonce: ByteArray?,
|
|
||||||
val unlockedDeviceRequired: Boolean?,
|
val unlockedDeviceRequired: Boolean?,
|
||||||
val includeUniqueId: Boolean?,
|
val includeUniqueId: Boolean?,
|
||||||
val rollbackResistance: Boolean?,
|
val rollbackResistance: Boolean?,
|
||||||
@@ -54,7 +54,6 @@ data class KeyMintAttestation(
|
|||||||
val allowWhileOnBody: Boolean?,
|
val allowWhileOnBody: Boolean?,
|
||||||
val trustedUserPresenceRequired: Boolean?,
|
val trustedUserPresenceRequired: Boolean?,
|
||||||
val trustedConfirmationRequired: Boolean?,
|
val trustedConfirmationRequired: Boolean?,
|
||||||
val noAuthRequired: Boolean?,
|
|
||||||
val maxUsesPerBoot: Int?,
|
val maxUsesPerBoot: Int?,
|
||||||
val maxBootLevel: Int?,
|
val maxBootLevel: Int?,
|
||||||
val minMacLength: Int?,
|
val minMacLength: Int?,
|
||||||
@@ -64,11 +63,13 @@ data class KeyMintAttestation(
|
|||||||
constructor(
|
constructor(
|
||||||
params: Array<KeyParameter>
|
params: Array<KeyParameter>
|
||||||
) : this(
|
) : this(
|
||||||
keySize = params.findInteger(Tag.KEY_SIZE) ?: params.deriveKeySizeFromCurve(),
|
|
||||||
|
|
||||||
// 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,
|
||||||
|
|
||||||
|
// AOSP: [key_param(tag = KEY_SIZE, field = Integer)]
|
||||||
|
// For EC keys, derive keySize from EC_CURVE when KEY_SIZE is absent.
|
||||||
|
keySize = params.findInteger(Tag.KEY_SIZE) ?: params.deriveKeySizeFromCurve(),
|
||||||
|
|
||||||
// AOSP: [key_param(tag = EC_CURVE, field = EcCurve)]
|
// AOSP: [key_param(tag = EC_CURVE, field = EcCurve)]
|
||||||
ecCurve = params.findEcCurve(Tag.EC_CURVE),
|
ecCurve = params.findEcCurve(Tag.EC_CURVE),
|
||||||
ecCurveName = params.deriveEcCurveName(),
|
ecCurveName = params.deriveEcCurveName(),
|
||||||
@@ -76,6 +77,9 @@ data class KeyMintAttestation(
|
|||||||
// AOSP: [key_param(tag = ORIGIN, field = Origin)]
|
// AOSP: [key_param(tag = ORIGIN, field = Origin)]
|
||||||
origin = params.findOrigin(Tag.ORIGIN),
|
origin = params.findOrigin(Tag.ORIGIN),
|
||||||
|
|
||||||
|
// AOSP: [key_param(tag = NO_AUTH_REQUIRED, field = BoolValue)]
|
||||||
|
noAuthRequired = params.findBoolean(Tag.NO_AUTH_REQUIRED),
|
||||||
|
|
||||||
// AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)]
|
// AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)]
|
||||||
blockMode = params.findAllBlockMode(Tag.BLOCK_MODE),
|
blockMode = params.findAllBlockMode(Tag.BLOCK_MODE),
|
||||||
|
|
||||||
@@ -117,12 +121,13 @@ 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),
|
||||||
|
|
||||||
|
// Enforcement tags
|
||||||
activeDateTime = params.findDate(Tag.ACTIVE_DATETIME),
|
activeDateTime = params.findDate(Tag.ACTIVE_DATETIME),
|
||||||
originationExpireDateTime = params.findDate(Tag.ORIGINATION_EXPIRE_DATETIME),
|
originationExpireDateTime = params.findDate(Tag.ORIGINATION_EXPIRE_DATETIME),
|
||||||
usageExpireDateTime = params.findDate(Tag.USAGE_EXPIRE_DATETIME),
|
usageExpireDateTime = params.findDate(Tag.USAGE_EXPIRE_DATETIME),
|
||||||
usageCountLimit = params.findInteger(Tag.USAGE_COUNT_LIMIT),
|
usageCountLimit = params.findInteger(Tag.USAGE_COUNT_LIMIT),
|
||||||
callerNonce = params.findBoolean(Tag.CALLER_NONCE),
|
callerNonce = params.findBoolean(Tag.CALLER_NONCE),
|
||||||
nonce = params.findBlob(Tag.NONCE),
|
|
||||||
unlockedDeviceRequired = params.findBoolean(Tag.UNLOCKED_DEVICE_REQUIRED),
|
unlockedDeviceRequired = params.findBoolean(Tag.UNLOCKED_DEVICE_REQUIRED),
|
||||||
includeUniqueId = params.findBoolean(Tag.INCLUDE_UNIQUE_ID),
|
includeUniqueId = params.findBoolean(Tag.INCLUDE_UNIQUE_ID),
|
||||||
rollbackResistance = params.findBoolean(Tag.ROLLBACK_RESISTANCE),
|
rollbackResistance = params.findBoolean(Tag.ROLLBACK_RESISTANCE),
|
||||||
@@ -130,7 +135,6 @@ data class KeyMintAttestation(
|
|||||||
allowWhileOnBody = params.findBoolean(Tag.ALLOW_WHILE_ON_BODY),
|
allowWhileOnBody = params.findBoolean(Tag.ALLOW_WHILE_ON_BODY),
|
||||||
trustedUserPresenceRequired = params.findBoolean(Tag.TRUSTED_USER_PRESENCE_REQUIRED),
|
trustedUserPresenceRequired = params.findBoolean(Tag.TRUSTED_USER_PRESENCE_REQUIRED),
|
||||||
trustedConfirmationRequired = params.findBoolean(Tag.TRUSTED_CONFIRMATION_REQUIRED),
|
trustedConfirmationRequired = params.findBoolean(Tag.TRUSTED_CONFIRMATION_REQUIRED),
|
||||||
noAuthRequired = params.findBoolean(Tag.NO_AUTH_REQUIRED),
|
|
||||||
maxUsesPerBoot = params.findInteger(Tag.MAX_USES_PER_BOOT),
|
maxUsesPerBoot = params.findInteger(Tag.MAX_USES_PER_BOOT),
|
||||||
maxBootLevel = params.findInteger(Tag.MAX_BOOT_LEVEL),
|
maxBootLevel = params.findInteger(Tag.MAX_BOOT_LEVEL),
|
||||||
minMacLength = params.findInteger(Tag.MIN_MAC_LENGTH),
|
minMacLength = params.findInteger(Tag.MIN_MAC_LENGTH),
|
||||||
@@ -140,13 +144,21 @@ data class KeyMintAttestation(
|
|||||||
params.forEach { KeyMintParameterLogger.logParameter(it) }
|
params.forEach { KeyMintParameterLogger.logParameter(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isAttestKey(): Boolean = purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY)
|
fun isAttestKey(): Boolean {
|
||||||
|
return purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
fun isImportKey(): Boolean = origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED
|
fun isImportKey(): Boolean {
|
||||||
|
return origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Private helper extension functions for parsing KeyParameter arrays ---
|
// --- Private helper extension functions for parsing KeyParameter arrays ---
|
||||||
|
|
||||||
|
/** Maps to AOSP field = Integer */
|
||||||
|
private fun Array<KeyParameter>.findBoolean(tag: Int): Boolean? =
|
||||||
|
this.find { it.tag == tag }?.value?.boolValue
|
||||||
|
|
||||||
/** Maps to AOSP field = Integer */
|
/** Maps to AOSP field = Integer */
|
||||||
private fun Array<KeyParameter>.findInteger(tag: Int): Int? =
|
private fun Array<KeyParameter>.findInteger(tag: Int): Int? =
|
||||||
this.find { it.tag == tag }?.value?.integer
|
this.find { it.tag == tag }?.value?.integer
|
||||||
@@ -179,7 +191,7 @@ private fun Array<KeyParameter>.findBlob(tag: Int): ByteArray? =
|
|||||||
private fun Array<KeyParameter>.findAllBlockMode(tag: Int): List<Int> =
|
private fun Array<KeyParameter>.findAllBlockMode(tag: Int): List<Int> =
|
||||||
this.filter { it.tag == tag }.map { it.value.blockMode }
|
this.filter { it.tag == tag }.map { it.value.blockMode }
|
||||||
|
|
||||||
/** Maps to AOSP field = BlockMode (Repeated) */
|
/** Maps to AOSP field = PaddingMode (Repeated) */
|
||||||
private fun Array<KeyParameter>.findAllPaddingMode(tag: Int): List<Int> =
|
private fun Array<KeyParameter>.findAllPaddingMode(tag: Int): List<Int> =
|
||||||
this.filter { it.tag == tag }.map { it.value.paddingMode }
|
this.filter { it.tag == tag }.map { it.value.paddingMode }
|
||||||
|
|
||||||
@@ -191,9 +203,7 @@ 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? =
|
/** Derives keySize from EC_CURVE tag when KEY_SIZE is not explicitly provided. */
|
||||||
if (this.any { it.tag == tag }) true else null
|
|
||||||
|
|
||||||
private fun Array<KeyParameter>.deriveKeySizeFromCurve(): Int {
|
private fun Array<KeyParameter>.deriveKeySizeFromCurve(): Int {
|
||||||
val curveId = this.find { it.tag == Tag.EC_CURVE }?.value?.ecCurve ?: return 0
|
val curveId = this.find { it.tag == Tag.EC_CURVE }?.value?.ecCurve ?: return 0
|
||||||
return when (curveId) {
|
return when (curveId) {
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package org.matrix.TEESimulator.config
|
|
||||||
|
|
||||||
import android.os.SystemProperties
|
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
|
||||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
|
||||||
|
|
||||||
object BootStateManager {
|
|
||||||
private val targets =
|
|
||||||
linkedMapOf(
|
|
||||||
"ro.boot.verifiedbootstate" to "green",
|
|
||||||
"ro.boot.flash.locked" to "1",
|
|
||||||
"ro.boot.veritymode" to "enforcing",
|
|
||||||
"ro.boot.vbmeta.device_state" to "locked",
|
|
||||||
)
|
|
||||||
|
|
||||||
private val fillIfAbsent =
|
|
||||||
linkedMapOf(
|
|
||||||
"ro.boot.vbmeta.invalidate_on_error" to "yes",
|
|
||||||
"ro.boot.vbmeta.avb_version" to "1.2",
|
|
||||||
"ro.boot.vbmeta.hash_alg" to "sha256",
|
|
||||||
"ro.boot.vbmeta.size" to "11904",
|
|
||||||
)
|
|
||||||
|
|
||||||
fun apply() {
|
|
||||||
for ((name, target) in targets) {
|
|
||||||
val current = SystemProperties.get(name, "")
|
|
||||||
if (current.isEmpty()) {
|
|
||||||
SystemLogger.debug("BootStateManager: $name absent on this device, skip")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (current == target) {
|
|
||||||
SystemLogger.debug("BootStateManager: $name already $target, skip")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
SystemLogger.info("BootStateManager: setting $name=$target (was: '$current')")
|
|
||||||
AndroidDeviceUtils.setProperty(name, target)
|
|
||||||
}
|
|
||||||
for ((name, value) in fillIfAbsent) {
|
|
||||||
val current = SystemProperties.get(name, "")
|
|
||||||
if (current.isNotEmpty()) {
|
|
||||||
SystemLogger.debug("BootStateManager: $name already '$current', skip")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
SystemLogger.info("BootStateManager: filling absent $name=$value")
|
|
||||||
AndroidDeviceUtils.setProperty(name, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
package org.matrix.TEESimulator.config
|
|
||||||
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.HandlerThread
|
|
||||||
import java.io.File
|
|
||||||
import java.net.URL
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.StandardCopyOption
|
|
||||||
import javax.net.ssl.HttpsURLConnection
|
|
||||||
import org.json.JSONArray
|
|
||||||
import org.json.JSONObject
|
|
||||||
import org.matrix.TEESimulator.BuildConfig
|
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
|
||||||
|
|
||||||
object BulletinPoller {
|
|
||||||
private const val BULLETIN_URL = "https://source.android.com/docs/security/bulletin/pixel"
|
|
||||||
private const val PATCH_FILE = "/data/adb/tricky_store/security_patch.txt"
|
|
||||||
private const val HISTORY_FILE = "/data/adb/tricky_store/last_bulletin_fetch.json"
|
|
||||||
private const val HISTORY_STAGING = "/data/adb/tricky_store/last_bulletin_fetch.json.next"
|
|
||||||
private const val HISTORY_CAP = 10
|
|
||||||
private const val CONNECT_TIMEOUT_MS = 10_000
|
|
||||||
private const val READ_TIMEOUT_MS = 15_000
|
|
||||||
private const val STEADY_INTERVAL_MS = 24L * 60 * 60 * 1000
|
|
||||||
|
|
||||||
private val BOOTSTRAP_INTERVALS = longArrayOf(5_000, 30_000, 120_000, 600_000, 1_800_000)
|
|
||||||
private val DATE_REGEX = Regex("<td>(\\d{4}-\\d{2}-\\d{2})</td>")
|
|
||||||
private val PATCH_DATE_PATTERN = Regex("^\\d{4}-\\d{2}-\\d{2}$")
|
|
||||||
|
|
||||||
private lateinit var handler: Handler
|
|
||||||
@Volatile private var bootstrapStep = 0
|
|
||||||
@Volatile private var steadyArmed = false
|
|
||||||
|
|
||||||
fun start() {
|
|
||||||
val thread = HandlerThread("BulletinPoller").apply { start() }
|
|
||||||
handler = Handler(thread.looper)
|
|
||||||
handler.postDelayed(::pollOnce, BOOTSTRAP_INTERVALS[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pollOnce() {
|
|
||||||
try {
|
|
||||||
val result = fetchAndParse()
|
|
||||||
appendHistory(result)
|
|
||||||
scheduleNext(result.status == "success")
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
SystemLogger.error("BulletinPoller: pollOnce failed", t)
|
|
||||||
scheduleNext(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun scheduleNext(success: Boolean) {
|
|
||||||
if (success || steadyArmed) {
|
|
||||||
steadyArmed = true
|
|
||||||
handler.postDelayed(::pollOnce, STEADY_INTERVAL_MS)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bootstrapStep++
|
|
||||||
if (bootstrapStep >= BOOTSTRAP_INTERVALS.size) {
|
|
||||||
steadyArmed = true
|
|
||||||
handler.postDelayed(::pollOnce, STEADY_INTERVAL_MS)
|
|
||||||
} else {
|
|
||||||
handler.postDelayed(::pollOnce, BOOTSTRAP_INTERVALS[bootstrapStep])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class FetchResult(
|
|
||||||
val ts: Long,
|
|
||||||
val status: String,
|
|
||||||
val httpCode: Int?,
|
|
||||||
val parsedDate: String?,
|
|
||||||
val applied: Boolean,
|
|
||||||
val error: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun fetchAndParse(): FetchResult {
|
|
||||||
val ts = System.currentTimeMillis()
|
|
||||||
var conn: HttpsURLConnection? = null
|
|
||||||
return try {
|
|
||||||
conn =
|
|
||||||
(URL(BULLETIN_URL).openConnection() as HttpsURLConnection).apply {
|
|
||||||
connectTimeout = CONNECT_TIMEOUT_MS
|
|
||||||
readTimeout = READ_TIMEOUT_MS
|
|
||||||
setRequestProperty(
|
|
||||||
"User-Agent",
|
|
||||||
"TEESimulator/${BuildConfig.VERSION_NAME}",
|
|
||||||
)
|
|
||||||
requestMethod = "GET"
|
|
||||||
}
|
|
||||||
val code = conn.responseCode
|
|
||||||
if (code != 200) {
|
|
||||||
return FetchResult(ts, "network_error", code, null, false, "HTTP $code")
|
|
||||||
}
|
|
||||||
val html = conn.inputStream.bufferedReader().use { it.readText() }
|
|
||||||
val date = DATE_REGEX.find(html)?.groupValues?.get(1)
|
|
||||||
if (date == null) {
|
|
||||||
return FetchResult(
|
|
||||||
ts,
|
|
||||||
"parse_error",
|
|
||||||
code,
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
"no <td>YYYY-MM-DD</td> match",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val current = currentPatch()
|
|
||||||
if (current == null || date <= current) {
|
|
||||||
return FetchResult(ts, "success", code, date, false, null)
|
|
||||||
}
|
|
||||||
if (PatchLevelManager.updateTo(date)) {
|
|
||||||
FetchResult(ts, "success", code, date, true, null)
|
|
||||||
} else {
|
|
||||||
FetchResult(
|
|
||||||
ts,
|
|
||||||
"validation_rejected",
|
|
||||||
code,
|
|
||||||
date,
|
|
||||||
false,
|
|
||||||
"PatchLevelManager.updateTo rejected $date",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
FetchResult(ts, "network_error", null, null, false, e.toString())
|
|
||||||
} finally {
|
|
||||||
conn?.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun currentPatch(): String? {
|
|
||||||
val f = File(PATCH_FILE)
|
|
||||||
if (!f.exists()) return null
|
|
||||||
val raw = try {
|
|
||||||
f.readLines()
|
|
||||||
.firstOrNull { it.startsWith("system=") }
|
|
||||||
?.substringAfter("system=")
|
|
||||||
?.trim()
|
|
||||||
?.takeIf { it != "prop" && it.isNotEmpty() }
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
if (raw == null) return null
|
|
||||||
if (PATCH_DATE_PATTERN.matches(raw)) return raw
|
|
||||||
SystemLogger.warning(
|
|
||||||
"BulletinPoller: ignoring malformed system='$raw' in $PATCH_FILE"
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun appendHistory(result: FetchResult) {
|
|
||||||
try {
|
|
||||||
val target = File(HISTORY_FILE)
|
|
||||||
val staging = File(HISTORY_STAGING)
|
|
||||||
val existing = if (target.exists()) runCatching { target.readText() }.getOrNull() else null
|
|
||||||
val history =
|
|
||||||
existing
|
|
||||||
?.let { runCatching { JSONObject(it).optJSONArray("history") }.getOrNull() }
|
|
||||||
?: JSONArray()
|
|
||||||
val entry =
|
|
||||||
JSONObject().apply {
|
|
||||||
put("ts", result.ts)
|
|
||||||
put("status", result.status)
|
|
||||||
put("http_code", result.httpCode ?: JSONObject.NULL)
|
|
||||||
put("parsed_date", result.parsedDate ?: JSONObject.NULL)
|
|
||||||
put("applied", result.applied)
|
|
||||||
put("error", result.error ?: JSONObject.NULL)
|
|
||||||
}
|
|
||||||
history.put(entry)
|
|
||||||
while (history.length() > HISTORY_CAP) history.remove(0)
|
|
||||||
|
|
||||||
val latestKnown =
|
|
||||||
(0 until history.length())
|
|
||||||
.mapNotNull {
|
|
||||||
history.optJSONObject(it)?.optString("parsed_date", "")?.takeIf { d ->
|
|
||||||
d.isNotBlank()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.lastOrNull()
|
|
||||||
|
|
||||||
val root =
|
|
||||||
JSONObject().apply {
|
|
||||||
put("latest_known_date", latestKnown ?: JSONObject.NULL)
|
|
||||||
put("history", history)
|
|
||||||
}
|
|
||||||
staging.writeText(root.toString(2))
|
|
||||||
Files.move(
|
|
||||||
staging.toPath(),
|
|
||||||
target.toPath(),
|
|
||||||
StandardCopyOption.ATOMIC_MOVE,
|
|
||||||
StandardCopyOption.REPLACE_EXISTING,
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SystemLogger.error("BulletinPoller: failed to persist history", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ import android.os.IBinder
|
|||||||
import android.os.ServiceManager
|
import android.os.ServiceManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import org.matrix.TEESimulator.attestation.DeviceAttestationService
|
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
import org.matrix.TEESimulator.pki.KeyBoxManager
|
import org.matrix.TEESimulator.pki.KeyBoxManager
|
||||||
|
|
||||||
@@ -66,6 +65,7 @@ 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))
|
||||||
|
|
||||||
// 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.")
|
||||||
@@ -83,6 +83,7 @@ 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 {
|
||||||
val mode = getPackageModeForUid(uid)
|
val mode = getPackageModeForUid(uid)
|
||||||
return mode == Mode.PATCH || mode == Mode.AUTO
|
return mode == Mode.PATCH || mode == Mode.AUTO
|
||||||
@@ -91,19 +92,13 @@ object ConfigurationManager {
|
|||||||
/** 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
|
||||||
|
|
||||||
fun isAutoMode(uid: Int): Boolean {
|
/** Determines if the UID is in AUTO mode (no explicit ! or ? suffix). */
|
||||||
for (pkg in getPackagesForUid(uid)) {
|
fun isAutoMode(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.AUTO
|
||||||
when (packageModes[pkg]) {
|
|
||||||
Mode.GENERATE, Mode.PATCH -> return false
|
|
||||||
Mode.AUTO -> return true
|
|
||||||
null -> continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/** Resolves the operating mode for a given UID based on its packages and the TEE status. */
|
||||||
private fun getPackageModeForUid(uid: Int): Mode? {
|
private fun getPackageModeForUid(uid: Int): Mode? {
|
||||||
val packages = getPackagesForUid(uid)
|
val packages = getPackagesForUid(uid)
|
||||||
if (packages.isEmpty()) return null
|
if (packages.isEmpty()) return null
|
||||||
@@ -112,7 +107,7 @@ object ConfigurationManager {
|
|||||||
when (packageModes[pkg]) {
|
when (packageModes[pkg]) {
|
||||||
Mode.GENERATE -> return Mode.GENERATE
|
Mode.GENERATE -> return Mode.GENERATE
|
||||||
Mode.PATCH -> return Mode.PATCH
|
Mode.PATCH -> return Mode.PATCH
|
||||||
Mode.AUTO -> return if (DeviceAttestationService.isTeeFunctional) Mode.PATCH else Mode.GENERATE
|
Mode.AUTO -> return Mode.AUTO
|
||||||
null -> continue
|
null -> continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,24 +156,25 @@ object ConfigurationManager {
|
|||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val mode: Mode
|
||||||
|
val rawPkg: String
|
||||||
when {
|
when {
|
||||||
// Suffix '!' means force GENERATE mode.
|
|
||||||
trimmedLine.endsWith("!") -> {
|
trimmedLine.endsWith("!") -> {
|
||||||
val pkg = trimmedLine.removeSuffix("!").trim()
|
mode = Mode.GENERATE
|
||||||
newModes[pkg] = Mode.GENERATE
|
rawPkg = trimmedLine.removeSuffix("!").trim()
|
||||||
newKeyboxes[pkg] = currentKeybox
|
|
||||||
}
|
}
|
||||||
// Suffix '?' means force PATCH mode.
|
|
||||||
trimmedLine.endsWith("?") -> {
|
trimmedLine.endsWith("?") -> {
|
||||||
val pkg = trimmedLine.removeSuffix("?").trim()
|
mode = Mode.PATCH
|
||||||
newModes[pkg] = Mode.PATCH
|
rawPkg = trimmedLine.removeSuffix("?").trim()
|
||||||
newKeyboxes[pkg] = currentKeybox
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
newModes[trimmedLine] = Mode.AUTO
|
mode = Mode.AUTO
|
||||||
newKeyboxes[trimmedLine] = currentKeybox
|
rawPkg = trimmedLine
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newModes[rawPkg] = mode
|
||||||
|
newKeyboxes[rawPkg] = currentKeybox
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atomically update the configuration maps.
|
// Atomically update the configuration maps.
|
||||||
@@ -255,14 +251,7 @@ object ConfigurationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse global and per-package configurations.
|
// Parse global and per-package configurations.
|
||||||
var newGlobalLevel = parseLines(contextLines[""])
|
val newGlobalLevel = parseLines(contextLines[""])
|
||||||
// TrickyAddon writes Pixel bulletin dates for boot/vendor but system=prop
|
|
||||||
// resolves to the real device prop — force boot/vendor through the same path
|
|
||||||
// to prevent cross-component date mismatches on non-Pixel devices.
|
|
||||||
if (newGlobalLevel?.system.equals("prop", ignoreCase = true)) {
|
|
||||||
SystemLogger.info("system=prop: forcing boot/vendor to derive from device props (were: boot=${newGlobalLevel?.boot}, vendor=${newGlobalLevel?.vendor})")
|
|
||||||
newGlobalLevel = newGlobalLevel?.copy(boot = "prop", vendor = "prop")
|
|
||||||
}
|
|
||||||
contextLines.remove("") // Remove global context to iterate over packages next
|
contextLines.remove("") // Remove global context to iterate over packages next
|
||||||
|
|
||||||
for ((pkg, lines) in contextLines) {
|
for ((pkg, lines) in contextLines) {
|
||||||
@@ -293,10 +282,8 @@ object ConfigurationManager {
|
|||||||
|
|
||||||
val file = if (event != DELETE) File(configRoot, path) else null
|
val file = if (event != DELETE) File(configRoot, path) else null
|
||||||
when (path) {
|
when (path) {
|
||||||
TARGET_PACKAGES_FILE -> file?.let { loadTargetPackages(it) }
|
TARGET_PACKAGES_FILE -> loadTargetPackages(file!!)
|
||||||
?: SystemLogger.warning("$TARGET_PACKAGES_FILE was deleted.")
|
PATCH_LEVEL_FILE -> loadPatchLevelConfig(file!!)
|
||||||
PATCH_LEVEL_FILE -> file?.let { loadPatchLevelConfig(it) }
|
|
||||||
?: SystemLogger.warning("$PATCH_LEVEL_FILE was deleted.")
|
|
||||||
// Any change to an XML file is assumed to be a keybox.
|
// Any change to an XML file is assumed to be a keybox.
|
||||||
// The cache in KeyBoxManager will handle reloading it on its next use.
|
// The cache in KeyBoxManager will handle reloading it on its next use.
|
||||||
else ->
|
else ->
|
||||||
@@ -306,15 +293,10 @@ object ConfigurationManager {
|
|||||||
)
|
)
|
||||||
KeyBoxManager.invalidateCache(path)
|
KeyBoxManager.invalidateCache(path)
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
|
||||||
// Drop only the patched cert chains so the next
|
// Clear cached keys possibly containing old certificates
|
||||||
// attestation request re-signs with the new keybox.
|
|
||||||
// Do NOT drop generatedKeys — that would destroy
|
|
||||||
// every alias/private key in memory and on disk,
|
|
||||||
// logging users out of any app that pinned a
|
|
||||||
// persisted keystore alias.
|
|
||||||
org.matrix.TEESimulator.interception.keystore.shim
|
org.matrix.TEESimulator.interception.keystore.shim
|
||||||
.KeyMintSecurityLevelInterceptor
|
.KeyMintSecurityLevelInterceptor
|
||||||
.invalidatePatchedChains("updating $file")
|
.clearAllGeneratedKeys("updating $file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,6 +326,8 @@ object ConfigurationManager {
|
|||||||
return iPackageManager
|
return iPackageManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Checks if any package belonging to the UID holds the given permission. */
|
||||||
|
/** Checks a SELinux permission for a caller identified by PID against the keystore context. */
|
||||||
fun checkSELinuxPermission(callingPid: Int, tclass: String, perm: String): Boolean {
|
fun checkSELinuxPermission(callingPid: Int, tclass: String, perm: String): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val callerCtx =
|
val callerCtx =
|
||||||
@@ -356,6 +340,7 @@ object ConfigurationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Checks if any package belonging to the UID holds the given permission. */
|
||||||
fun hasPermissionForUid(uid: Int, permission: String): Boolean {
|
fun hasPermissionForUid(uid: Int, permission: String): Boolean {
|
||||||
val userId = uid / 100000
|
val userId = uid / 100000
|
||||||
return getPackagesForUid(uid).any { pkg ->
|
return getPackagesForUid(uid).any { pkg ->
|
||||||
@@ -367,6 +352,7 @@ object ConfigurationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Retrieves the package names associated with a UID. */
|
||||||
fun getPackagesForUid(uid: Int): Array<String> {
|
fun getPackagesForUid(uid: Int): Array<String> {
|
||||||
return uidToPackagesCache.getOrPut(uid) {
|
return uidToPackagesCache.getOrPut(uid) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
package org.matrix.TEESimulator.config
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.FileObserver
|
|
||||||
import android.os.SystemProperties
|
|
||||||
import java.io.File
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.StandardCopyOption
|
|
||||||
import java.time.LocalDate
|
|
||||||
import org.json.JSONObject
|
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
|
||||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
|
||||||
|
|
||||||
object PatchLevelManager {
|
|
||||||
private const val PATCH_FILE = "/data/adb/tricky_store/security_patch.txt"
|
|
||||||
private const val STAGING_FILE = "/data/adb/tricky_store/security_patch.txt.next"
|
|
||||||
private const val PIF_DIR = "/data/adb/modules/playintegrityfix"
|
|
||||||
private const val FLOOR_YYYYMMDD = 20200101
|
|
||||||
private const val MAX_PAST_OFFSET = 10000
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pixel security bulletins publish monthly; pre-announced dates occasionally
|
|
||||||
* slip by 2-4 weeks. 60 days covers that window without admitting a
|
|
||||||
* far-future date from a hostile or mis-parsed bulletin response.
|
|
||||||
*/
|
|
||||||
private const val MAX_FUTURE_DAYS = 60L
|
|
||||||
|
|
||||||
private val PIF_FILENAMES =
|
|
||||||
setOf("pif.json", "pif.prop", "custom.pif.json", "custom.pif.prop")
|
|
||||||
|
|
||||||
private val DATE_PATTERN = Regex("^\\d{4}-\\d{2}-\\d{2}$")
|
|
||||||
private val PROP_PATTERN = Regex("^SECURITY_PATCH=(.+)$", RegexOption.MULTILINE)
|
|
||||||
private val SECTION_HEADER = Regex("^\\[[a-zA-Z0-9_.-]+]$")
|
|
||||||
private val GLOBAL_KEYS = setOf("system", "boot", "vendor", "all")
|
|
||||||
|
|
||||||
private val PIF_SOURCES =
|
|
||||||
listOf(
|
|
||||||
"/data/adb/modules/playintegrityfix/pif.json",
|
|
||||||
"/data/adb/pif.json",
|
|
||||||
"/data/adb/modules/playintegrityfix/pif.prop",
|
|
||||||
"/data/adb/pif.prop",
|
|
||||||
"/data/adb/modules/playintegrityfix/custom.pif.json",
|
|
||||||
"/data/adb/modules/playintegrityfix/custom.pif.prop",
|
|
||||||
)
|
|
||||||
|
|
||||||
fun initialize() {
|
|
||||||
refreshFromSources()
|
|
||||||
startPifObserver()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshFromSources() {
|
|
||||||
val date =
|
|
||||||
resolvePifPatch()
|
|
||||||
?: SystemProperties.get(
|
|
||||||
"ro.build.version.security_patch",
|
|
||||||
Build.VERSION.SECURITY_PATCH,
|
|
||||||
)
|
|
||||||
SystemLogger.info("PatchLevelManager: resolved patch date = $date")
|
|
||||||
applyToProps(date)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startPifObserver() {
|
|
||||||
if (!File(PIF_DIR).exists()) {
|
|
||||||
SystemLogger.debug("PatchLevelManager: PIF dir absent, hot-reload disabled")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
PifObserver.startWatching()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun applyToProps(date: String) {
|
|
||||||
if (!DATE_PATTERN.matches(date)) {
|
|
||||||
SystemLogger.warning(
|
|
||||||
"PatchLevelManager: skip resetprop for invalid date: $date"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
AndroidDeviceUtils.setProperty("ro.build.version.security_patch", date)
|
|
||||||
AndroidDeviceUtils.setProperty("ro.vendor.build.security_patch", date)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateTo(date: String): Boolean {
|
|
||||||
if (!DATE_PATTERN.matches(date)) {
|
|
||||||
SystemLogger.warning("PatchLevelManager: invalid date format: $date")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val dateInt = date.replace("-", "").toInt()
|
|
||||||
if (dateInt < FLOOR_YYYYMMDD) {
|
|
||||||
SystemLogger.warning("PatchLevelManager: $date below floor $FLOOR_YYYYMMDD")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val now = LocalDate.now()
|
|
||||||
val today = now.year * 10000 + now.monthValue * 100 + now.dayOfMonth
|
|
||||||
if (today >= dateInt + MAX_PAST_OFFSET) {
|
|
||||||
SystemLogger.warning(
|
|
||||||
"PatchLevelManager: $date more than 1y older than today ($today)"
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val maxFuture =
|
|
||||||
now.plusDays(MAX_FUTURE_DAYS).let {
|
|
||||||
it.year * 10000 + it.monthValue * 100 + it.dayOfMonth
|
|
||||||
}
|
|
||||||
if (dateInt > maxFuture) {
|
|
||||||
SystemLogger.warning(
|
|
||||||
"PatchLevelManager: $date more than $MAX_FUTURE_DAYS days in future ($maxFuture)"
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
atomicWrite(date)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SystemLogger.error("PatchLevelManager: atomicWrite failed for $date", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
applyToProps(date)
|
|
||||||
SystemLogger.info("PatchLevelManager: applied patch date $date")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolvePifPatch(): String? {
|
|
||||||
val source =
|
|
||||||
PIF_SOURCES.map(::File).lastOrNull { it.exists() && it.length() > 0 }
|
|
||||||
?: return null
|
|
||||||
return try {
|
|
||||||
val text = source.readText()
|
|
||||||
val parsed =
|
|
||||||
if (source.name.endsWith(".json")) {
|
|
||||||
JSONObject(text).optString("SECURITY_PATCH", "")
|
|
||||||
} else {
|
|
||||||
PROP_PATTERN.find(text)?.groupValues?.get(1)?.trim().orEmpty()
|
|
||||||
}
|
|
||||||
parsed.takeIf { it.isNotBlank() }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SystemLogger.warning(
|
|
||||||
"PatchLevelManager: failed to parse ${source.path}: ${e.message}"
|
|
||||||
)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun atomicWrite(date: String) {
|
|
||||||
val target = File(PATCH_FILE)
|
|
||||||
val staging = File(STAGING_FILE)
|
|
||||||
staging.writeText(mergedContents(target, date))
|
|
||||||
Files.move(
|
|
||||||
staging.toPath(),
|
|
||||||
target.toPath(),
|
|
||||||
StandardCopyOption.ATOMIC_MOVE,
|
|
||||||
StandardCopyOption.REPLACE_EXISTING,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mergedContents(target: File, date: String): String {
|
|
||||||
val globalBlock = "system=$date\nboot=$date\nvendor=$date\n"
|
|
||||||
if (!target.exists()) return globalBlock
|
|
||||||
val tail = stripGlobalAssignments(target.readLines())
|
|
||||||
if (tail.isEmpty()) return globalBlock
|
|
||||||
return globalBlock + tail.joinToString("\n", prefix = "\n", postfix = "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stripGlobalAssignments(lines: List<String>): List<String> {
|
|
||||||
val kept = mutableListOf<String>()
|
|
||||||
var inGlobal = true
|
|
||||||
for (line in lines) {
|
|
||||||
val trimmed = line.trim()
|
|
||||||
if (SECTION_HEADER.matches(trimmed)) {
|
|
||||||
inGlobal = false
|
|
||||||
kept += line
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (inGlobal && isGlobalKeyAssignment(trimmed)) continue
|
|
||||||
kept += line
|
|
||||||
}
|
|
||||||
return kept
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isGlobalKeyAssignment(trimmed: String): Boolean {
|
|
||||||
if (trimmed.isEmpty() || trimmed.startsWith("#") || '=' !in trimmed) return false
|
|
||||||
val key = trimmed.substringBefore('=').trim().lowercase()
|
|
||||||
return key in GLOBAL_KEYS
|
|
||||||
}
|
|
||||||
|
|
||||||
private object PifObserver :
|
|
||||||
FileObserver(File(PIF_DIR), CLOSE_WRITE or MOVED_TO or DELETE) {
|
|
||||||
override fun onEvent(event: Int, path: String?) {
|
|
||||||
if (path == null || path !in PIF_FILENAMES) return
|
|
||||||
SystemLogger.info("PatchLevelManager: PIF change ($path), refreshing")
|
|
||||||
refreshFromSources()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -293,6 +293,12 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses the backdoor binder to register an interceptor for a specific target service.
|
||||||
|
*
|
||||||
|
* @param filteredCodes If non-empty, only these transaction codes will be intercepted at
|
||||||
|
* the native level. All other codes pass through without the round-trip to Java.
|
||||||
|
*/
|
||||||
fun register(
|
fun register(
|
||||||
backdoor: IBinder,
|
backdoor: IBinder,
|
||||||
target: IBinder,
|
target: IBinder,
|
||||||
|
|||||||
+5
@@ -68,8 +68,13 @@ abstract class AbstractKeystoreInterceptor : BinderInterceptor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transaction codes this interceptor needs to handle at the native level. Override in
|
||||||
|
* subclasses to filter; empty means intercept everything (legacy behavior).
|
||||||
|
*/
|
||||||
protected open val interceptedCodes: IntArray = intArrayOf()
|
protected open val interceptedCodes: IntArray = intArrayOf()
|
||||||
|
|
||||||
|
/** Registers this interceptor with the native hook layer and sets up a death recipient. */
|
||||||
private fun setupInterceptor(service: IBinder, backdoor: IBinder) {
|
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")
|
||||||
|
|||||||
+20
-38
@@ -19,28 +19,11 @@ object InterceptorUtils {
|
|||||||
|
|
||||||
private const val EX_SERVICE_SPECIFIC = -8
|
private const val EX_SERVICE_SPECIFIC = -8
|
||||||
|
|
||||||
private fun synthesizeSseMessage(errorCode: Int): String =
|
|
||||||
when (errorCode) {
|
|
||||||
2 -> "Error::Rc(SYSTEM_ERROR)"
|
|
||||||
4 -> "Error::Rc(PERMISSION_DENIED)"
|
|
||||||
6 -> "Error::Rc(VALUE_CORRUPTED)"
|
|
||||||
7 -> "Error::Rc(KEY_NOT_FOUND)"
|
|
||||||
10 -> "Error::Rc(BACKEND_BUSY)"
|
|
||||||
-3 -> "Error::Km(UNSUPPORTED_KEY_SIZE)"
|
|
||||||
-6 -> "Error::Km(INCOMPATIBLE_PURPOSE)"
|
|
||||||
-7 -> "Error::Km(INCOMPATIBLE_ALGORITHM)"
|
|
||||||
-29 -> "Error::Km(TOO_MANY_OPERATIONS)"
|
|
||||||
-49 -> "Error::Km(UNSUPPORTED_TAG)"
|
|
||||||
-75 -> "Error::Km(INVALID_INPUT_LENGTH)"
|
|
||||||
-76 -> "Error::Km(INVALID_TAG)"
|
|
||||||
else -> if (errorCode > 0) "Error::Rc($errorCode)" else "Error::Km($errorCode)"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply {
|
fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply {
|
||||||
val parcel = Parcel.obtain().apply {
|
val parcel = Parcel.obtain().apply {
|
||||||
writeInt(EX_SERVICE_SPECIFIC)
|
writeInt(EX_SERVICE_SPECIFIC)
|
||||||
writeString(synthesizeSseMessage(errorCode))
|
writeString(null)
|
||||||
writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196)
|
writeInt(0)
|
||||||
writeInt(errorCode)
|
writeInt(errorCode)
|
||||||
}
|
}
|
||||||
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
@@ -147,29 +130,28 @@ object InterceptorUtils {
|
|||||||
return exception != null
|
return exception != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an `OverrideReply` that writes a `ServiceSpecificException` with the given error
|
||||||
|
* code via EX_SERVICE_SPECIFIC.
|
||||||
|
*/
|
||||||
fun createServiceSpecificErrorReply(
|
fun createServiceSpecificErrorReply(
|
||||||
errorCode: Int
|
errorCode: Int
|
||||||
): BinderInterceptor.TransactionResult.OverrideReply = createErrorReply(errorCode)
|
): BinderInterceptor.TransactionResult.OverrideReply {
|
||||||
|
val parcel =
|
||||||
fun normalizeServiceSpecificReply(reply: Parcel): Parcel? {
|
Parcel.obtain().apply {
|
||||||
reply.setDataPosition(0)
|
writeException(android.os.ServiceSpecificException(errorCode))
|
||||||
if (reply.readInt() != EX_SERVICE_SPECIFIC) {
|
}
|
||||||
reply.setDataPosition(0)
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
return null
|
|
||||||
}
|
|
||||||
// Advance position past message and stack header to reach errorCode.
|
|
||||||
reply.readString()
|
|
||||||
reply.readInt()
|
|
||||||
val errorCode = reply.readInt()
|
|
||||||
reply.setDataPosition(0)
|
|
||||||
return Parcel.obtain().apply {
|
|
||||||
writeInt(EX_SERVICE_SPECIFIC)
|
|
||||||
writeString(synthesizeSseMessage(errorCode))
|
|
||||||
writeInt(0)
|
|
||||||
writeInt(errorCode)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patches the system-level authorization values (OS_PATCHLEVEL, VENDOR_PATCHLEVEL,
|
||||||
|
* BOOT_PATCHLEVEL) in an authorization array to match the configured patch levels for the
|
||||||
|
* given calling UID. Each authorization's original [Authorization.securityLevel] is preserved.
|
||||||
|
*
|
||||||
|
* When a patch level is configured as "no" ([AndroidDeviceUtils.DO_NOT_REPORT]), the original
|
||||||
|
* hardware value is kept as-is.
|
||||||
|
*/
|
||||||
fun patchAuthorizations(
|
fun patchAuthorizations(
|
||||||
authorizations: Array<Authorization>?,
|
authorizations: Array<Authorization>?,
|
||||||
callingUid: Int,
|
callingUid: Int,
|
||||||
|
|||||||
+148
-142
@@ -6,16 +6,17 @@ 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.Domain
|
||||||
|
import android.system.keystore2.IKeystoreSecurityLevel
|
||||||
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
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
|
import java.util.Collections
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
||||||
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
import org.matrix.TEESimulator.interception.keystore.shim.GeneratedKeyPersistence
|
|
||||||
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
|
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
|
||||||
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
@@ -48,6 +49,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
else null
|
else null
|
||||||
private val GET_NUMBER_OF_ENTRIES_TRANSACTION =
|
private val GET_NUMBER_OF_ENTRIES_TRANSACTION =
|
||||||
InterceptorUtils.getTransactCode(stubBinderClass, "getNumberOfEntries")
|
InterceptorUtils.getTransactCode(stubBinderClass, "getNumberOfEntries")
|
||||||
|
private val GET_SECURITY_LEVEL_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(stubBinderClass, "getSecurityLevel")
|
||||||
|
|
||||||
private val transactionNames: Map<Int, String> by lazy {
|
private val transactionNames: Map<Int, String> by lazy {
|
||||||
stubBinderClass.declaredFields
|
stubBinderClass.declaredFields
|
||||||
@@ -58,15 +61,19 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
.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 const val RESPONSE_KEY_NOT_FOUND = 7
|
// Keys whose certs were updated via updateSubcomponent; skip re-patching on getKeyEntry.
|
||||||
private val deletedSoftwareKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
|
||||||
private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
|
private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
|
||||||
|
|
||||||
fun forgetDeletedKey(keyId: KeyIdentifier) {
|
// Backdoor binder for registering new interceptors at runtime.
|
||||||
if (deletedSoftwareKeys.remove(keyId)) {
|
private var backdoorBinder: IBinder? = null
|
||||||
SystemLogger.debug("Cleared deletion marker for ${keyId.alias}")
|
|
||||||
}
|
// Per-security-level interceptor instances, keyed by SecurityLevel constant.
|
||||||
}
|
private val securityLevelInterceptors = ConcurrentHashMap<Int, KeyMintSecurityLevelInterceptor>()
|
||||||
|
|
||||||
|
// Identity set of SecurityLevel binders already registered with the native hook,
|
||||||
|
// tracked by System.identityHashCode to avoid re-registering the same BBinder.
|
||||||
|
private val registeredSecurityLevelBinders: MutableSet<Int> =
|
||||||
|
Collections.newSetFromMap(ConcurrentHashMap())
|
||||||
|
|
||||||
override val serviceName = "android.system.keystore2.IKeystoreService/default"
|
override val serviceName = "android.system.keystore2.IKeystoreService/default"
|
||||||
override val processName = "keystore2"
|
override val processName = "keystore2"
|
||||||
@@ -80,6 +87,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
LIST_ENTRIES_TRANSACTION,
|
LIST_ENTRIES_TRANSACTION,
|
||||||
LIST_ENTRIES_BATCHED_TRANSACTION,
|
LIST_ENTRIES_BATCHED_TRANSACTION,
|
||||||
GET_NUMBER_OF_ENTRIES_TRANSACTION,
|
GET_NUMBER_OF_ENTRIES_TRANSACTION,
|
||||||
|
GET_SECURITY_LEVEL_TRANSACTION,
|
||||||
)
|
)
|
||||||
.toIntArray()
|
.toIntArray()
|
||||||
}
|
}
|
||||||
@@ -89,6 +97,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
* security level sub-services (e.g., TEE, StrongBox).
|
* security level sub-services (e.g., TEE, StrongBox).
|
||||||
*/
|
*/
|
||||||
override fun onInterceptorReady(service: IBinder, backdoor: IBinder) {
|
override fun onInterceptorReady(service: IBinder, backdoor: IBinder) {
|
||||||
|
backdoorBinder = backdoor
|
||||||
val keystoreInterface = IKeystoreService.Stub.asInterface(service)
|
val keystoreInterface = IKeystoreService.Stub.asInterface(service)
|
||||||
setupSecurityLevelInterceptors(keystoreInterface, backdoor)
|
setupSecurityLevelInterceptors(keystoreInterface, backdoor)
|
||||||
}
|
}
|
||||||
@@ -100,11 +109,11 @@ 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(
|
securityLevelInterceptors[SecurityLevel.TRUSTED_ENVIRONMENT] = interceptor
|
||||||
|
registerSecurityLevelBinder(
|
||||||
backdoor,
|
backdoor,
|
||||||
tee.asBinder(),
|
tee.asBinder(),
|
||||||
interceptor,
|
interceptor,
|
||||||
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
|
|
||||||
)
|
)
|
||||||
interceptor.loadPersistedKeys()
|
interceptor.loadPersistedKeys()
|
||||||
}
|
}
|
||||||
@@ -117,11 +126,11 @@ 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(
|
securityLevelInterceptors[SecurityLevel.STRONGBOX] = interceptor
|
||||||
|
registerSecurityLevelBinder(
|
||||||
backdoor,
|
backdoor,
|
||||||
strongbox.asBinder(),
|
strongbox.asBinder(),
|
||||||
interceptor,
|
interceptor,
|
||||||
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
|
|
||||||
)
|
)
|
||||||
interceptor.loadPersistedKeys()
|
interceptor.loadPersistedKeys()
|
||||||
}
|
}
|
||||||
@@ -129,6 +138,30 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
.onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) }
|
.onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an interceptor for a SecurityLevel binder, tracking the binder identity
|
||||||
|
* to avoid duplicate registrations when keystore2 returns the same BBinder.
|
||||||
|
*/
|
||||||
|
private fun registerSecurityLevelBinder(
|
||||||
|
backdoor: IBinder,
|
||||||
|
binder: IBinder,
|
||||||
|
interceptor: KeyMintSecurityLevelInterceptor,
|
||||||
|
) {
|
||||||
|
val identity = System.identityHashCode(binder)
|
||||||
|
if (registeredSecurityLevelBinders.add(identity)) {
|
||||||
|
register(
|
||||||
|
backdoor,
|
||||||
|
binder,
|
||||||
|
interceptor,
|
||||||
|
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SystemLogger.debug(
|
||||||
|
"SecurityLevel binder $binder (identity=$identity) already registered, skipping."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPreTransact(
|
override fun onPreTransact(
|
||||||
txId: Long,
|
txId: Long,
|
||||||
target: IBinder,
|
target: IBinder,
|
||||||
@@ -151,23 +184,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
|
|
||||||
if (isGMS || ConfigurationManager.shouldSkipUid(callingUid)) {
|
if (isGMS || ConfigurationManager.shouldSkipUid(callingUid)) {
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
} else {
|
||||||
|
return TransactionResult.Continue
|
||||||
}
|
}
|
||||||
|
|
||||||
return runCatching {
|
|
||||||
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
|
|
||||||
if (ListEntriesHandler.cacheParameters(txId, data, isBatchMode)) {
|
|
||||||
TransactionResult.Continue
|
|
||||||
} else {
|
|
||||||
TransactionResult.ContinueAndSkipPost
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.getOrElse {
|
|
||||||
SystemLogger.error(
|
|
||||||
"[TX_ID: $txId] Failed to parse parameters for ${transactionNames[code]!!}",
|
|
||||||
it,
|
|
||||||
)
|
|
||||||
TransactionResult.ContinueAndSkipPost
|
|
||||||
}
|
|
||||||
} else if (
|
} else if (
|
||||||
code == GET_KEY_ENTRY_TRANSACTION ||
|
code == GET_KEY_ENTRY_TRANSACTION ||
|
||||||
code == DELETE_KEY_TRANSACTION ||
|
code == DELETE_KEY_TRANSACTION ||
|
||||||
@@ -187,6 +206,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
?: return TransactionResult.ContinueAndSkipPost
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
|
|
||||||
if (code == DELETE_KEY_TRANSACTION) {
|
if (code == DELETE_KEY_TRANSACTION) {
|
||||||
|
// Handle delete by alias (APP domain) or nspace (KEY_ID domain).
|
||||||
val keyId =
|
val keyId =
|
||||||
if (descriptor.alias != null) {
|
if (descriptor.alias != null) {
|
||||||
KeyIdentifier(callingUid, descriptor.alias)
|
KeyIdentifier(callingUid, descriptor.alias)
|
||||||
@@ -205,7 +225,6 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(keyId)
|
KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(keyId)
|
||||||
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
||||||
if (isSoftwareKey) {
|
if (isSoftwareKey) {
|
||||||
deletedSoftwareKeys.add(keyId)
|
|
||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"[TX_ID: $txId] Deleted cached keypair ${keyId.alias}, replying with empty response."
|
"[TX_ID: $txId] Deleted cached keypair ${keyId.alias}, replying with empty response."
|
||||||
)
|
)
|
||||||
@@ -216,49 +235,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (descriptor.alias == null) {
|
if (descriptor.alias == null) {
|
||||||
if (descriptor.domain == Domain.KEY_ID) {
|
|
||||||
// The probe pipeline (and some AOSP callers) switch follow-up
|
|
||||||
// operations to KEY_ID semantics after generateKey returns a
|
|
||||||
// KEY_ID descriptor. Without this branch, our software keys
|
|
||||||
// are invisible to KEY_ID-based getKeyEntry calls and the
|
|
||||||
// request falls through to the real keystore2 daemon, which
|
|
||||||
// legitimately responds with KEY_NOT_FOUND. Duck Detector's
|
|
||||||
// TimingSideChannelProbe captures that exception during its
|
|
||||||
// warmup phase and surfaces it as
|
|
||||||
// "Captured private binder exception during timing skip".
|
|
||||||
// Resolving by KEY_ID and returning the cached response keeps
|
|
||||||
// the call on the happy path, eliminating the warmup signal.
|
|
||||||
val info = KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
|
||||||
callingUid, descriptor.nspace
|
|
||||||
)
|
|
||||||
if (info?.response != null) {
|
|
||||||
SystemLogger.info(
|
|
||||||
"[TX_ID: $txId] Found generated response via KEY_ID nspace=${descriptor.nspace}"
|
|
||||||
)
|
|
||||||
return InterceptorUtils.createTypedObjectReply(info.response)
|
|
||||||
}
|
|
||||||
val teeResp = KeyMintSecurityLevelInterceptor.findTeeResponseByKeyId(
|
|
||||||
callingUid, descriptor.nspace
|
|
||||||
)
|
|
||||||
if (teeResp != null) {
|
|
||||||
SystemLogger.info(
|
|
||||||
"[TX_ID: $txId] Found TEE response via KEY_ID nspace=${descriptor.nspace}"
|
|
||||||
)
|
|
||||||
return InterceptorUtils.createTypedObjectReply(teeResp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
||||||
|
|
||||||
val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
val response =
|
||||||
if (response == null) {
|
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
||||||
if (deletedSoftwareKeys.remove(keyId)) {
|
?: return TransactionResult.Continue
|
||||||
SystemLogger.info("[TX_ID: $txId] Returning KEY_NOT_FOUND for deleted key ${descriptor.alias}")
|
|
||||||
return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
|
|
||||||
}
|
|
||||||
return TransactionResult.Continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
|
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
|
||||||
SystemLogger.info("${descriptor.alias} was an attestation key")
|
SystemLogger.info("${descriptor.alias} was an attestation key")
|
||||||
@@ -268,6 +251,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
KeyMintParameterLogger.logParameter(it.keyParameter)
|
KeyMintParameterLogger.logParameter(it.keyParameter)
|
||||||
}
|
}
|
||||||
return InterceptorUtils.createTypedObjectReply(response)
|
return InterceptorUtils.createTypedObjectReply(response)
|
||||||
|
} else if (code == GET_SECURITY_LEVEL_TRANSACTION) {
|
||||||
|
// Pass through to post-hook so we can register interceptors for newly-created
|
||||||
|
// SecurityLevel binders. keystore2 may create a new BBinder per call, so the
|
||||||
|
// initial registration in setupSecurityLevelInterceptors might not cover all
|
||||||
|
// binder instances that clients receive.
|
||||||
|
logTransaction(txId, "getSecurityLevel", callingUid, callingPid)
|
||||||
|
return TransactionResult.Continue
|
||||||
} else {
|
} else {
|
||||||
logTransaction(
|
logTransaction(
|
||||||
txId,
|
txId,
|
||||||
@@ -293,11 +283,11 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
reply: Parcel?,
|
reply: Parcel?,
|
||||||
resultCode: Int,
|
resultCode: Int,
|
||||||
): TransactionResult {
|
): TransactionResult {
|
||||||
if (target != keystoreService || reply == null) return TransactionResult.SkipTransaction
|
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
|
||||||
if (InterceptorUtils.hasException(reply)) {
|
return TransactionResult.SkipTransaction
|
||||||
val normalized = InterceptorUtils.normalizeServiceSpecificReply(reply)
|
|
||||||
return if (normalized != null) TransactionResult.OverrideReply(normalized)
|
if (code == GET_SECURITY_LEVEL_TRANSACTION) {
|
||||||
else TransactionResult.SkipTransaction
|
return handlePostGetSecurityLevel(txId, data, reply)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) {
|
if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) {
|
||||||
@@ -323,8 +313,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||||
|
|
||||||
return runCatching {
|
return runCatching {
|
||||||
|
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
|
||||||
|
val params =
|
||||||
|
ListEntriesHandler.cacheParameters(txId, data, isBatchMode)
|
||||||
|
?: throw Exception("Abort updating entries for invalid parameters.")
|
||||||
val updatedKeyDescriptors =
|
val updatedKeyDescriptors =
|
||||||
ListEntriesHandler.injectGeneratedKeys(txId, callingUid, reply)
|
ListEntriesHandler.injectGeneratedKeys(txId, callingUid, params, reply)
|
||||||
InterceptorUtils.createTypedArrayReply(updatedKeyDescriptors)
|
InterceptorUtils.createTypedArrayReply(updatedKeyDescriptors)
|
||||||
}
|
}
|
||||||
.getOrElse {
|
.getOrElse {
|
||||||
@@ -354,8 +348,8 @@ 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)
|
||||||
|
|
||||||
|
// Skip patching for keys whose certs were explicitly set via updateSubcomponent.
|
||||||
if (userUpdatedKeys.remove(keyId)) {
|
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.")
|
SystemLogger.debug("[TX_ID: $txId] Skipping cert patch for user-updated key $keyId.")
|
||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
}
|
}
|
||||||
@@ -366,36 +360,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
|
authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
|
||||||
)
|
)
|
||||||
|
|
||||||
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: isImport=${parsedParameters.isImportKey()} origin=${parsedParameters.origin} inImportedKeys=${KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)} hasPatchedChain=${KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) != null} isAttestKey=${parsedParameters.isAttestKey()}" }
|
if (parsedParameters.isAttestKey() &&
|
||||||
|
!KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)
|
||||||
if (parsedParameters.isImportKey()) {
|
) {
|
||||||
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
|
||||||
if (retainedChain == null) {
|
|
||||||
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, no retained chain, skip" }
|
|
||||||
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
|
|
||||||
return TransactionResult.SkipTransaction
|
|
||||||
}
|
|
||||||
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, SERVING RETAINED CHAIN (detection vector!)" }
|
|
||||||
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
|
|
||||||
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
|
|
||||||
response.metadata.authorizations =
|
|
||||||
InterceptorUtils.patchAuthorizations(
|
|
||||||
response.metadata.authorizations,
|
|
||||||
callingUid,
|
|
||||||
)
|
|
||||||
return InterceptorUtils.createTypedObjectReply(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)) {
|
|
||||||
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: in importedKeys set, skip" }
|
|
||||||
SystemLogger.debug("[TX_ID: $txId] Skipping attest-key override for imported key $keyId")
|
|
||||||
return TransactionResult.SkipTransaction
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedParameters.isAttestKey()) {
|
|
||||||
SystemLogger.warning(
|
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."
|
||||||
)
|
)
|
||||||
|
// Attest keys that are not under our control should be overriden.
|
||||||
val keyData =
|
val keyData =
|
||||||
CertificateGenerator.generateAttestedKeyPair(
|
CertificateGenerator.generateAttestedKeyPair(
|
||||||
callingUid,
|
callingUid,
|
||||||
@@ -427,42 +398,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
parsedParameters,
|
parsedParameters,
|
||||||
)
|
)
|
||||||
KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId)
|
KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId)
|
||||||
|
|
||||||
// Snapshot metadata bytes for the same reason as the
|
|
||||||
// primary doSoftwareKeyGen path — loss-less restore
|
|
||||||
// after reboot.
|
|
||||||
val metadataBytesForPersist = response.metadata?.let { md ->
|
|
||||||
runCatching {
|
|
||||||
val parcel = android.os.Parcel.obtain()
|
|
||||||
try {
|
|
||||||
md.writeToParcel(parcel, 0)
|
|
||||||
parcel.marshall()
|
|
||||||
} finally {
|
|
||||||
parcel.recycle()
|
|
||||||
}
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
GeneratedKeyPersistence.save(
|
|
||||||
keyId = keyId,
|
|
||||||
keyPair = keyData.first,
|
|
||||||
secretKey = null,
|
|
||||||
nspace = newNspace,
|
|
||||||
securityLevel = response.metadata.keySecurityLevel,
|
|
||||||
certChain = keyData.second,
|
|
||||||
algorithm = parsedParameters.algorithm,
|
|
||||||
keySize = parsedParameters.keySize,
|
|
||||||
ecCurve = parsedParameters.ecCurve ?: 0,
|
|
||||||
purposes = parsedParameters.purpose,
|
|
||||||
digests = parsedParameters.digest,
|
|
||||||
isAttestationKey = true,
|
|
||||||
metadataBytes = metadataBytesForPersist,
|
|
||||||
)
|
|
||||||
|
|
||||||
return InterceptorUtils.createTypedObjectReply(response)
|
return InterceptorUtils.createTypedObjectReply(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
val originalChain = CertificateHelper.getCertificateChain(response)
|
val originalChain = CertificateHelper.getCertificateChain(response)
|
||||||
|
|
||||||
|
// Check if we should perform attestation patch.
|
||||||
if (originalChain == null || originalChain.size < 2) {
|
if (originalChain == null || originalChain.size < 2) {
|
||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}."
|
"[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}."
|
||||||
@@ -470,6 +411,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First, try to retrieve the already-patched chain from our cache to ensure
|
||||||
|
// consistency.
|
||||||
val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
||||||
|
|
||||||
val finalChain: Array<Certificate>
|
val finalChain: Array<Certificate>
|
||||||
@@ -479,12 +422,16 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
)
|
)
|
||||||
finalChain = cachedChain
|
finalChain = cachedChain
|
||||||
} else {
|
} else {
|
||||||
|
// If no chain is cached (e.g., key existed before simulator started),
|
||||||
|
// perform a live patch as a fallback. This may still be detectable.
|
||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
|
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
|
||||||
)
|
)
|
||||||
finalChain =
|
finalChain =
|
||||||
AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
||||||
|
|
||||||
KeyMintSecurityLevelInterceptor.patchedChains[keyId] = finalChain
|
KeyMintSecurityLevelInterceptor.patchedChains[keyId] = finalChain
|
||||||
|
SystemLogger.debug("Cached patched certificate chain for $keyId.")
|
||||||
}
|
}
|
||||||
|
|
||||||
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
|
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
|
||||||
@@ -513,11 +460,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
?: return TransactionResult.ContinueAndSkipPost
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
|
|
||||||
|
// Resolve by nspace (KEY_ID) or alias (APP), same as createOperation.
|
||||||
val generatedKeyInfo =
|
val generatedKeyInfo =
|
||||||
when (descriptor.domain) {
|
when (descriptor.domain) {
|
||||||
Domain.KEY_ID ->
|
Domain.KEY_ID ->
|
||||||
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||||
callingUid, descriptor.nspace
|
callingUid,
|
||||||
|
descriptor.nspace,
|
||||||
)
|
)
|
||||||
Domain.APP ->
|
Domain.APP ->
|
||||||
descriptor.alias?.let {
|
descriptor.alias?.let {
|
||||||
@@ -527,11 +476,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (generatedKeyInfo == null) {
|
if (generatedKeyInfo == null) {
|
||||||
descriptor.alias?.let {
|
// Hardware key: mark so getKeyEntry skips cert re-patching.
|
||||||
val kid = KeyIdentifier(callingUid, it)
|
descriptor.alias?.let { userUpdatedKeys.add(KeyIdentifier(callingUid, it)) }
|
||||||
userUpdatedKeys.add(kid)
|
|
||||||
SystemLogger.trace { "[TRACE] updateSubcomponent $kid: not generated key, added to userUpdatedKeys" }
|
|
||||||
}
|
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,13 +488,73 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
|
|
||||||
metadata.certificate = publicCert
|
metadata.certificate = publicCert
|
||||||
metadata.certificateChain = certificateChain
|
metadata.certificateChain = certificateChain
|
||||||
|
|
||||||
GeneratedKeyPersistence.rePersistIfNeeded(callingUid, generatedKeyInfo)
|
|
||||||
|
|
||||||
SystemLogger.verbose(
|
SystemLogger.verbose(
|
||||||
"Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]"
|
"Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]"
|
||||||
)
|
)
|
||||||
|
|
||||||
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intercepts the reply from getSecurityLevel to dynamically register our interceptor
|
||||||
|
* for the returned IKeystoreSecurityLevel binder.
|
||||||
|
*
|
||||||
|
* keystore2 may create a new BBinder for each getSecurityLevel call, so the binder
|
||||||
|
* registered during initial setup (in setupSecurityLevelInterceptors) might not be the
|
||||||
|
* same one that client apps receive. By intercepting every getSecurityLevel reply, we
|
||||||
|
* ensure that all SecurityLevel binders are covered.
|
||||||
|
*/
|
||||||
|
private fun handlePostGetSecurityLevel(
|
||||||
|
txId: Long,
|
||||||
|
data: Parcel,
|
||||||
|
reply: Parcel,
|
||||||
|
): TransactionResult {
|
||||||
|
val backdoor = backdoorBinder
|
||||||
|
if (backdoor == null) {
|
||||||
|
SystemLogger.warning("[TX_ID: $txId] post-getSecurityLevel: backdoor not available")
|
||||||
|
return TransactionResult.SkipTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
return runCatching {
|
||||||
|
// Read the security level argument from the original request.
|
||||||
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
val requestedLevel = data.readInt()
|
||||||
|
|
||||||
|
// hasException already consumed the exception header from the reply.
|
||||||
|
// Next item is the IKeystoreSecurityLevel binder.
|
||||||
|
val secLevelBinder = reply.readStrongBinder()
|
||||||
|
if (secLevelBinder == null) {
|
||||||
|
SystemLogger.verbose(
|
||||||
|
"[TX_ID: $txId] getSecurityLevel($requestedLevel) returned null binder"
|
||||||
|
)
|
||||||
|
return@runCatching TransactionResult.SkipTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only intercept TEE and StrongBox security levels.
|
||||||
|
if (requestedLevel != SecurityLevel.TRUSTED_ENVIRONMENT &&
|
||||||
|
requestedLevel != SecurityLevel.STRONGBOX
|
||||||
|
) {
|
||||||
|
return@runCatching TransactionResult.SkipTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create the interceptor for this security level. The interceptor may not
|
||||||
|
// exist yet if the initial setupSecurityLevelInterceptors call failed for this level.
|
||||||
|
val interceptor = securityLevelInterceptors.getOrPut(requestedLevel) {
|
||||||
|
val secLevelInterface =
|
||||||
|
IKeystoreSecurityLevel.Stub.asInterface(secLevelBinder)
|
||||||
|
SystemLogger.info(
|
||||||
|
"[TX_ID: $txId] Late-creating interceptor for security level $requestedLevel"
|
||||||
|
)
|
||||||
|
KeyMintSecurityLevelInterceptor(secLevelInterface, requestedLevel).also {
|
||||||
|
it.loadPersistedKeys()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerSecurityLevelBinder(backdoor, secLevelBinder, interceptor)
|
||||||
|
TransactionResult.SkipTransaction
|
||||||
|
}.getOrElse {
|
||||||
|
SystemLogger.error("[TX_ID: $txId] Failed to process post-getSecurityLevel.", it)
|
||||||
|
TransactionResult.SkipTransaction
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-6
@@ -399,17 +399,18 @@ private data class LegacyKeygenParameters(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the legacy parameters into the modern [KeyMintAttestation] data structure, which is
|
* Converts the legacy parameters into the modern [KeyMintAttestation] data structure, which is
|
||||||
* required by the refactored [AttestationBuilder] and [CertificateGenerator].
|
* required by [AttestationBuilder] and [CertificateGenerator].
|
||||||
*/
|
*/
|
||||||
fun toKeyMintAttestation(): KeyMintAttestation {
|
fun toKeyMintAttestation(): KeyMintAttestation {
|
||||||
// This conversion acts as a bridge, allowing our new generic components
|
// This conversion acts as a bridge, allowing our new generic components
|
||||||
// to be used by the legacy interceptor.
|
// to be used by the legacy interceptor.
|
||||||
return KeyMintAttestation(
|
return KeyMintAttestation(
|
||||||
keySize = this.keySize,
|
|
||||||
algorithm = this.algorithm,
|
algorithm = this.algorithm,
|
||||||
ecCurve = 0,
|
ecCurve = 0, // Not explicitly available in legacy args, but not critical
|
||||||
ecCurveName = this.ecCurveName ?: "",
|
ecCurveName = this.ecCurveName ?: "",
|
||||||
origin = null,
|
keySize = this.keySize,
|
||||||
|
origin = null, // Not needed to build attestaion
|
||||||
|
noAuthRequired = null,
|
||||||
blockMode = listOf<Int>(),
|
blockMode = listOf<Int>(),
|
||||||
padding = listOf<Int>(),
|
padding = listOf<Int>(),
|
||||||
purpose = this.purpose,
|
purpose = this.purpose,
|
||||||
@@ -436,7 +437,6 @@ private data class LegacyKeygenParameters(
|
|||||||
usageExpireDateTime = null,
|
usageExpireDateTime = null,
|
||||||
usageCountLimit = null,
|
usageCountLimit = null,
|
||||||
callerNonce = null,
|
callerNonce = null,
|
||||||
nonce = null,
|
|
||||||
unlockedDeviceRequired = null,
|
unlockedDeviceRequired = null,
|
||||||
includeUniqueId = null,
|
includeUniqueId = null,
|
||||||
rollbackResistance = null,
|
rollbackResistance = null,
|
||||||
@@ -444,7 +444,6 @@ private data class LegacyKeygenParameters(
|
|||||||
allowWhileOnBody = null,
|
allowWhileOnBody = null,
|
||||||
trustedUserPresenceRequired = null,
|
trustedUserPresenceRequired = null,
|
||||||
trustedConfirmationRequired = null,
|
trustedConfirmationRequired = null,
|
||||||
noAuthRequired = null,
|
|
||||||
maxUsesPerBoot = null,
|
maxUsesPerBoot = null,
|
||||||
maxBootLevel = null,
|
maxBootLevel = null,
|
||||||
minMacLength = null,
|
minMacLength = null,
|
||||||
|
|||||||
+14
-20
@@ -5,7 +5,6 @@ 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 java.util.TreeMap
|
import java.util.TreeMap
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
|
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
|
||||||
@@ -22,15 +21,6 @@ object ListEntriesHandler {
|
|||||||
// Estimate for maximum size of a Binder response in bytes.
|
// Estimate for maximum size of a Binder response in bytes.
|
||||||
private const val RESPONSE_SIZE_LIMIT = 358400
|
private const val RESPONSE_SIZE_LIMIT = 358400
|
||||||
|
|
||||||
// Parameters of AOSP function `list_key_entries` in utils.rs.
|
|
||||||
private data class ListEntriesParams(
|
|
||||||
val domain: Int,
|
|
||||||
val namespace: Long,
|
|
||||||
val startPastAlias: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val pendingParams = ConcurrentHashMap<Long, ListEntriesParams>()
|
|
||||||
|
|
||||||
// Based on AOSP function `estimate_safe_amount_to_return` in utils.rs.
|
// Based on AOSP function `estimate_safe_amount_to_return` in utils.rs.
|
||||||
private fun estimateSafeAmountToReturn(
|
private fun estimateSafeAmountToReturn(
|
||||||
keyDescriptors: Array<KeyDescriptor>,
|
keyDescriptors: Array<KeyDescriptor>,
|
||||||
@@ -60,7 +50,7 @@ object ListEntriesHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse and store parameters for later use (in post-transaction).
|
// Parse and store parameters for later use (in post-transaction).
|
||||||
fun cacheParameters(txId: Long, data: Parcel, isBatchMode: Boolean): Boolean {
|
fun cacheParameters(txId: Long, data: Parcel, isBatchMode: Boolean): ListEntriesParams? {
|
||||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
|
||||||
val domain = data.readInt()
|
val domain = data.readInt()
|
||||||
@@ -71,20 +61,21 @@ object ListEntriesHandler {
|
|||||||
// See AOSP function `get_key_descriptor_for_lookup` in service.rs.
|
// See AOSP function `get_key_descriptor_for_lookup` in service.rs.
|
||||||
// Note that all generated keys belong to Domain::APP.
|
// Note that all generated keys belong to Domain::APP.
|
||||||
if (domain == Domain.APP) {
|
if (domain == Domain.APP) {
|
||||||
pendingParams[txId] = ListEntriesParams(domain, namespace, startPastAlias)
|
val params = ListEntriesParams(domain, namespace, startPastAlias)
|
||||||
SystemLogger.debug("[TX_ID: $txId] Cached ${pendingParams[txId]}.")
|
SystemLogger.debug("[TX_ID: $txId] Cached $params.")
|
||||||
return true
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge software-backed keys with hardware-backed keys in the reply parcel.
|
// Merge software-backed keys with hardware-backed keys in the reply parcel.
|
||||||
fun injectGeneratedKeys(txId: Long, callingUid: Int, reply: Parcel): Array<KeyDescriptor> {
|
fun injectGeneratedKeys(
|
||||||
val params =
|
txId: Long,
|
||||||
pendingParams.remove(txId)
|
callingUid: Int,
|
||||||
?: throw IllegalStateException("No params found for listing entries")
|
params: ListEntriesParams,
|
||||||
|
reply: Parcel,
|
||||||
|
): Array<KeyDescriptor> {
|
||||||
// By default we use the calling uid as namespace if domain is Domain::APP.
|
// By default we use the calling uid as namespace if domain is Domain::APP.
|
||||||
// The namespace parameter is thus ignored for non-privileged applications.
|
// The namespace parameter is thus ignored for non-privileged applications.
|
||||||
// See AOSP function `get_key_descriptor_for_lookup` in service.rs.
|
// See AOSP function `get_key_descriptor_for_lookup` in service.rs.
|
||||||
@@ -140,3 +131,6 @@ object ListEntriesHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parameters of AOSP function `list_key_entries` in utils.rs.
|
||||||
|
data class ListEntriesParams(val domain: Int, val namespace: Long, val startPastAlias: String?)
|
||||||
|
|||||||
-74
@@ -1,74 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+7
-238
@@ -16,7 +16,7 @@ import java.util.concurrent.locks.ReentrantLock
|
|||||||
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
|
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
|
||||||
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
|
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
import org.matrix.TEESimulator.pki.CertificateHelper
|
|
||||||
|
|
||||||
data class PersistedKeyData(
|
data class PersistedKeyData(
|
||||||
val uid: Int,
|
val uid: Int,
|
||||||
@@ -29,47 +29,13 @@ data class PersistedKeyData(
|
|||||||
val ecCurve: Int,
|
val ecCurve: Int,
|
||||||
val purposes: List<Int>,
|
val purposes: List<Int>,
|
||||||
val digests: List<Int>,
|
val digests: List<Int>,
|
||||||
/** PKCS#8-encoded private key for asymmetric records, empty for symmetric. */
|
|
||||||
val privateKeyBytes: ByteArray,
|
val privateKeyBytes: ByteArray,
|
||||||
val certChainBytes: List<ByteArray>,
|
val certChainBytes: List<ByteArray>,
|
||||||
/**
|
|
||||||
* Byte-identical KeyMetadata parcel snapshot. Restoring authorizations
|
|
||||||
* directly from these bytes preserves tag count, order, and exact
|
|
||||||
* security-level annotations across reboots — the kind of structural
|
|
||||||
* details apps fingerprint to decide whether the alias is still
|
|
||||||
* "the same key".
|
|
||||||
*/
|
|
||||||
val metadataBytes: ByteArray,
|
|
||||||
/**
|
|
||||||
* Raw secret material for symmetric records (AES, HMAC, 3DES). Empty
|
|
||||||
* for asymmetric. Critical for AndroidX security crypto MasterKey
|
|
||||||
* (AES-GCM-256) — without this every reboot regenerates a fresh AES
|
|
||||||
* key and EncryptedSharedPreferences becomes undecryptable, which is
|
|
||||||
* what banking apps interpret as session expiry and force a relogin.
|
|
||||||
*/
|
|
||||||
val symmetricKeyBytes: ByteArray,
|
|
||||||
val symmetricAlgorithm: String,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
object GeneratedKeyPersistence {
|
object GeneratedKeyPersistence {
|
||||||
|
|
||||||
/**
|
private const val FORMAT_VERSION = 1
|
||||||
* Single source of truth for the on-disk format. Bump this every time
|
|
||||||
* the layout changes; older numbers are silently skipped on read so
|
|
||||||
* stale dev artifacts and pre-fix upstream files can't be partially
|
|
||||||
* rehydrated into broken in-memory state.
|
|
||||||
*
|
|
||||||
* History:
|
|
||||||
* 1 — original upstream layout (no metadata snapshot, no symmetric
|
|
||||||
* block; restored keys lose authorization tags and AES master
|
|
||||||
* keys altogether — apps relying on persisted keystore state
|
|
||||||
* across reboots get logged out)
|
|
||||||
* 2 — transitional dev-only format that added metadata but still
|
|
||||||
* missed the symmetric block; never shipped
|
|
||||||
* 3 — current: byte-identical KeyMetadata snapshot + raw symmetric
|
|
||||||
* key material so AES/HMAC keys survive reboots
|
|
||||||
*/
|
|
||||||
private const val FORMAT_VERSION = 3
|
|
||||||
private val PERSISTENCE_DIR = File(CONFIG_PATH, "persistent_keys")
|
private val PERSISTENCE_DIR = File(CONFIG_PATH, "persistent_keys")
|
||||||
|
|
||||||
// Per-filename locks to prevent concurrent writes to the same key file
|
// Per-filename locks to prevent concurrent writes to the same key file
|
||||||
@@ -81,8 +47,7 @@ object GeneratedKeyPersistence {
|
|||||||
|
|
||||||
fun save(
|
fun save(
|
||||||
keyId: KeyIdentifier,
|
keyId: KeyIdentifier,
|
||||||
keyPair: KeyPair?,
|
keyPair: KeyPair,
|
||||||
secretKey: javax.crypto.SecretKey?,
|
|
||||||
nspace: Long,
|
nspace: Long,
|
||||||
securityLevel: Int,
|
securityLevel: Int,
|
||||||
certChain: List<Certificate>,
|
certChain: List<Certificate>,
|
||||||
@@ -92,11 +57,7 @@ object GeneratedKeyPersistence {
|
|||||||
purposes: List<Int>,
|
purposes: List<Int>,
|
||||||
digests: List<Int>,
|
digests: List<Int>,
|
||||||
isAttestationKey: Boolean,
|
isAttestationKey: Boolean,
|
||||||
metadataBytes: ByteArray? = null,
|
|
||||||
) {
|
) {
|
||||||
require(keyPair != null || secretKey != null) {
|
|
||||||
"Either keyPair or secretKey must be provided"
|
|
||||||
}
|
|
||||||
val filename = keyFileName(keyId.uid, keyId.alias)
|
val filename = keyFileName(keyId.uid, keyId.alias)
|
||||||
val lock = getLockForKey(filename)
|
val lock = getLockForKey(filename)
|
||||||
SystemLogger.debug("[Persistence] Acquiring lock for $filename")
|
SystemLogger.debug("[Persistence] Acquiring lock for $filename")
|
||||||
@@ -126,8 +87,7 @@ object GeneratedKeyPersistence {
|
|||||||
out.writeInt(digests.size)
|
out.writeInt(digests.size)
|
||||||
digests.forEach { out.writeInt(it) }
|
digests.forEach { out.writeInt(it) }
|
||||||
|
|
||||||
// Asymmetric key block (empty for symmetric-only).
|
val pkBytes = keyPair.private.encoded
|
||||||
val pkBytes = keyPair?.private?.encoded ?: ByteArray(0)
|
|
||||||
out.writeInt(pkBytes.size)
|
out.writeInt(pkBytes.size)
|
||||||
out.write(pkBytes)
|
out.write(pkBytes)
|
||||||
|
|
||||||
@@ -137,23 +97,6 @@ object GeneratedKeyPersistence {
|
|||||||
out.writeInt(encoded.size)
|
out.writeInt(encoded.size)
|
||||||
out.write(encoded)
|
out.write(encoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata snapshot (always present, may be empty
|
|
||||||
// if the live KeyMetadata could not be marshalled).
|
|
||||||
val mdBytes = metadataBytes ?: ByteArray(0)
|
|
||||||
out.writeInt(mdBytes.size)
|
|
||||||
if (mdBytes.isNotEmpty()) out.write(mdBytes)
|
|
||||||
|
|
||||||
// Symmetric key block (empty for asymmetric keys).
|
|
||||||
if (secretKey != null) {
|
|
||||||
val skBytes = secretKey.encoded
|
|
||||||
out.writeUTF(secretKey.algorithm)
|
|
||||||
out.writeInt(skBytes.size)
|
|
||||||
out.write(skBytes)
|
|
||||||
} else {
|
|
||||||
out.writeUTF("")
|
|
||||||
out.writeInt(0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
tmpFile.delete()
|
tmpFile.delete()
|
||||||
@@ -246,17 +189,8 @@ object GeneratedKeyPersistence {
|
|||||||
DataInputStream(BufferedInputStream(FileInputStream(file))).use { input ->
|
DataInputStream(BufferedInputStream(FileInputStream(file))).use { input ->
|
||||||
val version = input.readInt()
|
val version = input.readInt()
|
||||||
if (version != FORMAT_VERSION) {
|
if (version != FORMAT_VERSION) {
|
||||||
// Old upstream files (v1) and dev-only intermediate
|
SystemLogger.warning(
|
||||||
// files (v2) are missing the metadata snapshot
|
"Skipping ${file.name}: unknown format version $version"
|
||||||
// and/or symmetric key block — restoring them
|
|
||||||
// would put broken state in memory (apps relying
|
|
||||||
// on those records get logged out). Skip and let
|
|
||||||
// the next generateKey re-create cleanly with the
|
|
||||||
// new format. Affected apps re-login once after
|
|
||||||
// upgrade, then never again.
|
|
||||||
SystemLogger.info(
|
|
||||||
"Skipping ${file.name}: legacy format version $version. " +
|
|
||||||
"It will be replaced on next generateKey for this alias."
|
|
||||||
)
|
)
|
||||||
return@runCatching
|
return@runCatching
|
||||||
}
|
}
|
||||||
@@ -278,7 +212,7 @@ object GeneratedKeyPersistence {
|
|||||||
|
|
||||||
val pkLen = requireBounds(input.readInt(), 8192, "pkLen")
|
val pkLen = requireBounds(input.readInt(), 8192, "pkLen")
|
||||||
val pkBytes = ByteArray(pkLen)
|
val pkBytes = ByteArray(pkLen)
|
||||||
if (pkLen > 0) input.readFully(pkBytes)
|
input.readFully(pkBytes)
|
||||||
|
|
||||||
val certCount = requireBounds(input.readInt(), 10, "certCount")
|
val certCount = requireBounds(input.readInt(), 10, "certCount")
|
||||||
val certChainBytes = (0 until certCount).map {
|
val certChainBytes = (0 until certCount).map {
|
||||||
@@ -288,17 +222,6 @@ object GeneratedKeyPersistence {
|
|||||||
certBytes
|
certBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
val metaLen = requireBounds(input.readInt(), 256 * 1024, "metaLen")
|
|
||||||
val metadataBytes = ByteArray(metaLen).also {
|
|
||||||
if (metaLen > 0) input.readFully(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val skAlgo = input.readUTF()
|
|
||||||
val skLen = requireBounds(input.readInt(), 8192, "skLen")
|
|
||||||
val skBytes = ByteArray(skLen).also {
|
|
||||||
if (skLen > 0) input.readFully(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (storedSecLevel == securityLevel) {
|
if (storedSecLevel == securityLevel) {
|
||||||
result.add(
|
result.add(
|
||||||
PersistedKeyData(
|
PersistedKeyData(
|
||||||
@@ -314,9 +237,6 @@ object GeneratedKeyPersistence {
|
|||||||
digests = digests,
|
digests = digests,
|
||||||
privateKeyBytes = pkBytes,
|
privateKeyBytes = pkBytes,
|
||||||
certChainBytes = certChainBytes,
|
certChainBytes = certChainBytes,
|
||||||
metadataBytes = metadataBytes,
|
|
||||||
symmetricKeyBytes = skBytes,
|
|
||||||
symmetricAlgorithm = skAlgo,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -330,97 +250,6 @@ object GeneratedKeyPersistence {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-persist updates the cert chain for an already-persisted key without
|
|
||||||
// reconstructing authorization parameters from the response. This avoids
|
|
||||||
// pulling keymint Tag dependencies into this file and is correct because
|
|
||||||
// the only field that changes post-generation is the patched cert chain.
|
|
||||||
fun rePersistIfNeeded(
|
|
||||||
callingUid: Int,
|
|
||||||
generatedKeyInfo: KeyMintSecurityLevelInterceptor.GeneratedKeyInfo,
|
|
||||||
) {
|
|
||||||
val metadata = generatedKeyInfo.response.metadata
|
|
||||||
if (metadata == null) {
|
|
||||||
SystemLogger.debug("rePersist: no metadata, skipping")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val secLevel = metadata.keySecurityLevel
|
|
||||||
|
|
||||||
val entry = KeyMintSecurityLevelInterceptor.generatedKeys.entries.find { (id, info) ->
|
|
||||||
id.uid == callingUid && info.nspace == generatedKeyInfo.nspace
|
|
||||||
}
|
|
||||||
if (entry == null) {
|
|
||||||
SystemLogger.debug("rePersist: key not found in map for uid=$callingUid nspace=${generatedKeyInfo.nspace}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val keyId = entry.key
|
|
||||||
val filename = keyFileName(keyId.uid, keyId.alias)
|
|
||||||
val existing = File(PERSISTENCE_DIR, filename)
|
|
||||||
|
|
||||||
if (!existing.exists()) {
|
|
||||||
SystemLogger.debug("rePersist: no existing file for $keyId, skipping")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val newChain = CertificateHelper.getCertificateChain(metadata)
|
|
||||||
if (newChain == null) {
|
|
||||||
SystemLogger.warning("rePersist: could not extract cert chain for $keyId")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val persisted = runCatching {
|
|
||||||
DataInputStream(BufferedInputStream(FileInputStream(existing))).use { input ->
|
|
||||||
val version = input.readInt()
|
|
||||||
if (version != FORMAT_VERSION) {
|
|
||||||
SystemLogger.warning("rePersist: legacy format version $version for $keyId, will not re-persist (next generateKey replaces it)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
readPersistedKeyData(input)
|
|
||||||
}
|
|
||||||
}.getOrNull()
|
|
||||||
if (persisted == null) {
|
|
||||||
SystemLogger.warning("rePersist: failed to read existing data for $keyId")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val keyPair = generatedKeyInfo.keyPair
|
|
||||||
val secretKey = generatedKeyInfo.secretKey
|
|
||||||
if (keyPair == null && secretKey == null) {
|
|
||||||
SystemLogger.warning("rePersist: no key material for $keyId")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Serialize the live KeyMetadata (now contains the user-installed cert
|
|
||||||
// chain via updateSubcomponent) so the next boot restores byte-identical
|
|
||||||
// metadata. KeyMetadata is binder-free, so marshall() is safe here.
|
|
||||||
val metadataBytes = runCatching {
|
|
||||||
android.os.Parcel.obtain().let { parcel ->
|
|
||||||
try {
|
|
||||||
metadata.writeToParcel(parcel, 0)
|
|
||||||
parcel.marshall()
|
|
||||||
} finally {
|
|
||||||
parcel.recycle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.getOrNull()
|
|
||||||
save(
|
|
||||||
keyId = keyId,
|
|
||||||
keyPair = keyPair,
|
|
||||||
secretKey = secretKey,
|
|
||||||
nspace = generatedKeyInfo.nspace,
|
|
||||||
securityLevel = secLevel,
|
|
||||||
certChain = newChain.toList(),
|
|
||||||
algorithm = persisted.algorithm,
|
|
||||||
keySize = persisted.keySize,
|
|
||||||
ecCurve = persisted.ecCurve,
|
|
||||||
purposes = persisted.purposes,
|
|
||||||
digests = persisted.digests,
|
|
||||||
isAttestationKey = persisted.isAttestationKey,
|
|
||||||
metadataBytes = metadataBytes,
|
|
||||||
)
|
|
||||||
SystemLogger.debug("Re-persisted key $keyId with updated cert chain")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Corrupted binary files can have arbitrary length fields — cap allocations
|
|
||||||
private fun requireBounds(value: Int, max: Int, name: String): Int {
|
private fun requireBounds(value: Int, max: Int, name: String): Int {
|
||||||
require(value in 0..max) { "$name out of bounds: $value (max $max)" }
|
require(value in 0..max) { "$name out of bounds: $value (max $max)" }
|
||||||
return value
|
return value
|
||||||
@@ -431,64 +260,4 @@ object GeneratedKeyPersistence {
|
|||||||
.digest("$uid:$alias".toByteArray(Charsets.UTF_8))
|
.digest("$uid:$alias".toByteArray(Charsets.UTF_8))
|
||||||
return digest.joinToString("") { "%02x".format(it) } + ".bin"
|
return digest.joinToString("") { "%02x".format(it) } + ".bin"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads all fields after the version int has already been consumed
|
|
||||||
// and validated by the caller.
|
|
||||||
private fun readPersistedKeyData(input: DataInputStream): PersistedKeyData {
|
|
||||||
val secLevel = input.readInt()
|
|
||||||
val uid = input.readInt()
|
|
||||||
val alias = input.readUTF()
|
|
||||||
val nspace = input.readLong()
|
|
||||||
val isAttestKey = input.readBoolean()
|
|
||||||
val algo = input.readInt()
|
|
||||||
val kSize = input.readInt()
|
|
||||||
val curve = input.readInt()
|
|
||||||
|
|
||||||
val purposeCount = requireBounds(input.readInt(), 64, "purposeCount")
|
|
||||||
val purposes = (0 until purposeCount).map { input.readInt() }
|
|
||||||
|
|
||||||
val digestCount = requireBounds(input.readInt(), 64, "digestCount")
|
|
||||||
val digests = (0 until digestCount).map { input.readInt() }
|
|
||||||
|
|
||||||
val pkLen = requireBounds(input.readInt(), 8192, "pkLen")
|
|
||||||
val pkBytes = ByteArray(pkLen)
|
|
||||||
if (pkLen > 0) input.readFully(pkBytes)
|
|
||||||
|
|
||||||
val certCount = requireBounds(input.readInt(), 10, "certCount")
|
|
||||||
val certChainBytes = (0 until certCount).map {
|
|
||||||
val certLen = requireBounds(input.readInt(), 65536, "certLen")
|
|
||||||
val certBytes = ByteArray(certLen)
|
|
||||||
input.readFully(certBytes)
|
|
||||||
certBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
val metaLen = requireBounds(input.readInt(), 256 * 1024, "metaLen")
|
|
||||||
val metadataBytes = ByteArray(metaLen).also {
|
|
||||||
if (metaLen > 0) input.readFully(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val skAlgo = input.readUTF()
|
|
||||||
val skLen = requireBounds(input.readInt(), 8192, "skLen")
|
|
||||||
val skBytes = ByteArray(skLen).also {
|
|
||||||
if (skLen > 0) input.readFully(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
return PersistedKeyData(
|
|
||||||
uid = uid,
|
|
||||||
alias = alias,
|
|
||||||
nspace = nspace,
|
|
||||||
securityLevel = secLevel,
|
|
||||||
isAttestationKey = isAttestKey,
|
|
||||||
algorithm = algo,
|
|
||||||
keySize = kSize,
|
|
||||||
ecCurve = curve,
|
|
||||||
purposes = purposes,
|
|
||||||
digests = digests,
|
|
||||||
privateKeyBytes = pkBytes,
|
|
||||||
certChainBytes = certChainBytes,
|
|
||||||
metadataBytes = metadataBytes,
|
|
||||||
symmetricKeyBytes = skBytes,
|
|
||||||
symmetricAlgorithm = skAlgo,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+591
-844
File diff suppressed because it is too large
Load Diff
+2
-7
@@ -13,7 +13,6 @@ import org.matrix.TEESimulator.interception.keystore.InterceptorUtils
|
|||||||
class OperationInterceptor(
|
class OperationInterceptor(
|
||||||
private val original: IKeystoreOperation,
|
private val original: IKeystoreOperation,
|
||||||
private val backdoor: IBinder,
|
private val backdoor: IBinder,
|
||||||
private val isAead: Boolean,
|
|
||||||
) : BinderInterceptor() {
|
) : BinderInterceptor() {
|
||||||
|
|
||||||
override fun onPreTransact(
|
override fun onPreTransact(
|
||||||
@@ -28,10 +27,6 @@ class OperationInterceptor(
|
|||||||
val methodName = transactionNames[code] ?: "unknown code=$code"
|
val methodName = transactionNames[code] ?: "unknown code=$code"
|
||||||
logTransaction(txId, methodName, callingUid, callingPid, true)
|
logTransaction(txId, methodName, callingUid, callingPid, true)
|
||||||
|
|
||||||
if (code == UPDATE_AAD_TRANSACTION && !isAead) {
|
|
||||||
return InterceptorUtils.createServiceSpecificErrorReply(KeystoreErrorCodes.invalidTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code == FINISH_TRANSACTION || code == ABORT_TRANSACTION) {
|
if (code == FINISH_TRANSACTION || code == ABORT_TRANSACTION) {
|
||||||
KeyMintSecurityLevelInterceptor.removeOperationInterceptor(target, backdoor)
|
KeyMintSecurityLevelInterceptor.removeOperationInterceptor(target, backdoor)
|
||||||
}
|
}
|
||||||
@@ -49,8 +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 =
|
/** Only intercept finish/abort for cleanup. Other ops pass through without round-trip. */
|
||||||
intArrayOf(UPDATE_AAD_TRANSACTION, FINISH_TRANSACTION, ABORT_TRANSACTION)
|
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
|
||||||
|
|||||||
+177
-254
@@ -8,8 +8,8 @@ import android.hardware.security.keymint.KeyParameterValue
|
|||||||
import android.hardware.security.keymint.KeyPurpose
|
import android.hardware.security.keymint.KeyPurpose
|
||||||
import android.hardware.security.keymint.PaddingMode
|
import android.hardware.security.keymint.PaddingMode
|
||||||
import android.hardware.security.keymint.Tag
|
import android.hardware.security.keymint.Tag
|
||||||
|
import android.os.RemoteException
|
||||||
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 android.system.keystore2.KeyParameters
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
@@ -20,16 +20,48 @@ 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
|
||||||
|
|
||||||
|
/** Keystore2 error codes for ServiceSpecificException. Negative = KeyMint, positive = Keystore. */
|
||||||
|
internal object KeystoreErrorCode {
|
||||||
|
const val INVALID_OPERATION_HANDLE = -28
|
||||||
|
const val VERIFICATION_FAILED = -30
|
||||||
|
const val UNSUPPORTED_PURPOSE = -2
|
||||||
|
const val INCOMPATIBLE_PURPOSE = -3
|
||||||
|
const val SYSTEM_ERROR = 4
|
||||||
|
const val TOO_MUCH_DATA = 21
|
||||||
|
const val KEY_EXPIRED = -25
|
||||||
|
const val KEY_NOT_YET_VALID = -24
|
||||||
|
|
||||||
|
/** KeyMint ErrorCode::CALLER_NONCE_PROHIBITED */
|
||||||
|
const val CALLER_NONCE_PROHIBITED = -55
|
||||||
|
|
||||||
|
/** KeyMint ErrorCode::INVALID_ARGUMENT */
|
||||||
|
const val INVALID_ARGUMENT = -38
|
||||||
|
|
||||||
|
/** KeyMint ErrorCode::INVALID_TAG */
|
||||||
|
const val INVALID_TAG = -40
|
||||||
|
|
||||||
|
/** Keystore2 ResponseCode::PERMISSION_DENIED */
|
||||||
|
const val PERMISSION_DENIED = 6
|
||||||
|
|
||||||
|
/** Keystore2 ResponseCode::KEY_NOT_FOUND */
|
||||||
|
const val KEY_NOT_FOUND = 7
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(data: 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()
|
||||||
|
|
||||||
|
/** Returns parameters from the begin phase (e.g. GCM nonce), or null if none. */
|
||||||
fun getBeginParameters(): Array<KeyParameter>? = null
|
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 =
|
||||||
@@ -39,18 +71,17 @@ private object JcaAlgorithmMapper {
|
|||||||
Digest.SHA_2_512 -> "SHA512"
|
Digest.SHA_2_512 -> "SHA512"
|
||||||
else -> "NONE"
|
else -> "NONE"
|
||||||
}
|
}
|
||||||
return when (params.algorithm) {
|
val keyAlgo =
|
||||||
Algorithm.EC -> "${digest}withECDSA"
|
when (params.algorithm) {
|
||||||
Algorithm.RSA -> {
|
Algorithm.EC -> "ECDSA"
|
||||||
val isPss = params.padding.firstOrNull() == PaddingMode.RSA_PSS
|
Algorithm.RSA -> "RSA"
|
||||||
if (isPss) "${digest}withRSA/PSS" else "${digest}withRSA"
|
else ->
|
||||||
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCode.SYSTEM_ERROR,
|
||||||
|
"Unsupported signature algorithm: ${params.algorithm}",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
else ->
|
return "${digest}with${keyAlgo}"
|
||||||
throw ServiceSpecificException(
|
|
||||||
KeystoreErrorCodes.incompatibleAlgorithm,
|
|
||||||
"Unsupported signature algorithm: ${params.algorithm}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
|
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
|
||||||
@@ -60,7 +91,7 @@ private object JcaAlgorithmMapper {
|
|||||||
Algorithm.AES -> "AES"
|
Algorithm.AES -> "AES"
|
||||||
else ->
|
else ->
|
||||||
throw ServiceSpecificException(
|
throw ServiceSpecificException(
|
||||||
KeystoreErrorCodes.incompatibleAlgorithm,
|
KeystoreErrorCode.SYSTEM_ERROR,
|
||||||
"Unsupported cipher algorithm: ${params.algorithm}",
|
"Unsupported cipher algorithm: ${params.algorithm}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -68,29 +99,32 @@ private object JcaAlgorithmMapper {
|
|||||||
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"
|
else -> "ECB" // Default for RSA
|
||||||
}
|
}
|
||||||
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"
|
else -> "NoPadding" // Default for GCM
|
||||||
}
|
}
|
||||||
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 {
|
||||||
initSign(keyPair.private)
|
initSign(keyPair.private)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateAad(data: ByteArray?) {
|
||||||
|
throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG)
|
||||||
|
}
|
||||||
|
|
||||||
override fun update(data: ByteArray?): ByteArray? {
|
override fun update(data: ByteArray?): ByteArray? {
|
||||||
if (data != null) signature.update(data)
|
if (data != null) signature.update(data)
|
||||||
return null
|
return null
|
||||||
@@ -104,12 +138,17 @@ 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 {
|
||||||
initVerify(keyPair.public)
|
initVerify(keyPair.public)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateAad(data: ByteArray?) {
|
||||||
|
throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG)
|
||||||
|
}
|
||||||
|
|
||||||
override fun update(data: ByteArray?): ByteArray? {
|
override fun update(data: ByteArray?): ByteArray? {
|
||||||
if (data != null) signature.update(data)
|
if (data != null) signature.update(data)
|
||||||
return null
|
return null
|
||||||
@@ -117,11 +156,16 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri
|
|||||||
|
|
||||||
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
if (data != null) update(data)
|
if (data != null) update(data)
|
||||||
if (signature == null) {
|
if (signature == null)
|
||||||
throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature to verify is null")
|
throw ServiceSpecificException(
|
||||||
}
|
KeystoreErrorCode.VERIFICATION_FAILED,
|
||||||
|
"Signature to verify is null",
|
||||||
|
)
|
||||||
if (!this.signature.verify(signature)) {
|
if (!this.signature.verify(signature)) {
|
||||||
throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature verification failed")
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCode.VERIFICATION_FAILED,
|
||||||
|
"Signature/MAC verification failed",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -129,27 +173,19 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri
|
|||||||
override fun abort() {}
|
override fun abort() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Concrete implementation for Encryption/Decryption.
|
||||||
private class CipherPrimitive(
|
private class CipherPrimitive(
|
||||||
cryptoKey: java.security.Key,
|
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 nonce = params.nonce
|
init(opMode, cryptoKey)
|
||||||
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?) {
|
override fun updateAad(data: ByteArray?) {
|
||||||
if (!isAead) throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
|
if (data != null) cipher.updateAAD(data)
|
||||||
if (aadInput != null) cipher.updateAAD(aadInput)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun update(data: ByteArray?): ByteArray? =
|
override fun update(data: ByteArray?): ByteArray? =
|
||||||
@@ -158,6 +194,9 @@ private class CipherPrimitive(
|
|||||||
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 abort() {}
|
||||||
|
|
||||||
|
/** Returns the cipher IV as a NONCE parameter for GCM operations. */
|
||||||
override fun getBeginParameters(): Array<KeyParameter>? {
|
override fun getBeginParameters(): Array<KeyParameter>? {
|
||||||
val iv = cipher.iv ?: return null
|
val iv = cipher.iv ?: return null
|
||||||
return arrayOf(
|
return arrayOf(
|
||||||
@@ -167,20 +206,23 @@ private class CipherPrimitive(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun abort() {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Concrete implementation for ECDH Key Agreement.
|
||||||
private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive {
|
private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive {
|
||||||
private val agreement: javax.crypto.KeyAgreement =
|
private val agreement: javax.crypto.KeyAgreement =
|
||||||
javax.crypto.KeyAgreement.getInstance("ECDH").apply { init(keyPair.private) }
|
javax.crypto.KeyAgreement.getInstance("ECDH").apply { init(keyPair.private) }
|
||||||
|
|
||||||
|
override fun updateAad(data: ByteArray?) {
|
||||||
|
throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG)
|
||||||
|
}
|
||||||
|
|
||||||
override fun update(data: ByteArray?): ByteArray? = null
|
override fun update(data: ByteArray?): ByteArray? = null
|
||||||
|
|
||||||
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
if (data == null)
|
if (data == null)
|
||||||
throw ServiceSpecificException(
|
throw ServiceSpecificException(
|
||||||
KeystoreErrorCodes.invalidArgument,
|
KeystoreErrorCode.INVALID_ARGUMENT,
|
||||||
"Peer public key required for key agreement",
|
"Peer public key required for key agreement",
|
||||||
)
|
)
|
||||||
val peerKey =
|
val peerKey =
|
||||||
@@ -193,19 +235,51 @@ private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive {
|
|||||||
override fun abort() {}
|
override fun abort() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A software-only implementation of a cryptographic operation. This class acts as a controller,
|
||||||
|
* delegating to a specific cryptographic primitive based on the operation's purpose.
|
||||||
|
*
|
||||||
|
* Tracks operation lifecycle: once [finish] or [abort] is called, subsequent calls throw
|
||||||
|
* [ServiceSpecificException] with [KeystoreErrorCode.INVALID_OPERATION_HANDLE].
|
||||||
|
*/
|
||||||
class SoftwareOperation(
|
class SoftwareOperation(
|
||||||
private val txId: Long,
|
private val txId: Long,
|
||||||
keyPair: KeyPair?,
|
keyPair: KeyPair?,
|
||||||
secretKey: javax.crypto.SecretKey?,
|
secretKey: javax.crypto.SecretKey?,
|
||||||
params: KeyMintAttestation,
|
params: KeyMintAttestation,
|
||||||
private val latencyFloorMs: Long = 0L,
|
var onFinishCallback: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
private val primitive: CryptoPrimitive
|
private val primitive: CryptoPrimitive
|
||||||
@Volatile var finalized = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
var onFinishCallback: (() -> Unit)? = null
|
@Volatile private var finalized = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
val purpose = params.purpose.firstOrNull()
|
||||||
|
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
|
||||||
|
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
|
||||||
|
|
||||||
|
primitive =
|
||||||
|
when (purpose) {
|
||||||
|
KeyPurpose.SIGN -> Signer(keyPair!!, params)
|
||||||
|
KeyPurpose.VERIFY -> Verifier(keyPair!!, params)
|
||||||
|
KeyPurpose.ENCRYPT -> {
|
||||||
|
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 ->
|
||||||
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCode.UNSUPPORTED_PURPOSE,
|
||||||
|
"Unsupported operation purpose: $purpose",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parameters produced during begin (e.g. GCM nonce), to populate CreateOperationResponse. */
|
||||||
val beginParameters: KeyParameters?
|
val beginParameters: KeyParameters?
|
||||||
get() {
|
get() {
|
||||||
val params = primitive.getBeginParameters() ?: return null
|
val params = primitive.getBeginParameters() ?: return null
|
||||||
@@ -213,258 +287,107 @@ class SoftwareOperation(
|
|||||||
return KeyParameters().apply { keyParameter = params }
|
return KeyParameters().apply { keyParameter = params }
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
|
||||||
val purpose = params.purpose.firstOrNull()
|
|
||||||
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
|
|
||||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
|
|
||||||
|
|
||||||
if (purpose == null) {
|
|
||||||
// Defensive: if params somehow restored without a PURPOSE tag
|
|
||||||
// (corrupt v2 metadata, mismatched authorizations array on load,
|
|
||||||
// or future format drift) the original code crashed with NPE
|
|
||||||
// because Signer/Verifier/Cipher all dereference keyPair!!
|
|
||||||
// before checking purpose. Surface a clean keystore error
|
|
||||||
// instead so callers see a normal-looking operation failure
|
|
||||||
// they can recover from rather than the process appearing to
|
|
||||||
// silently corrupt their session.
|
|
||||||
SystemLogger.warning(
|
|
||||||
"[SoftwareOp TX_ID: $txId] Purpose missing on restored key " +
|
|
||||||
"(authorizations=${params.purpose}, keyPair=${if (keyPair != null) "present" else "null"}, " +
|
|
||||||
"secretKey=${if (secretKey != null) "present" else "null"}). " +
|
|
||||||
"Returning unsupportedPurpose."
|
|
||||||
)
|
|
||||||
throw ServiceSpecificException(
|
|
||||||
KeystoreErrorCodes.unsupportedPurpose,
|
|
||||||
"Restored key has no PURPOSE authorization",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
primitive =
|
|
||||||
when (purpose) {
|
|
||||||
KeyPurpose.SIGN -> {
|
|
||||||
val kp = keyPair ?: throw ServiceSpecificException(
|
|
||||||
KeystoreErrorCodes.invalidArgument,
|
|
||||||
"[SoftwareOp TX_ID: $txId] SIGN requested but keyPair is null",
|
|
||||||
)
|
|
||||||
Signer(kp, params)
|
|
||||||
}
|
|
||||||
KeyPurpose.VERIFY -> {
|
|
||||||
val kp = keyPair ?: throw ServiceSpecificException(
|
|
||||||
KeystoreErrorCodes.invalidArgument,
|
|
||||||
"[SoftwareOp TX_ID: $txId] VERIFY requested but keyPair is null",
|
|
||||||
)
|
|
||||||
Verifier(kp, params)
|
|
||||||
}
|
|
||||||
KeyPurpose.ENCRYPT -> {
|
|
||||||
val key: java.security.Key = secretKey ?: keyPair?.public
|
|
||||||
?: throw ServiceSpecificException(
|
|
||||||
KeystoreErrorCodes.unsupportedPurpose,
|
|
||||||
"[SoftwareOp TX_ID: $txId] ENCRYPT requires either secretKey or keyPair.public",
|
|
||||||
)
|
|
||||||
CipherPrimitive(key, params, Cipher.ENCRYPT_MODE)
|
|
||||||
}
|
|
||||||
KeyPurpose.DECRYPT -> {
|
|
||||||
val key: java.security.Key = secretKey ?: keyPair?.private
|
|
||||||
?: throw ServiceSpecificException(
|
|
||||||
KeystoreErrorCodes.unsupportedPurpose,
|
|
||||||
"[SoftwareOp TX_ID: $txId] DECRYPT requires either secretKey or keyPair.private",
|
|
||||||
)
|
|
||||||
CipherPrimitive(key, params, Cipher.DECRYPT_MODE)
|
|
||||||
}
|
|
||||||
KeyPurpose.AGREE_KEY -> {
|
|
||||||
val kp = keyPair ?: throw ServiceSpecificException(
|
|
||||||
KeystoreErrorCodes.invalidArgument,
|
|
||||||
"[SoftwareOp TX_ID: $txId] AGREE_KEY requested but keyPair is null",
|
|
||||||
)
|
|
||||||
KeyAgreementPrimitive(kp)
|
|
||||||
}
|
|
||||||
else ->
|
|
||||||
throw ServiceSpecificException(
|
|
||||||
KeystoreErrorCodes.unsupportedPurpose,
|
|
||||||
"Unsupported operation purpose: $purpose",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkActive() {
|
private fun checkActive() {
|
||||||
if (finalized) {
|
if (finalized)
|
||||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)")
|
throw ServiceSpecificException(
|
||||||
throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle)
|
KeystoreErrorCode.INVALID_OPERATION_HANDLE,
|
||||||
}
|
"Operation already finalized.",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkInputLength(data: ByteArray?) {
|
fun updateAad(data: ByteArray?) {
|
||||||
if (data != null && data.size > MAX_RECEIVE_DATA) {
|
|
||||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] Input too large: ${data.size} > $MAX_RECEIVE_DATA, throwing TOO_MUCH_DATA(${KeystoreErrorCodes.tooMuchData})")
|
|
||||||
throw ServiceSpecificException(KeystoreErrorCodes.tooMuchData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateAad(aadInput: ByteArray?) {
|
|
||||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] updateAad() ENTRY inputSize=${aadInput?.size ?: 0} primitive=${primitive::class.simpleName}")
|
|
||||||
checkActive()
|
checkActive()
|
||||||
checkInputLength(aadInput)
|
|
||||||
try {
|
try {
|
||||||
primitive.updateAad(aadInput)
|
primitive.updateAad(data)
|
||||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] updateAad() RETURNED_NORMALLY (unexpected for non-AEAD)")
|
} catch (e: ServiceSpecificException) {
|
||||||
} catch (throwable: Throwable) {
|
finalized = true
|
||||||
val top = throwable.stackTrace.firstOrNull()?.toString() ?: "<no-frame>"
|
throw e
|
||||||
val code = (throwable as? ServiceSpecificException)?.errorCode
|
} catch (e: Exception) {
|
||||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] updateAad() THREW class=${throwable::class.java.name} code=$code msg=${throwable.message} top=$top")
|
finalized = true
|
||||||
throw throwable
|
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to updateAad.", e)
|
||||||
|
throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
try {
|
try {
|
||||||
return primitive.update(data)
|
return primitive.update(data)
|
||||||
} catch (e: ServiceSpecificException) {
|
} catch (e: ServiceSpecificException) {
|
||||||
|
finalized = true
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
finalized = true
|
||||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
|
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
|
||||||
throw mapToServiceSpecificException(e)
|
throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
checkActive()
|
checkActive()
|
||||||
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
|
|
||||||
onFinishCallback?.invoke()
|
|
||||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
|
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
|
||||||
|
onFinishCallback?.invoke()
|
||||||
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 mapToServiceSpecificException(e)
|
throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message)
|
||||||
|
} finally {
|
||||||
|
finalized = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun abort() {
|
fun abort() {
|
||||||
|
checkActive()
|
||||||
finalized = true
|
finalized = true
|
||||||
primitive.abort()
|
primitive.abort()
|
||||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
|
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun mapToServiceSpecificException(e: Exception): ServiceSpecificException = when (e) {
|
/** Binder interface for [SoftwareOperation]. Synchronized and input-length validated. */
|
||||||
is SignatureException -> ServiceSpecificException(KeystoreErrorCodes.verificationFailed, e.message)
|
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
||||||
is javax.crypto.BadPaddingException -> ServiceSpecificException(KeystoreErrorCodes.invalidArgument, e.message)
|
IKeystoreOperation.Stub() {
|
||||||
is javax.crypto.IllegalBlockSizeException -> ServiceSpecificException(KeystoreErrorCodes.invalidInputLength, e.message)
|
|
||||||
is java.security.InvalidKeyException -> ServiceSpecificException(KeystoreErrorCodes.incompatibleKey, e.message)
|
private fun checkInputLength(data: ByteArray?) {
|
||||||
else -> ServiceSpecificException(KeystoreErrorCodes.unknownError, e.message)
|
if (data != null && data.size > MAX_RECEIVE_DATA)
|
||||||
|
throw ServiceSpecificException(KeystoreErrorCode.TOO_MUCH_DATA)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(RemoteException::class)
|
||||||
|
override fun updateAad(aadInput: ByteArray?) {
|
||||||
|
synchronized(this) {
|
||||||
|
checkInputLength(aadInput)
|
||||||
|
operation.updateAad(aadInput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(RemoteException::class)
|
||||||
|
override fun update(input: ByteArray?): ByteArray? {
|
||||||
|
synchronized(this) {
|
||||||
|
checkInputLength(input)
|
||||||
|
return operation.update(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(RemoteException::class)
|
||||||
|
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
|
synchronized(this) {
|
||||||
|
checkInputLength(input)
|
||||||
|
checkInputLength(signature)
|
||||||
|
return operation.finish(input, signature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(RemoteException::class)
|
||||||
|
override fun abort() {
|
||||||
|
synchronized(this) { operation.abort() }
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MAX_RECEIVE_DATA = 0x8000
|
private const val MAX_RECEIVE_DATA = 0x8000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object KeystoreErrorCodes {
|
|
||||||
val tooMuchData: Int by lazy {
|
|
||||||
resolveField("android.system.keystore2.ResponseCode", "TOO_MUCH_DATA", 21)
|
|
||||||
}
|
|
||||||
|
|
||||||
val invalidOperationHandle: Int by lazy {
|
|
||||||
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_OPERATION_HANDLE", -28)
|
|
||||||
}
|
|
||||||
|
|
||||||
val invalidTag: Int by lazy {
|
|
||||||
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_TAG", -76)
|
|
||||||
}
|
|
||||||
|
|
||||||
val verificationFailed: Int by lazy {
|
|
||||||
resolveField("android.hardware.security.keymint.ErrorCode", "VERIFICATION_FAILED", -30)
|
|
||||||
}
|
|
||||||
|
|
||||||
val invalidArgument: Int by lazy {
|
|
||||||
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_ARGUMENT", -38)
|
|
||||||
}
|
|
||||||
|
|
||||||
val invalidInputLength: Int by lazy {
|
|
||||||
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_INPUT_LENGTH", -21)
|
|
||||||
}
|
|
||||||
|
|
||||||
val incompatibleKey: Int by lazy {
|
|
||||||
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_KEY", -31)
|
|
||||||
}
|
|
||||||
|
|
||||||
val incompatiblePurpose: Int by lazy {
|
|
||||||
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_PURPOSE", -13)
|
|
||||||
}
|
|
||||||
|
|
||||||
val unsupportedPurpose: Int by lazy {
|
|
||||||
resolveField("android.hardware.security.keymint.ErrorCode", "UNSUPPORTED_PURPOSE", -14)
|
|
||||||
}
|
|
||||||
|
|
||||||
val incompatibleAlgorithm: Int by lazy {
|
|
||||||
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_ALGORITHM", -18)
|
|
||||||
}
|
|
||||||
|
|
||||||
val keyNotYetValid: Int by lazy {
|
|
||||||
resolveField("android.hardware.security.keymint.ErrorCode", "KEY_NOT_YET_VALID", -39)
|
|
||||||
}
|
|
||||||
|
|
||||||
val keyExpired: Int by lazy {
|
|
||||||
resolveField("android.hardware.security.keymint.ErrorCode", "KEY_EXPIRED", -40)
|
|
||||||
}
|
|
||||||
|
|
||||||
val callerNonceProhibited: Int by lazy {
|
|
||||||
resolveField("android.hardware.security.keymint.ErrorCode", "CALLER_NONCE_PROHIBITED", -55)
|
|
||||||
}
|
|
||||||
|
|
||||||
val unknownError: Int by lazy {
|
|
||||||
resolveField("android.hardware.security.keymint.ErrorCode", "UNKNOWN_ERROR", -1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resolveField(className: String, fieldName: String, fallback: Int): Int =
|
|
||||||
runCatching {
|
|
||||||
Class.forName(className).getField(fieldName).getInt(null)
|
|
||||||
}.getOrElse {
|
|
||||||
SystemLogger.debug("Resolved $className.$fieldName via fallback: $fallback")
|
|
||||||
fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
|
||||||
IKeystoreOperation.Stub() {
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun updateAad(aadInput: ByteArray?) {
|
|
||||||
SystemLogger.info("[SoftwareOpBinder] updateAad() ENTRY callingUid=${android.os.Binder.getCallingUid()} size=${aadInput?.size ?: 0}")
|
|
||||||
try {
|
|
||||||
operation.updateAad(aadInput)
|
|
||||||
SystemLogger.info("[SoftwareOpBinder] updateAad() RETURNED_NORMALLY")
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
val code = (throwable as? ServiceSpecificException)?.errorCode
|
|
||||||
SystemLogger.info("[SoftwareOpBinder] updateAad() PROPAGATING class=${throwable::class.java.name} code=$code msg=${throwable.message}")
|
|
||||||
throw throwable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun update(input: ByteArray?): ByteArray? {
|
|
||||||
return operation.update(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
|
|
||||||
return operation.finish(input, signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun abort() {
|
|
||||||
operation.abort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -37,6 +37,22 @@ object KeyMintParameterLogger {
|
|||||||
.associate { field -> (field.get(null) as Int) to field.name }
|
.associate { field -> (field.get(null) as Int) to field.name }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val hardwareAuthenticatorTypeNames: Map<Int, String> by lazy {
|
||||||
|
HardwareAuthenticatorType::class
|
||||||
|
.java
|
||||||
|
.fields
|
||||||
|
.filter { it.type == Int::class.java }
|
||||||
|
.associate { field -> (field.get(null) as Int) to field.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyOriginNames: Map<Int, String> by lazy {
|
||||||
|
KeyOrigin::class
|
||||||
|
.java
|
||||||
|
.fields
|
||||||
|
.filter { it.type == Int::class.java }
|
||||||
|
.associate { field -> (field.get(null) as Int) to field.name }
|
||||||
|
}
|
||||||
|
|
||||||
val paddingNames: Map<Int, String> by lazy {
|
val paddingNames: Map<Int, String> by lazy {
|
||||||
PaddingMode::class
|
PaddingMode::class
|
||||||
.java
|
.java
|
||||||
@@ -81,22 +97,33 @@ object KeyMintParameterLogger {
|
|||||||
when (param.tag) {
|
when (param.tag) {
|
||||||
Tag.ALGORITHM -> algorithmNames[value.algorithm]
|
Tag.ALGORITHM -> algorithmNames[value.algorithm]
|
||||||
Tag.BLOCK_MODE -> blockModeNames[value.blockMode]
|
Tag.BLOCK_MODE -> blockModeNames[value.blockMode]
|
||||||
|
Tag.DIGEST -> digestNames[value.digest]
|
||||||
Tag.EC_CURVE -> ecCurveNames[value.ecCurve]
|
Tag.EC_CURVE -> ecCurveNames[value.ecCurve]
|
||||||
|
Tag.ORIGIN -> keyOriginNames[value.origin]
|
||||||
Tag.PADDING -> paddingNames[value.paddingMode]
|
Tag.PADDING -> paddingNames[value.paddingMode]
|
||||||
Tag.PURPOSE -> purposeNames[value.keyPurpose]
|
Tag.PURPOSE -> purposeNames[value.keyPurpose]
|
||||||
Tag.DIGEST -> digestNames[value.digest]
|
Tag.USER_AUTH_TYPE ->
|
||||||
|
hardwareAuthenticatorTypeNames[value.hardwareAuthenticatorType]
|
||||||
Tag.AUTH_TIMEOUT,
|
Tag.AUTH_TIMEOUT,
|
||||||
|
Tag.BOOT_PATCHLEVEL,
|
||||||
Tag.KEY_SIZE,
|
Tag.KEY_SIZE,
|
||||||
Tag.MIN_MAC_LENGTH -> value.integer.toString()
|
Tag.MAC_LENGTH,
|
||||||
|
Tag.MIN_MAC_LENGTH,
|
||||||
|
Tag.OS_VERSION,
|
||||||
|
Tag.OS_PATCHLEVEL,
|
||||||
|
Tag.USER_ID,
|
||||||
|
Tag.VENDOR_PATCHLEVEL -> value.integer.toString()
|
||||||
Tag.CERTIFICATE_SERIAL -> BigInteger(value.blob).toString()
|
Tag.CERTIFICATE_SERIAL -> BigInteger(value.blob).toString()
|
||||||
Tag.ACTIVE_DATETIME,
|
Tag.ACTIVE_DATETIME,
|
||||||
Tag.CERTIFICATE_NOT_AFTER,
|
Tag.CERTIFICATE_NOT_AFTER,
|
||||||
Tag.CERTIFICATE_NOT_BEFORE,
|
Tag.CERTIFICATE_NOT_BEFORE,
|
||||||
|
Tag.CREATION_DATETIME,
|
||||||
Tag.ORIGINATION_EXPIRE_DATETIME,
|
Tag.ORIGINATION_EXPIRE_DATETIME,
|
||||||
Tag.USAGE_EXPIRE_DATETIME -> Date(value.dateTime).toString()
|
Tag.USAGE_EXPIRE_DATETIME -> Date(value.dateTime).toString()
|
||||||
Tag.CERTIFICATE_SUBJECT -> X500Name(X500Principal(value.blob).name).toString()
|
Tag.CERTIFICATE_SUBJECT -> X500Name(X500Principal(value.blob).name).toString()
|
||||||
|
Tag.USER_SECURE_ID,
|
||||||
Tag.RSA_PUBLIC_EXPONENT -> value.longInteger.toString()
|
Tag.RSA_PUBLIC_EXPONENT -> value.longInteger.toString()
|
||||||
Tag.NO_AUTH_REQUIRED -> "true"
|
Tag.NO_AUTH_REQUIRED -> value.boolValue.toString()
|
||||||
Tag.ATTESTATION_CHALLENGE,
|
Tag.ATTESTATION_CHALLENGE,
|
||||||
Tag.ATTESTATION_ID_BRAND,
|
Tag.ATTESTATION_ID_BRAND,
|
||||||
Tag.ATTESTATION_ID_DEVICE,
|
Tag.ATTESTATION_ID_DEVICE,
|
||||||
|
|||||||
@@ -1,86 +1,42 @@
|
|||||||
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 {
|
||||||
@PublishedApi internal const val TAG = "TEESimulator"
|
// The tag used for all log messages from this application.
|
||||||
|
private const val TAG = "TEESimulator"
|
||||||
|
|
||||||
@PublishedApi internal val isDebugBuild = BuildConfig.DEBUG
|
private 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. Warnings are never rate-limited.
|
* Logs a warning message. Use this to report unexpected but non-fatal issues.
|
||||||
|
*
|
||||||
|
* @param message The message to log.
|
||||||
|
* @param throwable An optional exception to log with the message.
|
||||||
*/
|
*/
|
||||||
fun warning(message: String, throwable: Throwable? = null) {
|
fun warning(message: String, throwable: Throwable? = null) {
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
@@ -91,7 +47,11 @@ object SystemLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs an error message. Errors are never rate-limited.
|
* Logs an error message. Use this to report fatal errors or exceptions that disrupt
|
||||||
|
* functionality.
|
||||||
|
*
|
||||||
|
* @param message The message to log.
|
||||||
|
* @param throwable An optional exception to log with the message.
|
||||||
*/
|
*/
|
||||||
fun error(message: String, throwable: Throwable? = null) {
|
fun error(message: String, throwable: Throwable? = null) {
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
@@ -104,22 +64,11 @@ 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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
|||||||
*/
|
*/
|
||||||
object CertificateGenerator {
|
object CertificateGenerator {
|
||||||
|
|
||||||
|
// RFC 5280 GeneralizedTime maximum: 9999-12-31T23:59:59 UTC (millis since epoch).
|
||||||
private const val UNDEFINED_NOT_AFTER = 253402300799000L
|
private const val UNDEFINED_NOT_AFTER = 253402300799000L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,12 +94,6 @@ object CertificateGenerator {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return try {
|
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)
|
||||||
|
|
||||||
val (signingKey, issuer) =
|
val (signingKey, issuer) =
|
||||||
@@ -204,14 +199,16 @@ object CertificateGenerator {
|
|||||||
private fun buildKeyUsageFromPurposes(purposes: List<Int>): Int {
|
private fun buildKeyUsageFromPurposes(purposes: List<Int>): Int {
|
||||||
var bits = 0
|
var bits = 0
|
||||||
for (purpose in purposes) {
|
for (purpose in purposes) {
|
||||||
bits = bits or when (purpose) {
|
bits =
|
||||||
KeyPurpose.SIGN -> KeyUsage.digitalSignature
|
bits or
|
||||||
KeyPurpose.DECRYPT -> KeyUsage.dataEncipherment
|
when (purpose) {
|
||||||
KeyPurpose.WRAP_KEY -> KeyUsage.keyEncipherment
|
KeyPurpose.SIGN -> KeyUsage.digitalSignature
|
||||||
KeyPurpose.AGREE_KEY -> KeyUsage.keyAgreement
|
KeyPurpose.DECRYPT -> KeyUsage.dataEncipherment
|
||||||
KeyPurpose.ATTEST_KEY -> KeyUsage.keyCertSign
|
KeyPurpose.WRAP_KEY -> KeyUsage.keyEncipherment
|
||||||
else -> 0
|
KeyPurpose.AGREE_KEY -> KeyUsage.keyAgreement
|
||||||
}
|
KeyPurpose.ATTEST_KEY -> KeyUsage.keyCertSign
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return bits
|
return bits
|
||||||
}
|
}
|
||||||
@@ -226,6 +223,8 @@ 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")
|
||||||
|
|
||||||
|
// Default validity: epoch to 9999-12-31T23:59:59 UTC (matches add_required_parameters).
|
||||||
val notBefore = params.certificateNotBefore ?: Date(0)
|
val notBefore = params.certificateNotBefore ?: Date(0)
|
||||||
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
|
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
|
||||||
|
|
||||||
@@ -244,17 +243,21 @@ object CertificateGenerator {
|
|||||||
if (keyUsageBits != 0) {
|
if (keyUsageBits != 0) {
|
||||||
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
|
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
|
||||||
}
|
}
|
||||||
if (params.attestationChallenge != null) {
|
// Add our custom, simulated attestation extension.
|
||||||
builder.addExtension(
|
builder.addExtension(
|
||||||
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
|
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
|
// The signature algorithm must match the SIGNING key, not the subject key.
|
||||||
|
// An EC attestation key may sign an RSA subject key's certificate (or vice versa).
|
||||||
val signerAlgorithm =
|
val signerAlgorithm =
|
||||||
when (signingKeyPair.private.algorithm) {
|
when (signingKeyPair.private) {
|
||||||
"EC", "ECDSA" -> "SHA256withECDSA"
|
is java.security.interfaces.ECKey -> "SHA256withECDSA"
|
||||||
"RSA" -> "SHA256withRSA"
|
is java.security.interfaces.RSAKey -> "SHA256withRSA"
|
||||||
else -> throw IllegalArgumentException("Unsupported signing key: ${signingKeyPair.private.algorithm}")
|
else ->
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Unsupported signing key type: ${signingKeyPair.private.javaClass}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val contentSigner =
|
val contentSigner =
|
||||||
JcaContentSignerBuilder(signerAlgorithm)
|
JcaContentSignerBuilder(signerAlgorithm)
|
||||||
@@ -263,39 +266,4 @@ 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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.matrix.TEESimulator.util
|
package org.matrix.TEESimulator.util
|
||||||
|
|
||||||
|
import android.hardware.security.keymint.SecurityLevel
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.SystemProperties
|
import android.os.SystemProperties
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
@@ -90,33 +91,27 @@ object AndroidDeviceUtils {
|
|||||||
attestationValueProvider: () -> ByteArray?,
|
attestationValueProvider: () -> ByteArray?,
|
||||||
expectedSize: Int,
|
expectedSize: Int,
|
||||||
): ByteArray {
|
): ByteArray {
|
||||||
|
// 1. Attempt to get the value from the system property.
|
||||||
getProperty(propertyName, expectedSize)?.let {
|
getProperty(propertyName, expectedSize)?.let {
|
||||||
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
|
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
|
||||||
persistToFile(propertyName, it)
|
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Fallback to the value from a cached TEE attestation.
|
||||||
try {
|
try {
|
||||||
attestationValueProvider()?.let {
|
attestationValueProvider()?.let {
|
||||||
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
|
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
|
||||||
setProperty(propertyName, it)
|
setProperty(propertyName, it) // Persist for consistency
|
||||||
persistToFile(propertyName, it)
|
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("Failed to get $propertyName from attestation.", e)
|
SystemLogger.error("Failed to get $propertyName from attestation.", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
readFromFile(propertyName, expectedSize)?.let {
|
// 3. As a final fallback, generate a random value.
|
||||||
SystemLogger.debug("Using $propertyName from persistent file: ${it.toHex()}")
|
|
||||||
setProperty(propertyName, it)
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
|
|
||||||
return generateRandomBytes(expectedSize).also {
|
return generateRandomBytes(expectedSize).also {
|
||||||
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
|
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
|
||||||
setProperty(propertyName, it)
|
setProperty(propertyName, it)
|
||||||
persistToFile(propertyName, it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,55 +158,10 @@ object AndroidDeviceUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun setProperty(name: String, value: String) {
|
/** Generates a cryptographically random byte array of a specified length. */
|
||||||
try {
|
|
||||||
SystemLogger.debug("Setting system property '$name' to: $value")
|
|
||||||
val command = arrayOf("resetprop", name, value)
|
|
||||||
val process = Runtime.getRuntime().exec(command)
|
|
||||||
val exitCode = process.waitFor()
|
|
||||||
|
|
||||||
if (exitCode != 0) {
|
|
||||||
val errorOutput = process.errorStream.bufferedReader().readText()
|
|
||||||
SystemLogger.error(
|
|
||||||
"resetprop for '$name' failed with exit code $exitCode: $errorOutput"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SystemLogger.error("Failed to set '$name' property via resetprop.", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateRandomBytes(size: Int): ByteArray =
|
private fun generateRandomBytes(size: Int): ByteArray =
|
||||||
ByteArray(size).also { ThreadLocalRandom.current().nextBytes(it) }
|
ByteArray(size).also { ThreadLocalRandom.current().nextBytes(it) }
|
||||||
|
|
||||||
private val PERSIST_DIR = File("/data/adb/tricky_store")
|
|
||||||
|
|
||||||
private fun fileForProperty(propertyName: String): File = when (propertyName) {
|
|
||||||
"ro.boot.vbmeta.digest" -> File(PERSIST_DIR, "boot_hash.bin")
|
|
||||||
"ro.boot.vbmeta.public_key_digest" -> File(PERSIST_DIR, "boot_key.bin")
|
|
||||||
else -> File(PERSIST_DIR, "${propertyName.replace('.', '_')}.bin")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun persistToFile(propertyName: String, bytes: ByteArray) {
|
|
||||||
try {
|
|
||||||
fileForProperty(propertyName).writeBytes(bytes)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SystemLogger.error("Failed to persist $propertyName to file.", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun readFromFile(propertyName: String, expectedSize: Int): ByteArray? {
|
|
||||||
return try {
|
|
||||||
val file = fileForProperty(propertyName)
|
|
||||||
if (!file.exists()) return null
|
|
||||||
val bytes = file.readBytes()
|
|
||||||
if (bytes.size == expectedSize) bytes else null
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SystemLogger.error("Failed to read $propertyName from file.", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Patch Level Properties ---
|
// --- Patch Level Properties ---
|
||||||
|
|
||||||
fun getPatchLevel(uid: Int): Int {
|
fun getPatchLevel(uid: Int): Int {
|
||||||
@@ -290,12 +240,11 @@ object AndroidDeviceUtils {
|
|||||||
val resolvedValue = resolveDateKeywords(value)
|
val resolvedValue = resolveDateKeywords(value)
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
|
// "device_default" indicates falling back to the system property.
|
||||||
resolvedValue.equals("device_default", ignoreCase = true) -> null
|
resolvedValue.equals("device_default", ignoreCase = true) -> null
|
||||||
// Resolve from live system prop — matches what detectors see via getprop,
|
// "no" indicates this value should not be reported.
|
||||||
// even when PIF has spoofed ro.build.version.security_patch via resetprop
|
|
||||||
resolvedValue.equals("prop", ignoreCase = true) ->
|
|
||||||
parsePatchLevelValue(SystemProperties.get("ro.build.version.security_patch", ""), isLong)
|
|
||||||
resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
|
resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
|
||||||
|
// Otherwise, parse the resolved date string.
|
||||||
else -> parsePatchLevelValue(resolvedValue, isLong)
|
else -> parsePatchLevelValue(resolvedValue, isLong)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,9 +294,7 @@ object AndroidDeviceUtils {
|
|||||||
6 -> { // YYYYMM
|
6 -> { // YYYYMM
|
||||||
val year = normalized.substring(0, 4).toInt()
|
val year = normalized.substring(0, 4).toInt()
|
||||||
val month = normalized.substring(4, 6).toInt()
|
val month = normalized.substring(4, 6).toInt()
|
||||||
// Synthesizing day=01 from YYYY-MM disagrees with real device bulletins;
|
if (isLong) year * 10000 + month * 100 + 1 else year * 100 + month
|
||||||
// propagate null so callers fall back to a YYYY-MM-DD source.
|
|
||||||
if (isLong) null else year * 100 + month
|
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@@ -395,26 +342,20 @@ object AndroidDeviceUtils {
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the attestation version for the given security level. The value follows the device
|
* Retrieves the attestation version based on security level and OS version. StrongBox (level 2)
|
||||||
* OS: cached attestation data wins, then attestVersionMap[SDK_INT], then 400 as last resort.
|
* requires version 300.
|
||||||
* A static StrongBox=300 floor would force a major-version mismatch with the TEE chain on
|
|
||||||
* Android 16 devices that report keymaster 400 across both security levels.
|
|
||||||
*
|
*
|
||||||
* @param securityLevel The security level of the attestation (1 for TEE, 2 for StrongBox).
|
* @param securityLevel The security level of the attestation (1 for TEE, 2 for StrongBox).
|
||||||
* @return The appropriate attestation version number.
|
* @return The appropriate attestation version number.
|
||||||
*/
|
*/
|
||||||
fun getAttestVersion(securityLevel: Int): Int {
|
fun getAttestVersion(securityLevel: Int): Int {
|
||||||
val cached = DeviceAttestationService.CachedAttestationData?.attestVersion
|
// StrongBox security level requires an attestation version of at least 300.
|
||||||
val version = cached
|
if (securityLevel == SecurityLevel.STRONGBOX) {
|
||||||
|
return 300
|
||||||
|
}
|
||||||
|
return DeviceAttestationService.CachedAttestationData?.attestVersion
|
||||||
?: 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -423,11 +364,17 @@ 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 = getAttestVersion(securityLevel)
|
fun getKeymasterVersion(securityLevel: Int): Int {
|
||||||
|
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 ---
|
||||||
|
|
||||||
// Minimal protobuf parser for apex_manifest.pb (field 1: name, field 2: version)
|
// https://cs.android.com/android/platform/superproject/+/android-latest-release:system/apex/proto/apex_manifest.proto
|
||||||
|
// --- Minimal Protobuf Parser for ApexManifest ---
|
||||||
|
// Field 1: name (string)
|
||||||
|
// Field 2: version (int64)
|
||||||
private class MinimalApexManifestParser(private val data: ByteArray) {
|
private class MinimalApexManifestParser(private val data: ByteArray) {
|
||||||
var pos = 0
|
var pos = 0
|
||||||
|
|
||||||
@@ -441,13 +388,13 @@ object AndroidDeviceUtils {
|
|||||||
val wireType = (tag and 0x07).toInt()
|
val wireType = (tag and 0x07).toInt()
|
||||||
|
|
||||||
when (fieldNum) {
|
when (fieldNum) {
|
||||||
1L -> {
|
1L -> { // name
|
||||||
val length = readVarint().toInt()
|
val length = readVarint().toInt()
|
||||||
if (pos + length > data.size) return null
|
if (pos + length > data.size) return null
|
||||||
name = String(data, pos, length, Charsets.UTF_8)
|
name = String(data, pos, length, Charsets.UTF_8)
|
||||||
pos += length
|
pos += length
|
||||||
}
|
}
|
||||||
2L -> {
|
2L -> { // version
|
||||||
version = readVarint()
|
version = readVarint()
|
||||||
}
|
}
|
||||||
else -> skipField(wireType)
|
else -> skipField(wireType)
|
||||||
@@ -475,18 +422,19 @@ object AndroidDeviceUtils {
|
|||||||
|
|
||||||
private fun skipField(wireType: Int) {
|
private fun skipField(wireType: Int) {
|
||||||
when (wireType) {
|
when (wireType) {
|
||||||
0 -> readVarint()
|
0 -> readVarint() // Varint
|
||||||
1 -> pos += 8
|
1 -> pos += 8 // 64-bit
|
||||||
2 -> {
|
2 -> { // Length-delimited
|
||||||
val len = readVarint().toInt()
|
val len = readVarint().toInt()
|
||||||
pos += len
|
pos += len
|
||||||
}
|
}
|
||||||
5 -> pos += 4
|
5 -> pos += 4 // 32-bit
|
||||||
else -> throw IllegalStateException("Unknown wire type $wireType")
|
else -> throw IllegalStateException("Unknown wire type $wireType")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://cs.android.com/android/platform/superproject/main/+/main:system/apex/libs/libapexutil/apexutil.cpp
|
||||||
private val apexInfos: List<Pair<String, Long>> by lazy {
|
private val apexInfos: List<Pair<String, Long>> by lazy {
|
||||||
val results = mutableListOf<Pair<String, Long>>()
|
val results = mutableListOf<Pair<String, Long>>()
|
||||||
val apexRoot = File("/apex")
|
val apexRoot = File("/apex")
|
||||||
@@ -495,14 +443,22 @@ object AndroidDeviceUtils {
|
|||||||
return@lazy emptyList()
|
return@lazy emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logic from: GetActivePackages in apexutil.cpp
|
||||||
apexRoot.listFiles()?.forEach { file ->
|
apexRoot.listFiles()?.forEach { file ->
|
||||||
if (!file.isDirectory) return@forEach
|
if (!file.isDirectory) return@forEach
|
||||||
val name = file.name
|
val name = file.name
|
||||||
|
|
||||||
|
// 1. Ignore "." (and implicitly "..")
|
||||||
if (name.startsWith(".")) return@forEach
|
if (name.startsWith(".")) return@forEach
|
||||||
|
|
||||||
|
// 2. Ignore directories containing '@' (active mounts usually don't have version in
|
||||||
|
// path)
|
||||||
if (name.contains("@")) return@forEach
|
if (name.contains("@")) return@forEach
|
||||||
|
|
||||||
|
// 3. Ignore "sharedlibs"
|
||||||
if (name == "sharedlibs") return@forEach
|
if (name == "sharedlibs") return@forEach
|
||||||
|
|
||||||
|
// 4. Parse apex_manifest.pb
|
||||||
val manifestFile = File(file, "apex_manifest.pb")
|
val manifestFile = File(file, "apex_manifest.pb")
|
||||||
if (manifestFile.exists()) {
|
if (manifestFile.exists()) {
|
||||||
runCatching {
|
runCatching {
|
||||||
@@ -513,46 +469,59 @@ object AndroidDeviceUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure uniqueness (though filesystem scan usually prevents exact dupes,
|
||||||
|
// strictly speaking we want to behave like a Map keyed by package name)
|
||||||
results.distinctBy { it.first }
|
results.distinctBy { it.first }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/maintenance.rs
|
||||||
val moduleHash: ByteArray by lazy {
|
val moduleHash: ByteArray by lazy {
|
||||||
DeviceAttestationService.CachedAttestationData?.moduleHash
|
DeviceAttestationService.CachedAttestationData?.moduleHash
|
||||||
?: runCatching {
|
?: runCatching {
|
||||||
|
// 1. Create a container to hold the sort key (name encoded) and the full data
|
||||||
|
// (sequence encoded)
|
||||||
data class ModuleEntry(
|
data class ModuleEntry(
|
||||||
val nameEncoded: ByteArray,
|
val nameEncoded: ByteArray, // The sort key
|
||||||
val fullEncoded: ByteArray,
|
val fullEncoded: ByteArray, // The data to hash
|
||||||
)
|
)
|
||||||
|
|
||||||
val modules =
|
val modules =
|
||||||
apexInfos.map { (packageName, versionCode) ->
|
apexInfos.map { (packageName, versionCode) ->
|
||||||
|
// Create the components
|
||||||
val nameOctet = DEROctetString(packageName.toByteArray(Charsets.UTF_8))
|
val nameOctet = DEROctetString(packageName.toByteArray(Charsets.UTF_8))
|
||||||
val versionInt = ASN1Integer(versionCode)
|
val versionInt = ASN1Integer(versionCode)
|
||||||
|
|
||||||
|
// Create the Sequence: SEQUENCE { packageName, version }
|
||||||
val vec = ASN1EncodableVector()
|
val vec = ASN1EncodableVector()
|
||||||
vec.add(nameOctet)
|
vec.add(nameOctet)
|
||||||
vec.add(versionInt)
|
vec.add(versionInt)
|
||||||
val sequence = DERSequence(vec)
|
val sequence = DERSequence(vec)
|
||||||
|
|
||||||
// AOSP sorts by encoded name only, not full sequence
|
// We store the encoded name separately because Rust sorts ONLY by this
|
||||||
ModuleEntry(
|
ModuleEntry(
|
||||||
nameEncoded = nameOctet.encoded,
|
nameEncoded = nameOctet.encoded,
|
||||||
fullEncoded = sequence.encoded,
|
fullEncoded = sequence.encoded,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Sort manually based on the encoded Package Name (lexicographically)
|
||||||
|
// This mimics the Rust 'impl DerOrd for ModuleInfo' which delegates to
|
||||||
|
// 'self.name'
|
||||||
val sortedModules =
|
val sortedModules =
|
||||||
modules.sortedWith { m1, m2 ->
|
modules.sortedWith { m1, m2 ->
|
||||||
compareByteArrays(m1.nameEncoded, m2.nameEncoded)
|
compareByteArrays(m1.nameEncoded, m2.nameEncoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Concatenate the full sequences in the specific sorted order
|
||||||
val payloadStream = ByteArrayOutputStream()
|
val payloadStream = ByteArrayOutputStream()
|
||||||
sortedModules.forEach { payloadStream.write(it.fullEncoded) }
|
sortedModules.forEach { payloadStream.write(it.fullEncoded) }
|
||||||
val payload = payloadStream.toByteArray()
|
val payload = payloadStream.toByteArray()
|
||||||
|
|
||||||
// Wrap in DER SET tag manually — DERSet() re-sorts by full encoding
|
// 4. Wrap manually in a DER SET tag (0x31)
|
||||||
|
// We cannot use DERSet(vector) because it would re-sort incorrectly.
|
||||||
val finalDerSet = encodeAsDerSet(payload)
|
val finalDerSet = encodeAsDerSet(payload)
|
||||||
|
|
||||||
|
// 5. Compute SHA-256
|
||||||
MessageDigest.getInstance("SHA-256").digest(finalDerSet)
|
MessageDigest.getInstance("SHA-256").digest(finalDerSet)
|
||||||
}
|
}
|
||||||
.getOrElse {
|
.getOrElse {
|
||||||
@@ -561,6 +530,7 @@ object AndroidDeviceUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Compares two byte arrays lexicographically (unsigned). */
|
||||||
private fun compareByteArrays(a: ByteArray, b: ByteArray): Int {
|
private fun compareByteArrays(a: ByteArray, b: ByteArray): Int {
|
||||||
val length = minOf(a.size, b.size)
|
val length = minOf(a.size, b.size)
|
||||||
for (i in 0 until length) {
|
for (i in 0 until length) {
|
||||||
@@ -573,25 +543,31 @@ object AndroidDeviceUtils {
|
|||||||
return a.size - b.size
|
return a.size - b.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Manually wraps the payload in an ASN.1 SET (0x31) tag with correct length encoding. */
|
||||||
private fun encodeAsDerSet(payload: ByteArray): ByteArray {
|
private fun encodeAsDerSet(payload: ByteArray): ByteArray {
|
||||||
val out = ByteArrayOutputStream()
|
val out = ByteArrayOutputStream()
|
||||||
out.write(0x31)
|
out.write(0x31) // ASN.1 Tag for SET
|
||||||
writeDerLength(out, payload.size)
|
writeDerLength(out, payload.size)
|
||||||
out.write(payload)
|
out.write(payload)
|
||||||
return out.toByteArray()
|
return out.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Writes the ASN.1 length field to the stream. */
|
||||||
private fun writeDerLength(out: ByteArrayOutputStream, length: Int) {
|
private fun writeDerLength(out: ByteArrayOutputStream, length: Int) {
|
||||||
if (length < 128) {
|
if (length < 128) {
|
||||||
|
// Short form
|
||||||
out.write(length)
|
out.write(length)
|
||||||
} else {
|
} else {
|
||||||
|
// Long form
|
||||||
var size = length
|
var size = length
|
||||||
val bytes = ArrayList<Byte>()
|
val bytes = ArrayList<Byte>()
|
||||||
while (size > 0) {
|
while (size > 0) {
|
||||||
bytes.add((size and 0xFF).toByte())
|
bytes.add((size and 0xFF).toByte())
|
||||||
size = size ushr 8
|
size = size ushr 8
|
||||||
}
|
}
|
||||||
|
// First byte: 0x80 | number of length bytes
|
||||||
out.write(0x80 or bytes.size)
|
out.write(0x80 or bytes.size)
|
||||||
|
// Write length bytes in big-endian (reverse of how we extracted them)
|
||||||
for (i in bytes.indices.reversed()) {
|
for (i in bytes.indices.reversed()) {
|
||||||
out.write(bytes[i].toInt())
|
out.write(bytes[i].toInt())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
package org.matrix.TEESimulator.util
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
|
||||||
|
|
||||||
object AndroidPermissionUtils {
|
|
||||||
|
|
||||||
@SuppressLint("PrivateApi", "DiscouragedPrivateApi")
|
|
||||||
private fun getGlobalContext(): Context? {
|
|
||||||
return try {
|
|
||||||
// 1. Get the hidden ActivityThread class via reflection
|
|
||||||
val activityThreadClass = Class.forName("android.app.ActivityThread")
|
|
||||||
|
|
||||||
// 2. Invoke the static currentActivityThread() method
|
|
||||||
val currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread")
|
|
||||||
currentActivityThreadMethod.isAccessible = true
|
|
||||||
val activityThread = currentActivityThreadMethod.invoke(null)
|
|
||||||
|
|
||||||
if (activityThread == null) {
|
|
||||||
SystemLogger.warning("Reflection: ActivityThread.currentActivityThread() returned null")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Try to get the application context
|
|
||||||
val getApplicationMethod = activityThreadClass.getDeclaredMethod("getApplication")
|
|
||||||
getApplicationMethod.isAccessible = true
|
|
||||||
val application = getApplicationMethod.invoke(activityThread) as? Context
|
|
||||||
|
|
||||||
if (application != null) return application
|
|
||||||
|
|
||||||
// 4. Fallback to getSystemContext() if application is null (often happens in system_server)
|
|
||||||
val getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext")
|
|
||||||
getSystemContextMethod.isAccessible = true
|
|
||||||
getSystemContextMethod.invoke(activityThread) as? Context
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SystemLogger.error("Reflection failed to get global context for permission check", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Core permission check.
|
|
||||||
*/
|
|
||||||
fun hasPermission(uid: Int, permission: String): Boolean {
|
|
||||||
val context = getGlobalContext() ?: run {
|
|
||||||
SystemLogger.warning("AndroidPermissionUtils: Context is null, failing permission check safely.")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = context.checkPermission(permission, -1, uid)
|
|
||||||
return result == PackageManager.PERMISSION_GRANTED
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasDeviceAttestationPermission(uid: Int): Boolean {
|
|
||||||
return hasPermission(uid, "android.permission.READ_PRIVILEGED_PHONE_STATE")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasUniqueIdAttestationPermission(uid: Int): Boolean {
|
|
||||||
return hasPermission(uid, "android.permission.REQUEST_UNIQUE_ID_ATTESTATION")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasManageUsersPermission(uid: Int): Boolean {
|
|
||||||
return hasPermission(uid, "android.permission.MANAGE_USERS")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasDumpPermission(uid: Int): Boolean {
|
|
||||||
return hasPermission(uid, "android.permission.DUMP")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,21 @@ import kotlin.math.exp
|
|||||||
import kotlin.math.ln
|
import kotlin.math.ln
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulates realistic TEE hardware latency for software key generation.
|
||||||
|
*
|
||||||
|
* The delay model is derived from 64+ timing measurements across QTEE (Qualcomm) and Trustonic
|
||||||
|
* (MediaTek) hardware. It combines four independent noise sources that model different physical
|
||||||
|
* latency origins in a real TrustZone-based TEE:
|
||||||
|
*
|
||||||
|
* 1. Base crypto processing (log-normal): hardware RNG + key derivation + cert signing
|
||||||
|
* 2. Binder/kernel transit (exponential): IPC scheduling, context switches
|
||||||
|
* 3. TrustZone scheduler jitter (Gaussian): world-switch non-determinism
|
||||||
|
* 4. Cold-start penalty (half-normal): first operation after idle is slower due to TEE
|
||||||
|
* secure world re-initialization and TLB/cache warming
|
||||||
|
*
|
||||||
|
* Per-boot session bias models manufacturing variance between TEE hardware instances.
|
||||||
|
*/
|
||||||
object TeeLatencySimulator {
|
object TeeLatencySimulator {
|
||||||
|
|
||||||
private val rng = SecureRandom()
|
private val rng = SecureRandom()
|
||||||
@@ -41,6 +56,11 @@ object TeeLatencySimulator {
|
|||||||
return max(20.0, base + transit + jitter + sessionBiasMs + cold)
|
return max(20.0, base + transit + jitter + sessionBiasMs + cold)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log-normal base delay. Parameters tuned to match observed hardware profiles:
|
||||||
|
* EC P-256 on QTEE averages ~65ms, RSA-2048 ~75ms, AES ~40ms.
|
||||||
|
* Sigma kept low (0.08) to match the tight clustering seen in real measurements.
|
||||||
|
*/
|
||||||
private fun sampleBaseCryptoDelay(algorithm: Int): Double {
|
private fun sampleBaseCryptoDelay(algorithm: Int): Double {
|
||||||
val (mu, sigma) =
|
val (mu, sigma) =
|
||||||
when (algorithm) {
|
when (algorithm) {
|
||||||
|
|||||||
+2
-58
@@ -2,66 +2,10 @@
|
|||||||
MODDIR=${0%/*}
|
MODDIR=${0%/*}
|
||||||
CONFIG_DIR=/data/adb/tricky_store
|
CONFIG_DIR=/data/adb/tricky_store
|
||||||
|
|
||||||
. "$MODDIR/action_i18n.sh"
|
|
||||||
|
|
||||||
echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo " ⚠️ $(_msg confirm_header)"
|
|
||||||
echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo " "
|
|
||||||
echo " $(_msg confirm_warning_1)"
|
|
||||||
echo " $(_msg confirm_warning_2)"
|
|
||||||
echo " "
|
|
||||||
echo " 🔊 $(_msg confirm_vol_up)"
|
|
||||||
echo " 🔉 $(_msg confirm_vol_down)"
|
|
||||||
echo " "
|
|
||||||
|
|
||||||
confirm() {
|
|
||||||
vol_tmp="${TMPDIR:-/data/local/tmp}/teesim_vol_key"
|
|
||||||
seconds=10
|
|
||||||
|
|
||||||
: > "$vol_tmp"
|
|
||||||
getevent -qlc 1 > "$vol_tmp" 2>/dev/null &
|
|
||||||
ge_pid=$!
|
|
||||||
|
|
||||||
while [ "$seconds" -gt 0 ]; do
|
|
||||||
sleep 1
|
|
||||||
if ! kill -0 "$ge_pid" 2>/dev/null; then
|
|
||||||
key=$(awk '/KEY_/{print $3}' "$vol_tmp" 2>/dev/null)
|
|
||||||
case "$key" in
|
|
||||||
KEY_VOLUMEUP)
|
|
||||||
rm -f "$vol_tmp"
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
KEY_VOLUMEDOWN)
|
|
||||||
rm -f "$vol_tmp"
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
: > "$vol_tmp"
|
|
||||||
getevent -qlc 1 > "$vol_tmp" 2>/dev/null &
|
|
||||||
ge_pid=$!
|
|
||||||
fi
|
|
||||||
seconds=$((seconds - 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
kill "$ge_pid" 2>/dev/null
|
|
||||||
wait "$ge_pid" 2>/dev/null
|
|
||||||
rm -f "$vol_tmp"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if ! confirm; then
|
|
||||||
echo " "
|
|
||||||
echo " ❌ $(_msg confirm_cancelled)"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -d "$CONFIG_DIR/persistent_keys" ]; then
|
if [ -d "$CONFIG_DIR/persistent_keys" ]; then
|
||||||
rm -rf "$CONFIG_DIR/persistent_keys"
|
rm -rf "$CONFIG_DIR/persistent_keys"
|
||||||
mkdir -p "$CONFIG_DIR/persistent_keys"
|
mkdir -p "$CONFIG_DIR/persistent_keys"
|
||||||
echo " "
|
echo "Persistent key storage cleared"
|
||||||
echo " ✅ $(_msg confirm_cleared)"
|
|
||||||
else
|
else
|
||||||
echo " "
|
echo "No persistent key storage found"
|
||||||
echo " ℹ️ $(_msg confirm_not_found)"
|
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,255 +0,0 @@
|
|||||||
ACTION_LANG="en"
|
|
||||||
_detect_lang() {
|
|
||||||
local raw
|
|
||||||
raw=$(getprop persist.sys.locale 2>/dev/null)
|
|
||||||
[ -z "$raw" ] && raw=$(getprop ro.product.locale 2>/dev/null)
|
|
||||||
[ -z "$raw" ] && raw=$(getprop ro.system.locale 2>/dev/null)
|
|
||||||
local code=$(printf '%s' "$raw" | sed 's/_/-/g')
|
|
||||||
case "$code" in
|
|
||||||
zh-Hans*|zh-CN*) code="zh-CN" ;;
|
|
||||||
zh-Hant*|zh-TW*|zh-HK*) code="zh-TW" ;;
|
|
||||||
pt-BR*) code="pt-BR" ;;
|
|
||||||
pt*) code="pt-BR" ;;
|
|
||||||
es-ES*|es*) code="es-ES" ;;
|
|
||||||
*-*) code="${code%%-*}" ;;
|
|
||||||
esac
|
|
||||||
case "$code" in
|
|
||||||
ar|az|bn|de|el|es-ES|fa|fr|id|it|ja|ko|pl|pt-BR|ru|th|tl|tr|uk|vi|zh-CN|zh-TW) ACTION_LANG="$code" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
_detect_lang
|
|
||||||
|
|
||||||
_msg() {
|
|
||||||
case "$ACTION_LANG" in
|
|
||||||
zh-CN) case "$1" in
|
|
||||||
confirm_header) echo "清除持久化密钥存储" ;;
|
|
||||||
confirm_warning_1) echo "这将删除所有缓存的证明密钥。" ;;
|
|
||||||
confirm_warning_2) echo "使用证明的应用将在下次使用时重新注册。" ;;
|
|
||||||
confirm_vol_up) echo "音量+ = 确认清除" ;;
|
|
||||||
confirm_vol_down) echo "音量- = 取消(10秒后默认)" ;;
|
|
||||||
confirm_cancelled) echo "已取消 - 密钥已保留" ;;
|
|
||||||
confirm_cleared) echo "持久化密钥存储已清除" ;;
|
|
||||||
confirm_not_found) echo "未找到持久化密钥存储" ;;
|
|
||||||
esac ;;
|
|
||||||
zh-TW) case "$1" in
|
|
||||||
confirm_header) echo "清除持久化金鑰儲存" ;;
|
|
||||||
confirm_warning_1) echo "這將刪除所有快取的證明金鑰。" ;;
|
|
||||||
confirm_warning_2) echo "使用證明的應用程式將在下次使用時重新註冊。" ;;
|
|
||||||
confirm_vol_up) echo "音量+ = 確認清除" ;;
|
|
||||||
confirm_vol_down) echo "音量- = 取消(10秒後預設)" ;;
|
|
||||||
confirm_cancelled) echo "已取消 - 金鑰已保留" ;;
|
|
||||||
confirm_cleared) echo "持久化金鑰儲存已清除" ;;
|
|
||||||
confirm_not_found) echo "未找到持久化金鑰儲存" ;;
|
|
||||||
esac ;;
|
|
||||||
ja) case "$1" in
|
|
||||||
confirm_header) echo "永続キーストレージを消去" ;;
|
|
||||||
confirm_warning_1) echo "キャッシュされた証明キーをすべて削除します。" ;;
|
|
||||||
confirm_warning_2) echo "証明を使用するアプリは次回使用時に再登録されます。" ;;
|
|
||||||
confirm_vol_up) echo "音量+ = 消去を確認" ;;
|
|
||||||
confirm_vol_down) echo "音量- = キャンセル(10秒後デフォルト)" ;;
|
|
||||||
confirm_cancelled) echo "キャンセルされました - キーは保持されます" ;;
|
|
||||||
confirm_cleared) echo "永続キーストレージを消去しました" ;;
|
|
||||||
confirm_not_found) echo "永続キーストレージが見つかりません" ;;
|
|
||||||
esac ;;
|
|
||||||
ko) case "$1" in
|
|
||||||
confirm_header) echo "영구 키 저장소 지우기" ;;
|
|
||||||
confirm_warning_1) echo "캐시된 모든 증명 키를 삭제합니다." ;;
|
|
||||||
confirm_warning_2) echo "증명을 사용하는 앱은 다음 사용 시 재등록됩니다." ;;
|
|
||||||
confirm_vol_up) echo "볼륨+ = 지우기 확인" ;;
|
|
||||||
confirm_vol_down) echo "볼륨- = 취소 (10초 후 기본값)" ;;
|
|
||||||
confirm_cancelled) echo "취소됨 - 키 유지됨" ;;
|
|
||||||
confirm_cleared) echo "영구 키 저장소가 지워졌습니다" ;;
|
|
||||||
confirm_not_found) echo "영구 키 저장소를 찾을 수 없습니다" ;;
|
|
||||||
esac ;;
|
|
||||||
ru) case "$1" in
|
|
||||||
confirm_header) echo "Очистить постоянное хранилище ключей" ;;
|
|
||||||
confirm_warning_1) echo "Это удалит все кэшированные ключи аттестации." ;;
|
|
||||||
confirm_warning_2) echo "Приложения, использующие аттестацию, перерегистрируются при следующем использовании." ;;
|
|
||||||
confirm_vol_up) echo "Громкость+ = Подтвердить очистку" ;;
|
|
||||||
confirm_vol_down) echo "Громкость- = Отмена (по умолчанию через 10с)" ;;
|
|
||||||
confirm_cancelled) echo "Отменено - ключи сохранены" ;;
|
|
||||||
confirm_cleared) echo "Постоянное хранилище ключей очищено" ;;
|
|
||||||
confirm_not_found) echo "Постоянное хранилище ключей не найдено" ;;
|
|
||||||
esac ;;
|
|
||||||
de) case "$1" in
|
|
||||||
confirm_header) echo "Persistenten Schlüsselspeicher löschen" ;;
|
|
||||||
confirm_warning_1) echo "Dies löscht alle zwischengespeicherten Attestierungsschlüssel." ;;
|
|
||||||
confirm_warning_2) echo "Apps mit Attestierung registrieren sich bei der nächsten Nutzung neu." ;;
|
|
||||||
confirm_vol_up) echo "Laut+ = Löschen bestätigen" ;;
|
|
||||||
confirm_vol_down) echo "Leise- = Abbrechen (Standard nach 10s)" ;;
|
|
||||||
confirm_cancelled) echo "Abgebrochen - Schlüssel beibehalten" ;;
|
|
||||||
confirm_cleared) echo "Persistenter Schlüsselspeicher gelöscht" ;;
|
|
||||||
confirm_not_found) echo "Kein persistenter Schlüsselspeicher gefunden" ;;
|
|
||||||
esac ;;
|
|
||||||
fr) case "$1" in
|
|
||||||
confirm_header) echo "Effacer le stockage de clés persistant" ;;
|
|
||||||
confirm_warning_1) echo "Ceci supprime toutes les clés d'attestation en cache." ;;
|
|
||||||
confirm_warning_2) echo "Les apps utilisant l'attestation se réinscriront à la prochaine utilisation." ;;
|
|
||||||
confirm_vol_up) echo "Vol+ = Confirmer l'effacement" ;;
|
|
||||||
confirm_vol_down) echo "Vol- = Annuler (par défaut après 10s)" ;;
|
|
||||||
confirm_cancelled) echo "Annulé - clés conservées" ;;
|
|
||||||
confirm_cleared) echo "Stockage de clés persistant effacé" ;;
|
|
||||||
confirm_not_found) echo "Aucun stockage de clés persistant trouvé" ;;
|
|
||||||
esac ;;
|
|
||||||
es-ES) case "$1" in
|
|
||||||
confirm_header) echo "Borrar almacenamiento persistente de claves" ;;
|
|
||||||
confirm_warning_1) echo "Esto elimina todas las claves de atestación en caché." ;;
|
|
||||||
confirm_warning_2) echo "Las apps que usan atestación se volverán a registrar en el próximo uso." ;;
|
|
||||||
confirm_vol_up) echo "Vol+ = Confirmar borrado" ;;
|
|
||||||
confirm_vol_down) echo "Vol- = Cancelar (predeterminado tras 10s)" ;;
|
|
||||||
confirm_cancelled) echo "Cancelado - claves conservadas" ;;
|
|
||||||
confirm_cleared) echo "Almacenamiento persistente de claves borrado" ;;
|
|
||||||
confirm_not_found) echo "No se encontró almacenamiento persistente de claves" ;;
|
|
||||||
esac ;;
|
|
||||||
pt-BR) case "$1" in
|
|
||||||
confirm_header) echo "Limpar armazenamento persistente de chaves" ;;
|
|
||||||
confirm_warning_1) echo "Isso exclui todas as chaves de atestação em cache." ;;
|
|
||||||
confirm_warning_2) echo "Apps que usam atestação serão re-registrados no próximo uso." ;;
|
|
||||||
confirm_vol_up) echo "Vol+ = Confirmar limpeza" ;;
|
|
||||||
confirm_vol_down) echo "Vol- = Cancelar (padrão após 10s)" ;;
|
|
||||||
confirm_cancelled) echo "Cancelado - chaves preservadas" ;;
|
|
||||||
confirm_cleared) echo "Armazenamento persistente de chaves limpo" ;;
|
|
||||||
confirm_not_found) echo "Nenhum armazenamento persistente de chaves encontrado" ;;
|
|
||||||
esac ;;
|
|
||||||
it) case "$1" in
|
|
||||||
confirm_header) echo "Cancella archivio chiavi persistente" ;;
|
|
||||||
confirm_warning_1) echo "Questo elimina tutte le chiavi di attestazione in cache." ;;
|
|
||||||
confirm_warning_2) echo "Le app che usano l'attestazione si re-registreranno al prossimo utilizzo." ;;
|
|
||||||
confirm_vol_up) echo "Vol+ = Conferma cancellazione" ;;
|
|
||||||
confirm_vol_down) echo "Vol- = Annulla (predefinito dopo 10s)" ;;
|
|
||||||
confirm_cancelled) echo "Annullato - chiavi conservate" ;;
|
|
||||||
confirm_cleared) echo "Archivio chiavi persistente cancellato" ;;
|
|
||||||
confirm_not_found) echo "Nessun archivio chiavi persistente trovato" ;;
|
|
||||||
esac ;;
|
|
||||||
tr) case "$1" in
|
|
||||||
confirm_header) echo "Kalıcı Anahtar Deposunu Temizle" ;;
|
|
||||||
confirm_warning_1) echo "Bu, önbelleğe alınmış tüm doğrulama anahtarlarını siler." ;;
|
|
||||||
confirm_warning_2) echo "Doğrulama kullanan uygulamalar bir sonraki kullanımda yeniden kaydolacak." ;;
|
|
||||||
confirm_vol_up) echo "Ses+ = Temizlemeyi onayla" ;;
|
|
||||||
confirm_vol_down) echo "Ses- = İptal (10sn sonra varsayılan)" ;;
|
|
||||||
confirm_cancelled) echo "İptal edildi - anahtarlar korundu" ;;
|
|
||||||
confirm_cleared) echo "Kalıcı anahtar deposu temizlendi" ;;
|
|
||||||
confirm_not_found) echo "Kalıcı anahtar deposu bulunamadı" ;;
|
|
||||||
esac ;;
|
|
||||||
id) case "$1" in
|
|
||||||
confirm_header) echo "Hapus Penyimpanan Kunci Persisten" ;;
|
|
||||||
confirm_warning_1) echo "Ini menghapus semua kunci atestasi yang di-cache." ;;
|
|
||||||
confirm_warning_2) echo "Aplikasi yang menggunakan atestasi akan mendaftar ulang saat digunakan." ;;
|
|
||||||
confirm_vol_up) echo "Vol+ = Konfirmasi hapus" ;;
|
|
||||||
confirm_vol_down) echo "Vol- = Batal (default setelah 10 detik)" ;;
|
|
||||||
confirm_cancelled) echo "Dibatalkan - kunci dipertahankan" ;;
|
|
||||||
confirm_cleared) echo "Penyimpanan kunci persisten dihapus" ;;
|
|
||||||
confirm_not_found) echo "Penyimpanan kunci persisten tidak ditemukan" ;;
|
|
||||||
esac ;;
|
|
||||||
vi) case "$1" in
|
|
||||||
confirm_header) echo "Xóa lưu trữ khóa cố định" ;;
|
|
||||||
confirm_warning_1) echo "Thao tác này xóa tất cả khóa chứng thực được lưu cache." ;;
|
|
||||||
confirm_warning_2) echo "Các ứng dụng dùng chứng thực sẽ đăng ký lại khi sử dụng tiếp theo." ;;
|
|
||||||
confirm_vol_up) echo "Vol+ = Xác nhận xóa" ;;
|
|
||||||
confirm_vol_down) echo "Vol- = Hủy (mặc định sau 10s)" ;;
|
|
||||||
confirm_cancelled) echo "Đã hủy - giữ nguyên khóa" ;;
|
|
||||||
confirm_cleared) echo "Đã xóa lưu trữ khóa cố định" ;;
|
|
||||||
confirm_not_found) echo "Không tìm thấy lưu trữ khóa cố định" ;;
|
|
||||||
esac ;;
|
|
||||||
ar) case "$1" in
|
|
||||||
confirm_header) echo "مسح تخزين المفاتيح الدائم" ;;
|
|
||||||
confirm_warning_1) echo "يؤدي هذا إلى حذف جميع مفاتيح التصديق المخزنة مؤقتاً." ;;
|
|
||||||
confirm_warning_2) echo "التطبيقات التي تستخدم التصديق ستعيد التسجيل في الاستخدام التالي." ;;
|
|
||||||
confirm_vol_up) echo "رفع الصوت = تأكيد المسح" ;;
|
|
||||||
confirm_vol_down) echo "خفض الصوت = إلغاء (افتراضي بعد 10 ثوانٍ)" ;;
|
|
||||||
confirm_cancelled) echo "تم الإلغاء - تم الاحتفاظ بالمفاتيح" ;;
|
|
||||||
confirm_cleared) echo "تم مسح تخزين المفاتيح الدائم" ;;
|
|
||||||
confirm_not_found) echo "لم يتم العثور على تخزين مفاتيح دائم" ;;
|
|
||||||
esac ;;
|
|
||||||
th) case "$1" in
|
|
||||||
confirm_header) echo "ล้างที่จัดเก็บคีย์ถาวร" ;;
|
|
||||||
confirm_warning_1) echo "การดำเนินการนี้จะลบคีย์การรับรองที่แคชไว้ทั้งหมด" ;;
|
|
||||||
confirm_warning_2) echo "แอปที่ใช้การรับรองจะลงทะเบียนใหม่ในการใช้งานครั้งถัดไป" ;;
|
|
||||||
confirm_vol_up) echo "เพิ่มเสียง = ยืนยันการล้าง" ;;
|
|
||||||
confirm_vol_down) echo "ลดเสียง = ยกเลิก (ค่าเริ่มต้นหลัง 10 วินาที)" ;;
|
|
||||||
confirm_cancelled) echo "ยกเลิกแล้ว - คีย์ยังคงอยู่" ;;
|
|
||||||
confirm_cleared) echo "ล้างที่จัดเก็บคีย์ถาวรแล้ว" ;;
|
|
||||||
confirm_not_found) echo "ไม่พบที่จัดเก็บคีย์ถาวร" ;;
|
|
||||||
esac ;;
|
|
||||||
uk) case "$1" in
|
|
||||||
confirm_header) echo "Очистити постійне сховище ключів" ;;
|
|
||||||
confirm_warning_1) echo "Це видаляє всі кешовані ключі атестації." ;;
|
|
||||||
confirm_warning_2) echo "Програми, що використовують атестацію, повторно зареєструються при наступному використанні." ;;
|
|
||||||
confirm_vol_up) echo "Гучність+ = Підтвердити очищення" ;;
|
|
||||||
confirm_vol_down) echo "Гучність- = Скасувати (за замовчуванням через 10с)" ;;
|
|
||||||
confirm_cancelled) echo "Скасовано - ключі збережено" ;;
|
|
||||||
confirm_cleared) echo "Постійне сховище ключів очищено" ;;
|
|
||||||
confirm_not_found) echo "Постійне сховище ключів не знайдено" ;;
|
|
||||||
esac ;;
|
|
||||||
pl) case "$1" in
|
|
||||||
confirm_header) echo "Wyczyść trwały magazyn kluczy" ;;
|
|
||||||
confirm_warning_1) echo "To usuwa wszystkie buforowane klucze atestacji." ;;
|
|
||||||
confirm_warning_2) echo "Aplikacje używające atestacji zarejestrują się ponownie przy następnym użyciu." ;;
|
|
||||||
confirm_vol_up) echo "Głośność+ = Potwierdź czyszczenie" ;;
|
|
||||||
confirm_vol_down) echo "Głośność- = Anuluj (domyślnie po 10s)" ;;
|
|
||||||
confirm_cancelled) echo "Anulowano - klucze zachowane" ;;
|
|
||||||
confirm_cleared) echo "Trwały magazyn kluczy wyczyszczony" ;;
|
|
||||||
confirm_not_found) echo "Nie znaleziono trwałego magazynu kluczy" ;;
|
|
||||||
esac ;;
|
|
||||||
az) case "$1" in
|
|
||||||
confirm_header) echo "Davamlı Açar Yaddaşını Təmizlə" ;;
|
|
||||||
confirm_warning_1) echo "Bu, keşlənmiş bütün təsdiqləmə açarlarını silir." ;;
|
|
||||||
confirm_warning_2) echo "Təsdiqləmədən istifadə edən tətbiqlər növbəti istifadədə yenidən qeydiyyatdan keçəcək." ;;
|
|
||||||
confirm_vol_up) echo "Səs+ = Təmizləməni təsdiqlə" ;;
|
|
||||||
confirm_vol_down) echo "Səs- = Ləğv et (10 saniyə sonra defolt)" ;;
|
|
||||||
confirm_cancelled) echo "Ləğv edildi - açarlar saxlanıldı" ;;
|
|
||||||
confirm_cleared) echo "Davamlı açar yaddaşı təmizləndi" ;;
|
|
||||||
confirm_not_found) echo "Davamlı açar yaddaşı tapılmadı" ;;
|
|
||||||
esac ;;
|
|
||||||
bn) case "$1" in
|
|
||||||
confirm_header) echo "স্থায়ী কী সংরক্ষণ পরিষ্কার করুন" ;;
|
|
||||||
confirm_warning_1) echo "এটি সমস্ত ক্যাশড অ্যাটেস্টেশন কী মুছে ফেলে।" ;;
|
|
||||||
confirm_warning_2) echo "অ্যাটেস্টেশন ব্যবহারকারী অ্যাপগুলি পরবর্তী ব্যবহারে পুনরায় নিবন্ধন করবে।" ;;
|
|
||||||
confirm_vol_up) echo "ভলিউম+ = পরিষ্কার নিশ্চিত করুন" ;;
|
|
||||||
confirm_vol_down) echo "ভলিউম- = বাতিল (১০ সেকেন্ডে ডিফল্ট)" ;;
|
|
||||||
confirm_cancelled) echo "বাতিল করা হয়েছে - কী সংরক্ষিত" ;;
|
|
||||||
confirm_cleared) echo "স্থায়ী কী সংরক্ষণ পরিষ্কার করা হয়েছে" ;;
|
|
||||||
confirm_not_found) echo "কোনো স্থায়ী কী সংরক্ষণ পাওয়া যায়নি" ;;
|
|
||||||
esac ;;
|
|
||||||
el) case "$1" in
|
|
||||||
confirm_header) echo "Εκκαθάριση Μόνιμου Αποθηκευτικού Χώρου Κλειδιών" ;;
|
|
||||||
confirm_warning_1) echo "Διαγράφει όλα τα προσωρινά αποθηκευμένα κλειδιά πιστοποίησης." ;;
|
|
||||||
confirm_warning_2) echo "Οι εφαρμογές που χρησιμοποιούν πιστοποίηση θα επανεγγραφούν στην επόμενη χρήση." ;;
|
|
||||||
confirm_vol_up) echo "Ένταση+ = Επιβεβαίωση εκκαθάρισης" ;;
|
|
||||||
confirm_vol_down) echo "Ένταση- = Ακύρωση (προεπιλογή μετά από 10 δευτ)" ;;
|
|
||||||
confirm_cancelled) echo "Ακυρώθηκε - τα κλειδιά διατηρήθηκαν" ;;
|
|
||||||
confirm_cleared) echo "Ο μόνιμος αποθηκευτικός χώρος κλειδιών εκκαθαρίστηκε" ;;
|
|
||||||
confirm_not_found) echo "Δεν βρέθηκε μόνιμος αποθηκευτικός χώρος κλειδιών" ;;
|
|
||||||
esac ;;
|
|
||||||
fa) case "$1" in
|
|
||||||
confirm_header) echo "پاک کردن ذخیرهسازی دائمی کلید" ;;
|
|
||||||
confirm_warning_1) echo "این کار همه کلیدهای تأیید کششده را حذف میکند." ;;
|
|
||||||
confirm_warning_2) echo "برنامههای استفادهکننده از تأیید در استفاده بعدی دوباره ثبتنام میکنند." ;;
|
|
||||||
confirm_vol_up) echo "صدا+ = تأیید پاک کردن" ;;
|
|
||||||
confirm_vol_down) echo "صدا- = لغو (پیشفرض پس از ۱۰ ثانیه)" ;;
|
|
||||||
confirm_cancelled) echo "لغو شد - کلیدها حفظ شدند" ;;
|
|
||||||
confirm_cleared) echo "ذخیرهسازی دائمی کلید پاک شد" ;;
|
|
||||||
confirm_not_found) echo "ذخیرهسازی دائمی کلید یافت نشد" ;;
|
|
||||||
esac ;;
|
|
||||||
tl) case "$1" in
|
|
||||||
confirm_header) echo "Burahin ang Persistent Key Storage" ;;
|
|
||||||
confirm_warning_1) echo "Buburahin nito ang lahat ng naka-cache na attestation keys." ;;
|
|
||||||
confirm_warning_2) echo "Magre-rehistro muli ang mga app na gumagamit ng attestation sa susunod na paggamit." ;;
|
|
||||||
confirm_vol_up) echo "Vol+ = Kumpirmahin ang pagbura" ;;
|
|
||||||
confirm_vol_down) echo "Vol- = Kanselahin (default pagkatapos ng 10s)" ;;
|
|
||||||
confirm_cancelled) echo "Nakansela - napanatili ang mga key" ;;
|
|
||||||
confirm_cleared) echo "Nabura ang persistent key storage" ;;
|
|
||||||
confirm_not_found) echo "Walang nahanap na persistent key storage" ;;
|
|
||||||
esac ;;
|
|
||||||
*) case "$1" in
|
|
||||||
confirm_header) echo "Clear Persistent Key Storage" ;;
|
|
||||||
confirm_warning_1) echo "This deletes all cached attestation keys." ;;
|
|
||||||
confirm_warning_2) echo "Apps using attestation will re-enroll on next use." ;;
|
|
||||||
confirm_vol_up) echo "Vol+ = Confirm clear" ;;
|
|
||||||
confirm_vol_down) echo "Vol- = Cancel (default after 10s)" ;;
|
|
||||||
confirm_cancelled) echo "Cancelled - keys preserved" ;;
|
|
||||||
confirm_cleared) echo "Persistent key storage cleared" ;;
|
|
||||||
confirm_not_found) echo "No persistent key storage found" ;;
|
|
||||||
esac ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
+42
-18
@@ -1,26 +1,50 @@
|
|||||||
## TEESimulator-RS v6.0.0
|
## TEESimulator-RS v5.1: Interception Architecture Rewrite
|
||||||
|
|
||||||
Repository consolidation release. All tee-rebuild work merged as the new main branch.
|
Major release. 27 files changed, 2300 lines rewritten. The entire Kotlin interception layer has been rebuilt with a clean architecture, proper AIDL alignment, and significantly lower binder overhead.
|
||||||
|
|
||||||
### AOSP Self-Signed Cert Compliance
|
### Interception Layer Rewrite
|
||||||
- No-challenge keys now generate self-signed certs (subject == issuer, depth 1), matching AOSP `ta/src/keys.rs:451-478`
|
- KeyMintSecurityLevelInterceptor completely restructured: GeneratedKeyInfo now carries full KeyMintAttestation instead of nullable stub, eliminating scattered null checks across every operation path
|
||||||
- Both Kotlin (BouncyCastle) and Rust (native-certgen) paths corrected
|
- SoftwareOperation rewritten with sealed CryptoPrimitive interface separating Signer, Verifier, Encryptor, and Decryptor into isolated implementations with proper JCA algorithm mapping
|
||||||
- Eliminates attestation behavioral probes that detect keybox issuer on non-attested keys
|
- KeystoreErrorCode centralized object replaces scattered magic numbers for all KeyMint and Keystore2 error codes
|
||||||
|
- listEntries moved from pre-transact parameter caching to post-transact injection, eliminating a race condition where cached params could go stale
|
||||||
|
- deleteKey now handles both APP domain (by alias) and KEY_ID domain (by nspace) resolution paths correctly
|
||||||
|
- AuthorizeCreate and AndroidPermissionUtils removed, authorization logic consolidated into the operation dispatch path
|
||||||
|
- DeviceAttestationService removed, attestation routing simplified into the main interceptor
|
||||||
|
|
||||||
### Stability
|
### Software Crypto Operations
|
||||||
- Binder stress crash hardening for concurrent generateKey calls
|
- GCM nonce returned in CreateOperationResponse.parameters for encrypt operations, matching real KeyMint HAL behavior
|
||||||
- AUTO mode TEE race for consistent attestation on devices with working G10
|
- updateAad correctly throws INVALID_TAG on non-AEAD operations instead of silently succeeding
|
||||||
- Oversized transactions routed to software gen instead of crashing
|
- Cipher algorithm mapping cleaned up: dropped CTR block mode and RSA_PKCS1_1_5_SIGN padding that caused JCA provider mismatches
|
||||||
- Operation-time params (BLOCK_MODE, PADDING, DIGEST) passed through to CipherPrimitive
|
- All crypto exceptions wrapped as ServiceSpecificException with correct KeyMint error codes instead of raw exceptions
|
||||||
|
|
||||||
### Banking App Compatibility
|
### Attestation & Certificate Generation
|
||||||
- Bare `target.txt` entries now default to AUTO mode, resolved at config level to PATCH (working TEE) or GENERATE (broken TEE)
|
- CertificateGenerator rewritten with clean Kotlin Pair return type instead of Android's util.Pair
|
||||||
- Fixes BHIM and similar banking apps that require TEE-backed attestation keys
|
- AttestationBuilder field ordering aligned with AOSP KeyDescription ASN.1 schema
|
||||||
- Restores v5.0 behavior where AUTO was resolved before the interceptor dispatch, avoiding the non-deterministic `raceTeePatch` path
|
- Unique ID computation follows KeyMint HAL spec: HMAC-SHA256(temporal_counter || AAID || reset_flag, HBK) truncated to 128 bits
|
||||||
|
- Patch level logging removed from hot path to reduce logcat noise on every attestation
|
||||||
|
|
||||||
### Infrastructure
|
### Configuration & Device Properties
|
||||||
- Version scheme changed to semver (v6.0.0)
|
- ConfigurationManager target package parsing refactored: mode/package extraction deduplicated across GENERATE/PATCH/AUTO branches
|
||||||
- Repository moved to TEESimulator-RS as canonical source
|
- system=prop forced boot/vendor override removed, now respects explicit per-component patch level configuration
|
||||||
|
- FileObserver delete handler simplified with direct file access instead of defensive null-checks
|
||||||
|
- AndroidDeviceUtils expanded with additional device property accessors for attestation fields
|
||||||
|
|
||||||
|
### Binder Performance
|
||||||
|
- Safe parcel reads at 6 deserialization sites, replacing force-unwrap NPE paths with early-return on null. A single NPE generates a full stack trace that blocks the binder thread for ~2ms
|
||||||
|
- teeResponses cache populated on generateKey/importKey post-transact, reducing getKeyEntry from 2+ binder round-trips to 1
|
||||||
|
- pingBinder liveness check removed from pre-transact failure path, eliminating a synchronous IPC call on every failed transaction
|
||||||
|
- Native transaction code filtering at C++ level, skipping JNI entirely for PING/INTERFACE/DUMP
|
||||||
|
|
||||||
|
### Dynamic SecurityLevel Binder Registration
|
||||||
|
- Intercepts getSecurityLevel replies to register hooks on every new BBinder instance keystore2 returns, not just the initial one from setup
|
||||||
|
- Identity hash deduplication prevents double-hooking when keystore2 returns the same binder across multiple calls
|
||||||
|
- Resolves apps that call getSecurityLevel independently and receive a different binder than the one registered at startup
|
||||||
|
|
||||||
|
### Build & Packaging
|
||||||
|
- Rust native build task integrated into Gradle with cargo-ndk for aarch64/armv7/x86/x86_64
|
||||||
|
- Module ZIP includes all 4 native libraries (libTEESimulator, libsupervisor, libcertgen, libinject)
|
||||||
|
- customize.sh extraction restored for supervisor daemon and native cert gen library
|
||||||
|
- TeeLatencySimulator added as standalone utility for log-normal hardware latency emulation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+1
-12
@@ -48,7 +48,7 @@ install_file() {
|
|||||||
|
|
||||||
# --- Installation ---
|
# --- Installation ---
|
||||||
ui_print "- Extracting module files"
|
ui_print "- Extracting module files"
|
||||||
for file in customize.sh module.prop service.sh sepolicy.rule daemon action.sh action_i18n.sh uninstall.sh; do
|
for file in customize.sh module.prop service.sh sepolicy.rule daemon action.sh uninstall.sh; do
|
||||||
install_file "$file" "$MODPATH"
|
install_file "$file" "$MODPATH"
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -92,17 +92,6 @@ if [ ! -f "$CONFIG_DIR/target.txt" ]; then
|
|||||||
install_file "target.txt" "$CONFIG_DIR"
|
install_file "target.txt" "$CONFIG_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "$CONFIG_DIR/security_patch.txt" ]; then
|
|
||||||
ui_print "- Adding default security patch config (mirror device props)"
|
|
||||||
printf '%s\n' \
|
|
||||||
'# TEESimulator default: mirror live device props.' \
|
|
||||||
'# system=prop reads ro.build.version.security_patch at cert-gen time;' \
|
|
||||||
'# boot and vendor are auto-forced to prop too (ConfigurationManager.kt:253-256).' \
|
|
||||||
'# Override with explicit YYYY-MM-DD dates if you want active spoofing.' \
|
|
||||||
'system=prop' > "$CONFIG_DIR/security_patch.txt"
|
|
||||||
chmod 644 "$CONFIG_DIR/security_patch.txt"
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$CONFIG_DIR/tee_status.txt"
|
rm -f "$CONFIG_DIR/tee_status.txt"
|
||||||
|
|
||||||
if [ ! -f "$CONFIG_DIR/hbk" ]; then
|
if [ ! -f "$CONFIG_DIR/hbk" ]; then
|
||||||
|
|||||||
@@ -1,16 +1,2 @@
|
|||||||
allow keystore {adb_data_file shell_data_file} file *
|
allow keystore {adb_data_file shell_data_file} file *
|
||||||
allow crash_dump keystore process *
|
allow crash_dump keystore process *
|
||||||
|
|
||||||
allow ksu self:tcp_socket { create connect read write getopt setopt }
|
|
||||||
allow ksu node:tcp_socket node_bind
|
|
||||||
allow ksu port:tcp_socket name_connect
|
|
||||||
allow magisk self:tcp_socket { create connect read write getopt setopt }
|
|
||||||
allow magisk node:tcp_socket node_bind
|
|
||||||
allow magisk port:tcp_socket name_connect
|
|
||||||
|
|
||||||
allow ksu self:udp_socket { create connect read write getopt setopt }
|
|
||||||
allow ksu node:udp_socket node_bind
|
|
||||||
allow ksu port:udp_socket name_connect
|
|
||||||
allow magisk self:udp_socket { create connect read write getopt setopt }
|
|
||||||
allow magisk node:udp_socket node_bind
|
|
||||||
allow magisk port:udp_socket name_connect
|
|
||||||
|
|||||||
@@ -10,4 +10,3 @@ done
|
|||||||
rm -rf "$CONFIG_DIR/persistent_keys"
|
rm -rf "$CONFIG_DIR/persistent_keys"
|
||||||
rm -f "$CONFIG_DIR/tee_status.txt"
|
rm -f "$CONFIG_DIR/tee_status.txt"
|
||||||
rm -f "$CONFIG_DIR/boot_hash.bin" "$CONFIG_DIR/boot_key.bin"
|
rm -f "$CONFIG_DIR/boot_hash.bin" "$CONFIG_DIR/boot_key.bin"
|
||||||
rm -f "$CONFIG_DIR/security_patch.txt" "$CONFIG_DIR/security_patch.txt.next" "$CONFIG_DIR/last_bulletin_fetch.json"
|
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "v6.0.0-211",
|
"version": "v4.5",
|
||||||
"versionCode": 211,
|
"versionCode": 111,
|
||||||
"zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/download/v6.0.0-211/TEESimulator-RS-v6.0.0-211-Release.zip",
|
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.5/TEESimulator-v4.5-Release.zip",
|
||||||
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/changelog.md"
|
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,69 +14,9 @@ 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: Option<&[u8]>,
|
attestation_ext_der: &[u8],
|
||||||
keybox: &ParsedKeybox,
|
keybox: &ParsedKeybox,
|
||||||
params: &CertGenParams,
|
params: &CertGenParams,
|
||||||
) -> Result<Vec<Vec<u8>>> {
|
) -> Result<Vec<Vec<u8>>> {
|
||||||
@@ -93,7 +33,7 @@ pub fn build_certificate_chain(
|
|||||||
|
|
||||||
fn build_leaf_cert(
|
fn build_leaf_cert(
|
||||||
key_pair: &GeneratedKeyPair,
|
key_pair: &GeneratedKeyPair,
|
||||||
attestation_ext_der: Option<&[u8]>,
|
attestation_ext_der: &[u8],
|
||||||
keybox: &ParsedKeybox,
|
keybox: &ParsedKeybox,
|
||||||
params: &CertGenParams,
|
params: &CertGenParams,
|
||||||
) -> Result<Vec<u8>> {
|
) -> Result<Vec<u8>> {
|
||||||
@@ -123,6 +63,7 @@ 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
|
||||||
@@ -315,19 +256,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: Option<&[u8]>, purposes: &[i32]) -> Result<Vec<u8>> {
|
fn build_extensions(attestation_ext_der: &[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);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(attest_der) = attestation_ext_der {
|
// Attestation extension (non-critical)
|
||||||
let attest_ext = build_extension(&encode_der_oid(ATTESTATION_OID), false, attest_der);
|
let attest_ext = build_extension(&encode_der_oid(ATTESTATION_OID), false, attestation_ext_der);
|
||||||
extensions.push(attest_ext);
|
extensions.push(attest_ext);
|
||||||
}
|
|
||||||
|
|
||||||
Ok(encode_der_sequence_of(&extensions))
|
Ok(encode_der_sequence_of(&extensions))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,13 +62,14 @@ 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 cert_chain = if params.attestation_challenge.is_some() {
|
let attest_ext = attestation::build_attestation_extension(¶ms)?;
|
||||||
let attest_ext = attestation::build_attestation_extension(¶ms)?;
|
|
||||||
certbuilder::build_certificate_chain(&key_pair, Some(&attest_ext), &keybox, ¶ms)?
|
let cert_chain = certbuilder::build_certificate_chain(
|
||||||
} else {
|
&key_pair,
|
||||||
tracing::info!("no attestation challenge, generating self-signed cert (depth 1)");
|
&attest_ext,
|
||||||
certbuilder::build_self_signed_cert(&key_pair, ¶ms)?
|
&keybox,
|
||||||
};
|
¶ms,
|
||||||
|
)?;
|
||||||
|
|
||||||
let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain);
|
let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain);
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,6 @@
|
|||||||
# ./scripts/package.sh --rust --release # build Rust crate first, then release
|
# ./scripts/package.sh --rust --release # build Rust crate first, then release
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Gradle's buildRustCertgen resolves `cargo` against the daemon's inherited PATH,
|
|
||||||
# not the env we inject via gradle's Exec.environment(). Prepend the per-user
|
|
||||||
# rustup install so non-login shells (CI, IDE-launched terminals, fresh tmux)
|
|
||||||
# still find it without sourcing /etc/profile.d/cargo-path.sh.
|
|
||||||
[ -d "$HOME/.cargo/bin" ] && PATH="$HOME/.cargo/bin:$PATH"
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
OUT_DIR="$PROJECT_ROOT/out"
|
OUT_DIR="$PROJECT_ROOT/out"
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package android.hardware.security.keymint;
|
||||||
|
|
||||||
|
public @interface HardwareAuthenticatorType {
|
||||||
|
int NONE = 0;
|
||||||
|
int PASSWORD = 1;
|
||||||
|
int FINGERPRINT = 2;
|
||||||
|
int ANY = -1;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package android.os;
|
package android.os;
|
||||||
|
|
||||||
|
/** Stub for android.os.SELinux. */
|
||||||
public class SELinux {
|
public class SELinux {
|
||||||
public static boolean checkSELinuxAccess(
|
public static boolean checkSELinuxAccess(
|
||||||
String scon, String tcon, String tclass, String perm) {
|
String scon, String tcon, String tclass, String perm) {
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
package android.os;
|
package android.os;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stub for android.os.ServiceSpecificException.
|
||||||
|
*
|
||||||
|
* <p>Used by AIDL-generated binder stubs to report service-specific errors with numeric codes.
|
||||||
|
* The binder framework serializes this as EX_SERVICE_SPECIFIC on the wire, preserving the integer
|
||||||
|
* error code for the client.
|
||||||
|
*/
|
||||||
public class ServiceSpecificException extends RuntimeException {
|
public class ServiceSpecificException extends RuntimeException {
|
||||||
public final int errorCode;
|
public final int errorCode;
|
||||||
|
|
||||||
public ServiceSpecificException(int errorCode) {
|
|
||||||
this.errorCode = errorCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ServiceSpecificException(int errorCode, String message) {
|
public ServiceSpecificException(int errorCode, String message) {
|
||||||
super(message);
|
super(message);
|
||||||
this.errorCode = errorCode;
|
this.errorCode = errorCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ServiceSpecificException(int errorCode) {
|
||||||
|
this(errorCode, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user