Compare commits
144
Commits
v2.1
...
v5.1.1-164
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e74ebbe82 | ||
|
|
315f41f434 | ||
|
|
9be0874e93 | ||
|
|
25dbddf733 | ||
|
|
9a7011eb5e | ||
|
|
b63570a3e2 | ||
|
|
3d7fd427a6 | ||
|
|
b2bf0ce599 | ||
|
|
63789ba29d | ||
|
|
40c7b6bd15 | ||
|
|
1df30b9345 | ||
|
|
4e83a846c9 | ||
|
|
3aff09e7dd | ||
|
|
c7b0af2d29 | ||
|
|
13a1dd7887 | ||
|
|
21a3cb1ec0 | ||
|
|
2b31d4ef47 | ||
|
|
38e9b547a5 | ||
|
|
94c8e5b182 | ||
|
|
8fdc59a142 | ||
|
|
b6f9d7b486 | ||
|
|
99e6b0b5ea | ||
|
|
397bb6338b | ||
|
|
cb81116701 | ||
|
|
45477a7898 | ||
|
|
634a1293c1 | ||
|
|
f115eda2dc | ||
|
|
217edf61fe | ||
|
|
ee5bf2e1a7 | ||
|
|
2181157cb6 | ||
|
|
aa4917e623 | ||
|
|
f06cb30b40 | ||
|
|
03c71bd202 | ||
|
|
7e2fc0b288 | ||
|
|
258a65ba59 | ||
|
|
d2b8a92fbd | ||
|
|
0723865eab | ||
|
|
7cb44b9999 | ||
|
|
eddd9908af | ||
|
|
36ccd22cdc | ||
|
|
81e6fbf97e | ||
|
|
7f63713f07 | ||
|
|
5df76eacd1 | ||
|
|
ca3978888e | ||
|
|
023d7f929d | ||
|
|
bd40f4b950 | ||
|
|
23696d2f61 | ||
|
|
dfacb34cf9 | ||
|
|
06d9db443c | ||
|
|
7e87766493 | ||
|
|
f8bfa0dfd8 | ||
|
|
90ff59e0aa | ||
|
|
8bdf0d59fa | ||
|
|
6ab09f4889 | ||
|
|
f4559bcd19 | ||
|
|
3b5043a1bb | ||
|
|
8001a8678a | ||
|
|
d21822eb9d | ||
|
|
095a658996 | ||
|
|
70e8968e44 | ||
|
|
c122ded7bf | ||
|
|
7b510a9915 | ||
|
|
8f63dda31b | ||
|
|
9896df93de | ||
|
|
0280bcf189 | ||
|
|
438a462bdf | ||
|
|
40b08cd648 | ||
|
|
c5ed627f68 | ||
|
|
bee73eb39b | ||
|
|
a0ee77202c | ||
|
|
09d9896228 | ||
|
|
5a599025ad | ||
|
|
f5c2bcc024 | ||
|
|
85e34ed42f | ||
|
|
52b04675e7 | ||
|
|
ba0628c687 | ||
|
|
c11465d660 | ||
|
|
0d5fd44992 | ||
|
|
776a7b2343 | ||
|
|
14786ecce0 | ||
|
|
41b9cc8f10 | ||
|
|
6df266b688 | ||
|
|
32cfcb3ece | ||
|
|
727b32f6b0 | ||
|
|
ac9641ed2a | ||
|
|
c87759c6c3 | ||
|
|
76b18706f1 | ||
|
|
e76f5115e0 | ||
|
|
0725aed094 | ||
|
|
9d61af6ce8 | ||
|
|
7863e8dd17 | ||
|
|
f840eed42b | ||
|
|
4c7f0a09ea | ||
|
|
4971f7b4a5 | ||
|
|
9ea39f0545 | ||
|
|
c332f8ad0d | ||
|
|
d76e6abeed | ||
|
|
142fe7bf13 | ||
|
|
e41a679928 | ||
|
|
209d5c8902 | ||
|
|
3817b37e18 | ||
|
|
53ae250fc7 | ||
|
|
34ad97366e | ||
|
|
593bcfef83 | ||
|
|
5e68cb5f4b | ||
|
|
a1bb3bbfa3 | ||
|
|
e13adb925d | ||
|
|
c3f8f087a6 | ||
|
|
51f32b9db2 | ||
|
|
d60ad8fe47 | ||
|
|
9a1fbe8c79 | ||
|
|
68b660dfe1 | ||
|
|
068188503c | ||
|
|
1bbc50d138 | ||
|
|
d2492df02e | ||
|
|
e7d7b21daa | ||
|
|
c29bc35a36 | ||
|
|
549b5cecc2 | ||
|
|
04d003ff4d | ||
|
|
0a842c6e07 | ||
|
|
9f77771e7b | ||
|
|
ab4fe643a3 | ||
|
|
ce740542f7 | ||
|
|
c27523fd97 | ||
|
|
5a8454af7b | ||
|
|
83b65f09c9 | ||
|
|
2a76b18308 | ||
|
|
d9e47712f3 | ||
|
|
d846de4332 | ||
|
|
13d89c4314 | ||
|
|
00c91adfaa | ||
|
|
119350f24b | ||
|
|
7d4c753d66 | ||
|
|
b988d04971 | ||
|
|
d0cc5e3b56 | ||
|
|
e66e558ce5 | ||
|
|
8d431cc946 | ||
|
|
30746892b0 | ||
|
|
28cfe70a85 | ||
|
|
65a613ae0e | ||
|
|
9146b86648 | ||
|
|
457a58da04 | ||
|
|
b2838ac04b | ||
|
|
a7534feac7 |
@@ -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
|
||||||
+93
-73
@@ -3,16 +3,10 @@ name: Build
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
paths-ignore:
|
paths-ignore: [ '**.md' ]
|
||||||
- '**.md'
|
|
||||||
- '.github/**'
|
|
||||||
- '!.github/workflows/**'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
paths-ignore:
|
paths-ignore: [ '**.md' ]
|
||||||
- '**.md'
|
|
||||||
- '.github/**'
|
|
||||||
- '!.github/workflows/**'
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -22,18 +16,9 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
attestations: write
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
outputs:
|
|
||||||
releaseName: ${{ steps.prepareArtifact.outputs.releaseName }}
|
|
||||||
debugName: ${{ steps.prepareArtifact.outputs.debugName }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
with:
|
||||||
submodules: "recursive"
|
submodules: "recursive"
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -45,6 +30,25 @@ jobs:
|
|||||||
java-version: 21
|
java-version: 21
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
|
- name: Setup Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android
|
||||||
|
|
||||||
|
- name: Cache Rust artifacts
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
~/.cargo/bin/cargo-ndk
|
||||||
|
native-certgen/target
|
||||||
|
key: rust-${{ runner.os }}-${{ hashFiles('native-certgen/Cargo.lock') }}
|
||||||
|
restore-keys: rust-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Install cargo-ndk
|
||||||
|
run: command -v cargo-ndk || cargo install cargo-ndk
|
||||||
|
|
||||||
- name: Set up ccache
|
- name: Set up ccache
|
||||||
uses: hendrikmuhs/ccache-action@v1.2
|
uses: hendrikmuhs/ccache-action@v1.2
|
||||||
with:
|
with:
|
||||||
@@ -60,72 +64,88 @@ jobs:
|
|||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: |
|
run: |
|
||||||
chmod +x ./gradlew
|
chmod +x ./gradlew
|
||||||
./gradlew --parallel zipRelease zipDebug --stacktrace
|
./gradlew zipRelease zipDebug -Porg.gradle.parallel=true -Porg.gradle.vfs.watch=true -Dorg.gradle.jvmargs=-Xmx2048m
|
||||||
|
|
||||||
- name: Prepare artifact
|
- name: Read version
|
||||||
if: success()
|
id: ver
|
||||||
id: prepareArtifact
|
|
||||||
run: |
|
run: |
|
||||||
set -e
|
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
|
||||||
RELEASE_FILE=$(find out -name "*Release*.zip" | head -1)
|
count=$(git rev-list HEAD --count)
|
||||||
DEBUG_FILE=$(find out -name "*Debug*.zip" | head -1)
|
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
if [[ -z "$RELEASE_FILE" || -z "$DEBUG_FILE" ]]; then
|
|
||||||
echo "Error: Could not find release or debug files in out/"
|
|
||||||
echo "Contents of out/ directory:"
|
|
||||||
ls -la out/ || echo "out/ directory does not exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract names
|
|
||||||
RELEASE_NAME=$(basename "$RELEASE_FILE" .zip)
|
|
||||||
DEBUG_NAME=$(basename "$DEBUG_FILE" .zip)
|
|
||||||
|
|
||||||
echo "releaseName=$RELEASE_NAME" >> $GITHUB_OUTPUT
|
|
||||||
echo "debugName=$DEBUG_NAME" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
mkdir -p module-release module-debug
|
- name: List build artifacts
|
||||||
unzip -q "$RELEASE_FILE" -d module-release
|
run: |
|
||||||
unzip -q "$DEBUG_FILE" -d module-debug
|
echo "Release: $(ls out/*Release*.zip | head -1) ($(du -h out/*Release*.zip | head -1 | cut -f1))"
|
||||||
echo " Release: $RELEASE_NAME"
|
echo "Debug: $(ls out/*Debug*.zip | head -1) ($(du -h out/*Debug*.zip | head -1 | cut -f1))"
|
||||||
echo " Debug: $DEBUG_NAME"
|
|
||||||
|
|
||||||
- name: Upload release
|
- uses: actions/upload-artifact@v4
|
||||||
if: success()
|
|
||||||
id: release
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.prepareArtifact.outputs.releaseName }}
|
name: TEESimulator-RS-release-zip
|
||||||
path: "./module-release/*"
|
path: out/TEESimulator-RS-*-Release.zip
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
compression-level: 6
|
compression-level: 0
|
||||||
|
|
||||||
- name: Upload debug
|
- uses: actions/upload-artifact@v4
|
||||||
if: success()
|
|
||||||
id: debug
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.prepareArtifact.outputs.debugName }}
|
name: TEESimulator-RS-debug-zip
|
||||||
path: "./module-debug/*"
|
path: out/TEESimulator-RS-*-Debug.zip
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
compression-level: 6
|
compression-level: 0
|
||||||
|
|
||||||
- name: Upload release mappings
|
- uses: actions/upload-artifact@v4
|
||||||
if: success()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: release-mappings-${{ github.run_number }}
|
name: release-mappings
|
||||||
path: "./app/build/outputs/mapping/release"
|
path: app/build/outputs/mapping/release
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
compression-level: 9
|
compression-level: 9
|
||||||
|
|
||||||
- name: Summary
|
release:
|
||||||
if: always()
|
needs: build
|
||||||
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Read version
|
||||||
|
id: ver
|
||||||
run: |
|
run: |
|
||||||
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
|
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
|
||||||
echo "- **Status**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
|
count=$(git rev-list HEAD --count)
|
||||||
echo "- **Gradle Tasks**: assembleRelease, assembleDebug" >> $GITHUB_STEP_SUMMARY
|
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
|
||||||
if [[ "${{ job.status }}" == "success" ]]; then
|
|
||||||
echo "- **Release Artifact**: ${{ steps.prepareArtifact.outputs.releaseName }}" >> $GITHUB_STEP_SUMMARY
|
- uses: actions/download-artifact@v4
|
||||||
echo "- **Debug Artifact**: ${{ steps.prepareArtifact.outputs.debugName }}" >> $GITHUB_STEP_SUMMARY
|
with:
|
||||||
fi
|
name: TEESimulator-RS-release-zip
|
||||||
|
path: zips
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: TEESimulator-RS-debug-zip
|
||||||
|
path: zips
|
||||||
|
|
||||||
|
- name: Extract changelog
|
||||||
|
run: |
|
||||||
|
ver="${VER#v}"
|
||||||
|
awk "/^## TEESimulator-RS v${ver%%-*}/{flag=1; next} /^## TEESimulator-RS v/{if(flag) exit} flag" module/changelog.md > /tmp/notes.md
|
||||||
|
cat /tmp/notes.md
|
||||||
|
env:
|
||||||
|
VER: ${{ steps.ver.outputs.version }}
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
run: |
|
||||||
|
gh release delete "$VER" --yes 2>/dev/null || true
|
||||||
|
RELEASE=$(ls zips/*Release*.zip | head -1)
|
||||||
|
DEBUG=$(ls zips/*Debug*.zip | head -1)
|
||||||
|
gh release create "$VER" \
|
||||||
|
--title "$VER" \
|
||||||
|
--latest \
|
||||||
|
--notes-file /tmp/notes.md \
|
||||||
|
"$RELEASE" \
|
||||||
|
"$DEBUG"
|
||||||
|
env:
|
||||||
|
VER: ${{ steps.ver.outputs.version }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
out
|
out
|
||||||
|
.gradle
|
||||||
|
.kotlin
|
||||||
|
app/build
|
||||||
|
build
|
||||||
|
native-certgen/target
|
||||||
|
app/src/main/jniLibs
|
||||||
|
|||||||
@@ -1,32 +1,143 @@
|
|||||||
# TEESimulator – A Full TEE Emulation Framework
|
<p align="center">
|
||||||
|
<h1 align="center">🔐 TEESimulator</h1>
|
||||||
|
<p align="center"><b>Full TEE Emulation for Rooted Android</b></p>
|
||||||
|
<p align="center">Hardware attestation. Software keys. Zero detection.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml"><img src="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml/badge.svg" alt="Build"></a>
|
||||||
|
<img src="https://img.shields.io/badge/version-v4.2-blue?style=for-the-badge" alt="v4.2">
|
||||||
|
<img src="https://img.shields.io/badge/Android-10%2B-green?style=for-the-badge&logo=android" alt="Android 10+">
|
||||||
|
<img src="https://img.shields.io/badge/Telegram-community-blue?style=for-the-badge&logo=telegram" alt="Telegram">
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
|
||||||
**TEESimulator** is a system module designed to create a complete, software-based simulation of a hardware-backed Trusted Execution Environment ([TEE](https://source.android.com/docs/security/features/trusty)) for [Key Attestation](https://developer.android.com/privacy-and-security/security-key-attestation).
|
---
|
||||||
|
|
||||||
The project's goal is to move beyond simple certificate patching and build a robust framework that can create and manage virtual, self-consistent cryptographic keys.
|
> [!NOTE]
|
||||||
|
> **This is a personal fork of [JingMatrix/TEESimulator](https://github.com/JingMatrix/TEESimulator)** with additional hardening, native Rust certificate generation, key persistence, and anti-detection features. For the upstream project, see the original repo.
|
||||||
|
|
||||||
## ✨ Core Principles
|
---
|
||||||
|
|
||||||
* **Bypass Hardware-Backed Attestation:** The primary goal of this project is to defeat Key Attestation, a security mechanism that allows apps to verify that they are running on a secure, unmodified device. This module provides the tools to bypass these checks on rooted or modified devices.
|
## 🧬 What is TEESimulator?
|
||||||
* **Stateful Emulation:** Instead of patching responses from the real TEE, the ultimate goal is to create and manage virtual keys entirely in a simulated software environment. Any request concerning a virtual key will be handled by the simulator, ensuring perfect consistency without ever touching the real hardware.
|
|
||||||
* **Architectural Interception:** By hooking low-level Binder IPC calls to the Keystore, the framework can transparently redirect requests for virtual keys to the software-based simulator, while allowing requests for real keys to pass through to the hardware TEE.
|
|
||||||
* **100% FOSS:** Licensed under GPLv3, ensuring it stays free, auditable, and compliant with open-source laws.
|
|
||||||
|
|
||||||
## 📱 Requirements
|
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.
|
||||||
- Android 10 or above
|
|
||||||
|
|
||||||
## 📦 Installation & Configuration
|
The result: **apps that verify hardware attestation see a legitimate, unmodified device** — even on rooted hardware with an unlocked bootloader.
|
||||||
|
|
||||||
1. Flash this module via (Magisk / KernelSU / APatch) and reboot. It will replace [TrickyStore](https://github.com/5ec1cff/TrickyStore), [TrickyStoreOSS](https://github.com/beakthoven/TrickyStoreOSS) and their forks.
|
> **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.
|
||||||
2. (Optional) Place a hardware-backed `keybox.xml` at `/data/adb/tricky_store/keybox.xml`. This provides the cryptographic "root of trust" for the simulator.
|
|
||||||
3. (Optional) Customize target packages in `/data/adb/tricky_store/target.txt`.
|
|
||||||
4. (Optional) Customize the simulated security patch level in `/data/adb/tricky_store/security_patch.txt`.
|
|
||||||
5. Enjoy!
|
|
||||||
|
|
||||||
**All configuration files are monitored and will take effect immediately upon saving.**
|
---
|
||||||
|
|
||||||
|
## 🔥 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]
|
||||||
|
> 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.
|
||||||
|
|
||||||
|
**You need:**
|
||||||
|
1. Android 10 or above
|
||||||
|
2. A supported root manager (KernelSU, Magisk, or APatch)
|
||||||
|
3. A hardware-backed `keybox.xml` placed at `/data/adb/tricky_store/keybox.xml`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Compatibility
|
||||||
|
|
||||||
|
### Root Managers
|
||||||
|
|
||||||
|
| Manager | Status | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| KernelSU | ✅ Tested | Full support including Action button and lifecycle scripts |
|
||||||
|
| Magisk | ✅ Supported | Standard module install |
|
||||||
|
| APatch | ✅ Supported | Standard module install |
|
||||||
|
|
||||||
|
### Tested Devices
|
||||||
|
|
||||||
|
| Device | Android | TEE | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Redmi 14C (2409BRN2CA) | 14 (SDK 34) | Beanpod KeyMaster | ✅ Daily driver |
|
||||||
|
|
||||||
|
> Tested against DuckDetector, Luna, Play Integrity, and Key Attestation Demo. If you test on a different device, [open an issue](https://github.com/Enginex0/TEESimulator/issues) with your results.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
TEESimulator replaces TrickyStore, TrickyStoreOSS, and their forks. Existing config files are compatible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔨 Building from Source
|
||||||
|
|
||||||
|
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
|
### The `keybox.xml` Root of Trust
|
||||||
|
|
||||||
This file provides the master cryptographic identity for the simulator. It contains a private key and a valid, hardware-backed certificate chain from a real device. The simulator uses this to sign the virtual certificates it generates, making them appear legitimate to verifiers.
|
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
|
||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
@@ -40,48 +151,109 @@ This file provides the master cryptographic identity for the simulator. It conta
|
|||||||
</AndroidAttestation>
|
</AndroidAttestation>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Mode and Keybox Configuration (`target.txt`)
|
### Target Packages (`target.txt`)
|
||||||
|
|
||||||
TEESimulator currently operates in two primary modes as it transitions towards full emulation.
|
Controls which apps get intercepted and what simulation mode to use.
|
||||||
You can control the simulation mode and the specific keybox.xml file used on a per-package basis.
|
|
||||||
|
|
||||||
#### Mode Suffixes
|
#### Mode Suffixes
|
||||||
|
|
||||||
* **`!` → Force Generation Mode:** Creates a complete, software-based virtual key. This is the foundation of the full TEE simulation.
|
* **`!` → Force Generation** — Creates a complete software-based virtual key. Full TEE simulation.
|
||||||
* **`?` → Force Leaf Hacking Mode:** A legacy mode where a real TEE key is generated, but its attestation certificate is intercepted and modified.
|
* **`?` → Force Leaf Hacking** — Real TEE key generated, but its attestation certificate is intercepted and patched.
|
||||||
* **No symbol → Automatic Mode:** The module selects the most appropriate mode for the device.
|
* **No symbol → Automatic** — Module selects the best mode for your device.
|
||||||
|
|
||||||
#### Multi-Keybox Configuration
|
#### Multi-Keybox
|
||||||
|
|
||||||
You can specify different keybox files for different groups of applications. This is done by adding a line with the filename in square brackets (e.g., [demo_keybox.xml]).
|
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`.
|
||||||
|
|
||||||
All applications listed after this line will use the specified keybox file, until a new keybox is declared. Applications listed before any custom keybox declaration will use the default `keybox.xml`.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
```
|
```
|
||||||
# These two apps will use the default /data/adb/tricky_store/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.
|
# Switch to a different keybox for the following apps
|
||||||
# The file must be located at /data/adb/tricky_store/aosp_keybox.xml
|
|
||||||
[aosp_keybox.xml]
|
[aosp_keybox.xml]
|
||||||
com.google.android.gsf
|
com.google.android.gsf
|
||||||
|
|
||||||
# Switch again to another keybox.
|
# Another keybox
|
||||||
# The file must be located at /data/adb/tricky_store/demo_keybox.xml
|
|
||||||
[demo_keybox.xml]
|
[demo_keybox.xml]
|
||||||
org.matrix.demo
|
org.matrix.demo
|
||||||
```
|
```
|
||||||
|
|
||||||
### Security Patch Level (`security_patch.txt`)
|
### Security Patch Level (`security_patch.txt`)
|
||||||
|
|
||||||
This allows you to configure the security patch level that the simulator will report in its forged attestation certificates.
|
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 |
|
||||||
|
|---|---|
|
||||||
|
| `system` | OS patch level |
|
||||||
|
| `vendor` | Vendor patch level |
|
||||||
|
| `boot` | Boot/kernel patch level |
|
||||||
|
| `all` | Shorthand — sets all three at once |
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
|
||||||
```
|
```
|
||||||
# Advanced Configuration
|
# Global — default for all apps
|
||||||
system=2025-11
|
system=YYYY-MM-05
|
||||||
boot=no # Do not report a boot patch level
|
vendor=device_default
|
||||||
vendor=20251101 # Report a specific vendor patch level
|
boot=no
|
||||||
|
|
||||||
|
# Override for GMS
|
||||||
|
[com.google.android.gms]
|
||||||
|
system=2024-10-01
|
||||||
|
|
||||||
|
# Custom config for a demo app
|
||||||
|
[org.matrix.demo]
|
||||||
|
all=2025-09-15
|
||||||
|
boot=device_default
|
||||||
```
|
```
|
||||||
**Note:** This only affects the Key Attestation data generated by the simulator. It does not change system properties.
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💬 Community
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://t.me/superpowers9">
|
||||||
|
<img src="https://img.shields.io/badge/⚡_JOIN_THE_GRID-SuperPowers_Telegram-black?style=for-the-badge&logo=telegram&logoColor=cyan&labelColor=0d1117&color=00d4ff" alt="Telegram">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🙏 Credits
|
||||||
|
|
||||||
|
- **[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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 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>
|
||||||
|
|||||||
+41
-11
@@ -29,7 +29,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java)
|
|||||||
|
|
||||||
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
|
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
|
||||||
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
|
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
|
||||||
val verName = "v2.1"
|
val verName = "v5.1.1"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "org.matrix.TEESimulator"
|
namespace = "org.matrix.TEESimulator"
|
||||||
@@ -56,6 +56,7 @@ android {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_21
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
targetCompatibility = JavaVersion.VERSION_21
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
}
|
}
|
||||||
|
buildFeatures { buildConfig = true }
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
path = file("src/main/cpp/CMakeLists.txt")
|
path = file("src/main/cpp/CMakeLists.txt")
|
||||||
@@ -70,6 +71,35 @@ dependencies {
|
|||||||
implementation(libs.bcpkix)
|
implementation(libs.bcpkix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Rust native cert gen build task ---
|
||||||
|
val buildRustCertgen by tasks.registering(Exec::class) {
|
||||||
|
group = "TEESimulator-RS Native Build"
|
||||||
|
description = "Builds libcertgen.so via cargo-ndk for arm64-v8a."
|
||||||
|
|
||||||
|
workingDir = rootProject.projectDir.resolve("native-certgen")
|
||||||
|
|
||||||
|
commandLine(
|
||||||
|
"cargo", "ndk",
|
||||||
|
"-t", "arm64-v8a",
|
||||||
|
"-o", rootProject.projectDir.resolve("app/src/main/jniLibs").absolutePath,
|
||||||
|
"build", "--release"
|
||||||
|
)
|
||||||
|
|
||||||
|
inputs.dir(rootProject.projectDir.resolve("native-certgen/src"))
|
||||||
|
inputs.file(rootProject.projectDir.resolve("native-certgen/Cargo.toml"))
|
||||||
|
inputs.file(rootProject.projectDir.resolve("native-certgen/Cargo.lock"))
|
||||||
|
outputs.dir(rootProject.projectDir.resolve("app/src/main/jniLibs"))
|
||||||
|
|
||||||
|
environment("ANDROID_NDK_HOME", android.ndkDirectory.absolutePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AGP auto-detects jniLibs/ as an input to mergeJniLibFolders — wire the dependency
|
||||||
|
tasks.configureEach {
|
||||||
|
if (name.endsWith("JniLibFolders") && name.startsWith("merge")) {
|
||||||
|
dependsOn(buildRustCertgen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
androidComponents {
|
androidComponents {
|
||||||
onVariants(selector().all()) { variant ->
|
onVariants(selector().all()) { variant ->
|
||||||
val capitalized = variant.name.replaceFirstChar { it.uppercase() }
|
val capitalized = variant.name.replaceFirstChar { it.uppercase() }
|
||||||
@@ -78,13 +108,13 @@ androidComponents {
|
|||||||
// --- Define output locations and file names ---
|
// --- Define output locations and file names ---
|
||||||
// Stage all files in a temporary directory inside 'build' before zipping
|
// Stage all files in a temporary directory inside 'build' before zipping
|
||||||
val tempModuleDir = project.layout.buildDirectory.dir("module/${variant.name}")
|
val tempModuleDir = project.layout.buildDirectory.dir("module/${variant.name}")
|
||||||
val zipFileName = "TEESimulator-$verName-$gitCommitCount-$gitCommitHash-$capitalized.zip"
|
val zipFileName = "TEESimulator-RS-$verName-$gitCommitCount-$capitalized.zip"
|
||||||
|
|
||||||
// Task 1: Prepare all module files in the temporary build directory.
|
// Task 1: Prepare all module files in the temporary build directory.
|
||||||
// Using Sync ensures that stale files from previous runs are removed.
|
// Using Sync ensures that stale files from previous runs are removed.
|
||||||
val prepareModuleFilesTask =
|
val prepareModuleFilesTask =
|
||||||
tasks.register<Sync>("prepareModuleFiles${capitalized}") {
|
tasks.register<Sync>("prepareModuleFiles${capitalized}") {
|
||||||
group = "TEESimulator Module Packaging"
|
group = "TEESimulator-RS Module Packaging"
|
||||||
description = "Prepares all files for the ${variant.name} module zip."
|
description = "Prepares all files for the ${variant.name} module zip."
|
||||||
|
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
@@ -93,6 +123,7 @@ androidComponents {
|
|||||||
dependsOn("minify${capitalized}WithR8")
|
dependsOn("minify${capitalized}WithR8")
|
||||||
}
|
}
|
||||||
dependsOn("strip${capitalized}DebugSymbols")
|
dependsOn("strip${capitalized}DebugSymbols")
|
||||||
|
dependsOn(buildRustCertgen)
|
||||||
|
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
from(variant.artifacts.get(SingleArtifact.APK)) {
|
from(variant.artifacts.get(SingleArtifact.APK)) {
|
||||||
@@ -114,8 +145,8 @@ androidComponents {
|
|||||||
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
|
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
into("lib") // Place them in the 'lib' subfolder of the staging directory.
|
into("lib")
|
||||||
include("**/libinject.so", "**/libTEESimulator.so")
|
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so", "**/libcertgen.so")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now, copy and process the files from 'module' directory.
|
// Now, copy and process the files from 'module' directory.
|
||||||
@@ -130,8 +161,7 @@ androidComponents {
|
|||||||
// Use expand() for simple key-value replacement.
|
// Use expand() for simple key-value replacement.
|
||||||
expand(
|
expand(
|
||||||
"REPLACEMEVERCODE" to gitCommitCount.toString(),
|
"REPLACEMEVERCODE" to gitCommitCount.toString(),
|
||||||
"REPLACEMEVER" to
|
"REPLACEMEVER" to "$verName-$gitCommitCount",
|
||||||
"$verName ($gitCommitCount-$gitCommitHash-${variant.name})",
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +172,7 @@ androidComponents {
|
|||||||
// Task 2: Zip the prepared files from the temporary directory.
|
// Task 2: Zip the prepared files from the temporary directory.
|
||||||
val zipTask =
|
val zipTask =
|
||||||
tasks.register<Zip>("zip${capitalized}") {
|
tasks.register<Zip>("zip${capitalized}") {
|
||||||
group = "TEESimulator Module Packaging"
|
group = "TEESimulator-RS Module Packaging"
|
||||||
description = "Creates the flashable zip for the ${variant.name} module."
|
description = "Creates the flashable zip for the ${variant.name} module."
|
||||||
dependsOn(prepareModuleFilesTask)
|
dependsOn(prepareModuleFilesTask)
|
||||||
|
|
||||||
@@ -155,7 +185,7 @@ androidComponents {
|
|||||||
fun createInstallTasks(rootProvider: String, installCli: String) {
|
fun createInstallTasks(rootProvider: String, installCli: String) {
|
||||||
val pushTask =
|
val pushTask =
|
||||||
tasks.register<Exec>("push${rootProvider}Module${capitalized}") {
|
tasks.register<Exec>("push${rootProvider}Module${capitalized}") {
|
||||||
group = "TEESimulator Module Installation"
|
group = "TEESimulator-RS Module Installation"
|
||||||
description =
|
description =
|
||||||
"Pushes the ${variant.name} module to the device for $rootProvider."
|
"Pushes the ${variant.name} module to the device for $rootProvider."
|
||||||
dependsOn(zipTask)
|
dependsOn(zipTask)
|
||||||
@@ -169,7 +199,7 @@ androidComponents {
|
|||||||
|
|
||||||
val installTask =
|
val installTask =
|
||||||
tasks.register<Exec>("install${rootProvider}${capitalized}") {
|
tasks.register<Exec>("install${rootProvider}${capitalized}") {
|
||||||
group = "TEESimulator Module Installation"
|
group = "TEESimulator-RS Module Installation"
|
||||||
description = "Installs the ${variant.name} module via $rootProvider."
|
description = "Installs the ${variant.name} module via $rootProvider."
|
||||||
dependsOn(pushTask)
|
dependsOn(pushTask)
|
||||||
commandLine(
|
commandLine(
|
||||||
@@ -182,7 +212,7 @@ androidComponents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Exec>("install${rootProvider}AndReboot${capitalized}") {
|
tasks.register<Exec>("install${rootProvider}AndReboot${capitalized}") {
|
||||||
group = "TEESimulator Module Installation"
|
group = "TEESimulator-RS Module Installation"
|
||||||
description = "Installs the ${variant.name} module via $rootProvider and reboots."
|
description = "Installs the ${variant.name} module via $rootProvider and reboots."
|
||||||
dependsOn(installTask)
|
dependsOn(installTask)
|
||||||
commandLine("adb", "reboot")
|
commandLine("adb", "reboot")
|
||||||
|
|||||||
Vendored
+6
@@ -7,3 +7,9 @@
|
|||||||
-keepclasseswithmembers class org.matrix.TEESimulator.App {
|
-keepclasseswithmembers class org.matrix.TEESimulator.App {
|
||||||
public static void main(java.lang.String[]);
|
public static void main(java.lang.String[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class org.matrix.TEESimulator.pki.NativeCertGen {
|
||||||
|
native <methods>;
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class org.matrix.TEESimulator.pki.CertGenConfig { *; }
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ set(CMAKE_CXX_STANDARD 23)
|
|||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti")
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions")
|
||||||
|
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -DNDEBUG")
|
||||||
|
|
||||||
# LSPlt configuration
|
# LSPlt configuration
|
||||||
OPTION(LSPLT_BUILD_SHARED OFF)
|
OPTION(LSPLT_BUILD_SHARED OFF)
|
||||||
@@ -22,6 +23,9 @@ add_executable(libinject.so inject/main.cpp inject/utils.cpp)
|
|||||||
target_include_directories(libinject.so PUBLIC include)
|
target_include_directories(libinject.so PUBLIC include)
|
||||||
target_link_libraries(libinject.so PRIVATE lsplt_static)
|
target_link_libraries(libinject.so PRIVATE lsplt_static)
|
||||||
|
|
||||||
|
add_executable(libsupervisor.so supervisor.cpp)
|
||||||
|
target_link_libraries(libsupervisor.so PRIVATE log)
|
||||||
|
|
||||||
add_library(${CMAKE_PROJECT_NAME} SHARED binder_interceptor.cpp)
|
add_library(${CMAKE_PROJECT_NAME} SHARED binder_interceptor.cpp)
|
||||||
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC external/linux-kernel/include include)
|
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC external/linux-kernel/include include)
|
||||||
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE binder lsplt_static utils)
|
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE binder lsplt_static utils)
|
||||||
|
|||||||
@@ -235,6 +235,8 @@ 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reader-Writer lock for the registry to allow concurrent reads (lookups)
|
// Reader-Writer lock for the registry to allow concurrent reads (lookups)
|
||||||
@@ -244,10 +246,15 @@ class BinderInterceptor : public BBinder {
|
|||||||
public:
|
public:
|
||||||
BinderInterceptor() = default;
|
BinderInterceptor() = default;
|
||||||
|
|
||||||
// Checks if a specific Binder instance is currently registered for interception
|
// Checks if a specific Binder+code combination should be intercepted.
|
||||||
bool isBinderIntercepted(const wp<BBinder> &target) const {
|
// 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 {
|
||||||
std::shared_lock lock(registry_mutex_);
|
std::shared_lock lock(registry_mutex_);
|
||||||
return registry_.find(target) != registry_.end();
|
auto it = registry_.find(target);
|
||||||
|
if (it == registry_.end()) return false;
|
||||||
|
const auto &codes = it->second.filtered_codes;
|
||||||
|
return codes.empty() || std::find(codes.begin(), codes.end(), code) != codes.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main entry point for processing the "Man-in-the-Middle" logic
|
// Main entry point for processing the "Man-in-the-Middle" logic
|
||||||
@@ -276,6 +283,12 @@ static sp<BinderInterceptor> g_interceptor_instance = nullptr;
|
|||||||
// =============================================================================================
|
// =============================================================================================
|
||||||
|
|
||||||
class BinderStub : public BBinder {
|
class BinderStub : public BBinder {
|
||||||
|
public:
|
||||||
|
const String16& getInterfaceDescriptor() const override {
|
||||||
|
static const String16 kDescriptor("org.matrix.TEESimulator.BinderStub");
|
||||||
|
return kDescriptor;
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
status_t onTransact(uint32_t code, const Parcel &data, Parcel *reply, uint32_t flags) override {
|
status_t onTransact(uint32_t code, const Parcel &data, Parcel *reply, uint32_t flags) override {
|
||||||
if (code != intercept::kBackdoorCode) {
|
if (code != intercept::kBackdoorCode) {
|
||||||
@@ -342,15 +355,14 @@ static sp<BinderStub> g_stub_instance = nullptr;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Analyses a binder transaction. If the target is monitored,
|
|
||||||
* hijacks the transaction by rewriting its destination to our BinderStub.
|
|
||||||
* @param txn_data Pointer to the transaction data within the ioctl buffer.
|
|
||||||
*/
|
|
||||||
void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
||||||
if (!txn_data || txn_data->target.ptr == 0)
|
if (!txn_data || txn_data->target.ptr == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Skip system transactions (PING, INTERFACE, DUMP) to avoid latency detectors
|
||||||
|
if (txn_data->code > 0x00ffffffu && txn_data->code != intercept::kBackdoorCode)
|
||||||
|
return;
|
||||||
|
|
||||||
bool hijack = false;
|
bool hijack = false;
|
||||||
ThreadTransactionInfo info;
|
ThreadTransactionInfo info;
|
||||||
|
|
||||||
@@ -359,9 +371,15 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
|||||||
info.transaction_code = intercept::kBackdoorCode;
|
info.transaction_code = intercept::kBackdoorCode;
|
||||||
info.target_binder = nullptr;
|
info.target_binder = nullptr;
|
||||||
hijack = true;
|
hijack = true;
|
||||||
}
|
// Check 2: Spoof uid of KeyStore requests from the daemon to bypass permission check
|
||||||
// Check 2: Normal interception based on registry of monitored binders
|
} else if (txn_data->sender_euid == 0) {
|
||||||
else {
|
// The kernel driver fills sender_euid.
|
||||||
|
// libbinder.so trusts this value to populate IPCThreadState.
|
||||||
|
txn_data->sender_euid = 1000;
|
||||||
|
LOGV("[Hook] Spoofing UID for transaction: 0 -> %d", txn_data->sender_euid);
|
||||||
|
hijack = false; // Never hijack to avoid recursion
|
||||||
|
// Check 3: Normal interception based on registry of monitored binders
|
||||||
|
} else {
|
||||||
// Safe casting based on Binder driver ABI
|
// Safe casting based on Binder driver ABI
|
||||||
RefBase::weakref_type *weak_ref = reinterpret_cast<RefBase::weakref_type *>(txn_data->target.ptr);
|
RefBase::weakref_type *weak_ref = reinterpret_cast<RefBase::weakref_type *>(txn_data->target.ptr);
|
||||||
|
|
||||||
@@ -370,18 +388,17 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
|||||||
// The raw pointer to the binder object itself is stored in the cookie
|
// The raw pointer to the binder object itself is stored in the cookie
|
||||||
BBinder *target_binder_ptr = reinterpret_cast<BBinder *>(txn_data->cookie);
|
BBinder *target_binder_ptr = reinterpret_cast<BBinder *>(txn_data->cookie);
|
||||||
|
|
||||||
// This is safe ONLY because we successfully called attemptIncStrong().
|
// Create a weak pointer for the lookup and to store in our context map.
|
||||||
// The sp<> constructor will not increment the ref count again, it just adopts the one we have.
|
// This is safe because we are holding a strong reference.
|
||||||
// When sp_target goes out of scope, it will call decStrong(), releasing our temporary reference.
|
wp<BBinder> wp_target = target_binder_ptr;
|
||||||
sp<BBinder> sp_target = sp<BBinder>::fromExisting(target_binder_ptr);
|
|
||||||
|
|
||||||
// Now we can safely use sp_target (which implicitly converts to a wp) for the lookup.
|
if (g_interceptor_instance->shouldIntercept(wp_target, txn_data->code)) {
|
||||||
if (g_interceptor_instance->isBinderIntercepted(sp_target)) {
|
|
||||||
info.transaction_code = txn_data->code;
|
info.transaction_code = txn_data->code;
|
||||||
info.target_binder = sp_target; // Assign the valid weak pointer
|
info.target_binder = wp_target; // Assign the valid weak pointer
|
||||||
hijack = true;
|
hijack = true;
|
||||||
}
|
}
|
||||||
// No need to manually call decStrong(); the sp destructor handles it.
|
// Manually release the temporary strong reference we acquired at the start.
|
||||||
|
target_binder_ptr->decStrong(nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,12 +543,26 @@ status_t BinderInterceptor::handleRegister(const Parcel &data) {
|
|||||||
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;
|
||||||
|
int32_t code_count = 0;
|
||||||
|
if (data.dataAvail() >= sizeof(int32_t) && data.readInt32(&code_count) == OK && code_count > 0) {
|
||||||
|
codes.reserve(code_count);
|
||||||
|
for (int32_t i = 0; i < code_count; i++) {
|
||||||
|
uint32_t c = 0;
|
||||||
|
if (data.readUint32(&c) == OK) codes.push_back(c);
|
||||||
|
}
|
||||||
|
LOGI("Interceptor registered for binder %p with %zu filtered codes", target.get(), codes.size());
|
||||||
|
} else {
|
||||||
|
LOGI("Interceptor registered for binder %p (all codes)", target.get());
|
||||||
|
}
|
||||||
|
|
||||||
wp<IBinder> weak_target = target;
|
wp<IBinder> weak_target = target;
|
||||||
|
|
||||||
std::unique_lock lock(registry_mutex_);
|
std::unique_lock lock(registry_mutex_);
|
||||||
registry_[weak_target] = {weak_target, callback};
|
registry_[weak_target] = {weak_target, callback, std::move(codes)};
|
||||||
|
|
||||||
LOGI("Interceptor registered for binder %p", target.get());
|
|
||||||
return OK;
|
return OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,7 +614,7 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
|
|||||||
|
|
||||||
if (callback->transact(intercept::kPreTransact, pre_req, &pre_resp) != OK) {
|
if (callback->transact(intercept::kPreTransact, pre_req, &pre_resp) != OK) {
|
||||||
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed. Forwarding original call.", tx_id);
|
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed. Forwarding original call.", tx_id);
|
||||||
return false; // Callback failed, proceed as if not intercepted
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t action = pre_resp.readInt32();
|
int32_t action = pre_resp.readInt32();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
#include <sys/ptrace.h>
|
#include <sys/ptrace.h>
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
#include <sys/system_properties.h>
|
#include <sys/system_properties.h>
|
||||||
#include <sys/uio.h>
|
#include <sys/uio.h>
|
||||||
#include <sys/un.h>
|
#include <sys/un.h>
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
#include <csignal>
|
#include <csignal>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -95,10 +97,6 @@ constexpr size_t kMagicLength = 16;
|
|||||||
constexpr size_t kMaxPathLength = PATH_MAX;
|
constexpr size_t kMaxPathLength = PATH_MAX;
|
||||||
// Maximum length for file paths.
|
// Maximum length for file paths.
|
||||||
|
|
||||||
constexpr const char *kSystemFileContext = "u:object_r:system_file:s0";
|
|
||||||
// SELinux context for system files,
|
|
||||||
// used for socket creation and library file context.
|
|
||||||
|
|
||||||
constexpr const char *kLibcModule = "libc.so";
|
constexpr const char *kLibcModule = "libc.so";
|
||||||
// Name of the C standard library.
|
// Name of the C standard library.
|
||||||
|
|
||||||
@@ -215,8 +213,8 @@ private:
|
|||||||
* @brief Transfers a file descriptor from the injector process to the remote process.
|
* @brief Transfers a file descriptor from the injector process to the remote process.
|
||||||
*
|
*
|
||||||
* This function uses Unix domain sockets with SCM_RIGHTS to send a file descriptor.
|
* This function uses Unix domain sockets with SCM_RIGHTS to send a file descriptor.
|
||||||
* It involves setting SELinux contexts, creating local and remote sockets, binding,
|
* It involves creating local and remote sockets, binding, and then coordinating
|
||||||
* and then coordinating sendmsg/recvmsg calls using ptrace.
|
* sendmsg/recvmsg calls using ptrace.
|
||||||
*
|
*
|
||||||
* @param pid The target process ID.
|
* @param pid The target process ID.
|
||||||
* @param lib_path The path to the library file being transferred.
|
* @param lib_path The path to the library file being transferred.
|
||||||
@@ -233,29 +231,14 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
uintptr_t libc_return_addr) {
|
uintptr_t libc_return_addr) {
|
||||||
LOGD("Attempting to transfer file descriptor for library: %s", lib_path);
|
LOGD("Attempting to transfer file descriptor for library: %s", lib_path);
|
||||||
|
|
||||||
// 1. Set SELinux context for socket creation in the injector process.
|
// Create a local Unix domain socket for FD transfer.
|
||||||
// This is crucial for Android where SELinux might prevent socket operations.
|
|
||||||
if (!set_sockcreate_con(constants::kSystemFileContext)) {
|
|
||||||
LOGE("Failed to set socket creation context.");
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Create a local Unix domain socket for FD transfer.
|
|
||||||
UniqueFd local_socket = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
UniqueFd local_socket = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
||||||
if (local_socket == -1) {
|
if (local_socket == -1) {
|
||||||
PLOGE("Failed to create local Unix domain socket.");
|
PLOGE("Failed to create local Unix domain socket.");
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Set SELinux context for the library file if possible.
|
// Open the local library file to get a file descriptor.
|
||||||
// This might be required for the target process to open/access it later if directly opening by path.
|
|
||||||
// For FD transfer, this is less critical as the FD's context is inherited, but good practice.
|
|
||||||
if (setfilecon(lib_path, constants::kSystemFileContext) == -1) {
|
|
||||||
// Log a warning, but don't fail, as FD transfer might still work.
|
|
||||||
PLOGE("Failed to set context of library file: %s. This might cause issues.", lib_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Open the local library file to get a file descriptor.
|
|
||||||
UniqueFd local_lib_fd = open(lib_path, O_RDONLY | O_CLOEXEC);
|
UniqueFd local_lib_fd = open(lib_path, O_RDONLY | O_CLOEXEC);
|
||||||
if (local_lib_fd == -1) {
|
if (local_lib_fd == -1) {
|
||||||
PLOGE("Failed to open library file: %s", lib_path);
|
PLOGE("Failed to open library file: %s", lib_path);
|
||||||
@@ -271,7 +254,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
void *errno_addr; // Address of __errno for getting remote errno.
|
void *errno_addr; // Address of __errno for getting remote errno.
|
||||||
} funcs{};
|
} funcs{};
|
||||||
|
|
||||||
// 5. Resolve required libc functions in the remote process.
|
// Resolve required libc functions in the remote process.
|
||||||
funcs.socket_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "socket");
|
funcs.socket_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "socket");
|
||||||
funcs.bind_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "bind");
|
funcs.bind_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "bind");
|
||||||
funcs.recvmsg_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "recvmsg");
|
funcs.recvmsg_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "recvmsg");
|
||||||
@@ -306,25 +289,28 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 6. Create a Unix domain socket in the remote process.
|
// Create a Unix domain socket in the remote process.
|
||||||
std::vector<uintptr_t> args = {AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0};
|
std::vector<uintptr_t> args = {AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0};
|
||||||
int remote_fd = static_cast<int>(
|
int remote_fd = static_cast<int>(
|
||||||
remote_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.socket_addr), libc_return_addr, args));
|
remote_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.socket_addr), libc_return_addr, args));
|
||||||
if (remote_fd == -1) {
|
if (remote_fd <= 0) {
|
||||||
|
// remote_call returns 0 on failure.
|
||||||
|
// socket() returning 0 is technically possible (if stdin closed),
|
||||||
|
// but highly unlikely for a daemon. We treat 0 as failure here to catch the injection error.
|
||||||
errno = get_remote_errno(); // Set local errno for PLOGE.
|
errno = get_remote_errno(); // Set local errno for PLOGE.
|
||||||
PLOGE("Failed to create remote socket.");
|
PLOGE("Failed to create remote socket (returned %d).", remote_fd);
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
LOGD("Successfully created remote socket with FD: %d", remote_fd);
|
LOGD("Successfully created remote socket with FD: %d", remote_fd);
|
||||||
|
|
||||||
// 7. Generate a unique magic string for the abstract Unix domain socket path.
|
// Generate a unique magic string for the abstract Unix domain socket path.
|
||||||
auto magic = generateMagic(constants::kMagicLength);
|
auto magic = generateMagic(constants::kMagicLength);
|
||||||
struct sockaddr_un sock_addr{.sun_family = AF_UNIX, .sun_path = {0}};
|
struct sockaddr_un sock_addr{.sun_family = AF_UNIX, .sun_path = {0}};
|
||||||
// Abstract Unix domain sockets have sun_path[0] as null, and the name starts from sun_path[1].
|
// Abstract Unix domain sockets have sun_path[0] as null, and the name starts from sun_path[1].
|
||||||
memcpy(sock_addr.sun_path + 1, magic.c_str(), magic.size());
|
memcpy(sock_addr.sun_path + 1, magic.c_str(), magic.size());
|
||||||
socklen_t addr_len = sizeof(sock_addr.sun_family) + 1 + magic.size(); // Length includes null byte and magic.
|
socklen_t addr_len = sizeof(sock_addr.sun_family) + 1 + magic.size(); // Length includes null byte and magic.
|
||||||
|
|
||||||
// 8. Push the sockaddr_un structure to the remote process's stack.
|
// Push the sockaddr_un structure to the remote process's stack.
|
||||||
auto remote_addr = push_memory(pid, regs, &sock_addr, sizeof(sock_addr));
|
auto remote_addr = push_memory(pid, regs, &sock_addr, sizeof(sock_addr));
|
||||||
if (remote_addr == 0) {
|
if (remote_addr == 0) {
|
||||||
LOGE("Failed to push socket address to remote memory.");
|
LOGE("Failed to push socket address to remote memory.");
|
||||||
@@ -332,7 +318,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. Bind the remote socket to the abstract Unix domain socket path.
|
// Bind the remote socket to the abstract Unix domain socket path.
|
||||||
args = {static_cast<uintptr_t>(remote_fd), remote_addr, static_cast<uintptr_t>(addr_len)};
|
args = {static_cast<uintptr_t>(remote_fd), remote_addr, static_cast<uintptr_t>(addr_len)};
|
||||||
auto bind_result = remote_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.bind_addr), libc_return_addr, args);
|
auto bind_result = remote_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.bind_addr), libc_return_addr, args);
|
||||||
if (bind_result == static_cast<uintptr_t>(-1)) {
|
if (bind_result == static_cast<uintptr_t>(-1)) {
|
||||||
@@ -346,7 +332,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
// Prepare control message buffer for SCM_RIGHTS (file descriptor passing).
|
// Prepare control message buffer for SCM_RIGHTS (file descriptor passing).
|
||||||
char cmsgbuf[CMSG_SPACE(sizeof(int))] = {0};
|
char cmsgbuf[CMSG_SPACE(sizeof(int))] = {0};
|
||||||
|
|
||||||
// 10. Push the control message buffer to the remote process's stack.
|
// Push the control message buffer to the remote process's stack.
|
||||||
auto remote_cmsgbuf = push_memory(pid, regs, &cmsgbuf, sizeof(cmsgbuf));
|
auto remote_cmsgbuf = push_memory(pid, regs, &cmsgbuf, sizeof(cmsgbuf));
|
||||||
if (remote_cmsgbuf == 0) {
|
if (remote_cmsgbuf == 0) {
|
||||||
LOGE("Failed to push control message buffer to remote memory.");
|
LOGE("Failed to push control message buffer to remote memory.");
|
||||||
@@ -359,7 +345,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
msg_hdr.msg_control = reinterpret_cast<void *>(remote_cmsgbuf);
|
msg_hdr.msg_control = reinterpret_cast<void *>(remote_cmsgbuf);
|
||||||
msg_hdr.msg_controllen = sizeof(cmsgbuf);
|
msg_hdr.msg_controllen = sizeof(cmsgbuf);
|
||||||
|
|
||||||
// 11. Push the msghdr structure to the remote process's stack.
|
// Push the msghdr structure to the remote process's stack.
|
||||||
auto remote_hdr = push_memory(pid, regs, &msg_hdr, sizeof(msg_hdr));
|
auto remote_hdr = push_memory(pid, regs, &msg_hdr, sizeof(msg_hdr));
|
||||||
if (remote_hdr == 0) {
|
if (remote_hdr == 0) {
|
||||||
LOGE("Failed to push message header to remote memory.");
|
LOGE("Failed to push message header to remote memory.");
|
||||||
@@ -367,16 +353,16 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 12. Initiate the remote recvmsg call. This will block the remote process.
|
// Initiate the remote recvmsg call. This will block the remote process.
|
||||||
args = {static_cast<uintptr_t>(remote_fd), remote_hdr, MSG_WAITALL};
|
args = {static_cast<uintptr_t>(remote_fd), remote_hdr, MSG_WAITALL};
|
||||||
if (!remote_pre_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.recvmsg_addr), 0, args)) {
|
if (!remote_pre_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.recvmsg_addr), libc_return_addr, args)) {
|
||||||
LOGE("Failed to initiate remote recvmsg call.");
|
LOGE("Failed to initiate remote recvmsg call.");
|
||||||
close_remote(remote_fd);
|
close_remote(remote_fd);
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
LOGD("Remote recvmsg initiated, waiting for FD transfer...");
|
LOGD("Remote recvmsg initiated, waiting for FD transfer...");
|
||||||
|
|
||||||
// 13. Prepare the local msghdr for sending the file descriptor.
|
// Prepare the local msghdr for sending the file descriptor.
|
||||||
// The msg_control and msg_name fields of the local msghdr are set up.
|
// The msg_control and msg_name fields of the local msghdr are set up.
|
||||||
msg_hdr.msg_control = &cmsgbuf; // Use local cmsgbuf for sending.
|
msg_hdr.msg_control = &cmsgbuf; // Use local cmsgbuf for sending.
|
||||||
msg_hdr.msg_name = &sock_addr;
|
msg_hdr.msg_name = &sock_addr;
|
||||||
@@ -396,7 +382,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
*reinterpret_cast<int *>(CMSG_DATA(cmsg)) = local_lib_fd; // The FD to send.
|
*reinterpret_cast<int *>(CMSG_DATA(cmsg)) = local_lib_fd; // The FD to send.
|
||||||
}
|
}
|
||||||
|
|
||||||
// 14. Send the file descriptor from the injector to the remote process.
|
// Send the file descriptor from the injector to the remote process.
|
||||||
if (sendmsg(local_socket, &msg_hdr, 0) == -1) {
|
if (sendmsg(local_socket, &msg_hdr, 0) == -1) {
|
||||||
PLOGE("Failed to send file descriptor to remote process.");
|
PLOGE("Failed to send file descriptor to remote process.");
|
||||||
// We do not close local_lib_fd here as it might be transferred even if
|
// We do not close local_lib_fd here as it might be transferred even if
|
||||||
@@ -407,9 +393,9 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
}
|
}
|
||||||
LOGD("Local FD %d sent to remote process.", local_lib_fd.operator const int &());
|
LOGD("Local FD %d sent to remote process.", local_lib_fd.operator const int &());
|
||||||
|
|
||||||
// 15. Complete the remote recvmsg call. This will retrieve the return value.
|
// Complete the remote recvmsg call. This will retrieve the return value.
|
||||||
auto recvmsg_result =
|
auto recvmsg_result =
|
||||||
static_cast<ssize_t>(remote_post_call(pid, regs, 0)); // No specific expected return address for recvmsg
|
static_cast<ssize_t>(remote_post_call(pid, regs, libc_return_addr));
|
||||||
if (recvmsg_result == -1) {
|
if (recvmsg_result == -1) {
|
||||||
errno = get_remote_errno();
|
errno = get_remote_errno();
|
||||||
PLOGE("Remote recvmsg call failed.");
|
PLOGE("Remote recvmsg call failed.");
|
||||||
@@ -418,7 +404,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
}
|
}
|
||||||
LOGD("Remote recvmsg completed with result: %zd", recvmsg_result);
|
LOGD("Remote recvmsg completed with result: %zd", recvmsg_result);
|
||||||
|
|
||||||
// 16. Read the control message buffer back from the remote process to extract the FD.
|
// Read the control message buffer back from the remote process to extract the FD.
|
||||||
if (read_proc(pid, remote_cmsgbuf, &cmsgbuf, sizeof(cmsgbuf)) != sizeof(cmsgbuf)) {
|
if (read_proc(pid, remote_cmsgbuf, &cmsgbuf, sizeof(cmsgbuf)) != sizeof(cmsgbuf)) {
|
||||||
LOGE("Failed to read control message buffer from remote process.");
|
LOGE("Failed to read control message buffer from remote process.");
|
||||||
close_remote(remote_fd);
|
close_remote(remote_fd);
|
||||||
@@ -439,7 +425,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
LOGI("Successfully transferred FD %d to remote process, new remote FD: %d", local_lib_fd.operator const int &(),
|
LOGI("Successfully transferred FD %d to remote process, new remote FD: %d", local_lib_fd.operator const int &(),
|
||||||
transferred_fd);
|
transferred_fd);
|
||||||
|
|
||||||
// 17. Close the remote socket.
|
// Close the remote socket.
|
||||||
close_remote(remote_fd);
|
close_remote(remote_fd);
|
||||||
|
|
||||||
return transferred_fd;
|
return transferred_fd;
|
||||||
@@ -642,6 +628,130 @@ static bool remote_call_entry(int pid, struct user_regs_struct ®s, uintptr_t
|
|||||||
return true; // Return true if the call itself completed, regardless of its return value.
|
return true; // Return true if the call itself completed, regardless of its return value.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief RAII wrapper to ensure a temporary file is deleted (unlinked)
|
||||||
|
* when the object goes out of scope.
|
||||||
|
*
|
||||||
|
* This is crucial for stealth: we want the library to exist on the filesystem
|
||||||
|
* for the shortest time possible.
|
||||||
|
*/
|
||||||
|
class ScopedFileDeleter {
|
||||||
|
public:
|
||||||
|
explicit ScopedFileDeleter(std::string path) : path_(std::move(path)) {}
|
||||||
|
|
||||||
|
~ScopedFileDeleter() {
|
||||||
|
if (!path_.empty()) {
|
||||||
|
LOGD("Cleaning up staged file: %s", path_.c_str());
|
||||||
|
unlink(path_.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable copy to prevent double-deletion issues
|
||||||
|
ScopedFileDeleter(const ScopedFileDeleter&) = delete;
|
||||||
|
ScopedFileDeleter& operator=(const ScopedFileDeleter&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string path_;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Copies a file from source to destination.
|
||||||
|
*
|
||||||
|
* @param src Absolute path to source file.
|
||||||
|
* @param dst Absolute path to destination file.
|
||||||
|
* @return True on success, false on failure.
|
||||||
|
*/
|
||||||
|
static bool copy_file(const char* src, const char* dst) {
|
||||||
|
std::ifstream src_file(src, std::ios::binary);
|
||||||
|
std::ofstream dst_file(dst, std::ios::binary);
|
||||||
|
|
||||||
|
if (!src_file) {
|
||||||
|
PLOGE("Failed to open source file for copying: %s", src);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!dst_file) {
|
||||||
|
PLOGE("Failed to open destination file for copying: %s", dst);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dst_file << src_file.rdbuf();
|
||||||
|
return src_file.good() && dst_file.good();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Performs injection via the "Staging" method.
|
||||||
|
*
|
||||||
|
* This strategy is used when direct FD passing fails (e.g., due to Seccomp filters).
|
||||||
|
* 1. Copies the library to a world-readable location (/data/local/tmp).
|
||||||
|
* 2. Loads it via standard dlopen().
|
||||||
|
* 3. Immediately deletes the file to hide tracks.
|
||||||
|
*
|
||||||
|
* @param pid The target process ID.
|
||||||
|
* @param regs The target process registers (must be Red-Zone adjusted if x86_64).
|
||||||
|
* @param local_map Local memory map.
|
||||||
|
* @param remote_map Remote memory map.
|
||||||
|
* @param lib_path The path to the original library.
|
||||||
|
* @param libc_return_addr Return address for remote calls.
|
||||||
|
* @return The handle of the loaded library, or std::nullopt on failure.
|
||||||
|
*/
|
||||||
|
static std::optional<uintptr_t> inject_via_staging(int pid, struct user_regs_struct ®s,
|
||||||
|
const std::vector<lsplt::MapInfo> &local_map,
|
||||||
|
const std::vector<lsplt::MapInfo> &remote_map,
|
||||||
|
const char *lib_path, uintptr_t libc_return_addr) {
|
||||||
|
LOGI("Initiating Staging Fallback mechanism...");
|
||||||
|
|
||||||
|
// Generate a random path in /data/local/tmp
|
||||||
|
// /data/local/tmp is chosen because it is traversable by most contexts.
|
||||||
|
std::string staged_path = "/data/local/tmp/lib" + generateMagic(8) + ".so";
|
||||||
|
|
||||||
|
// Ensure the file is deleted when this function exits (Success or Failure).
|
||||||
|
// The kernel keeps the inode alive for the mapped process even after unlink.
|
||||||
|
ScopedFileDeleter file_guard(staged_path);
|
||||||
|
|
||||||
|
LOGD("Staging library to: %s", staged_path.c_str());
|
||||||
|
|
||||||
|
// Copy the library
|
||||||
|
if (!copy_file(lib_path, staged_path.c_str())) {
|
||||||
|
LOGE("Failed to copy library during staging.");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Permissions to 644 (RW-R--R--)
|
||||||
|
// This allows the target process (likely running as a specific UID) to read the file.
|
||||||
|
if (chmod(staged_path.c_str(), 0644) != 0) {
|
||||||
|
PLOGE("Failed to chmod staged file.");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve 'dlopen' in the remote process
|
||||||
|
auto dlopen_addr = find_func_addr(local_map, remote_map, constants::kLibdlModule, "dlopen");
|
||||||
|
if (!dlopen_addr) {
|
||||||
|
LOGE("Failed to find 'dlopen' in remote process.");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the staged path to remote memory
|
||||||
|
uintptr_t remote_path_addr = push_string(pid, regs, staged_path.c_str());
|
||||||
|
if (remote_path_addr == 0) {
|
||||||
|
LOGE("Failed to push staged path string to remote memory.");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call dlopen(path, RTLD_NOW)
|
||||||
|
std::vector<uintptr_t> args = {remote_path_addr, RTLD_NOW};
|
||||||
|
uintptr_t handle = remote_call(pid, regs, reinterpret_cast<uintptr_t>(dlopen_addr),
|
||||||
|
libc_return_addr, args);
|
||||||
|
|
||||||
|
if (handle == 0) {
|
||||||
|
std::string error_msg = get_remote_dlerror(pid, regs, local_map, remote_map, libc_return_addr);
|
||||||
|
LOGE("Staged dlopen failed. dlerror: %s", error_msg.c_str());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGI("Successfully loaded staged library. Handle: %p", reinterpret_cast<void*>(handle));
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief RAII wrapper for ptrace attachment and detachment.
|
* @brief RAII wrapper for ptrace attachment and detachment.
|
||||||
*
|
*
|
||||||
@@ -694,12 +804,31 @@ private:
|
|||||||
bool attached_; // Flag indicating current attachment status.
|
bool attached_; // Flag indicating current attachment status.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// RAII Class to ensure registers are always restored
|
||||||
|
class RegisterRestorer {
|
||||||
|
public:
|
||||||
|
RegisterRestorer(int pid, const struct user_regs_struct& original_regs)
|
||||||
|
: pid_(pid), regs_(original_regs) {}
|
||||||
|
|
||||||
|
~RegisterRestorer() {
|
||||||
|
// Always restore registers when this object goes out of scope
|
||||||
|
if (set_regs(pid_, regs_)) {
|
||||||
|
LOGD("Original registers for process %d restored.", pid_);
|
||||||
|
} else {
|
||||||
|
PLOGE("Failed to restore original registers for process %d.", pid_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
int pid_;
|
||||||
|
struct user_regs_struct regs_;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Injects a shared library into a target process using ptrace.
|
* @brief Injects a shared library into a target process using ptrace.
|
||||||
*
|
*
|
||||||
* This is the main orchestration function for the library injection.
|
* This is the main orchestration function for the library injection.
|
||||||
* It handles attachment, remote memory/register manipulation, FD transfer,
|
* It handles attachment, remote memory/register manipulation, FD transfer,
|
||||||
* remote dlopen/dlsym, and remote entry point execution.
|
* staging fallback, remote dlopen/dlsym, and remote entry point execution.
|
||||||
*
|
*
|
||||||
* @param pid The target process ID.
|
* @param pid The target process ID.
|
||||||
* @param lib_path The absolute path to the shared library to inject.
|
* @param lib_path The absolute path to the shared library to inject.
|
||||||
@@ -742,6 +871,14 @@ bool inject_library(int pid, const char *lib_path, const char *entry_name) {
|
|||||||
backup_regs = current_regs; // Store a copy for restoration.
|
backup_regs = current_regs; // Store a copy for restoration.
|
||||||
LOGD("Process %d registers backed up.", pid);
|
LOGD("Process %d registers backed up.", pid);
|
||||||
|
|
||||||
|
// Skip the Red Zone (128 bytes) on x86_64 to prevent stack corruption
|
||||||
|
#if defined(__x86_64__)
|
||||||
|
current_regs.rsp -= 128;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Ensures original state is restored even if injection fails/crashes mid-way.
|
||||||
|
RegisterRestorer reg_guard(pid, backup_regs);
|
||||||
|
|
||||||
// Create a scope to ensure RAII objects are destroyed BEFORE register restoration
|
// Create a scope to ensure RAII objects are destroyed BEFORE register restoration
|
||||||
{
|
{
|
||||||
// 4. Scan local and remote memory maps to resolve function addresses.
|
// 4. Scan local and remote memory maps to resolve function addresses.
|
||||||
@@ -760,53 +897,57 @@ bool inject_library(int pid, const char *lib_path, const char *entry_name) {
|
|||||||
}
|
}
|
||||||
LOGD("Found libc return address: %p", reinterpret_cast<void *>(libc_return_addr));
|
LOGD("Found libc return address: %p", reinterpret_cast<void *>(libc_return_addr));
|
||||||
|
|
||||||
// 6. Transfer the library's file descriptor to the remote process.
|
// 6. Attempt to transfer the library's file descriptor to the remote process.
|
||||||
|
int remote_fd = -1;
|
||||||
auto lib_fd_opt = transfer_fd_to_remote(pid, lib_path, current_regs, local_map, remote_map,
|
auto lib_fd_opt = transfer_fd_to_remote(pid, lib_path, current_regs, local_map, remote_map,
|
||||||
reinterpret_cast<uintptr_t>(libc_return_addr));
|
reinterpret_cast<uintptr_t>(libc_return_addr));
|
||||||
if (!lib_fd_opt) {
|
std::optional<RemoteLibraryHandle> remote_lib_guard;
|
||||||
LOGE("Failed to transfer library file descriptor for '%s' to target process %d.", lib_path, pid);
|
std::optional<uintptr_t> handle_opt;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
RemoteLibraryHandle remote_lib_guard(pid, *lib_fd_opt);
|
|
||||||
LOGD("Library FD %d transferred to remote process %d.", remote_lib_guard.fd(), pid);
|
|
||||||
remote_lib_guard.set_libc_return_addr(reinterpret_cast<uintptr_t>(libc_return_addr));
|
|
||||||
|
|
||||||
// 7. Remotely load the library using the transferred file descriptor.
|
if (lib_fd_opt) {
|
||||||
auto handle_opt = remote_dlopen(pid, current_regs, local_map, remote_map, remote_lib_guard.fd(), lib_path,
|
remote_fd = *lib_fd_opt;
|
||||||
reinterpret_cast<uintptr_t>(libc_return_addr));
|
remote_lib_guard.emplace(pid, remote_fd);
|
||||||
|
remote_lib_guard->set_libc_return_addr(reinterpret_cast<uintptr_t>(libc_return_addr));
|
||||||
|
|
||||||
|
LOGD("FD Transfer successful (FD: %d). Attempting android_dlopen_ext...", remote_fd);
|
||||||
|
handle_opt = remote_dlopen(pid, current_regs, local_map, remote_map, remote_fd, lib_path,
|
||||||
|
reinterpret_cast<uintptr_t>(libc_return_addr));
|
||||||
|
} else {
|
||||||
|
LOGW("Failed to transfer library file descriptor for '%s' to target process %d.", lib_path, pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Staging Fallback (Copy-Inject-Delete) if FD transfer failed.
|
||||||
if (!handle_opt) {
|
if (!handle_opt) {
|
||||||
|
handle_opt = inject_via_staging(pid, current_regs, local_map, remote_map,
|
||||||
|
lib_path, reinterpret_cast<uintptr_t>(libc_return_addr));
|
||||||
|
}
|
||||||
|
if (!handle_opt || *handle_opt == 0) {
|
||||||
LOGE("Failed to load library '%s' in remote process %d.", lib_path, pid);
|
LOGE("Failed to load library '%s' in remote process %d.", lib_path, pid);
|
||||||
// If dlopen fails, the remote_lib_guard.fd() is still valid in the target process and needs to be closed.
|
// If dlopen fails, the remote_lib_guard.fd() is still valid in the target process and needs to be closed.
|
||||||
// The RemoteLibraryHandle constructor takes care of this.
|
// The RemoteLibraryHandle constructor takes care of this.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
remote_lib_guard.set_handle(*handle_opt);
|
uintptr_t handle = *handle_opt;
|
||||||
|
if (remote_lib_guard) remote_lib_guard->set_handle(handle);
|
||||||
|
|
||||||
// 8. Find the entry point symbol in the remotely loaded library.
|
// 8. Find the entry point symbol in the remotely loaded library.
|
||||||
auto entry_opt = remote_find_entry(pid, current_regs, entry_name, local_map, remote_map,
|
auto entry_opt = remote_find_entry(pid, current_regs, entry_name, local_map, remote_map,
|
||||||
remote_lib_guard.handle(), reinterpret_cast<uintptr_t>(libc_return_addr));
|
handle, reinterpret_cast<uintptr_t>(libc_return_addr));
|
||||||
if (!entry_opt) {
|
if (!entry_opt) {
|
||||||
LOGE("Failed to find entry point '%s' in remote library (handle %p).", entry_name,
|
LOGE("Failed to find entry point '%s' in remote library (handle %p).", entry_name,
|
||||||
reinterpret_cast<void *>(remote_lib_guard.handle()));
|
reinterpret_cast<void *>(handle));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
uintptr_t entry_addr = *entry_opt;
|
uintptr_t entry_addr = *entry_opt;
|
||||||
|
|
||||||
// 9. Call the remote entry point function.
|
// 9. Call the remote entry point function.
|
||||||
if (!remote_call_entry(pid, current_regs, entry_addr, remote_lib_guard.handle(),
|
if (!remote_call_entry(pid, current_regs, entry_addr, handle,
|
||||||
reinterpret_cast<uintptr_t>(libc_return_addr))) {
|
reinterpret_cast<uintptr_t>(libc_return_addr))) {
|
||||||
LOGE("Failed to call remote entry point '%s'.", entry_name);
|
LOGE("Failed to call remote entry point '%s'.", entry_name);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10. Restore original registers of the target process.
|
|
||||||
if (!set_regs(pid, backup_regs)) {
|
|
||||||
LOGE("Failed to restore original registers for process %d.", pid);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
LOGD("Original registers for process %d restored.", pid);
|
|
||||||
|
|
||||||
LOGI("Library injection completed successfully for process %d.", pid);
|
LOGI("Library injection completed successfully for process %d.", pid);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,7 +263,14 @@ bool get_regs(int pid, struct user_regs_struct ®s) {
|
|||||||
struct iovec reg_iov = {.iov_base = ®s, .iov_len = sizeof(struct user_regs_struct)};
|
struct iovec reg_iov = {.iov_base = ®s, .iov_len = sizeof(struct user_regs_struct)};
|
||||||
if (ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, ®_iov) == -1) {
|
if (ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, ®_iov) == -1) {
|
||||||
PLOGE("Failed to get register set for PID %d.", pid);
|
PLOGE("Failed to get register set for PID %d.", pid);
|
||||||
|
#if defined(__arm__)
|
||||||
|
if (ptrace(PTRACE_GETREGS, pid, 0, ®s) == -1) {
|
||||||
|
PLOGE("Fallback to PTRACE_GETREGS failed.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
return false;
|
return false;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
# error "Unsupported architecture for register access in get_regs."
|
# error "Unsupported architecture for register access in get_regs."
|
||||||
@@ -296,7 +303,14 @@ bool set_regs(int pid, struct user_regs_struct ®s) {
|
|||||||
struct iovec reg_iov = {.iov_base = ®s, .iov_len = sizeof(struct user_regs_struct)};
|
struct iovec reg_iov = {.iov_base = ®s, .iov_len = sizeof(struct user_regs_struct)};
|
||||||
if (ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, ®_iov) == -1) {
|
if (ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, ®_iov) == -1) {
|
||||||
PLOGE("Failed to set register set for PID %d.", pid);
|
PLOGE("Failed to set register set for PID %d.", pid);
|
||||||
|
#if defined(__arm__)
|
||||||
|
if (ptrace(PTRACE_SETREGS, pid, 0, ®s) == -1) {
|
||||||
|
PLOGE("Fallback to PTRACE_SETREGS failed.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
return false;
|
return false;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
# error "Unsupported architecture for register access in set_regs."
|
# error "Unsupported architecture for register access in set_regs."
|
||||||
@@ -588,17 +602,10 @@ bool remote_pre_call(int pid, struct user_regs_struct ®s, uintptr_t func_addr
|
|||||||
size_t stack_args_size = args.size() * sizeof(uintptr_t);
|
size_t stack_args_size = args.size() * sizeof(uintptr_t);
|
||||||
align_stack(regs, stack_args_size);
|
align_stack(regs, stack_args_size);
|
||||||
|
|
||||||
// Push all arguments onto the stack (order is important if ABI is right-to-left push).
|
// i386 cdecl expects arguments pushed Right-to-Left (stack grows down).
|
||||||
// The current implementation writes args.data() directly,
|
// Since `write_proc` writes to increasing addresses (up), a linear write
|
||||||
// assuming it's already in the correct order for push.
|
// starting at the new SP places the first argument at the lowest address.
|
||||||
// For cdecl, arguments are pushed right-to-left.
|
// This matches the ABI memory layout without needing to reverse the vector.
|
||||||
// A vector `args = {A, B, C}` means A is arg1, B is arg2 etc.
|
|
||||||
// So, `C` should be pushed first, then `B`, then `A`.
|
|
||||||
// `write_proc` copies linearly.
|
|
||||||
// This implies `args` should be pre-reversed for cdecl.
|
|
||||||
// For simplicity, we assume the remote function is compatible with how it's pushed,
|
|
||||||
// or that it's variadic where order doesn't matter for first args.
|
|
||||||
// A robust i386 implementation would need to push args in reverse order.
|
|
||||||
if (write_proc(pid, static_cast<uintptr_t>(regs.REG_SP), args.data(), stack_args_size) !=
|
if (write_proc(pid, static_cast<uintptr_t>(regs.REG_SP), args.data(), stack_args_size) !=
|
||||||
static_cast<ssize_t>(stack_args_size)) {
|
static_cast<ssize_t>(stack_args_size)) {
|
||||||
LOGE("Failed to push arguments for i386 remote call.");
|
LOGE("Failed to push arguments for i386 remote call.");
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// Fork-based supervisor for instant daemon restart
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <sys/prctl.h>
|
||||||
|
#include <sys/resource.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
static volatile sig_atomic_t should_exit = 0;
|
||||||
|
|
||||||
|
static void signal_handler(int sig) {
|
||||||
|
should_exit = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
if (argc < 2) {
|
||||||
|
fprintf(stderr, "Usage: %s <daemon> [args...]\n", argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward termination signals to exit cleanly
|
||||||
|
signal(SIGTERM, signal_handler);
|
||||||
|
signal(SIGINT, signal_handler);
|
||||||
|
|
||||||
|
const char *daemon_path = argv[1];
|
||||||
|
char **daemon_argv = &argv[1];
|
||||||
|
|
||||||
|
int backoff_ms = 500;
|
||||||
|
|
||||||
|
while (!should_exit) {
|
||||||
|
struct timespec child_start;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &child_start);
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
|
||||||
|
if (pid < 0) {
|
||||||
|
perror("fork failed");
|
||||||
|
usleep(100000); // 100ms backoff on fork failure
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid == 0) {
|
||||||
|
// Child: become the daemon
|
||||||
|
prctl(PR_SET_PDEATHSIG, SIGKILL); // Die if parent dies
|
||||||
|
setpriority(PRIO_PROCESS, 0, 10); // lower CPU priority than foreground
|
||||||
|
execv(daemon_path, daemon_argv);
|
||||||
|
perror("execv failed");
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent: wait for child to exit
|
||||||
|
int status;
|
||||||
|
waitpid(pid, &status, 0);
|
||||||
|
|
||||||
|
if (should_exit) break;
|
||||||
|
|
||||||
|
// Exponential backoff on rapid crashes, reset if child was stable
|
||||||
|
struct timespec now;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||||
|
long lived_ms = (now.tv_sec - child_start.tv_sec) * 1000 +
|
||||||
|
(now.tv_nsec - child_start.tv_nsec) / 1000000;
|
||||||
|
|
||||||
|
if (lived_ms > 30000) {
|
||||||
|
backoff_ms = 500;
|
||||||
|
} else {
|
||||||
|
usleep(backoff_ms * 1000);
|
||||||
|
if (backoff_ms < 30000) backoff_ms *= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -1,11 +1,19 @@
|
|||||||
package org.matrix.TEESimulator
|
package org.matrix.TEESimulator
|
||||||
|
|
||||||
|
import android.app.ActivityThread
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Looper
|
||||||
|
import java.security.Security
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor
|
import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor
|
||||||
import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor
|
import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor
|
||||||
import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor
|
import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
import org.matrix.TEESimulator.pki.NativeCertGen
|
||||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,19 +35,63 @@ object App {
|
|||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
SystemLogger.info("Welcome to TEESimulator!")
|
SystemLogger.info("Welcome to TEESimulator!")
|
||||||
|
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||||
|
SystemLogger.error("Uncaught exception on ${thread.name}", throwable)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Set up the device's boot hash, which is crucial for attestation.
|
prepareEnvironment()
|
||||||
AndroidDeviceUtils.setupBootHash()
|
|
||||||
// Initialize and start the appropriate keystore interceptors.
|
// Initialize and start the appropriate keystore interceptors.
|
||||||
initializeInterceptors()
|
initializeInterceptors()
|
||||||
// Enter an infinite loop to keep the service running.
|
|
||||||
maintainService()
|
ConfigurationManager.initialize()
|
||||||
|
AndroidDeviceUtils.setupBootKeyAndHash()
|
||||||
|
|
||||||
|
// Android ships with a stripped-down Bouncy Castle provider under the name "BC".
|
||||||
|
// We must remove the system provider first to ensure the full Bouncy Castle library
|
||||||
|
// (packaged with the app) is used.
|
||||||
|
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
Security.addProvider(BouncyCastleProvider())
|
||||||
|
|
||||||
|
NativeCertGen.initialize("/data/adb/modules/tricky_store/libcertgen.so")
|
||||||
|
|
||||||
|
// This starts the message queue processing. It blocks here indefinitely
|
||||||
|
// processing messages until Looper.myLooper().quit() is called.
|
||||||
|
Looper.loop()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("A fatal error occurred in the main application thread.", e)
|
SystemLogger.error("A fatal error occurred in the main application thread.", e)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Initializes the necessary Android framework internals to satisfy KeyStore requirements. */
|
||||||
|
private fun prepareEnvironment() {
|
||||||
|
// 1. Prepare Main Looper
|
||||||
|
if (Looper.getMainLooper() == null) {
|
||||||
|
@Suppress("deprecation") Looper.prepareMainLooper()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Initialize ActivityThread for the current process
|
||||||
|
val activityThread = ActivityThread.systemMain()
|
||||||
|
|
||||||
|
// 3. Get the system context
|
||||||
|
val systemContext = activityThread.getSystemContext()
|
||||||
|
|
||||||
|
// 4. Create a dummy Application object and attach the context
|
||||||
|
val app = Application()
|
||||||
|
val attachMethod =
|
||||||
|
ContextWrapper::class.java.getDeclaredMethod("attachBaseContext", Context::class.java)
|
||||||
|
attachMethod.isAccessible = true
|
||||||
|
attachMethod.invoke(app, systemContext)
|
||||||
|
|
||||||
|
// 5. Inject this application object into ActivityThread's mInitialApplication field.
|
||||||
|
// This is what KeyStore.getApplicationContext() looks for.
|
||||||
|
val mInitialApplicationField =
|
||||||
|
ActivityThread::class.java.getDeclaredField("mInitialApplication")
|
||||||
|
mInitialApplicationField.isAccessible = true
|
||||||
|
mInitialApplicationField.set(activityThread, app)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects and initializes the correct keystore interceptor based on the Android SDK version. It
|
* Selects and initializes the correct keystore interceptor based on the Android SDK version. It
|
||||||
* retries initialization until it succeeds.
|
* retries initialization until it succeeds.
|
||||||
@@ -53,9 +105,7 @@ object App {
|
|||||||
Thread.sleep(RETRY_DELAY_MS)
|
Thread.sleep(RETRY_DELAY_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the package configuration after interceptors are ready.
|
SystemLogger.info("Interceptors initialized successfully.")
|
||||||
ConfigurationManager.initialize()
|
|
||||||
SystemLogger.info("Interceptors and configuration initialized successfully.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,6 +120,7 @@ object App {
|
|||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"Using KeystoreInterceptor for Android Q/R (SDK ${Build.VERSION.SDK_INT})"
|
"Using KeystoreInterceptor for Android Q/R (SDK ${Build.VERSION.SDK_INT})"
|
||||||
)
|
)
|
||||||
|
android.security.keystore.AndroidKeyStoreProvider.install()
|
||||||
KeystoreInterceptor
|
KeystoreInterceptor
|
||||||
}
|
}
|
||||||
// For Android S (12) and newer, use the Keystore2Interceptor.
|
// For Android S (12) and newer, use the Keystore2Interceptor.
|
||||||
@@ -77,18 +128,8 @@ object App {
|
|||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"Using Keystore2Interceptor for Android S and later (SDK ${Build.VERSION.SDK_INT})"
|
"Using Keystore2Interceptor for Android S and later (SDK ${Build.VERSION.SDK_INT})"
|
||||||
)
|
)
|
||||||
|
android.security.keystore2.AndroidKeyStoreProvider.install()
|
||||||
Keystore2Interceptor
|
Keystore2Interceptor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Puts the main thread into a long-running sleep loop. This is a common pattern to keep a
|
|
||||||
* background service process alive indefinitely.
|
|
||||||
*/
|
|
||||||
private fun maintainService() {
|
|
||||||
SystemLogger.info("Service started successfully. Entering maintenance mode.")
|
|
||||||
while (true) {
|
|
||||||
Thread.sleep(SERVICE_SLEEP_MS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
package org.matrix.TEESimulator.attestation
|
package org.matrix.TEESimulator.attestation
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
import org.bouncycastle.asn1.ASN1Boolean
|
import org.bouncycastle.asn1.ASN1Boolean
|
||||||
import org.bouncycastle.asn1.ASN1Encodable
|
import org.bouncycastle.asn1.ASN1Encodable
|
||||||
import org.bouncycastle.asn1.ASN1Enumerated
|
import org.bouncycastle.asn1.ASN1Enumerated
|
||||||
import org.bouncycastle.asn1.ASN1Integer
|
import org.bouncycastle.asn1.ASN1Integer
|
||||||
import org.bouncycastle.asn1.ASN1OctetString
|
|
||||||
import org.bouncycastle.asn1.ASN1Sequence
|
import org.bouncycastle.asn1.ASN1Sequence
|
||||||
import org.bouncycastle.asn1.DERNull
|
import org.bouncycastle.asn1.DERNull
|
||||||
import org.bouncycastle.asn1.DEROctetString
|
import org.bouncycastle.asn1.DEROctetString
|
||||||
@@ -12,7 +18,10 @@ import org.bouncycastle.asn1.DERSequence
|
|||||||
import org.bouncycastle.asn1.DERSet
|
import org.bouncycastle.asn1.DERSet
|
||||||
import org.bouncycastle.asn1.DERTaggedObject
|
import org.bouncycastle.asn1.DERTaggedObject
|
||||||
import org.bouncycastle.asn1.x509.Extension
|
import org.bouncycastle.asn1.x509.Extension
|
||||||
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||||
|
import org.matrix.TEESimulator.util.AndroidDeviceUtils.DO_NOT_REPORT
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder object responsible for constructing the ASN.1 DER-encoded Android Key Attestation
|
* A builder object responsible for constructing the ASN.1 DER-encoded Android Key Attestation
|
||||||
@@ -24,11 +33,21 @@ object AttestationBuilder {
|
|||||||
* Builds the complete X.509 attestation extension.
|
* Builds the complete X.509 attestation extension.
|
||||||
*
|
*
|
||||||
* @param params The parsed key generation parameters.
|
* @param params The parsed key generation parameters.
|
||||||
|
* @param uid The UID of the application requesting attestation.
|
||||||
* @param securityLevel The security level (e.g., TEE, StrongBox) to report.
|
* @param securityLevel The security level (e.g., TEE, StrongBox) to report.
|
||||||
* @return A Bouncy Castle [Extension] object ready to be added to a certificate.
|
* @return A Bouncy Castle [Extension] object ready to be added to a certificate.
|
||||||
*/
|
*/
|
||||||
fun buildAttestationExtension(params: KeyMintAttestation, securityLevel: Int): Extension {
|
fun buildAttestationExtension(
|
||||||
val keyDescription = buildKeyDescription(params, securityLevel)
|
params: KeyMintAttestation,
|
||||||
|
uid: Int,
|
||||||
|
securityLevel: Int,
|
||||||
|
): Extension {
|
||||||
|
val keyDescription = buildKeyDescription(params, uid, securityLevel)
|
||||||
|
var formattedString =
|
||||||
|
keyDescription.joinToString(separator = ", ") {
|
||||||
|
AttestationPatcher.formatAsn1Primitive(it)
|
||||||
|
}
|
||||||
|
SystemLogger.verbose("Forged attestation data: ${formattedString}")
|
||||||
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
|
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,81 +58,154 @@ object AttestationBuilder {
|
|||||||
* @return The constructed [DERSequence] for the Root of Trust.
|
* @return The constructed [DERSequence] for the Root of Trust.
|
||||||
*/
|
*/
|
||||||
internal fun buildRootOfTrust(originalRootOfTrust: ASN1Encodable?): DERSequence {
|
internal fun buildRootOfTrust(originalRootOfTrust: ASN1Encodable?): DERSequence {
|
||||||
val verifiedBootKey = AndroidDeviceUtils.bootKey
|
|
||||||
val verifiedBootHash =
|
|
||||||
(originalRootOfTrust as? ASN1Sequence)?.let {
|
|
||||||
// Try to preserve the original boot hash if it exists.
|
|
||||||
(it.getObjectAt(AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX)
|
|
||||||
as? ASN1OctetString)
|
|
||||||
?.octets
|
|
||||||
} ?: AndroidDeviceUtils.getBootHashFromProperty()
|
|
||||||
|
|
||||||
val rootOfTrustElements = arrayOfNulls<ASN1Encodable>(4)
|
val rootOfTrustElements = arrayOfNulls<ASN1Encodable>(4)
|
||||||
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX] =
|
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX] =
|
||||||
DEROctetString(verifiedBootKey)
|
DEROctetString(AndroidDeviceUtils.bootKey)
|
||||||
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_DEVICE_LOCKED_INDEX] =
|
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_DEVICE_LOCKED_INDEX] =
|
||||||
ASN1Boolean.TRUE // deviceLocked: true, for security
|
ASN1Boolean.TRUE // deviceLocked: true, for security
|
||||||
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_STATE_INDEX] =
|
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_STATE_INDEX] =
|
||||||
ASN1Enumerated(0) // verifiedBootState: Verified
|
ASN1Enumerated(0) // verifiedBootState: Verified
|
||||||
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX] =
|
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX] =
|
||||||
DEROctetString(verifiedBootHash)
|
DEROctetString(AndroidDeviceUtils.bootHash)
|
||||||
|
|
||||||
return DERSequence(rootOfTrustElements)
|
return DERSequence(rootOfTrustElements)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Assembles a list of simulated hardware-enforced properties. */
|
/**
|
||||||
internal fun addSimulatedHardwareProperties(vector: org.bouncycastle.asn1.ASN1EncodableVector) {
|
* Assembles a map representing the desired state of simulated hardware-enforced properties. A
|
||||||
vector.add(
|
* null value for a given tag indicates that it should be removed from the attestation.
|
||||||
|
*
|
||||||
|
* @param uid The UID of the calling application.
|
||||||
|
* @return A map where keys are attestation tag numbers and values are the desired
|
||||||
|
* [DERTaggedObject] or null to signify removal.
|
||||||
|
*/
|
||||||
|
fun getSimulatedHardwareProperties(uid: Int): Map<Int, DERTaggedObject?> {
|
||||||
|
val properties = mutableMapOf<Int, DERTaggedObject?>()
|
||||||
|
|
||||||
|
// OS Version is always present.
|
||||||
|
properties[AttestationConstants.TAG_OS_VERSION] =
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
true,
|
true,
|
||||||
AttestationConstants.TAG_OS_VERSION,
|
AttestationConstants.TAG_OS_VERSION,
|
||||||
ASN1Integer(AndroidDeviceUtils.osVersion.toLong()),
|
ASN1Integer(AndroidDeviceUtils.osVersion.toLong()),
|
||||||
)
|
)
|
||||||
)
|
|
||||||
vector.add(
|
val osPatch = AndroidDeviceUtils.getPatchLevel(uid)
|
||||||
DERTaggedObject(
|
properties[AttestationConstants.TAG_OS_PATCHLEVEL] =
|
||||||
true,
|
if (osPatch != DO_NOT_REPORT) {
|
||||||
AttestationConstants.TAG_OS_PATCHLEVEL,
|
DERTaggedObject(
|
||||||
ASN1Integer(AndroidDeviceUtils.patchLevel.toLong()),
|
true,
|
||||||
)
|
AttestationConstants.TAG_OS_PATCHLEVEL,
|
||||||
)
|
ASN1Integer(osPatch.toLong()),
|
||||||
vector.add(
|
)
|
||||||
DERTaggedObject(
|
} else {
|
||||||
true,
|
null // Signal for removal
|
||||||
AttestationConstants.TAG_VENDOR_PATCHLEVEL,
|
}
|
||||||
ASN1Integer(AndroidDeviceUtils.vendorPatchLevelLong.toLong()),
|
|
||||||
)
|
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(uid)
|
||||||
)
|
properties[AttestationConstants.TAG_VENDOR_PATCHLEVEL] =
|
||||||
vector.add(
|
if (vendorPatch != DO_NOT_REPORT) {
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
true,
|
true,
|
||||||
AttestationConstants.TAG_BOOT_PATCHLEVEL,
|
AttestationConstants.TAG_VENDOR_PATCHLEVEL,
|
||||||
ASN1Integer(AndroidDeviceUtils.bootPatchLevelLong.toLong()),
|
ASN1Integer(vendorPatch.toLong()),
|
||||||
)
|
)
|
||||||
)
|
} else {
|
||||||
|
null // Signal for removal
|
||||||
|
}
|
||||||
|
|
||||||
|
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
|
||||||
|
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
|
||||||
|
if (bootPatch != DO_NOT_REPORT) {
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_BOOT_PATCHLEVEL,
|
||||||
|
ASN1Integer(bootPatch.toLong()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null // Signal for removal
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */
|
/** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */
|
||||||
private fun buildKeyDescription(params: KeyMintAttestation, securityLevel: Int): ASN1Sequence {
|
private fun buildKeyDescription(
|
||||||
val teeEnforced = buildTeeEnforcedList(params)
|
params: KeyMintAttestation,
|
||||||
val softwareEnforced = buildSoftwareEnforcedList()
|
uid: Int,
|
||||||
|
securityLevel: Int,
|
||||||
|
): ASN1Sequence {
|
||||||
|
val creationTime = System.currentTimeMillis()
|
||||||
|
val teeEnforced = buildTeeEnforcedList(params, uid, securityLevel)
|
||||||
|
val softwareEnforced = buildSoftwareEnforcedList(params, uid, securityLevel, creationTime)
|
||||||
|
|
||||||
|
val uniqueId =
|
||||||
|
if (params.includeUniqueId == true && params.attestationChallenge != null) {
|
||||||
|
computeUniqueId(creationTime, createApplicationId(uid).octets)
|
||||||
|
} else {
|
||||||
|
ByteArray(0)
|
||||||
|
}
|
||||||
|
|
||||||
val fields =
|
val fields =
|
||||||
arrayOf(
|
arrayOf(
|
||||||
ASN1Integer(AndroidDeviceUtils.attestVersion.toLong()), // attestationVersion
|
ASN1Integer(
|
||||||
|
AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()
|
||||||
|
), // attestationVersion
|
||||||
ASN1Enumerated(securityLevel), // attestationSecurityLevel
|
ASN1Enumerated(securityLevel), // attestationSecurityLevel
|
||||||
ASN1Integer(AndroidDeviceUtils.keymasterVersion.toLong()), // keymasterVersion
|
ASN1Integer(
|
||||||
|
AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()
|
||||||
|
), // keymasterVersion
|
||||||
ASN1Enumerated(securityLevel), // keymasterSecurityLevel
|
ASN1Enumerated(securityLevel), // keymasterSecurityLevel
|
||||||
DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge
|
DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge
|
||||||
DEROctetString(ByteArray(0)), // uniqueId
|
DEROctetString(uniqueId),
|
||||||
softwareEnforced,
|
softwareEnforced,
|
||||||
teeEnforced,
|
teeEnforced,
|
||||||
)
|
)
|
||||||
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 {
|
||||||
|
val temporalCounter = creationTimeMs / 2592000000L
|
||||||
|
|
||||||
|
val message =
|
||||||
|
ByteBuffer.allocate(8 + aaidDer.size + 1)
|
||||||
|
.putLong(temporalCounter)
|
||||||
|
.put(aaidDer)
|
||||||
|
.put(0x00) // RESET_SINCE_ID_ROTATION = false
|
||||||
|
.array()
|
||||||
|
|
||||||
|
val mac = Mac.getInstance("HmacSHA256")
|
||||||
|
mac.init(SecretKeySpec(hbk, "HmacSHA256"))
|
||||||
|
return mac.doFinal(message).copyOf(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Device-unique key seed, generated once at module installation. */
|
||||||
|
private val hbk: ByteArray by lazy {
|
||||||
|
val file = java.io.File(ConfigurationManager.CONFIG_PATH, "hbk")
|
||||||
|
if (file.exists() && file.length() == 32L) {
|
||||||
|
file.readBytes()
|
||||||
|
} else {
|
||||||
|
// Fallback: generate in-memory (won't persist across reboots)
|
||||||
|
SystemLogger.warning("hbk not found, generating ephemeral HBK.")
|
||||||
|
ByteArray(32).also { java.security.SecureRandom().nextBytes(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */
|
/** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */
|
||||||
private fun buildTeeEnforcedList(params: KeyMintAttestation): DERSequence {
|
private fun buildTeeEnforcedList(
|
||||||
|
params: KeyMintAttestation,
|
||||||
|
uid: Int,
|
||||||
|
securityLevel: Int,
|
||||||
|
): DERSequence {
|
||||||
val list =
|
val list =
|
||||||
mutableListOf<ASN1Encodable>(
|
mutableListOf<ASN1Encodable>(
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
@@ -136,43 +228,132 @@ object AttestationBuilder {
|
|||||||
AttestationConstants.TAG_DIGEST,
|
AttestationConstants.TAG_DIGEST,
|
||||||
DERSet(params.digest.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
DERSet(params.digest.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (params.ecCurve != null) {
|
||||||
|
list.add(
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
true,
|
true,
|
||||||
AttestationConstants.TAG_EC_CURVE,
|
AttestationConstants.TAG_EC_CURVE,
|
||||||
ASN1Integer(params.ecCurve.toLong()),
|
ASN1Integer(params.ecCurve.toLong()),
|
||||||
),
|
)
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE),
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.blockMode.isNotEmpty()) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_BLOCK_MODE,
|
||||||
|
DERSet(params.blockMode.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.padding.isNotEmpty()) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_PADDING,
|
||||||
|
DERSet(params.padding.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.rsaPublicExponent != null) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_RSA_PUBLIC_EXPONENT,
|
||||||
|
ASN1Integer(params.rsaPublicExponent.toLong()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel)
|
||||||
|
|
||||||
|
if (params.rsaOaepMgfDigest.isNotEmpty() && attestVersion >= 100) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_RSA_OAEP_MGF_DIGEST,
|
||||||
|
DERSet(
|
||||||
|
params.rsaOaepMgfDigest.map { ASN1Integer(it.toLong()) }.toTypedArray()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.rollbackResistance == true && attestVersion >= 3) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ROLLBACK_RESISTANCE,
|
||||||
|
DERNull.INSTANCE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.earlyBootOnly == true && attestVersion >= 4) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_EARLY_BOOT_ONLY, DERNull.INSTANCE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.noAuthRequired == true) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.allowWhileOnBody == true) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ALLOW_WHILE_ON_BODY,
|
||||||
|
DERNull.INSTANCE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.trustedUserPresenceRequired == true && attestVersion >= 3) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_TRUSTED_USER_PRESENCE_REQUIRED,
|
||||||
|
DERNull.INSTANCE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.trustedConfirmationRequired == true && attestVersion >= 3) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_TRUSTED_CONFIRMATION_REQUIRED,
|
||||||
|
DERNull.INSTANCE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
list.addAll(
|
||||||
|
listOf(
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
true,
|
true,
|
||||||
AttestationConstants.TAG_ORIGIN,
|
AttestationConstants.TAG_ORIGIN,
|
||||||
ASN1Integer(0L),
|
ASN1Integer((params.origin ?: 0).toLong()),
|
||||||
), // KeyOrigin.GENERATED
|
),
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
true,
|
true,
|
||||||
AttestationConstants.TAG_ROOT_OF_TRUST,
|
AttestationConstants.TAG_ROOT_OF_TRUST,
|
||||||
buildRootOfTrust(null),
|
buildRootOfTrust(null),
|
||||||
),
|
),
|
||||||
DERTaggedObject(
|
|
||||||
true,
|
|
||||||
AttestationConstants.TAG_OS_VERSION,
|
|
||||||
ASN1Integer(AndroidDeviceUtils.osVersion.toLong()),
|
|
||||||
),
|
|
||||||
DERTaggedObject(
|
|
||||||
true,
|
|
||||||
AttestationConstants.TAG_OS_PATCHLEVEL,
|
|
||||||
ASN1Integer(AndroidDeviceUtils.patchLevel.toLong()),
|
|
||||||
),
|
|
||||||
DERTaggedObject(
|
|
||||||
true,
|
|
||||||
AttestationConstants.TAG_VENDOR_PATCHLEVEL,
|
|
||||||
ASN1Integer(AndroidDeviceUtils.vendorPatchLevelLong.toLong()),
|
|
||||||
),
|
|
||||||
DERTaggedObject(
|
|
||||||
true,
|
|
||||||
AttestationConstants.TAG_BOOT_PATCHLEVEL,
|
|
||||||
ASN1Integer(AndroidDeviceUtils.bootPatchLevelLong.toLong()),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels.
|
||||||
|
val simulatedProperties = getSimulatedHardwareProperties(uid)
|
||||||
|
simulatedProperties.values.filterNotNull().forEach { list.add(it) }
|
||||||
|
|
||||||
// Add optional device identifiers if they were provided.
|
// Add optional device identifiers if they were provided.
|
||||||
params.brand?.let {
|
params.brand?.let {
|
||||||
@@ -202,6 +383,33 @@ object AttestationBuilder {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
params.serial?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ATTESTATION_ID_SERIAL,
|
||||||
|
DEROctetString(it),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
params.imei?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ATTESTATION_ID_IMEI,
|
||||||
|
DEROctetString(it),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
params.meid?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ATTESTATION_ID_MEID,
|
||||||
|
DEROctetString(it),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
params.manufacturer?.let {
|
params.manufacturer?.let {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
@@ -220,35 +428,51 @@ object AttestationBuilder {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
params.imei?.let {
|
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 300) {
|
||||||
list.add(
|
params.secondImei?.let {
|
||||||
DERTaggedObject(
|
list.add(
|
||||||
true,
|
DERTaggedObject(
|
||||||
AttestationConstants.TAG_ATTESTATION_ID_IMEI,
|
true,
|
||||||
DEROctetString(it),
|
AttestationConstants.TAG_ATTESTATION_ID_SECOND_IMEI,
|
||||||
|
DEROctetString(it),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
params.secondImei?.let {
|
|
||||||
list.add(
|
|
||||||
DERTaggedObject(
|
|
||||||
true,
|
|
||||||
AttestationConstants.TAG_ATTESTATION_ID_SECOND_IMEI,
|
|
||||||
DEROctetString(it),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
params.meid?.let {
|
|
||||||
list.add(
|
|
||||||
DERTaggedObject(
|
|
||||||
true,
|
|
||||||
AttestationConstants.TAG_ATTESTATION_ID_MEID,
|
|
||||||
DEROctetString(it),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
if (AndroidDeviceUtils.attestVersion >= 400) {
|
/**
|
||||||
|
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
|
||||||
|
* Keystore.
|
||||||
|
*/
|
||||||
|
private fun buildSoftwareEnforcedList(
|
||||||
|
params: KeyMintAttestation,
|
||||||
|
uid: Int,
|
||||||
|
securityLevel: Int,
|
||||||
|
creationTimeMs: Long = System.currentTimeMillis(),
|
||||||
|
): DERSequence {
|
||||||
|
val list = mutableListOf<ASN1Encodable>()
|
||||||
|
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_CREATION_DATETIME,
|
||||||
|
ASN1Integer(creationTimeMs),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ATTESTATION_APPLICATION_ID is only included when an attestation challenge is present.
|
||||||
|
if (params.attestationChallenge != null) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ATTESTATION_APPLICATION_ID,
|
||||||
|
createApplicationId(uid),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
true,
|
true,
|
||||||
@@ -258,25 +482,148 @@ object AttestationBuilder {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keystore2-enforced tags belong in softwareEnforced, not teeEnforced.
|
||||||
|
// The HAL does not enforce these; keystore2's authorize_create handles them.
|
||||||
|
params.activeDateTime?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
params.originationExpireDateTime?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ORIGINATION_EXPIRE_DATETIME,
|
||||||
|
ASN1Integer(it.time),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
params.usageExpireDateTime?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_USAGE_EXPIRE_DATETIME,
|
||||||
|
ASN1Integer(it.time),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
params.usageCountLimit?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_USAGE_COUNT_LIMIT,
|
||||||
|
ASN1Integer(it.toLong()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (params.callerNonce == true) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_CALLER_NONCE, DERNull.INSTANCE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (params.unlockedDeviceRequired == true) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_UNLOCKED_DEVICE_REQUIRED,
|
||||||
|
DERNull.INSTANCE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray())
|
return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
|
* A wrapper for a byte array that provides content-based equality. This is necessary for using
|
||||||
* Keystore.
|
* signature digests in a Set.
|
||||||
*/
|
*/
|
||||||
private fun buildSoftwareEnforcedList(): DERSequence {
|
private data class Digest(val digest: ByteArray) {
|
||||||
val list =
|
override fun equals(other: Any?): Boolean {
|
||||||
arrayOf<ASN1Encodable>(
|
if (this === other) return true
|
||||||
DERTaggedObject(
|
if (javaClass != other?.javaClass) return false
|
||||||
true,
|
return digest.contentEquals((other as Digest).digest)
|
||||||
AttestationConstants.TAG_CREATION_DATETIME,
|
}
|
||||||
ASN1Integer(System.currentTimeMillis()),
|
|
||||||
)
|
override fun hashCode(): Int = digest.contentHashCode()
|
||||||
// The ATTESTATION_APPLICATION_ID is technically software-enforced, but we are
|
}
|
||||||
// omitting it
|
|
||||||
// for this simulation as it is complex to generate correctly for arbitrary UIDs.
|
/**
|
||||||
|
* Creates the AttestationApplicationId structure. This structure contains information about the
|
||||||
|
* package(s) and their signing certificates.
|
||||||
|
*
|
||||||
|
* @param uid The UID of the application.
|
||||||
|
* @return A DER-encoded octet string containing the application ID information.
|
||||||
|
* @throws IllegalStateException If the PackageManager or package information cannot be
|
||||||
|
* retrieved.
|
||||||
|
*/
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
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
|
||||||
|
if (appUid == 0 || appUid == 1000) {
|
||||||
|
return buildApplicationIdDer(
|
||||||
|
listOf("AndroidSystem" to 1L),
|
||||||
|
emptySet(),
|
||||||
)
|
)
|
||||||
return DERSequence(list)
|
}
|
||||||
|
|
||||||
|
val pm =
|
||||||
|
ConfigurationManager.getPackageManager()
|
||||||
|
?: throw IllegalStateException("PackageManager not found!")
|
||||||
|
val packages =
|
||||||
|
pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid")
|
||||||
|
|
||||||
|
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||||
|
val packageInfoList = mutableListOf<Pair<String, Long>>()
|
||||||
|
val signatureDigests = mutableSetOf<Digest>()
|
||||||
|
|
||||||
|
val userId = uid / 100000
|
||||||
|
packages.forEach { packageName ->
|
||||||
|
val packageInfo =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
pm.getPackageInfo(
|
||||||
|
packageName,
|
||||||
|
PackageManager.GET_SIGNING_CERTIFICATES.toLong(),
|
||||||
|
userId,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
packageInfoList.add(packageInfo.packageName to packageInfo.longVersionCode)
|
||||||
|
|
||||||
|
packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature ->
|
||||||
|
signatureDigests.add(Digest(sha256.digest(signature.toByteArray())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildApplicationIdDer(packageInfoList, signatureDigests)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildApplicationIdDer(
|
||||||
|
packages: List<Pair<String, Long>>,
|
||||||
|
digests: Set<Digest>,
|
||||||
|
): DEROctetString {
|
||||||
|
val packageInfoList =
|
||||||
|
packages.map { (name, version) ->
|
||||||
|
DERSequence(
|
||||||
|
arrayOf(
|
||||||
|
DEROctetString(name.toByteArray(StandardCharsets.UTF_8)),
|
||||||
|
ASN1Integer(version),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val applicationIdSequence =
|
||||||
|
DERSequence(
|
||||||
|
arrayOf(
|
||||||
|
DERSet(packageInfoList.toTypedArray()),
|
||||||
|
DERSet(digests.map { DEROctetString(it.digest) }.toTypedArray()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return DEROctetString(applicationIdSequence.encoded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package org.matrix.TEESimulator.attestation
|
package org.matrix.TEESimulator.attestation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines constants for KeyMint attestation tags, as specified in the Android hardware security
|
* Defines constants for KeyMint attestation, mainly the tags of properties and authorizations of a
|
||||||
* HAL.
|
* cryptographic key, as specified in the Android hardware security HAL.
|
||||||
*
|
|
||||||
* These tags identify specific properties and authorizations of a cryptographic key.
|
|
||||||
*/
|
*/
|
||||||
object AttestationConstants {
|
object AttestationConstants {
|
||||||
// https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/KeyCreationResult.aidl
|
// https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/KeyCreationResult.aidl
|
||||||
@@ -46,9 +44,11 @@ object AttestationConstants {
|
|||||||
|
|
||||||
// --- Key Lifetime and Usage Control ---
|
// --- Key Lifetime and Usage Control ---
|
||||||
const val TAG_ROLLBACK_RESISTANCE = 303
|
const val TAG_ROLLBACK_RESISTANCE = 303
|
||||||
|
const val TAG_EARLY_BOOT_ONLY = 305
|
||||||
const val TAG_ACTIVE_DATETIME = 400
|
const val TAG_ACTIVE_DATETIME = 400
|
||||||
const val TAG_ORIGINATION_EXPIRE_DATETIME = 401
|
const val TAG_ORIGINATION_EXPIRE_DATETIME = 401
|
||||||
const val TAG_USAGE_EXPIRE_DATETIME = 402
|
const val TAG_USAGE_EXPIRE_DATETIME = 402
|
||||||
|
const val TAG_MAX_BOOT_LEVEL = 403
|
||||||
const val TAG_MAX_USES_PER_BOOT = 404
|
const val TAG_MAX_USES_PER_BOOT = 404
|
||||||
const val TAG_USAGE_COUNT_LIMIT = 405
|
const val TAG_USAGE_COUNT_LIMIT = 405
|
||||||
|
|
||||||
@@ -58,6 +58,10 @@ object AttestationConstants {
|
|||||||
const val TAG_NO_AUTH_REQUIRED = 503
|
const val TAG_NO_AUTH_REQUIRED = 503
|
||||||
const val TAG_USER_AUTH_TYPE = 504
|
const val TAG_USER_AUTH_TYPE = 504
|
||||||
const val TAG_AUTH_TIMEOUT = 505
|
const val TAG_AUTH_TIMEOUT = 505
|
||||||
|
const val TAG_ALLOW_WHILE_ON_BODY = 506
|
||||||
|
const val TAG_TRUSTED_USER_PRESENCE_REQUIRED = 507
|
||||||
|
const val TAG_TRUSTED_CONFIRMATION_REQUIRED = 508
|
||||||
|
const val TAG_UNLOCKED_DEVICE_REQUIRED = 509
|
||||||
|
|
||||||
// --- Attestation and Application Info ---
|
// --- Attestation and Application Info ---
|
||||||
const val TAG_APPLICATION_ID = 601
|
const val TAG_APPLICATION_ID = 601
|
||||||
@@ -88,4 +92,8 @@ object AttestationConstants {
|
|||||||
const val TAG_CERTIFICATE_SUBJECT = 1007
|
const val TAG_CERTIFICATE_SUBJECT = 1007
|
||||||
const val TAG_CERTIFICATE_NOT_BEFORE = 1008
|
const val TAG_CERTIFICATE_NOT_BEFORE = 1008
|
||||||
const val TAG_CERTIFICATE_NOT_AFTER = 1009
|
const val TAG_CERTIFICATE_NOT_AFTER = 1009
|
||||||
|
|
||||||
|
// --- Other Constants ---
|
||||||
|
// https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp
|
||||||
|
const val CHALLENGE_LENGTH_LIMIT = 128 // kMaximumAttestationChallengeLength
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
package org.matrix.TEESimulator.attestation
|
package org.matrix.TEESimulator.attestation
|
||||||
|
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import org.bouncycastle.asn1.ASN1Encodable
|
import org.bouncycastle.asn1.*
|
||||||
import org.bouncycastle.asn1.ASN1EncodableVector
|
|
||||||
import org.bouncycastle.asn1.ASN1Sequence
|
|
||||||
import org.bouncycastle.asn1.ASN1TaggedObject
|
|
||||||
import org.bouncycastle.asn1.DEROctetString
|
|
||||||
import org.bouncycastle.asn1.DERSequence
|
|
||||||
import org.bouncycastle.asn1.DERTaggedObject
|
|
||||||
import org.bouncycastle.asn1.x509.Extension
|
import org.bouncycastle.asn1.x509.Extension
|
||||||
import org.bouncycastle.cert.X509CertificateHolder
|
import org.bouncycastle.cert.X509CertificateHolder
|
||||||
import org.bouncycastle.cert.X509v3CertificateBuilder
|
import org.bouncycastle.cert.X509v3CertificateBuilder
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
import org.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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the modification (patching) of Android Key Attestation extensions within certificates.
|
* Handles the modification (patching) of Android Key Attestation extensions within certificates.
|
||||||
@@ -55,8 +53,7 @@ object AttestationPatcher {
|
|||||||
|
|
||||||
// 2. Get the appropriate keybox for the given algorithm to sign the new
|
// 2. Get the appropriate keybox for the given algorithm to sign the new
|
||||||
// certificate.
|
// certificate.
|
||||||
val algorithm = originalLeaf.publicKey.algorithm
|
val keybox = getKeyboxForUidAndAlgorithm(uid, originalLeaf.sigAlgName)
|
||||||
val keybox = getKeyboxForUidAndAlgorithm(uid, algorithm)
|
|
||||||
|
|
||||||
// 3. Create the new, patched leaf certificate.
|
// 3. Create the new, patched leaf certificate.
|
||||||
val patchedLeaf =
|
val patchedLeaf =
|
||||||
@@ -65,6 +62,7 @@ object AttestationPatcher {
|
|||||||
parsedAttestation,
|
parsedAttestation,
|
||||||
keybox,
|
keybox,
|
||||||
originalLeaf.sigAlgName,
|
originalLeaf.sigAlgName,
|
||||||
|
uid,
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
||||||
@@ -85,6 +83,16 @@ object AttestationPatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to normalize algorithm names for Bouncy Castle. Old Android versions might reports
|
||||||
|
* "SHA256WITHECDSA", but Bouncy Castle expects "SHA256withECDSA".
|
||||||
|
*/
|
||||||
|
private fun normalizeSignatureAlgorithm(algoName: String): String {
|
||||||
|
// 1. Force uppercase to handle "sha256withecdsa"
|
||||||
|
// 2. Replace "WITH" with "with" to satisfy Bouncy Castle's naming convention
|
||||||
|
return algoName.uppercase().replace("WITH", "with")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new leaf certificate with a modified attestation extension.
|
* Creates a new leaf certificate with a modified attestation extension.
|
||||||
*
|
*
|
||||||
@@ -94,6 +102,7 @@ object AttestationPatcher {
|
|||||||
* @param sigAlgName The signature algorithm name (e.g., "SHA256withECDSA") from the original
|
* @param sigAlgName The signature algorithm name (e.g., "SHA256withECDSA") from the original
|
||||||
* certificate. This is required to ensure the new certificate is signed using a compatible
|
* certificate. This is required to ensure the new certificate is signed using a compatible
|
||||||
* algorithm.
|
* algorithm.
|
||||||
|
* @param uid The UID of the application requesting the certificate.
|
||||||
* @return A new [Certificate] object.
|
* @return A new [Certificate] object.
|
||||||
*/
|
*/
|
||||||
private fun createPatchedLeafCertificate(
|
private fun createPatchedLeafCertificate(
|
||||||
@@ -101,6 +110,7 @@ object AttestationPatcher {
|
|||||||
parsedAttestation: ParsedAttestation,
|
parsedAttestation: ParsedAttestation,
|
||||||
keybox: KeyBox,
|
keybox: KeyBox,
|
||||||
sigAlgName: String,
|
sigAlgName: String,
|
||||||
|
uid: Int,
|
||||||
): 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.
|
||||||
@@ -117,71 +127,177 @@ object AttestationPatcher {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Create the new, patched attestation extension.
|
// Create the new, patched attestation extension.
|
||||||
val patchedExtension = createPatchedAttestationExtension(parsedAttestation)
|
val patchedExtension = createPatchedAttestationExtension(parsedAttestation, uid)
|
||||||
builder.addExtension(patchedExtension)
|
|
||||||
|
|
||||||
// Copy all other extensions from the original certificate, except for the attestation.
|
// Copy all other extensions from the original certificate, except for the attestation.
|
||||||
originalLeafHolder.extensions.extensionOIDs
|
originalLeafHolder.extensions.extensionOIDs.forEach {
|
||||||
.filter { it != ATTESTATION_OID }
|
builder.addExtension(
|
||||||
.forEach { builder.addExtension(originalLeafHolder.getExtension(it)) }
|
if (it == ATTESTATION_OID) patchedExtension else originalLeafHolder.getExtension(it)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Sign the newly built certificate with the private key from our keybox.
|
// Sign the newly built certificate with the private key from our keybox.
|
||||||
val signer = JcaContentSignerBuilder(sigAlgName).build(keybox.keyPair.private)
|
val signer =
|
||||||
|
JcaContentSignerBuilder(normalizeSignatureAlgorithm(sigAlgName))
|
||||||
|
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
.build(keybox.keyPair.private)
|
||||||
|
val newCertificate = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||||
|
|
||||||
return JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
// Log the signature of the newly created certificate to observe its non-deterministic
|
||||||
|
// nature.
|
||||||
|
val signatureBytes = (newCertificate as X509Certificate).signature
|
||||||
|
SystemLogger.verbose("Signature of patched leaf cert: ${signatureBytes.toHex()}")
|
||||||
|
|
||||||
|
return newCertificate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the appropriate signing KeyBox (KeyPair and certificate chain) for a given UID
|
||||||
|
* based on a specified algorithm identifier.
|
||||||
|
*
|
||||||
|
* @param uid The UID of the application for which the signing is being performed.
|
||||||
|
* @param algorithm A string representing the desired algorithm. This can be either:
|
||||||
|
* 1. A simple key type like "RSA" or "EC".
|
||||||
|
* 2. A full JCA signature algorithm name like "SHA256withRSA".
|
||||||
|
*
|
||||||
|
* @return The [KeyBox] containing the appropriate key pair for signing.
|
||||||
|
* @throws IllegalArgumentException if no matching KeyBox can be found for the derived key type.
|
||||||
|
*/
|
||||||
private fun getKeyboxForUidAndAlgorithm(uid: Int, algorithm: String): KeyBox {
|
private fun getKeyboxForUidAndAlgorithm(uid: Int, algorithm: String): KeyBox {
|
||||||
val keyboxFile = ConfigurationManager.getKeyboxFileForUid(uid)
|
val keyboxFile = ConfigurationManager.getKeyboxFileForUid(uid)
|
||||||
return KeyBoxManager.getAttestationKey(keyboxFile, algorithm)
|
|
||||||
|
// Normalize the algorithm name. The input might be a full signature algorithm
|
||||||
|
// (e.g., "SHA256withRSA") or just the key type (e.g., "RSA").
|
||||||
|
val keyType =
|
||||||
|
when {
|
||||||
|
algorithm.contains("RSA", ignoreCase = true) -> KeyProperties.KEY_ALGORITHM_RSA
|
||||||
|
algorithm.contains("EC", ignoreCase = true) ->
|
||||||
|
KeyProperties.KEY_ALGORITHM_EC // This also covers "ECDSA"
|
||||||
|
else -> algorithm // If no match, assume it's already a simple key type string.
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyBoxManager.getAttestationKey(keyboxFile, keyType)
|
||||||
?: throw IllegalArgumentException(
|
?: throw IllegalArgumentException(
|
||||||
"No keybox found for UID $uid and algorithm $algorithm in file $keyboxFile"
|
"No keybox found for UID $uid and algorithm '$keyType' (derived from input '$algorithm') in file $keyboxFile"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Recursively formats an ASN1Primitive into a concise, readable string. */
|
||||||
|
fun formatAsn1Primitive(obj: ASN1Encodable?): String {
|
||||||
|
val primitive = obj?.toASN1Primitive()
|
||||||
|
return when (primitive) {
|
||||||
|
null -> "NULL"
|
||||||
|
is ASN1Integer -> primitive.value.toString()
|
||||||
|
is ASN1Enumerated -> primitive.value.toString()
|
||||||
|
is ASN1Boolean -> primitive.isTrue.toString()
|
||||||
|
is ASN1Null -> "NULL"
|
||||||
|
is ASN1OctetString -> {
|
||||||
|
val bytes = primitive.octets
|
||||||
|
// Attempt to decode as a printable string, otherwise show hex
|
||||||
|
if (bytes.all { it >= 32 && it < 127 }) {
|
||||||
|
"\"${String(bytes, StandardCharsets.UTF_8)}\""
|
||||||
|
} else if (bytes.isEmpty()) {
|
||||||
|
"\"\""
|
||||||
|
} else {
|
||||||
|
"#" + bytes.toHex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ASN1TaggedObject ->
|
||||||
|
"[TAG ${primitive.tagNo}]${formatAsn1Primitive(primitive.baseObject)}"
|
||||||
|
is ASN1Sequence ->
|
||||||
|
primitive
|
||||||
|
.map { formatAsn1Primitive(it) }
|
||||||
|
.joinToString(prefix = "[", postfix = "]", separator = ", ")
|
||||||
|
is ASN1Set ->
|
||||||
|
primitive
|
||||||
|
.map { formatAsn1Primitive(it) }
|
||||||
|
.joinToString(prefix = "{", postfix = "}", separator = ", ")
|
||||||
|
else -> primitive.toString() // Fallback for other types
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to check if a given ASN1Sequence contains the Root of Trust tag.
|
||||||
|
private fun sequenceContainsRootOfTrust(seq: ASN1Encodable): Boolean {
|
||||||
|
if (seq !is ASN1Sequence) return false
|
||||||
|
return seq.any { element ->
|
||||||
|
(element as? ASN1TaggedObject)?.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Parses the critical components from an existing attestation extension. */
|
/** Parses the critical components from an existing attestation extension. */
|
||||||
private fun parseAttestationExtension(certHolder: X509CertificateHolder): ParsedAttestation? {
|
private fun parseAttestationExtension(certHolder: X509CertificateHolder): ParsedAttestation? {
|
||||||
val extension = certHolder.getExtension(ATTESTATION_OID) ?: return null
|
val extension = certHolder.getExtension(ATTESTATION_OID) ?: return null
|
||||||
val sequence = ASN1Sequence.getInstance(extension.extnValue.octets)
|
val sequence = ASN1Sequence.getInstance(extension.extnValue.octets)
|
||||||
val allFields = sequence.toArray()
|
val allFields = sequence.toArray()
|
||||||
|
|
||||||
|
// Check if the fields are in the wrong order and swap them if necessary.
|
||||||
|
val softwareEnforcedCandidate =
|
||||||
|
allFields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX]
|
||||||
|
val teeEnforcedCandidate =
|
||||||
|
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX]
|
||||||
|
// The signature of a swapped order: the RoT is in the software list's position.
|
||||||
|
if (
|
||||||
|
sequenceContainsRootOfTrust(softwareEnforcedCandidate) &&
|
||||||
|
!sequenceContainsRootOfTrust(teeEnforcedCandidate)
|
||||||
|
) {
|
||||||
|
// Swap the elements in the array to restore the standard order.
|
||||||
|
allFields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX] =
|
||||||
|
teeEnforcedCandidate
|
||||||
|
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] =
|
||||||
|
softwareEnforcedCandidate
|
||||||
|
}
|
||||||
|
|
||||||
val teeEnforced =
|
val teeEnforced =
|
||||||
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] as ASN1Sequence
|
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] as ASN1Sequence
|
||||||
|
|
||||||
val teeEnforcedVector = ASN1EncodableVector()
|
|
||||||
var originalRootOfTrust: ASN1Encodable? = null
|
var originalRootOfTrust: ASN1Encodable? = null
|
||||||
|
val teeEnforcedMap = mutableMapOf<Int, ASN1TaggedObject>()
|
||||||
|
|
||||||
teeEnforced.forEach { element ->
|
teeEnforced.forEach { element ->
|
||||||
val taggedObject = element as ASN1TaggedObject
|
val taggedObject = element as ASN1TaggedObject
|
||||||
if (taggedObject.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST) {
|
if (taggedObject.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST) {
|
||||||
originalRootOfTrust = taggedObject.baseObject.toASN1Primitive()
|
originalRootOfTrust = taggedObject.baseObject.toASN1Primitive()
|
||||||
} else {
|
} else {
|
||||||
teeEnforcedVector.add(taggedObject)
|
teeEnforcedMap[taggedObject.tagNo] = taggedObject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ParsedAttestation(allFields, teeEnforcedVector, originalRootOfTrust)
|
return ParsedAttestation(allFields, teeEnforcedMap, originalRootOfTrust)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Constructs a new, patched attestation extension using simulated device properties. */
|
/** Constructs a new, patched attestation extension using simulated device properties. */
|
||||||
private fun createPatchedAttestationExtension(parsed: ParsedAttestation): Extension {
|
private fun createPatchedAttestationExtension(parsed: ParsedAttestation, uid: Int): Extension {
|
||||||
val (allFields, teeEnforcedVector, originalRootOfTrust) = parsed
|
val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed
|
||||||
|
|
||||||
// Build the new Root of Trust with our simulated values.
|
var formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
||||||
|
SystemLogger.verbose("Original attestation data: ${formattedString}")
|
||||||
|
|
||||||
|
// Build the new Root of Trust and add/replace it in the map.
|
||||||
val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust)
|
val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust)
|
||||||
teeEnforcedVector.add(
|
teeEnforcedMap[AttestationConstants.TAG_ROOT_OF_TRUST] =
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_ROOT_OF_TRUST, newRootOfTrust)
|
DERTaggedObject(true, AttestationConstants.TAG_ROOT_OF_TRUST, newRootOfTrust)
|
||||||
)
|
|
||||||
|
|
||||||
// Add other simulated hardware properties.
|
// Get the desired state for simulated properties.
|
||||||
AttestationBuilder.addSimulatedHardwareProperties(teeEnforcedVector)
|
val simulatedProperties = AttestationBuilder.getSimulatedHardwareProperties(uid)
|
||||||
|
|
||||||
// Re-assemble the ASN.1 sequences.
|
// Apply the desired state: update, add, or remove properties from the original map.
|
||||||
// The list MUST be sorted by tag number for DER compliance.
|
simulatedProperties.forEach { (tag, value) ->
|
||||||
// Manually convert the vector to a List, then sort it.
|
if (value != null) {
|
||||||
val elementList = (0 until teeEnforcedVector.size()).map { teeEnforcedVector.get(it) }
|
// If the value is not null, add or update it.
|
||||||
val sortedElements = elementList.sortedBy { (it as ASN1TaggedObject).tagNo }
|
teeEnforcedMap[tag] = value
|
||||||
|
} else {
|
||||||
|
// If the value is null, remove the tag from the map.
|
||||||
|
teeEnforcedMap.remove(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-assemble the TEE enforced list from the map's values, sorting for DER compliance.
|
||||||
|
val sortedElements = teeEnforcedMap.values.sortedBy { it.tagNo }
|
||||||
val sortedTeeEnforced = DERSequence(sortedElements.toTypedArray())
|
val sortedTeeEnforced = DERSequence(sortedElements.toTypedArray())
|
||||||
|
|
||||||
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
|
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
|
||||||
val patchedSequence = DERSequence(allFields)
|
val patchedSequence = DERSequence(allFields)
|
||||||
|
formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
||||||
|
SystemLogger.verbose("Patched attestation data: ${formattedString}")
|
||||||
val patchedOctets = DEROctetString(patchedSequence)
|
val patchedOctets = DEROctetString(patchedSequence)
|
||||||
|
|
||||||
return Extension(ATTESTATION_OID, false, patchedOctets)
|
return Extension(ATTESTATION_OID, false, patchedOctets)
|
||||||
@@ -190,7 +306,7 @@ object AttestationPatcher {
|
|||||||
/** Helper data class to hold the parsed components of an attestation extension. */
|
/** Helper data class to hold the parsed components of an attestation extension. */
|
||||||
private data class ParsedAttestation(
|
private data class ParsedAttestation(
|
||||||
val allFields: Array<ASN1Encodable>,
|
val allFields: Array<ASN1Encodable>,
|
||||||
val teeEnforcedVector: ASN1EncodableVector,
|
val teeEnforcedMap: MutableMap<Int, ASN1TaggedObject>,
|
||||||
val rootOfTrust: ASN1Encodable?,
|
val rootOfTrust: ASN1Encodable?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
package org.matrix.TEESimulator.attestation
|
package org.matrix.TEESimulator.attestation
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.ActivityThread
|
|
||||||
import android.os.Build
|
|
||||||
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
|
||||||
@@ -38,77 +31,35 @@ object DeviceAttestationService {
|
|||||||
* Holds key data extracted from a genuine device attestation. This data can be used as a
|
* Holds key data extracted from a genuine device attestation. This data can be used as a
|
||||||
* baseline for creating simulated attestations.
|
* baseline for creating simulated attestations.
|
||||||
*
|
*
|
||||||
|
* @property verifiedBootKey The verified boot public key digest from the root of trust.
|
||||||
* @property verifiedBootHash The verified boot hash from the root of trust.
|
* @property verifiedBootHash The verified boot hash from the root of trust.
|
||||||
* @property attestVersion The attestation version (e.g., 400 for KeyMint 4.0).
|
* @property attestVersion The attestation version (e.g., 400 for KeyMint 4.0).
|
||||||
* @property keymasterVersion The Keymaster or KeyMint HAL version.
|
* @property keymasterVersion The Keymaster or KeyMint HAL version.
|
||||||
* @property osVersion The Android OS version integer.
|
* @property osVersion The Android OS version integer.
|
||||||
|
* @property osPatchLevel The Android security patch level (e.g., 202511).
|
||||||
|
* @property vendorPatchLevel The vendor-specific security patch level.
|
||||||
|
* @property bootPatchLevel The bootloader's security patch level.
|
||||||
*/
|
*/
|
||||||
data class AttestationData(
|
data class AttestationData(
|
||||||
|
val moduleHash: ByteArray?,
|
||||||
|
val verifiedBootKey: ByteArray?,
|
||||||
val verifiedBootHash: ByteArray?,
|
val verifiedBootHash: ByteArray?,
|
||||||
val attestVersion: Int?,
|
val attestVersion: Int?,
|
||||||
val keymasterVersion: Int?,
|
val keymasterVersion: Int?,
|
||||||
val osVersion: Int?,
|
val osVersion: Int?,
|
||||||
|
val osPatchLevel: Int?,
|
||||||
|
val vendorPatchLevel: 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 {
|
|
||||||
// Ensure mainline modules and the correct Keystore provider are initialized.
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
android.app.ActivityThread.initializeMainlineModules()
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
android.security.keystore2.AndroidKeyStoreProvider.install()
|
|
||||||
} else {
|
|
||||||
android.security.keystore.AndroidKeyStoreProvider.install()
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
||||||
@@ -116,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)
|
||||||
@@ -151,6 +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)
|
||||||
|
var formattedString =
|
||||||
|
keyDescriptionSeq.joinToString(separator = ", ") {
|
||||||
|
AttestationPatcher.formatAsn1Primitive(it)
|
||||||
|
}
|
||||||
|
SystemLogger.verbose("Cached attestation data: ${formattedString}")
|
||||||
val fields = keyDescriptionSeq.toArray()
|
val fields = keyDescriptionSeq.toArray()
|
||||||
|
|
||||||
val attestVersion =
|
val attestVersion =
|
||||||
@@ -166,8 +120,27 @@ object DeviceAttestationService {
|
|||||||
.positiveValue
|
.positiveValue
|
||||||
.toInt()
|
.toInt()
|
||||||
|
|
||||||
|
var moduleHash: ByteArray? = null
|
||||||
|
var verifiedBootKey: ByteArray? = null
|
||||||
var verifiedBootHash: ByteArray? = null
|
var verifiedBootHash: ByteArray? = null
|
||||||
var osVersion: Int? = null
|
var osVersion: Int? = null
|
||||||
|
var osPatchLevel: Int? = null
|
||||||
|
var vendorPatchLevel: Int? = null
|
||||||
|
var bootPatchLevel: Int? = null
|
||||||
|
|
||||||
|
val softwareEnforced =
|
||||||
|
ASN1Sequence.getInstance(
|
||||||
|
fields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX]
|
||||||
|
)
|
||||||
|
moduleHash =
|
||||||
|
softwareEnforced
|
||||||
|
.toArray()
|
||||||
|
.firstOrNull {
|
||||||
|
(it as? ASN1TaggedObject)?.tagNo == AttestationConstants.TAG_MODULE_HASH
|
||||||
|
}
|
||||||
|
?.let {
|
||||||
|
ASN1OctetString.getInstance((it as ASN1TaggedObject).baseObject).octets
|
||||||
|
}
|
||||||
|
|
||||||
val teeEnforced =
|
val teeEnforced =
|
||||||
ASN1Sequence.getInstance(
|
ASN1Sequence.getInstance(
|
||||||
@@ -179,6 +152,14 @@ object DeviceAttestationService {
|
|||||||
AttestationConstants.TAG_ROOT_OF_TRUST -> {
|
AttestationConstants.TAG_ROOT_OF_TRUST -> {
|
||||||
val rotSeq = ASN1Sequence.getInstance(tagged.baseObject.toASN1Primitive())
|
val rotSeq = ASN1Sequence.getInstance(tagged.baseObject.toASN1Primitive())
|
||||||
if (rotSeq.size() >= 4) {
|
if (rotSeq.size() >= 4) {
|
||||||
|
verifiedBootKey =
|
||||||
|
ASN1OctetString.getInstance(
|
||||||
|
rotSeq.getObjectAt(
|
||||||
|
AttestationConstants
|
||||||
|
.ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.octets
|
||||||
verifiedBootHash =
|
verifiedBootHash =
|
||||||
ASN1OctetString.getInstance(
|
ASN1OctetString.getInstance(
|
||||||
rotSeq.getObjectAt(
|
rotSeq.getObjectAt(
|
||||||
@@ -189,19 +170,55 @@ object DeviceAttestationService {
|
|||||||
.octets
|
.octets
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AttestationConstants.TAG_OS_VERSION -> { // OS Version (TAG_OS_VERSION)
|
AttestationConstants.TAG_OS_VERSION -> {
|
||||||
osVersion =
|
osVersion =
|
||||||
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
|
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
|
||||||
.positiveValue
|
.positiveValue
|
||||||
.toInt()
|
.toInt()
|
||||||
}
|
}
|
||||||
|
AttestationConstants.TAG_OS_PATCHLEVEL -> {
|
||||||
|
osPatchLevel =
|
||||||
|
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
|
||||||
|
.positiveValue
|
||||||
|
.toInt()
|
||||||
|
}
|
||||||
|
AttestationConstants.TAG_VENDOR_PATCHLEVEL -> {
|
||||||
|
vendorPatchLevel =
|
||||||
|
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
|
||||||
|
.positiveValue
|
||||||
|
.toInt()
|
||||||
|
}
|
||||||
|
AttestationConstants.TAG_BOOT_PATCHLEVEL -> {
|
||||||
|
bootPatchLevel =
|
||||||
|
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
|
||||||
|
.positiveValue
|
||||||
|
.toInt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (verifiedBootKey?.all { it == 0.toByte() } == true) {
|
||||||
|
verifiedBootKey = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verifiedBootHash?.all { it == 0.toByte() } == true) {
|
||||||
|
verifiedBootHash = null
|
||||||
|
}
|
||||||
|
|
||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, bootHash=${verifiedBootHash?.toHex()}"
|
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, osPatch=$osPatchLevel, vendorPatch=$vendorPatchLevel, bootPatch=$bootPatchLevel, moduleHash=${moduleHash?.toHex()}, bootKey=${verifiedBootKey?.toHex()}, bootHash=${verifiedBootHash?.toHex()}"
|
||||||
|
)
|
||||||
|
return AttestationData(
|
||||||
|
moduleHash,
|
||||||
|
verifiedBootKey,
|
||||||
|
verifiedBootHash,
|
||||||
|
attestVersion,
|
||||||
|
keymasterVersion,
|
||||||
|
osVersion,
|
||||||
|
osPatchLevel,
|
||||||
|
vendorPatchLevel,
|
||||||
|
bootPatchLevel,
|
||||||
)
|
)
|
||||||
return AttestationData(verifiedBootHash, attestVersion, keymasterVersion, osVersion)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("Failed to parse attestation data from certificate.", e)
|
SystemLogger.error("Failed to parse attestation data from certificate.", e)
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package org.matrix.TEESimulator.attestation
|
package org.matrix.TEESimulator.attestation
|
||||||
|
|
||||||
import android.hardware.security.keymint.EcCurve
|
import android.hardware.security.keymint.*
|
||||||
import android.hardware.security.keymint.KeyParameter
|
|
||||||
import android.hardware.security.keymint.Tag
|
|
||||||
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
|
||||||
@@ -18,10 +16,14 @@ 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 noAuthRequired: Boolean?,
|
||||||
|
val blockMode: List<Int>,
|
||||||
|
val padding: List<Int>,
|
||||||
val purpose: List<Int>,
|
val purpose: List<Int>,
|
||||||
val digest: List<Int>,
|
val digest: List<Int>,
|
||||||
val rsaPublicExponent: BigInteger?,
|
val rsaPublicExponent: BigInteger?,
|
||||||
@@ -33,26 +35,57 @@ data class KeyMintAttestation(
|
|||||||
val brand: ByteArray?,
|
val brand: ByteArray?,
|
||||||
val device: ByteArray?,
|
val device: ByteArray?,
|
||||||
val product: ByteArray?,
|
val product: ByteArray?,
|
||||||
|
val serial: ByteArray?,
|
||||||
|
val imei: ByteArray?,
|
||||||
|
val meid: ByteArray?,
|
||||||
val manufacturer: ByteArray?,
|
val manufacturer: ByteArray?,
|
||||||
val model: ByteArray?,
|
val model: ByteArray?,
|
||||||
val imei: ByteArray?,
|
|
||||||
val secondImei: ByteArray?,
|
val secondImei: ByteArray?,
|
||||||
val meid: ByteArray?,
|
// Enforcement tags
|
||||||
|
val activeDateTime: Date?,
|
||||||
|
val originationExpireDateTime: Date?,
|
||||||
|
val usageExpireDateTime: Date?,
|
||||||
|
val usageCountLimit: Int?,
|
||||||
|
val callerNonce: Boolean?,
|
||||||
|
val unlockedDeviceRequired: Boolean?,
|
||||||
|
val includeUniqueId: Boolean?,
|
||||||
|
val rollbackResistance: Boolean?,
|
||||||
|
val earlyBootOnly: Boolean?,
|
||||||
|
val allowWhileOnBody: Boolean?,
|
||||||
|
val trustedUserPresenceRequired: Boolean?,
|
||||||
|
val trustedConfirmationRequired: Boolean?,
|
||||||
|
val maxUsesPerBoot: Int?,
|
||||||
|
val maxBootLevel: Int?,
|
||||||
|
val minMacLength: Int?,
|
||||||
|
val rsaOaepMgfDigest: List<Int>,
|
||||||
) {
|
) {
|
||||||
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */
|
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */
|
||||||
constructor(
|
constructor(
|
||||||
params: Array<KeyParameter>
|
params: Array<KeyParameter>
|
||||||
) : this(
|
) : this(
|
||||||
// AOSP: [key_param(tag = KEY_SIZE, field = Integer)]
|
|
||||||
keySize = params.findInteger(Tag.KEY_SIZE) ?: 0,
|
|
||||||
|
|
||||||
// AOSP: [key_param(tag = ALGORITHM, field = Algorithm)]
|
// AOSP: [key_param(tag = ALGORITHM, field = Algorithm)]
|
||||||
algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0,
|
algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0,
|
||||||
|
|
||||||
|
// 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) ?: 0,
|
ecCurve = params.findEcCurve(Tag.EC_CURVE),
|
||||||
ecCurveName = params.deriveEcCurveName(),
|
ecCurveName = params.deriveEcCurveName(),
|
||||||
|
|
||||||
|
// AOSP: [key_param(tag = ORIGIN, field = 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)]
|
||||||
|
blockMode = params.findAllBlockMode(Tag.BLOCK_MODE),
|
||||||
|
|
||||||
|
// AOSP: [key_param(tag = PADDING, field = PaddingMode)]
|
||||||
|
padding = params.findAllPaddingMode(Tag.PADDING),
|
||||||
|
|
||||||
// AOSP: [key_param(tag = PURPOSE, field = KeyPurpose)]
|
// AOSP: [key_param(tag = PURPOSE, field = KeyPurpose)]
|
||||||
purpose = params.findAllKeyPurpose(Tag.PURPOSE),
|
purpose = params.findAllKeyPurpose(Tag.PURPOSE),
|
||||||
|
|
||||||
@@ -82,19 +115,50 @@ data class KeyMintAttestation(
|
|||||||
brand = params.findBlob(Tag.ATTESTATION_ID_BRAND),
|
brand = params.findBlob(Tag.ATTESTATION_ID_BRAND),
|
||||||
device = params.findBlob(Tag.ATTESTATION_ID_DEVICE),
|
device = params.findBlob(Tag.ATTESTATION_ID_DEVICE),
|
||||||
product = params.findBlob(Tag.ATTESTATION_ID_PRODUCT),
|
product = params.findBlob(Tag.ATTESTATION_ID_PRODUCT),
|
||||||
|
serial = params.findBlob(Tag.ATTESTATION_ID_SERIAL),
|
||||||
|
imei = params.findBlob(Tag.ATTESTATION_ID_IMEI),
|
||||||
|
meid = params.findBlob(Tag.ATTESTATION_ID_MEID),
|
||||||
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),
|
||||||
imei = params.findBlob(Tag.ATTESTATION_ID_IMEI),
|
|
||||||
secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI),
|
secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI),
|
||||||
meid = params.findBlob(Tag.ATTESTATION_ID_MEID),
|
|
||||||
|
// Enforcement tags
|
||||||
|
activeDateTime = params.findDate(Tag.ACTIVE_DATETIME),
|
||||||
|
originationExpireDateTime = params.findDate(Tag.ORIGINATION_EXPIRE_DATETIME),
|
||||||
|
usageExpireDateTime = params.findDate(Tag.USAGE_EXPIRE_DATETIME),
|
||||||
|
usageCountLimit = params.findInteger(Tag.USAGE_COUNT_LIMIT),
|
||||||
|
callerNonce = params.findBoolean(Tag.CALLER_NONCE),
|
||||||
|
unlockedDeviceRequired = params.findBoolean(Tag.UNLOCKED_DEVICE_REQUIRED),
|
||||||
|
includeUniqueId = params.findBoolean(Tag.INCLUDE_UNIQUE_ID),
|
||||||
|
rollbackResistance = params.findBoolean(Tag.ROLLBACK_RESISTANCE),
|
||||||
|
earlyBootOnly = params.findBoolean(Tag.EARLY_BOOT_ONLY),
|
||||||
|
allowWhileOnBody = params.findBoolean(Tag.ALLOW_WHILE_ON_BODY),
|
||||||
|
trustedUserPresenceRequired = params.findBoolean(Tag.TRUSTED_USER_PRESENCE_REQUIRED),
|
||||||
|
trustedConfirmationRequired = params.findBoolean(Tag.TRUSTED_CONFIRMATION_REQUIRED),
|
||||||
|
maxUsesPerBoot = params.findInteger(Tag.MAX_USES_PER_BOOT),
|
||||||
|
maxBootLevel = params.findInteger(Tag.MAX_BOOT_LEVEL),
|
||||||
|
minMacLength = params.findInteger(Tag.MIN_MAC_LENGTH),
|
||||||
|
rsaOaepMgfDigest = params.findAllDigests(Tag.RSA_OAEP_MGF_DIGEST),
|
||||||
) {
|
) {
|
||||||
// Log all parsed parameters for debugging purposes.
|
// Log all parsed parameters for debugging purposes.
|
||||||
params.forEach { KeyMintParameterLogger.logParameter(it) }
|
params.forEach { KeyMintParameterLogger.logParameter(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isAttestKey(): Boolean {
|
||||||
|
return purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
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? =
|
||||||
|
if (this.any { it.tag == tag }) true else null
|
||||||
|
|
||||||
/** 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
|
||||||
@@ -107,6 +171,10 @@ private fun Array<KeyParameter>.findAlgorithm(tag: Int): Int? =
|
|||||||
private fun Array<KeyParameter>.findEcCurve(tag: Int): Int? =
|
private fun Array<KeyParameter>.findEcCurve(tag: Int): Int? =
|
||||||
this.find { it.tag == tag }?.value?.ecCurve
|
this.find { it.tag == tag }?.value?.ecCurve
|
||||||
|
|
||||||
|
/** Maps to AOSP field = Origin */
|
||||||
|
private fun Array<KeyParameter>.findOrigin(tag: Int): Int? =
|
||||||
|
this.find { it.tag == tag }?.value?.origin
|
||||||
|
|
||||||
/** Maps to AOSP field = LongInteger */
|
/** Maps to AOSP field = LongInteger */
|
||||||
private fun Array<KeyParameter>.findLongInteger(tag: Int): BigInteger? =
|
private fun Array<KeyParameter>.findLongInteger(tag: Int): BigInteger? =
|
||||||
this.find { it.tag == tag }?.value?.longInteger?.toBigInteger()
|
this.find { it.tag == tag }?.value?.longInteger?.toBigInteger()
|
||||||
@@ -119,6 +187,14 @@ private fun Array<KeyParameter>.findDate(tag: Int): Date? =
|
|||||||
private fun Array<KeyParameter>.findBlob(tag: Int): ByteArray? =
|
private fun Array<KeyParameter>.findBlob(tag: Int): ByteArray? =
|
||||||
this.find { it.tag == tag }?.value?.blob
|
this.find { it.tag == tag }?.value?.blob
|
||||||
|
|
||||||
|
/** Maps to AOSP field = BlockMode (Repeated) */
|
||||||
|
private fun Array<KeyParameter>.findAllBlockMode(tag: Int): List<Int> =
|
||||||
|
this.filter { it.tag == tag }.map { it.value.blockMode }
|
||||||
|
|
||||||
|
/** Maps to AOSP field = PaddingMode (Repeated) */
|
||||||
|
private fun Array<KeyParameter>.findAllPaddingMode(tag: Int): List<Int> =
|
||||||
|
this.filter { it.tag == tag }.map { it.value.paddingMode }
|
||||||
|
|
||||||
/** Maps to AOSP field = KeyPurpose (Repeated) */
|
/** Maps to AOSP field = KeyPurpose (Repeated) */
|
||||||
private fun Array<KeyParameter>.findAllKeyPurpose(tag: Int): List<Int> =
|
private fun Array<KeyParameter>.findAllKeyPurpose(tag: Int): List<Int> =
|
||||||
this.filter { it.tag == tag }.map { it.value.keyPurpose }
|
this.filter { it.tag == tag }.map { it.value.keyPurpose }
|
||||||
@@ -127,6 +203,19 @@ 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 }
|
||||||
|
|
||||||
|
/** Derives keySize from EC_CURVE tag when KEY_SIZE is not explicitly provided. */
|
||||||
|
private fun Array<KeyParameter>.deriveKeySizeFromCurve(): Int {
|
||||||
|
val curveId = this.find { it.tag == Tag.EC_CURVE }?.value?.ecCurve ?: return 0
|
||||||
|
return when (curveId) {
|
||||||
|
EcCurve.P_224 -> 224
|
||||||
|
EcCurve.P_256 -> 256
|
||||||
|
EcCurve.P_384 -> 384
|
||||||
|
EcCurve.P_521 -> 521
|
||||||
|
EcCurve.CURVE_25519 -> 256
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derives the EC Curve name. Logic: Checks specific EC_CURVE tag first (field=EcCurve), falls back
|
* Derives the EC Curve name. Logic: Checks specific EC_CURVE tag first (field=EcCurve), falls back
|
||||||
* to KEY_SIZE (field=Integer).
|
* to KEY_SIZE (field=Integer).
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -31,7 +30,6 @@ object ConfigurationManager {
|
|||||||
// --- Configuration Paths ---
|
// --- Configuration Paths ---
|
||||||
const val CONFIG_PATH = "/data/adb/tricky_store"
|
const val CONFIG_PATH = "/data/adb/tricky_store"
|
||||||
private const val TARGET_PACKAGES_FILE = "target.txt"
|
private const val TARGET_PACKAGES_FILE = "target.txt"
|
||||||
private const val TEE_STATUS_FILE = "tee_status.txt"
|
|
||||||
private const val PATCH_LEVEL_FILE = "security_patch.txt"
|
private const val PATCH_LEVEL_FILE = "security_patch.txt"
|
||||||
private const val DEFAULT_KEYBOX_FILE = "keybox.xml"
|
private const val DEFAULT_KEYBOX_FILE = "keybox.xml"
|
||||||
private val configRoot = File(CONFIG_PATH)
|
private val configRoot = File(CONFIG_PATH)
|
||||||
@@ -39,8 +37,8 @@ object ConfigurationManager {
|
|||||||
// --- In-Memory Configuration State ---
|
// --- In-Memory Configuration State ---
|
||||||
@Volatile private var packageModes = mapOf<String, Mode>()
|
@Volatile private var packageModes = mapOf<String, Mode>()
|
||||||
@Volatile private var packageKeyboxes = mapOf<String, String>()
|
@Volatile private var packageKeyboxes = mapOf<String, String>()
|
||||||
@Volatile private var isTeeBroken: Boolean? = null
|
@Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null
|
||||||
@Volatile var customPatchLevelOverride: CustomPatchLevel? = null
|
@Volatile private var packagePatchLevels = mapOf<String, CustomPatchLevel>()
|
||||||
|
|
||||||
// Cache for UID to package name resolution.
|
// Cache for UID to package name resolution.
|
||||||
private val uidToPackagesCache = ConcurrentHashMap<Int, Array<String>>()
|
private val uidToPackagesCache = ConcurrentHashMap<Int, Array<String>>()
|
||||||
@@ -53,10 +51,20 @@ object ConfigurationManager {
|
|||||||
configRoot.mkdirs()
|
configRoot.mkdirs()
|
||||||
SystemLogger.info("Configuration root is: ${configRoot.absolutePath}")
|
SystemLogger.info("Configuration root is: ${configRoot.absolutePath}")
|
||||||
|
|
||||||
|
// First, ensure the package manager service is running, as the TEE check depends on it.
|
||||||
|
// This prevents a race condition on startup.
|
||||||
|
SystemLogger.info("Waiting for PackageManagerService to be ready...")
|
||||||
|
if (getPackageManager() == null) {
|
||||||
|
SystemLogger.error(
|
||||||
|
"PackageManagerService is not available. TEE check will likely fail."
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SystemLogger.info("PackageManagerService is ready.")
|
||||||
|
}
|
||||||
|
|
||||||
// Initial load of all configuration files.
|
// Initial load of all configuration files.
|
||||||
loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE))
|
loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE))
|
||||||
loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE))
|
loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE))
|
||||||
storeTeeStatus() // Check and store the current TEE status.
|
|
||||||
|
|
||||||
// Start watching for any subsequent file changes.
|
// Start watching for any subsequent file changes.
|
||||||
ConfigObserver.startWatching()
|
ConfigObserver.startWatching()
|
||||||
@@ -76,7 +84,10 @@ object ConfigurationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Determines if the certificate for a given UID needs to be patched. */
|
/** Determines if the certificate for a given UID needs to be patched. */
|
||||||
fun shouldPatch(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.PATCH
|
fun shouldPatch(uid: Int): Boolean {
|
||||||
|
val mode = getPackageModeForUid(uid)
|
||||||
|
return mode == Mode.PATCH || mode == Mode.AUTO
|
||||||
|
}
|
||||||
|
|
||||||
/** Determines if a new certificate needs to be generated for a given UID. */
|
/** 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
|
||||||
@@ -84,24 +95,38 @@ object ConfigurationManager {
|
|||||||
/** Determines if no operation is needed for a given UID. */
|
/** 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
|
||||||
|
|
||||||
|
/** Determines if the UID is in AUTO mode (no explicit ! or ? suffix). */
|
||||||
|
fun isAutoMode(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.AUTO
|
||||||
|
|
||||||
/** Resolves the operating mode for a given UID based on its packages and the TEE status. */
|
/** 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
|
||||||
|
|
||||||
// Lazily load TEE status if it hasn't been checked yet.
|
|
||||||
if (isTeeBroken == null) loadTeeStatus()
|
|
||||||
|
|
||||||
// Find the first configured mode for any of the UID's packages.
|
|
||||||
for (pkg in packages) {
|
for (pkg in packages) {
|
||||||
when (packageModes[pkg]) {
|
when (packageModes[pkg]) {
|
||||||
Mode.GENERATE -> return Mode.GENERATE
|
Mode.GENERATE -> return Mode.GENERATE
|
||||||
Mode.PATCH -> return Mode.PATCH
|
Mode.PATCH -> return Mode.PATCH
|
||||||
Mode.AUTO -> return if (isTeeBroken == true) Mode.GENERATE else Mode.PATCH
|
Mode.AUTO -> return Mode.AUTO
|
||||||
null -> continue // No config for this package, check the next one.
|
null -> continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null // No configuration found for this UID.
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the custom patch level configuration for a given UID. It first checks for a
|
||||||
|
* package-specific override and falls back to the global configuration.
|
||||||
|
*
|
||||||
|
* @param uid The UID of the calling application.
|
||||||
|
* @return The applicable [CustomPatchLevel], or null if no custom configuration exists.
|
||||||
|
*/
|
||||||
|
fun getPatchLevelForUid(uid: Int): CustomPatchLevel? {
|
||||||
|
val packages = getPackagesForUid(uid)
|
||||||
|
// Find the first package-specific configuration for this UID.
|
||||||
|
val packageSpecificPatchLevel =
|
||||||
|
packages.firstNotNullOfOrNull { pkg -> packagePatchLevels[pkg] }
|
||||||
|
return packageSpecificPatchLevel ?: globalCustomPatchLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -131,25 +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
|
|
||||||
}
|
}
|
||||||
// No suffix means AUTO mode.
|
|
||||||
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.
|
||||||
@@ -162,26 +187,48 @@ object ConfigurationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Loads the security patch level override configuration from `security_patch.txt`. */
|
/**
|
||||||
|
* Loads and parses the `security_patch.txt` file, which can define both global and per-package
|
||||||
|
* security patch levels.
|
||||||
|
*/
|
||||||
private fun loadPatchLevelConfig(file: File) {
|
private fun loadPatchLevelConfig(file: File) {
|
||||||
if (file.exists()) {
|
if (!file.exists()) {
|
||||||
try {
|
globalCustomPatchLevel = null
|
||||||
val lines =
|
packagePatchLevels = emptyMap()
|
||||||
file.readLines().mapNotNull { line ->
|
return
|
||||||
val trimmed = line.trim()
|
}
|
||||||
if (trimmed.isNotEmpty() && !trimmed.startsWith("#")) trimmed else null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lines.isEmpty()) {
|
try {
|
||||||
customPatchLevelOverride = null
|
val newPackageLevels = mutableMapOf<String, CustomPatchLevel>()
|
||||||
return
|
var currentContext = "" // Empty string for global context
|
||||||
}
|
val contextLines = mutableMapOf<String, MutableList<String>>()
|
||||||
|
val contextRegex = Regex("^\\[([a-zA-Z0-9_.-]+)]$")
|
||||||
|
|
||||||
|
// First pass: group lines by context (global or package-specific).
|
||||||
|
file.readLines().forEach { line ->
|
||||||
|
val trimmedLine = line.trim()
|
||||||
|
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) return@forEach
|
||||||
|
|
||||||
|
contextRegex.find(trimmedLine)?.let { currentContext = it.groupValues[1] }
|
||||||
|
?: run {
|
||||||
|
contextLines
|
||||||
|
.computeIfAbsent(currentContext) { mutableListOf() }
|
||||||
|
.add(trimmedLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to parse a set of lines into a CustomPatchLevel object.
|
||||||
|
fun parseLines(lines: List<String>?): CustomPatchLevel? {
|
||||||
|
if (lines.isNullOrEmpty()) return null
|
||||||
|
|
||||||
// Handle simple case: one line sets the patch level for all components.
|
// Handle simple case: one line sets the patch level for all components.
|
||||||
if (lines.size == 1 && '=' !in lines[0]) {
|
if (lines.size == 1 && '=' !in lines[0]) {
|
||||||
customPatchLevelOverride =
|
return CustomPatchLevel(
|
||||||
CustomPatchLevel(system = null, vendor = null, boot = null, all = lines[0])
|
system = null,
|
||||||
return
|
vendor = null,
|
||||||
|
boot = null,
|
||||||
|
all = lines[0],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle key-value pair configuration.
|
// Handle key-value pair configuration.
|
||||||
@@ -195,45 +242,40 @@ object ConfigurationManager {
|
|||||||
.toMap()
|
.toMap()
|
||||||
|
|
||||||
val all = map["all"]
|
val all = map["all"]
|
||||||
customPatchLevelOverride =
|
return CustomPatchLevel(
|
||||||
CustomPatchLevel(
|
system = map["system"] ?: all,
|
||||||
system = map["system"] ?: all,
|
vendor = map["vendor"] ?: all,
|
||||||
vendor = map["vendor"] ?: all,
|
boot = map["boot"] ?: all,
|
||||||
boot = map["boot"] ?: all,
|
all = all,
|
||||||
all = all,
|
)
|
||||||
)
|
|
||||||
SystemLogger.info("Loaded custom security patch levels.")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SystemLogger.error("Failed to load or parse ${file.name}", e)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
customPatchLevelOverride = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Checks the device's TEE status and writes the result to a file for persistence. */
|
var newGlobalLevel = parseLines(contextLines[""])
|
||||||
private fun storeTeeStatus() {
|
contextLines.remove("")
|
||||||
val statusFile = File(configRoot, TEE_STATUS_FILE)
|
|
||||||
isTeeBroken = !DeviceAttestationService.isTeeFunctional
|
// system=prop means all components should derive from device props
|
||||||
try {
|
if (newGlobalLevel?.system.equals("prop", ignoreCase = true)) {
|
||||||
statusFile.writeText("tee_broken=$isTeeBroken")
|
SystemLogger.info("system=prop: forcing boot/vendor to derive from device props")
|
||||||
SystemLogger.info("TEE status stored: isTeeBroken=$isTeeBroken")
|
newGlobalLevel = newGlobalLevel?.copy(boot = "prop", vendor = "prop")
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((pkg, lines) in contextLines) {
|
||||||
|
parseLines(lines)?.let { newPackageLevels[pkg] = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomically update the configuration state.
|
||||||
|
globalCustomPatchLevel = newGlobalLevel
|
||||||
|
packagePatchLevels = newPackageLevels
|
||||||
|
|
||||||
|
SystemLogger.info(
|
||||||
|
"Loaded custom security patch levels: global config exists=${newGlobalLevel != null}, " +
|
||||||
|
"${newPackageLevels.size} package-specific configs."
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("Failed to write TEE status to file.", e)
|
SystemLogger.error("Failed to load or parse ${file.name}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Loads the TEE status from the file. */
|
|
||||||
private fun loadTeeStatus() {
|
|
||||||
val statusFile = File(configRoot, TEE_STATUS_FILE)
|
|
||||||
isTeeBroken =
|
|
||||||
if (statusFile.exists()) {
|
|
||||||
statusFile.readText().trim() == "tee_broken=true"
|
|
||||||
} else {
|
|
||||||
null // Status is unknown.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A FileObserver that monitors the configuration directory for changes and triggers reloads of
|
* A FileObserver that monitors the configuration directory for changes and triggers reloads of
|
||||||
* the relevant settings.
|
* the relevant settings.
|
||||||
@@ -245,8 +287,10 @@ 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 -> loadTargetPackages(file!!)
|
TARGET_PACKAGES_FILE -> file?.let { loadTargetPackages(it) }
|
||||||
PATCH_LEVEL_FILE -> loadPatchLevelConfig(file!!)
|
?: SystemLogger.warning("$TARGET_PACKAGES_FILE was deleted.")
|
||||||
|
PATCH_LEVEL_FILE -> file?.let { loadPatchLevelConfig(it) }
|
||||||
|
?: SystemLogger.warning("$PATCH_LEVEL_FILE was deleted.")
|
||||||
// Any change to an XML file is assumed to be a keybox.
|
// Any change to an XML file is assumed to be a keybox.
|
||||||
// The cache in KeyBoxManager will handle reloading it on its next use.
|
// The cache in KeyBoxManager will handle reloading it on its next use.
|
||||||
else ->
|
else ->
|
||||||
@@ -289,6 +333,32 @@ 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 {
|
||||||
|
return try {
|
||||||
|
val callerCtx =
|
||||||
|
java.io.File("/proc/$callingPid/attr/current").readText().trim('\u0000', ' ', '\n')
|
||||||
|
val selfCtx =
|
||||||
|
java.io.File("/proc/self/attr/current").readText().trim('\u0000', ' ', '\n')
|
||||||
|
android.os.SELinux.checkSELinuxAccess(callerCtx, selfCtx, tclass, perm)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks if any package belonging to the UID holds the given permission. */
|
||||||
|
fun hasPermissionForUid(uid: Int, permission: String): Boolean {
|
||||||
|
val userId = uid / 100000
|
||||||
|
return getPackagesForUid(uid).any { pkg ->
|
||||||
|
try {
|
||||||
|
getPackageManager()?.checkPermission(permission, pkg, userId) == 0
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Retrieves the package names associated with a UID. */
|
/** 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) {
|
||||||
@@ -303,7 +373,7 @@ object ConfigurationManager {
|
|||||||
|
|
||||||
/** Waits for a system service to become available, with retries. */
|
/** Waits for a system service to become available, with retries. */
|
||||||
private fun waitForSystemService(name: String): IBinder? {
|
private fun waitForSystemService(name: String): IBinder? {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
return ServiceManager.waitForService(name)
|
return ServiceManager.waitForService(name)
|
||||||
}
|
}
|
||||||
// Fallback for older Android versions.
|
// Fallback for older Android versions.
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
* Skips the original call and immediately returns a custom reply parcel to the caller. The
|
* Skips the original call and immediately returns a custom reply parcel to the caller. The
|
||||||
* provided parcel will be recycled after use.
|
* provided parcel will be recycled after use.
|
||||||
*/
|
*/
|
||||||
data class OverrideReply(val code: Int = 0, val reply: Parcel) : TransactionResult()
|
data class OverrideReply(val reply: Parcel, val code: Int = 0) : TransactionResult()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modifies the transaction's input data before forwarding it to the original binder method.
|
* Modifies the transaction's input data before forwarding it to the original binder method.
|
||||||
@@ -109,17 +109,17 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
* `handlePostTransact`).
|
* `handlePostTransact`).
|
||||||
*/
|
*/
|
||||||
final override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
|
final override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
|
||||||
// The native hook prepends a transaction ID to the data parcel.
|
|
||||||
val txId = data.readLong()
|
val txId = data.readLong()
|
||||||
val result =
|
val result = try {
|
||||||
when (code) {
|
when (code) {
|
||||||
// These codes are defined in the native layer to distinguish hook types.
|
|
||||||
PRE_TRANSACT_CODE -> handlePreTransact(txId, data)
|
PRE_TRANSACT_CODE -> handlePreTransact(txId, data)
|
||||||
POST_TRANSACT_CODE -> handlePostTransact(txId, data)
|
POST_TRANSACT_CODE -> handlePostTransact(txId, data)
|
||||||
else -> return super.onTransact(code, data, reply, flags)
|
else -> return super.onTransact(code, data, reply, flags)
|
||||||
}
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
// The reply parcel is guaranteed to be non-null for our custom transactions.
|
SystemLogger.error("[TX_ID: $txId] Interceptor exception, falling through to HAL", e)
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
writeResultToReply(result, reply!!)
|
writeResultToReply(result, reply!!)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -226,13 +226,18 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
methodName: String,
|
methodName: String,
|
||||||
callingUid: Int,
|
callingUid: Int,
|
||||||
callingPid: Int,
|
callingPid: Int,
|
||||||
isIntercepting: Boolean = true,
|
skipPost: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
val isIntercepting = !skipPost && !ConfigurationManager.shouldSkipUid(callingUid)
|
||||||
val action = if (isIntercepting) "Intercept" else "Observe"
|
val action = if (isIntercepting) "Intercept" else "Observe"
|
||||||
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
|
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
|
||||||
SystemLogger.debug(
|
val message =
|
||||||
"[TX_ID: $txId] $action $methodName for packages=[$packages] (uid=$callingUid, pid=$callingPid)"
|
"[TX_ID: $txId] $action $methodName for packages=[$packages] (uid=$callingUid, pid=$callingPid)"
|
||||||
)
|
if (isIntercepting) {
|
||||||
|
SystemLogger.debug(message)
|
||||||
|
} else {
|
||||||
|
SystemLogger.verbose(message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -243,6 +248,8 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
private const val BACKDOOR_TRANSACTION_CODE = 0xdeadbeef.toInt()
|
private const val BACKDOOR_TRANSACTION_CODE = 0xdeadbeef.toInt()
|
||||||
// Code used by the backdoor binder to register a new interceptor.
|
// Code used by the backdoor binder to register a new interceptor.
|
||||||
private const val REGISTER_INTERCEPTOR_CODE = 1
|
private const val REGISTER_INTERCEPTOR_CODE = 1
|
||||||
|
// Code used by the backdoor binder to unregister an interceptor.
|
||||||
|
private const val UNREGISTER_INTERCEPTOR_CODE = 2
|
||||||
|
|
||||||
// --- Hook Type Codes ---
|
// --- Hook Type Codes ---
|
||||||
// Indicates that the call is for a pre-transaction hook.
|
// Indicates that the call is for a pre-transaction hook.
|
||||||
@@ -286,15 +293,27 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Uses the backdoor binder to register an interceptor for a specific target service. */
|
/**
|
||||||
fun register(backdoor: IBinder, target: IBinder, interceptor: BinderInterceptor) {
|
* 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(
|
||||||
|
backdoor: IBinder,
|
||||||
|
target: IBinder,
|
||||||
|
interceptor: BinderInterceptor,
|
||||||
|
filteredCodes: IntArray = intArrayOf(),
|
||||||
|
) {
|
||||||
val data = Parcel.obtain()
|
val data = Parcel.obtain()
|
||||||
val reply = Parcel.obtain()
|
val reply = Parcel.obtain()
|
||||||
try {
|
try {
|
||||||
data.writeStrongBinder(target)
|
data.writeStrongBinder(target)
|
||||||
data.writeStrongBinder(interceptor)
|
data.writeStrongBinder(interceptor)
|
||||||
|
data.writeInt(filteredCodes.size)
|
||||||
|
for (code in filteredCodes) data.writeInt(code)
|
||||||
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
|
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
|
||||||
SystemLogger.info("Registered interceptor for target: $target")
|
SystemLogger.info("Registered interceptor for target: $target (${filteredCodes.size} filtered codes)")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("Failed to register binder interceptor.", e)
|
SystemLogger.error("Failed to register binder interceptor.", e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -302,5 +321,21 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
reply.recycle()
|
reply.recycle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Uses the backdoor binder to unregister an interceptor for a specific target service. */
|
||||||
|
fun unregister(backdoor: IBinder, target: IBinder) {
|
||||||
|
val data = Parcel.obtain()
|
||||||
|
val reply = Parcel.obtain()
|
||||||
|
try {
|
||||||
|
data.writeStrongBinder(target)
|
||||||
|
backdoor.transact(UNREGISTER_INTERCEPTOR_CODE, data, reply, 0)
|
||||||
|
SystemLogger.info("Unregistered interceptor for target: $target")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("Failed to unregister binder interceptor.", e)
|
||||||
|
} finally {
|
||||||
|
data.recycle()
|
||||||
|
reply.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -68,11 +68,17 @@ 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()
|
||||||
|
|
||||||
/** Registers this interceptor with the native hook layer and sets up a death recipient. */
|
/** 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")
|
||||||
register(backdoor, service, this)
|
register(backdoor, service, this, interceptedCodes)
|
||||||
service.linkToDeath(createDeathRecipient(), 0)
|
service.linkToDeath(createDeathRecipient(), 0)
|
||||||
onInterceptorReady(service, backdoor)
|
onInterceptorReady(service, backdoor)
|
||||||
}
|
}
|
||||||
|
|||||||
+123
-11
@@ -1,16 +1,34 @@
|
|||||||
package org.matrix.TEESimulator.interception.keystore
|
package org.matrix.TEESimulator.interception.keystore
|
||||||
|
|
||||||
|
import android.hardware.security.keymint.KeyParameter
|
||||||
|
import android.hardware.security.keymint.KeyParameterValue
|
||||||
|
import android.hardware.security.keymint.Tag
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.security.KeyStore
|
import android.security.KeyStore
|
||||||
|
import android.security.keystore.KeystoreResponse
|
||||||
|
import android.system.keystore2.Authorization
|
||||||
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||||
|
|
||||||
data class KeyIdentifier(val uid: Int, val alias: String)
|
data class KeyIdentifier(val uid: Int, val alias: String)
|
||||||
|
|
||||||
/** A collection of utility functions to support binder interception. */
|
/** A collection of utility functions to support binder interception. */
|
||||||
object InterceptorUtils {
|
object InterceptorUtils {
|
||||||
|
|
||||||
|
private const val EX_SERVICE_SPECIFIC = -8
|
||||||
|
|
||||||
|
fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply {
|
||||||
|
val parcel = Parcel.obtain().apply {
|
||||||
|
writeInt(EX_SERVICE_SPECIFIC)
|
||||||
|
writeString(null)
|
||||||
|
writeInt(0)
|
||||||
|
writeInt(errorCode)
|
||||||
|
}
|
||||||
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses reflection to get the integer transaction code for a given method name from a Stub
|
* Uses reflection to get the integer transaction code for a given method name from a Stub
|
||||||
* class. This is necessary for older Android versions where codes are not public constants.
|
* class. This is necessary for older Android versions where codes are not public constants.
|
||||||
@@ -27,14 +45,31 @@ object InterceptorUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Creates an `KeystoreResponse` parcel that indicates success with no data. */
|
||||||
|
fun createSuccessKeystoreResponse(): KeystoreResponse {
|
||||||
|
val parcel = Parcel.obtain()
|
||||||
|
try {
|
||||||
|
parcel.writeInt(KeyStore.NO_ERROR)
|
||||||
|
parcel.writeString("")
|
||||||
|
parcel.setDataPosition(0)
|
||||||
|
return KeystoreResponse.CREATOR.createFromParcel(parcel)
|
||||||
|
} finally {
|
||||||
|
parcel.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Creates an `OverrideReply` parcel that indicates success with no data. */
|
/** Creates an `OverrideReply` parcel that indicates success with no data. */
|
||||||
fun createSuccessReply(): BinderInterceptor.TransactionResult.OverrideReply {
|
fun createSuccessReply(
|
||||||
|
writeResultCode: Boolean = true
|
||||||
|
): BinderInterceptor.TransactionResult.OverrideReply {
|
||||||
val parcel =
|
val parcel =
|
||||||
Parcel.obtain().apply {
|
Parcel.obtain().apply {
|
||||||
writeNoException()
|
writeNoException()
|
||||||
writeInt(KeyStore.NO_ERROR)
|
if (writeResultCode) {
|
||||||
|
writeInt(KeyStore.NO_ERROR)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return BinderInterceptor.TransactionResult.OverrideReply(0, parcel)
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Creates an `OverrideReply` parcel containing a raw byte array. */
|
/** Creates an `OverrideReply` parcel containing a raw byte array. */
|
||||||
@@ -44,7 +79,20 @@ object InterceptorUtils {
|
|||||||
writeNoException()
|
writeNoException()
|
||||||
writeByteArray(data)
|
writeByteArray(data)
|
||||||
}
|
}
|
||||||
return BinderInterceptor.TransactionResult.OverrideReply(KeyStore.NO_ERROR, parcel)
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates an `OverrideReply` parcel containing a typed array. */
|
||||||
|
fun <T : Parcelable> createTypedArrayReply(
|
||||||
|
array: Array<T>,
|
||||||
|
flags: Int = 0,
|
||||||
|
): BinderInterceptor.TransactionResult.OverrideReply {
|
||||||
|
val parcel =
|
||||||
|
Parcel.obtain().apply {
|
||||||
|
writeNoException()
|
||||||
|
writeTypedArray(array, flags)
|
||||||
|
}
|
||||||
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Creates an `OverrideReply` parcel containing a Parcelable object. */
|
/** Creates an `OverrideReply` parcel containing a Parcelable object. */
|
||||||
@@ -57,25 +105,89 @@ object InterceptorUtils {
|
|||||||
writeNoException()
|
writeNoException()
|
||||||
writeTypedObject(obj, flags)
|
writeTypedObject(obj, flags)
|
||||||
}
|
}
|
||||||
return BinderInterceptor.TransactionResult.OverrideReply(0, parcel)
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the true key alias from the keystore-prefixed string (e.g., "user_cert_my-alias" ->
|
* Extracts the base alias from a potentially prefixed alias string. For example, it converts
|
||||||
* "my-alias").
|
* "USRCERT_my_key" to "my_key".
|
||||||
*/
|
*/
|
||||||
fun extractAlias(prefixedAlias: String): String {
|
fun extractAlias(prefixedAlias: String): String {
|
||||||
val underscoreIndex = prefixedAlias.indexOf('_')
|
val underscoreIndex = prefixedAlias.indexOf('_')
|
||||||
val secondUnderscoreIndex = prefixedAlias.indexOf('_', underscoreIndex + 1)
|
return if (underscoreIndex != -1) {
|
||||||
return if (secondUnderscoreIndex != -1) {
|
// Return the part of the string after the first underscore.
|
||||||
prefixedAlias.substring(secondUnderscoreIndex + 1)
|
prefixedAlias.substring(underscoreIndex + 1)
|
||||||
} else {
|
} else {
|
||||||
|
// If there's no underscore, return the original string.
|
||||||
prefixedAlias
|
prefixedAlias
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Checks if a reply parcel contains an exception without consuming it. */
|
/** Checks if a reply parcel contains an exception without consuming it. */
|
||||||
fun hasException(reply: Parcel): Boolean {
|
fun hasException(reply: Parcel): Boolean {
|
||||||
return runCatching { reply.readException() }.exceptionOrNull() != null
|
val exception = runCatching { reply.readException() }.exceptionOrNull()
|
||||||
|
if (exception != null) reply.setDataPosition(0)
|
||||||
|
return exception != null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an `OverrideReply` that writes a `ServiceSpecificException` with the given error
|
||||||
|
* code via EX_SERVICE_SPECIFIC.
|
||||||
|
*/
|
||||||
|
fun createServiceSpecificErrorReply(
|
||||||
|
errorCode: Int
|
||||||
|
): BinderInterceptor.TransactionResult.OverrideReply {
|
||||||
|
val parcel =
|
||||||
|
Parcel.obtain().apply {
|
||||||
|
writeException(android.os.ServiceSpecificException(errorCode))
|
||||||
|
}
|
||||||
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
||||||
|
authorizations: Array<Authorization>?,
|
||||||
|
callingUid: Int,
|
||||||
|
): Array<Authorization>? {
|
||||||
|
if (authorizations == null) return null
|
||||||
|
|
||||||
|
val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid)
|
||||||
|
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
|
||||||
|
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
|
||||||
|
|
||||||
|
return authorizations
|
||||||
|
.map { auth ->
|
||||||
|
val replacement =
|
||||||
|
when (auth.keyParameter.tag) {
|
||||||
|
Tag.OS_PATCHLEVEL ->
|
||||||
|
if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) osPatch else null
|
||||||
|
Tag.VENDOR_PATCHLEVEL ->
|
||||||
|
if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) vendorPatch
|
||||||
|
else null
|
||||||
|
Tag.BOOT_PATCHLEVEL ->
|
||||||
|
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) bootPatch else null
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (replacement != null) {
|
||||||
|
Authorization().apply {
|
||||||
|
keyParameter =
|
||||||
|
KeyParameter().apply {
|
||||||
|
tag = auth.keyParameter.tag
|
||||||
|
value = KeyParameterValue.integer(replacement)
|
||||||
|
}
|
||||||
|
securityLevel = auth.securityLevel
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
auth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toTypedArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+415
-49
@@ -1,19 +1,26 @@
|
|||||||
package org.matrix.TEESimulator.interception.keystore
|
package org.matrix.TEESimulator.interception.keystore
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.hardware.security.keymint.KeyOrigin
|
|
||||||
import android.hardware.security.keymint.SecurityLevel
|
import android.hardware.security.keymint.SecurityLevel
|
||||||
import android.hardware.security.keymint.Tag
|
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.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.cert.Certificate
|
||||||
|
import java.util.Collections
|
||||||
|
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.config.ConfigurationManager
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
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
|
||||||
|
import org.matrix.TEESimulator.pki.CertificateGenerator
|
||||||
import org.matrix.TEESimulator.pki.CertificateHelper
|
import org.matrix.TEESimulator.pki.CertificateHelper
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,16 +32,28 @@ import org.matrix.TEESimulator.pki.CertificateHelper
|
|||||||
*/
|
*/
|
||||||
@SuppressLint("BlockedPrivateApi")
|
@SuppressLint("BlockedPrivateApi")
|
||||||
object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||||
|
private val stubBinderClass = IKeystoreService.Stub::class.java
|
||||||
|
|
||||||
// Transaction codes for the IKeystoreService interface methods we are interested in.
|
// Transaction codes for the IKeystoreService interface methods we are interested in.
|
||||||
private val GET_KEY_ENTRY_TRANSACTION =
|
private val GET_KEY_ENTRY_TRANSACTION =
|
||||||
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "getKeyEntry")
|
InterceptorUtils.getTransactCode(stubBinderClass, "getKeyEntry")
|
||||||
private val DELETE_KEY_TRANSACTION =
|
private val DELETE_KEY_TRANSACTION =
|
||||||
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "deleteKey")
|
InterceptorUtils.getTransactCode(stubBinderClass, "deleteKey")
|
||||||
|
private val UPDATE_SUBCOMPONENT_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(stubBinderClass, "updateSubcomponent")
|
||||||
|
private val LIST_ENTRIES_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(stubBinderClass, "listEntries")
|
||||||
|
private val LIST_ENTRIES_BATCHED_TRANSACTION =
|
||||||
|
if (Build.VERSION.SDK_INT >= 34)
|
||||||
|
InterceptorUtils.getTransactCode(stubBinderClass, "listEntriesBatched")
|
||||||
|
else null
|
||||||
|
private val GET_NUMBER_OF_ENTRIES_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(stubBinderClass, "getNumberOfEntries")
|
||||||
|
private val GET_SECURITY_LEVEL_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(stubBinderClass, "getSecurityLevel")
|
||||||
|
|
||||||
private val transactionNames: Map<Int, String> by lazy {
|
private val transactionNames: Map<Int, String> by lazy {
|
||||||
IKeystoreService.Stub::class
|
stubBinderClass.declaredFields
|
||||||
.java
|
|
||||||
.declaredFields
|
|
||||||
.filter {
|
.filter {
|
||||||
it.isAccessible = true
|
it.isAccessible = true
|
||||||
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
|
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
|
||||||
@@ -42,15 +61,43 @@ 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 val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
|
||||||
|
private val deletedSoftwareKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
|
||||||
|
|
||||||
|
// Backdoor binder for registering new interceptors at runtime.
|
||||||
|
private var backdoorBinder: IBinder? = null
|
||||||
|
|
||||||
|
// 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"
|
||||||
override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry"
|
override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry"
|
||||||
|
|
||||||
|
override val interceptedCodes: IntArray by lazy {
|
||||||
|
listOfNotNull(
|
||||||
|
GET_KEY_ENTRY_TRANSACTION,
|
||||||
|
DELETE_KEY_TRANSACTION,
|
||||||
|
UPDATE_SUBCOMPONENT_TRANSACTION,
|
||||||
|
LIST_ENTRIES_TRANSACTION,
|
||||||
|
LIST_ENTRIES_BATCHED_TRANSACTION,
|
||||||
|
GET_NUMBER_OF_ENTRIES_TRANSACTION,
|
||||||
|
GET_SECURITY_LEVEL_TRANSACTION,
|
||||||
|
)
|
||||||
|
.toIntArray()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is called once the main service is hooked. It proceeds to find and hook the
|
* This method is called once the main service is hooked. It proceeds to find and hook the
|
||||||
* security level sub-services (e.g., TEE, StrongBox).
|
* security level sub-services (e.g., TEE, StrongBox).
|
||||||
*/
|
*/
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -62,7 +109,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...")
|
SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...")
|
||||||
val interceptor =
|
val interceptor =
|
||||||
KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
|
KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
|
||||||
register(backdoor, tee.asBinder(), interceptor)
|
securityLevelInterceptors[SecurityLevel.TRUSTED_ENVIRONMENT] = interceptor
|
||||||
|
registerSecurityLevelBinder(
|
||||||
|
backdoor,
|
||||||
|
tee.asBinder(),
|
||||||
|
interceptor,
|
||||||
|
)
|
||||||
|
interceptor.loadPersistedKeys()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) }
|
.onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) }
|
||||||
@@ -73,12 +126,42 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
|
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
|
||||||
val interceptor =
|
val interceptor =
|
||||||
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
|
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
|
||||||
register(backdoor, strongbox.asBinder(), interceptor)
|
securityLevelInterceptors[SecurityLevel.STRONGBOX] = interceptor
|
||||||
|
registerSecurityLevelBinder(
|
||||||
|
backdoor,
|
||||||
|
strongbox.asBinder(),
|
||||||
|
interceptor,
|
||||||
|
)
|
||||||
|
interceptor.loadPersistedKeys()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.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,
|
||||||
@@ -88,25 +171,79 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
callingPid: Int,
|
callingPid: Int,
|
||||||
data: Parcel,
|
data: Parcel,
|
||||||
): TransactionResult {
|
): TransactionResult {
|
||||||
if (code == GET_KEY_ENTRY_TRANSACTION || code == DELETE_KEY_TRANSACTION) {
|
if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) {
|
||||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
|
||||||
|
return if (ConfigurationManager.shouldSkipUid(callingUid))
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
else TransactionResult.Continue
|
||||||
|
} else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||||
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
|
||||||
|
|
||||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
|
||||||
val descriptor =
|
val isGMS = packages.contains("com.google.android.gms")
|
||||||
data.readTypedObject(KeyDescriptor.CREATOR)
|
|
||||||
?: return TransactionResult.SkipTransaction
|
if (isGMS || ConfigurationManager.shouldSkipUid(callingUid)) {
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
} else {
|
||||||
|
return TransactionResult.Continue
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
code == GET_KEY_ENTRY_TRANSACTION ||
|
||||||
|
code == DELETE_KEY_TRANSACTION ||
|
||||||
|
code == UPDATE_SUBCOMPONENT_TRANSACTION
|
||||||
|
) {
|
||||||
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||||
|
|
||||||
if (ConfigurationManager.shouldSkipUid(callingUid))
|
if (ConfigurationManager.shouldSkipUid(callingUid))
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
|
||||||
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
|
if (code == UPDATE_SUBCOMPONENT_TRANSACTION)
|
||||||
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
return handleUpdateSubcomponent(callingUid, data)
|
||||||
|
|
||||||
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
val descriptor =
|
||||||
|
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
|
|
||||||
if (code == DELETE_KEY_TRANSACTION) {
|
if (code == DELETE_KEY_TRANSACTION) {
|
||||||
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
// Handle delete by alias (APP domain) or nspace (KEY_ID domain).
|
||||||
|
val keyId =
|
||||||
|
if (descriptor.alias != null) {
|
||||||
|
KeyIdentifier(callingUid, descriptor.alias)
|
||||||
|
} else if (descriptor.domain == Domain.KEY_ID) {
|
||||||
|
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||||
|
callingUid, descriptor.nspace
|
||||||
|
)?.let { info ->
|
||||||
|
KeyMintSecurityLevelInterceptor.generatedKeys.entries
|
||||||
|
.find { it.value.nspace == info.nspace && it.key.uid == callingUid }
|
||||||
|
?.key
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
|
||||||
|
if (keyId != null) {
|
||||||
|
val isSoftwareKey =
|
||||||
|
KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(keyId)
|
||||||
|
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
||||||
|
if (isSoftwareKey) {
|
||||||
|
deletedSoftwareKeys.add(keyId)
|
||||||
|
SystemLogger.info(
|
||||||
|
"[TX_ID: $txId] Deleted cached keypair ${keyId.alias}, replying with empty response."
|
||||||
|
)
|
||||||
|
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (descriptor.alias == null) {
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
||||||
|
|
||||||
|
if (deletedSoftwareKeys.remove(keyId)) {
|
||||||
|
return InterceptorUtils.createErrorReply(7) // KEY_NOT_FOUND
|
||||||
|
}
|
||||||
|
|
||||||
val response =
|
val response =
|
||||||
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
||||||
?: return TransactionResult.Continue
|
?: return TransactionResult.Continue
|
||||||
@@ -119,13 +256,20 @@ 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,
|
||||||
transactionNames[code] ?: "unknown code=$code",
|
transactionNames[code] ?: "unknown code=$code",
|
||||||
callingUid,
|
callingUid,
|
||||||
callingPid,
|
callingPid,
|
||||||
false,
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,53 +291,275 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
|
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
|
||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
|
|
||||||
if (code == GET_KEY_ENTRY_TRANSACTION) {
|
if (code == GET_SECURITY_LEVEL_TRANSACTION) {
|
||||||
|
return handlePostGetSecurityLevel(txId, data, reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) {
|
||||||
|
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||||
|
return runCatching {
|
||||||
|
val hardwareCount = reply.readInt()
|
||||||
|
val softwareCount =
|
||||||
|
KeyMintSecurityLevelInterceptor.generatedKeys.keys.count {
|
||||||
|
it.uid == callingUid
|
||||||
|
}
|
||||||
|
val totalCount = hardwareCount + softwareCount
|
||||||
|
val parcel = Parcel.obtain().apply {
|
||||||
|
writeNoException()
|
||||||
|
writeInt(totalCount)
|
||||||
|
}
|
||||||
|
TransactionResult.OverrideReply(parcel)
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.error("[TX_ID: $txId] Failed to modify getNumberOfEntries.", it)
|
||||||
|
TransactionResult.SkipTransaction
|
||||||
|
}
|
||||||
|
} else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||||
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||||
|
|
||||||
|
return runCatching {
|
||||||
|
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
|
||||||
|
val params =
|
||||||
|
ListEntriesHandler.cacheParameters(txId, data, isBatchMode)
|
||||||
|
?: throw Exception("Abort updating entries for invalid parameters.")
|
||||||
|
val updatedKeyDescriptors =
|
||||||
|
ListEntriesHandler.injectGeneratedKeys(txId, callingUid, params, reply)
|
||||||
|
InterceptorUtils.createTypedArrayReply(updatedKeyDescriptors)
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.error(
|
||||||
|
"[TX_ID: $txId] Failed to update the result of ${transactionNames[code]!!}.",
|
||||||
|
it,
|
||||||
|
)
|
||||||
|
TransactionResult.SkipTransaction
|
||||||
|
}
|
||||||
|
} else if (code == GET_KEY_ENTRY_TRANSACTION) {
|
||||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
val keyDescriptor =
|
val keyDescriptor =
|
||||||
data.readTypedObject(KeyDescriptor.CREATOR)
|
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
?: return TransactionResult.SkipTransaction
|
?: return TransactionResult.SkipTransaction
|
||||||
|
|
||||||
|
logTransaction(
|
||||||
|
txId,
|
||||||
|
"post-${transactionNames[code]!!} ${keyDescriptor.alias}",
|
||||||
|
callingUid,
|
||||||
|
callingPid,
|
||||||
|
)
|
||||||
|
|
||||||
if (!ConfigurationManager.shouldPatch(callingUid))
|
if (!ConfigurationManager.shouldPatch(callingUid))
|
||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
|
|
||||||
SystemLogger.info("Handling post-${transactionNames[code]!!} ${keyDescriptor.alias}")
|
runCatching {
|
||||||
return try {
|
val response = reply.readTypedObject(KeyEntryResponse.CREATOR)!!
|
||||||
val response =
|
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||||
reply.readTypedObject(KeyEntryResponse.CREATOR)
|
|
||||||
?: return TransactionResult.SkipTransaction
|
|
||||||
reply.setDataPosition(0) // Reset for potential reuse.
|
|
||||||
|
|
||||||
val originalChain = CertificateHelper.getCertificateChain(response)
|
// Skip patching for keys whose certs were explicitly set via updateSubcomponent.
|
||||||
val authorizations = response.metadata?.authorizations
|
if (userUpdatedKeys.remove(keyId)) {
|
||||||
val origin =
|
SystemLogger.debug("[TX_ID: $txId] Skipping cert patch for user-updated key $keyId.")
|
||||||
authorizations
|
return TransactionResult.SkipTransaction
|
||||||
?.find { it.keyParameter.tag == Tag.ORIGIN }
|
}
|
||||||
?.let { it.keyParameter.value.origin }
|
|
||||||
|
|
||||||
if (origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED) {
|
val authorizations = response.metadata.authorizations
|
||||||
SystemLogger.info("[TX_ID: $txId] Skip patching for imported keys.")
|
val parsedParameters =
|
||||||
return TransactionResult.SkipTransaction
|
KeyMintAttestation(
|
||||||
|
authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (parsedParameters.isAttestKey() &&
|
||||||
|
!KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)
|
||||||
|
) {
|
||||||
|
SystemLogger.warning(
|
||||||
|
"[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 =
|
||||||
|
CertificateGenerator.generateAttestedKeyPair(
|
||||||
|
callingUid,
|
||||||
|
keyId.alias,
|
||||||
|
null,
|
||||||
|
parsedParameters,
|
||||||
|
response.metadata.keySecurityLevel,
|
||||||
|
) ?: throw Exception("Failed to create overriding attest key pair.")
|
||||||
|
|
||||||
|
CertificateHelper.updateCertificateChain(
|
||||||
|
response.metadata,
|
||||||
|
keyData.second.toTypedArray(),
|
||||||
|
)
|
||||||
|
.getOrThrow()
|
||||||
|
response.metadata.authorizations =
|
||||||
|
InterceptorUtils.patchAuthorizations(
|
||||||
|
response.metadata.authorizations,
|
||||||
|
callingUid,
|
||||||
|
)
|
||||||
|
|
||||||
|
val key = response.metadata.key!!
|
||||||
|
key.nspace = SecureRandom().nextLong()
|
||||||
|
KeyMintSecurityLevelInterceptor.generatedKeys[keyId] =
|
||||||
|
KeyMintSecurityLevelInterceptor.GeneratedKeyInfo(
|
||||||
|
keyData.first,
|
||||||
|
null,
|
||||||
|
key.nspace,
|
||||||
|
response,
|
||||||
|
parsedParameters,
|
||||||
|
)
|
||||||
|
KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId)
|
||||||
|
return InterceptorUtils.createTypedObjectReply(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
val originalChain = CertificateHelper.getCertificateChain(response)
|
||||||
|
|
||||||
|
// Check if we should perform attestation patch.
|
||||||
|
if (originalChain == null || originalChain.size < 2) {
|
||||||
|
SystemLogger.info(
|
||||||
|
"[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}."
|
||||||
|
)
|
||||||
|
return TransactionResult.SkipTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, try to retrieve the already-patched chain from our cache to ensure
|
||||||
|
// consistency.
|
||||||
|
val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
||||||
|
|
||||||
|
val finalChain: Array<Certificate>
|
||||||
|
if (cachedChain != null) {
|
||||||
|
SystemLogger.debug(
|
||||||
|
"[TX_ID: $txId] Using cached patched certificate chain for $keyId."
|
||||||
|
)
|
||||||
|
finalChain = cachedChain
|
||||||
|
} 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(
|
||||||
|
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
|
||||||
|
)
|
||||||
|
finalChain =
|
||||||
|
AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
||||||
|
|
||||||
|
KeyMintSecurityLevelInterceptor.patchedChains[keyId] = finalChain
|
||||||
|
SystemLogger.debug("Cached patched certificate chain for $keyId.")
|
||||||
|
}
|
||||||
|
|
||||||
|
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
|
||||||
|
.getOrThrow()
|
||||||
|
response.metadata.authorizations =
|
||||||
|
InterceptorUtils.patchAuthorizations(
|
||||||
|
response.metadata.authorizations,
|
||||||
|
callingUid,
|
||||||
|
)
|
||||||
|
|
||||||
|
return InterceptorUtils.createTypedObjectReply(response)
|
||||||
}
|
}
|
||||||
|
.onFailure {
|
||||||
if (originalChain == null || originalChain.size < 2) {
|
SystemLogger.error(
|
||||||
SystemLogger.info(
|
"[TX_ID: $txId] Failed to modify hardware KeyEntryResponse.",
|
||||||
"[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}."
|
it,
|
||||||
)
|
)
|
||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the attestation patch.
|
|
||||||
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
|
||||||
CertificateHelper.updateCertificateChain(response.metadata, newChain).getOrThrow()
|
|
||||||
|
|
||||||
InterceptorUtils.createTypedObjectReply(response)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SystemLogger.error("[TX_ID: $txId] Failed to patch certificate chain.", e)
|
|
||||||
TransactionResult.SkipTransaction
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult {
|
||||||
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
|
|
||||||
|
// Resolve by nspace (KEY_ID) or alias (APP), same as createOperation.
|
||||||
|
val generatedKeyInfo =
|
||||||
|
when (descriptor.domain) {
|
||||||
|
Domain.KEY_ID ->
|
||||||
|
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||||
|
callingUid,
|
||||||
|
descriptor.nspace,
|
||||||
|
)
|
||||||
|
Domain.APP ->
|
||||||
|
descriptor.alias?.let {
|
||||||
|
KeyMintSecurityLevelInterceptor.generatedKeys[KeyIdentifier(callingUid, it)]
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generatedKeyInfo == null) {
|
||||||
|
// Hardware key: mark so getKeyEntry skips cert re-patching.
|
||||||
|
descriptor.alias?.let { userUpdatedKeys.add(KeyIdentifier(callingUid, it)) }
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemLogger.info("Updating sub-component with key[${generatedKeyInfo.nspace}]")
|
||||||
|
val metadata = generatedKeyInfo.response.metadata
|
||||||
|
val publicCert = data.createByteArray()
|
||||||
|
val certificateChain = data.createByteArray()
|
||||||
|
|
||||||
|
metadata.certificate = publicCert
|
||||||
|
metadata.certificateChain = certificateChain
|
||||||
|
SystemLogger.verbose(
|
||||||
|
"Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+359
-25
@@ -4,12 +4,28 @@ import android.annotation.SuppressLint
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.security.Credentials
|
import android.security.Credentials
|
||||||
|
import android.security.KeyStore
|
||||||
|
import android.security.keymaster.ExportResult
|
||||||
|
import android.security.keymaster.KeyCharacteristics
|
||||||
|
import android.security.keymaster.KeymasterArguments
|
||||||
|
import android.security.keymaster.KeymasterCertificateChain
|
||||||
|
import android.security.keymaster.KeymasterDefs
|
||||||
|
import android.security.keystore.IKeystoreCertificateChainCallback
|
||||||
|
import android.security.keystore.IKeystoreExportKeyCallback
|
||||||
|
import android.security.keystore.IKeystoreKeyCharacteristicsCallback
|
||||||
import android.security.keystore.IKeystoreService
|
import android.security.keystore.IKeystoreService
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.security.KeyPair
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
|
import java.util.Date
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import org.matrix.TEESimulator.attestation.AttestationBuilder
|
||||||
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
||||||
|
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
|
import org.matrix.TEESimulator.interception.keystore.InterceptorUtils.extractAlias
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
import org.matrix.TEESimulator.pki.CertificateGenerator
|
||||||
import org.matrix.TEESimulator.pki.CertificateHelper
|
import org.matrix.TEESimulator.pki.CertificateHelper
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,11 +55,35 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
|
|||||||
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "attestKey")
|
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "attestKey")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val transactionNames: Map<Int, String> by lazy {
|
||||||
|
IKeystoreService.Stub::class
|
||||||
|
.java
|
||||||
|
.declaredFields
|
||||||
|
.filter {
|
||||||
|
it.isAccessible = true
|
||||||
|
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
|
||||||
|
}
|
||||||
|
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// A map to dispatch transaction handling for software key generation.
|
||||||
|
private val generateKeyHandlers:
|
||||||
|
Map<Int, (Long, Int, Int, Parcel) -> TransactionResult> by lazy {
|
||||||
|
mapOf(
|
||||||
|
GENERATE_KEY_TRANSACTION to ::handleGenerateKey,
|
||||||
|
GET_KEY_CHARACTERISTICS_TRANSACTION to ::handleGetKeyCharacteristics,
|
||||||
|
EXPORT_KEY_TRANSACTION to ::handleExportKey,
|
||||||
|
ATTEST_KEY_TRANSACTION to ::handleAttestKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override val serviceName = "android.security.keystore"
|
override val serviceName = "android.security.keystore"
|
||||||
override val processName = "keystore"
|
override val processName = "keystore"
|
||||||
override val injectionCommand = "exec ./inject `pidof keystore` libTEESimulator.so entry"
|
override val injectionCommand = "exec ./inject `pidof keystore` libTEESimulator.so entry"
|
||||||
|
|
||||||
private const val SERVICE_DESCRIPTOR = "android.security.keystore.IKeystoreService"
|
// State management for the multi-step key generation process.
|
||||||
|
private val keygenParameters = ConcurrentHashMap<KeyIdentifier, LegacyKeygenParameters>()
|
||||||
|
private val generatedKeyPairs = ConcurrentHashMap<KeyIdentifier, KeyPair>()
|
||||||
|
|
||||||
// Cache to store the fully patched chain after the leaf is requested.
|
// Cache to store the fully patched chain after the leaf is requested.
|
||||||
private val patchedChainCache = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
|
private val patchedChainCache = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
|
||||||
@@ -58,24 +98,187 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
|
|||||||
data: Parcel,
|
data: Parcel,
|
||||||
): TransactionResult {
|
): TransactionResult {
|
||||||
// This interceptor only needs to act on pre-transaction for software key generation.
|
// This interceptor only needs to act on pre-transaction for software key generation.
|
||||||
|
// Handle 'generate' mode interceptions using the handler map.
|
||||||
if (ConfigurationManager.shouldGenerate(callingUid)) {
|
if (ConfigurationManager.shouldGenerate(callingUid)) {
|
||||||
when (code) {
|
generateKeyHandlers[code]?.let { handler ->
|
||||||
GENERATE_KEY_TRANSACTION,
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||||
GET_KEY_CHARACTERISTICS_TRANSACTION,
|
return handler(txId, callingUid, callingPid, data)
|
||||||
EXPORT_KEY_TRANSACTION,
|
|
||||||
ATTEST_KEY_TRANSACTION -> {
|
|
||||||
// TODO: Implement the full software simulation logic.
|
|
||||||
logTransaction(txId, "unimplemented-generate-flow", callingUid, callingPid)
|
|
||||||
return InterceptorUtils.createSuccessReply()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (ConfigurationManager.shouldGenerate(callingUid)) {
|
|
||||||
if (code == GET_TRANSACTION) return TransactionResult.Continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle 'patch' mode interceptions for the 'get' transaction.
|
||||||
|
if (ConfigurationManager.shouldPatch(callingUid) && code == GET_TRANSACTION) {
|
||||||
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
|
||||||
|
return TransactionResult.Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default behavior for all other transactions.
|
||||||
|
logTransaction(
|
||||||
|
txId,
|
||||||
|
transactionNames[code] ?: "unknown code=$code",
|
||||||
|
callingUid,
|
||||||
|
callingPid,
|
||||||
|
true,
|
||||||
|
)
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleGenerateKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
|
||||||
|
return runCatching {
|
||||||
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
val callback =
|
||||||
|
IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
|
||||||
|
val alias = InterceptorUtils.extractAlias(data.readString()!!)
|
||||||
|
val keyId = KeyIdentifier(uid, alias)
|
||||||
|
|
||||||
|
// Read and parse the key generation arguments.
|
||||||
|
val keymasterArgs = KeymasterArguments()
|
||||||
|
if (data.readInt() == 1) {
|
||||||
|
keymasterArgs.readFromParcel(data)
|
||||||
|
}
|
||||||
|
keygenParameters[keyId] =
|
||||||
|
LegacyKeygenParameters.fromKeymasterArguments(keymasterArgs)
|
||||||
|
|
||||||
|
// Create a fake successful response for the callback.
|
||||||
|
val characteristics = KeyCharacteristics()
|
||||||
|
characteristics.swEnforced = KeymasterArguments()
|
||||||
|
characteristics.hwEnforced = keymasterArgs
|
||||||
|
|
||||||
|
val keystoreResponse = InterceptorUtils.createSuccessKeystoreResponse()
|
||||||
|
callback.onFinished(keystoreResponse, characteristics)
|
||||||
|
|
||||||
|
InterceptorUtils.createSuccessReply()
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.error("[TX_ID: $txId] Failed during handleGenerateKey.", it)
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleGetKeyCharacteristics(
|
||||||
|
txId: Long,
|
||||||
|
uid: Int,
|
||||||
|
pid: Int,
|
||||||
|
data: Parcel,
|
||||||
|
): TransactionResult {
|
||||||
|
return runCatching {
|
||||||
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
val callback =
|
||||||
|
IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
|
||||||
|
val alias = InterceptorUtils.extractAlias(data.readString()!!)
|
||||||
|
val keyId = KeyIdentifier(uid, alias)
|
||||||
|
|
||||||
|
val params =
|
||||||
|
keygenParameters[keyId]
|
||||||
|
?: throw IllegalStateException("No params found for $keyId")
|
||||||
|
|
||||||
|
val characteristics =
|
||||||
|
KeyCharacteristics().apply {
|
||||||
|
swEnforced = KeymasterArguments()
|
||||||
|
hwEnforced =
|
||||||
|
KeymasterArguments().apply {
|
||||||
|
addEnum(KeymasterDefs.KM_TAG_ALGORITHM, params.algorithm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback.onFinished(
|
||||||
|
InterceptorUtils.createSuccessKeystoreResponse(),
|
||||||
|
characteristics,
|
||||||
|
)
|
||||||
|
|
||||||
|
InterceptorUtils.createSuccessReply()
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.error("[TX_ID: $txId] Failed during handleGetKeyCharacteristics.", it)
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleExportKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
|
||||||
|
return runCatching {
|
||||||
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
val callback = IKeystoreExportKeyCallback.Stub.asInterface(data.readStrongBinder())
|
||||||
|
val alias = InterceptorUtils.extractAlias(data.readString()!!)
|
||||||
|
val keyId = KeyIdentifier(uid, alias)
|
||||||
|
|
||||||
|
val params =
|
||||||
|
keygenParameters[keyId]
|
||||||
|
?: throw IllegalStateException("No params found for $keyId")
|
||||||
|
|
||||||
|
// Generate a software key pair using the new generator.
|
||||||
|
val keyPair =
|
||||||
|
CertificateGenerator.generateSoftwareKeyPair(params.toKeyMintAttestation())
|
||||||
|
?: throw Exception("Failed to generate software key pair.")
|
||||||
|
generatedKeyPairs[keyId] = keyPair
|
||||||
|
|
||||||
|
// Create a successful ExportResult containing the public key.
|
||||||
|
val exportResultParcel =
|
||||||
|
Parcel.obtain().apply {
|
||||||
|
writeInt(KeyStore.NO_ERROR)
|
||||||
|
writeByteArray(keyPair.public.encoded)
|
||||||
|
setDataPosition(0)
|
||||||
|
}
|
||||||
|
val exportResult = ExportResult.CREATOR.createFromParcel(exportResultParcel)
|
||||||
|
exportResultParcel.recycle()
|
||||||
|
|
||||||
|
callback.onFinished(exportResult)
|
||||||
|
|
||||||
|
InterceptorUtils.createSuccessReply()
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.error("[TX_ID: $txId] Failed during handleExportKey.", it)
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleAttestKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
|
||||||
|
return runCatching {
|
||||||
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
val callback =
|
||||||
|
IKeystoreCertificateChainCallback.Stub.asInterface(data.readStrongBinder())
|
||||||
|
val alias = InterceptorUtils.extractAlias(data.readString()!!)
|
||||||
|
val keyId = KeyIdentifier(uid, alias)
|
||||||
|
|
||||||
|
// Get the attestation challenge from the arguments.
|
||||||
|
val params =
|
||||||
|
keygenParameters[keyId]
|
||||||
|
?: throw IllegalStateException("No params found for $keyId")
|
||||||
|
val keyPair =
|
||||||
|
generatedKeyPairs[keyId]
|
||||||
|
?: throw IllegalStateException("No keypair found for $keyId")
|
||||||
|
|
||||||
|
val attestationArgs = KeymasterArguments()
|
||||||
|
if (data.readInt() == 1) {
|
||||||
|
attestationArgs.readFromParcel(data)
|
||||||
|
val challenge =
|
||||||
|
attestationArgs.getBytes(
|
||||||
|
KeymasterDefs.KM_TAG_ATTESTATION_CHALLENGE,
|
||||||
|
ByteArray(0),
|
||||||
|
)
|
||||||
|
params.attestationChallenge = challenge
|
||||||
|
}
|
||||||
|
|
||||||
|
val certificateChain =
|
||||||
|
CertificateGenerator.generateCertificateChain(
|
||||||
|
uid,
|
||||||
|
keyPair,
|
||||||
|
null, // No attestKeyAlias in legacy flow
|
||||||
|
params.toKeyMintAttestation(), // Convert to modern format
|
||||||
|
1, // SecurityLevel.TRUSTED_ENVIRONMENT
|
||||||
|
) ?: throw Exception("CertificateGenerator failed to create attested key pair.")
|
||||||
|
|
||||||
|
val chainAsByteList = certificateChain.map { it.encoded }
|
||||||
|
val certChain = KeymasterCertificateChain(chainAsByteList)
|
||||||
|
|
||||||
|
callback.onFinished(InterceptorUtils.createSuccessKeystoreResponse(), certChain)
|
||||||
|
InterceptorUtils.createSuccessReply()
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.error("[TX_ID: $txId] Failed during handleAttestKey.", it)
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPostTransact(
|
override fun onPostTransact(
|
||||||
txId: Long,
|
txId: Long,
|
||||||
target: IBinder,
|
target: IBinder,
|
||||||
@@ -93,16 +296,22 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
|
|||||||
reply == null ||
|
reply == null ||
|
||||||
InterceptorUtils.hasException(reply)
|
InterceptorUtils.hasException(reply)
|
||||||
) {
|
) {
|
||||||
|
SystemLogger.debug(
|
||||||
|
"[TX_ID: $txId] Skip parsing post-transaction for [target, code, reply]: [$target, $code, $reply]"
|
||||||
|
)
|
||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ConfigurationManager.shouldPatch(callingUid)) return TransactionResult.SkipTransaction
|
if (!ConfigurationManager.shouldPatch(callingUid)) return TransactionResult.SkipTransaction
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
data.enforceInterface(SERVICE_DESCRIPTOR)
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
val alias = data.readString() ?: ""
|
val alias = data.readString() ?: ""
|
||||||
val extractedAlias = InterceptorUtils.extractAlias(alias)
|
val extractedAlias = InterceptorUtils.extractAlias(alias)
|
||||||
val keyId = KeyIdentifier(callingUid, extractedAlias)
|
val keyId = KeyIdentifier(callingUid, extractedAlias)
|
||||||
|
SystemLogger.debug(
|
||||||
|
"[TX_ID: $txId] Parsed $keyId during post-transaction of ${transactionNames[code]}"
|
||||||
|
)
|
||||||
|
|
||||||
when {
|
when {
|
||||||
// Case 1: The app is requesting the leaf certificate.
|
// Case 1: The app is requesting the leaf certificate.
|
||||||
@@ -111,13 +320,11 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
|
|||||||
val originalLeafBytes =
|
val originalLeafBytes =
|
||||||
reply.createByteArray() ?: return TransactionResult.SkipTransaction
|
reply.createByteArray() ?: return TransactionResult.SkipTransaction
|
||||||
|
|
||||||
// The original chain is not available,
|
val originalLeafCertResult = CertificateHelper.toCertificate(originalLeafBytes)
|
||||||
// so we must pass a temporary one to the patcher.
|
if (originalLeafCertResult !is CertificateHelper.OperationResult.Success) {
|
||||||
// The patcher only needs the original leaf to extract details.
|
return TransactionResult.SkipTransaction
|
||||||
val originalLeafCert =
|
}
|
||||||
(CertificateHelper.toCertificate(originalLeafBytes)
|
val originalLeafCert = originalLeafCertResult.data
|
||||||
as CertificateHelper.OperationResult.Success)
|
|
||||||
.data
|
|
||||||
val tempChain = arrayOf<Certificate>(originalLeafCert)
|
val tempChain = arrayOf<Certificate>(originalLeafCert)
|
||||||
|
|
||||||
// Perform the COMPLETE patch and rebuild operation.
|
// Perform the COMPLETE patch and rebuild operation.
|
||||||
@@ -157,11 +364,6 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
|
|||||||
)
|
)
|
||||||
InterceptorUtils.createByteArrayReply(caCertsBytes!!)
|
InterceptorUtils.createByteArrayReply(caCertsBytes!!)
|
||||||
} else {
|
} else {
|
||||||
// We have no cached chain.
|
|
||||||
// This could mean the app requested the CA without requesting the leaf
|
|
||||||
// first, or patching failed.
|
|
||||||
// In this case, we cannot safely intervene.
|
|
||||||
// Let the original reply pass through.
|
|
||||||
SystemLogger.warning(
|
SystemLogger.warning(
|
||||||
"[TX_ID: $txId] No cached chain found for CA request on alias '$extractedAlias'. Skipping."
|
"[TX_ID: $txId] No cached chain found for CA request on alias '$extractedAlias'. Skipping."
|
||||||
)
|
)
|
||||||
@@ -177,3 +379,135 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data class to hold key generation parameters parsed from the legacy IKeystoreService's
|
||||||
|
* KeymasterArguments. It is used exclusively by the KeystoreInterceptor to manage state during the
|
||||||
|
* software key generation flow.
|
||||||
|
*/
|
||||||
|
private data class LegacyKeygenParameters(
|
||||||
|
val algorithm: Int,
|
||||||
|
val keySize: Int,
|
||||||
|
val purpose: List<Int>,
|
||||||
|
val digest: List<Int>,
|
||||||
|
val certificateNotBefore: Date?,
|
||||||
|
val rsaPublicExponent: BigInteger?,
|
||||||
|
val ecCurveName: String?, // Derived from keySize
|
||||||
|
) {
|
||||||
|
// The challenge is provided in a separate transaction (attestKey), so it must be mutable.
|
||||||
|
var attestationChallenge: ByteArray? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the legacy parameters into the modern [KeyMintAttestation] data structure, which is
|
||||||
|
* required by [AttestationBuilder] and [CertificateGenerator].
|
||||||
|
*/
|
||||||
|
fun toKeyMintAttestation(): KeyMintAttestation {
|
||||||
|
// This conversion acts as a bridge, allowing our new generic components
|
||||||
|
// to be used by the legacy interceptor.
|
||||||
|
return KeyMintAttestation(
|
||||||
|
algorithm = this.algorithm,
|
||||||
|
ecCurve = 0, // Not explicitly available in legacy args, but not critical
|
||||||
|
ecCurveName = this.ecCurveName ?: "",
|
||||||
|
keySize = this.keySize,
|
||||||
|
origin = null, // Not needed to build attestaion
|
||||||
|
noAuthRequired = null,
|
||||||
|
blockMode = listOf<Int>(),
|
||||||
|
padding = listOf<Int>(),
|
||||||
|
purpose = this.purpose,
|
||||||
|
digest = this.digest,
|
||||||
|
rsaPublicExponent = this.rsaPublicExponent,
|
||||||
|
certificateSerial = null, // Not provided in legacy generateKey
|
||||||
|
certificateSubject = null, // Not provided in legacy generateKey
|
||||||
|
certificateNotBefore = this.certificateNotBefore,
|
||||||
|
certificateNotAfter = null, // Not provided in legacy generateKey
|
||||||
|
attestationChallenge = this.attestationChallenge,
|
||||||
|
// Device identifiers are not passed in legacy args;
|
||||||
|
// AttestationBuilder will fetch them from system properties.
|
||||||
|
brand = null,
|
||||||
|
device = null,
|
||||||
|
product = null,
|
||||||
|
serial = null,
|
||||||
|
imei = null,
|
||||||
|
meid = null,
|
||||||
|
manufacturer = null,
|
||||||
|
model = null,
|
||||||
|
secondImei = null,
|
||||||
|
activeDateTime = null,
|
||||||
|
originationExpireDateTime = null,
|
||||||
|
usageExpireDateTime = null,
|
||||||
|
usageCountLimit = null,
|
||||||
|
callerNonce = null,
|
||||||
|
unlockedDeviceRequired = null,
|
||||||
|
includeUniqueId = null,
|
||||||
|
rollbackResistance = null,
|
||||||
|
earlyBootOnly = null,
|
||||||
|
allowWhileOnBody = null,
|
||||||
|
trustedUserPresenceRequired = null,
|
||||||
|
trustedConfirmationRequired = null,
|
||||||
|
maxUsesPerBoot = null,
|
||||||
|
maxBootLevel = null,
|
||||||
|
minMacLength = null,
|
||||||
|
rsaOaepMgfDigest = emptyList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Factory method to create an instance from a [KeymasterArguments] object. */
|
||||||
|
fun fromKeymasterArguments(args: KeymasterArguments): LegacyKeygenParameters {
|
||||||
|
val algorithm = args.getEnum(KeymasterDefs.KM_TAG_ALGORITHM, 0)
|
||||||
|
val keySize = args.getUnsignedInt(KeymasterDefs.KM_TAG_KEY_SIZE, 0).toInt()
|
||||||
|
|
||||||
|
return LegacyKeygenParameters(
|
||||||
|
algorithm = algorithm,
|
||||||
|
keySize = keySize,
|
||||||
|
purpose = args.getEnums(KeymasterDefs.KM_TAG_PURPOSE),
|
||||||
|
digest = args.getEnums(KeymasterDefs.KM_TAG_DIGEST),
|
||||||
|
certificateNotBefore = args.getDate(KeymasterDefs.KM_TAG_ACTIVE_DATETIME, Date()),
|
||||||
|
rsaPublicExponent =
|
||||||
|
if (algorithm == KeymasterDefs.KM_ALGORITHM_RSA) getRsaExponent(args) else null,
|
||||||
|
ecCurveName =
|
||||||
|
if (algorithm == KeymasterDefs.KM_ALGORITHM_EC) deriveEcCurveName(keySize)
|
||||||
|
else null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deriveEcCurveName(keySize: Int): String =
|
||||||
|
when (keySize) {
|
||||||
|
224 -> "secp224r1"
|
||||||
|
256 -> "secp256r1"
|
||||||
|
384 -> "secp384r1"
|
||||||
|
521 -> "secp521r1"
|
||||||
|
else -> "secp256r1" // Default fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The RSA public exponent is not accessible via a public API in KeymasterArguments, so we
|
||||||
|
* must use reflection to extract it.
|
||||||
|
*/
|
||||||
|
private fun getRsaExponent(args: KeymasterArguments): BigInteger? {
|
||||||
|
return runCatching {
|
||||||
|
val getArgumentByTag =
|
||||||
|
KeymasterArguments::class
|
||||||
|
.java
|
||||||
|
.getDeclaredMethod("getArgumentByTag", Int::class.java)
|
||||||
|
getArgumentByTag.isAccessible = true
|
||||||
|
val rsaArgument =
|
||||||
|
getArgumentByTag.invoke(args, KeymasterDefs.KM_TAG_RSA_PUBLIC_EXPONENT)
|
||||||
|
|
||||||
|
val getLongTagValue =
|
||||||
|
KeymasterArguments::class
|
||||||
|
.java
|
||||||
|
.getDeclaredMethod(
|
||||||
|
"getLongTagValue",
|
||||||
|
Class.forName("android.security.keymaster.KeymasterArgument"),
|
||||||
|
)
|
||||||
|
getLongTagValue.isAccessible = true
|
||||||
|
getLongTagValue.invoke(args, rsaArgument) as BigInteger
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
SystemLogger.error("Failed to read rsaPublicExponent via reflection.", it)
|
||||||
|
}
|
||||||
|
.getOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
package org.matrix.TEESimulator.interception.keystore
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.system.keystore2.Domain
|
||||||
|
import android.system.keystore2.IKeystoreService
|
||||||
|
import android.system.keystore2.KeyDescriptor
|
||||||
|
import java.util.TreeMap
|
||||||
|
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
|
||||||
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to intercept listEntries and listEntriesBatched transactions.
|
||||||
|
*
|
||||||
|
* References for all mentioned functions in AOSP:
|
||||||
|
* https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/database.rs
|
||||||
|
* https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/service.rs
|
||||||
|
* https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/utils.rs
|
||||||
|
*/
|
||||||
|
object ListEntriesHandler {
|
||||||
|
|
||||||
|
// Estimate for maximum size of a Binder response in bytes.
|
||||||
|
private const val RESPONSE_SIZE_LIMIT = 358400
|
||||||
|
|
||||||
|
// Based on AOSP function `estimate_safe_amount_to_return` in utils.rs.
|
||||||
|
private fun estimateSafeAmountToReturn(
|
||||||
|
keyDescriptors: Array<KeyDescriptor>,
|
||||||
|
responseSizeLimit: Int,
|
||||||
|
): Int {
|
||||||
|
var itemsToReturn = 0
|
||||||
|
var returnedBytes = 0
|
||||||
|
|
||||||
|
for (kd in keyDescriptors) {
|
||||||
|
// 4 bytes for the Domain enum
|
||||||
|
// 8 bytes for the Namespace long
|
||||||
|
returnedBytes += 4 + 8
|
||||||
|
|
||||||
|
kd.alias?.let { returnedBytes += 4 + it.toByteArray(Charsets.UTF_8).size }
|
||||||
|
kd.blob?.let { returnedBytes += 4 + it.size }
|
||||||
|
|
||||||
|
if (returnedBytes > responseSizeLimit) {
|
||||||
|
SystemLogger.warning(
|
||||||
|
"Key descriptors list (${keyDescriptors.size} items) may exceed binder size limit, returning $itemsToReturn items with estimated size: $returnedBytes bytes."
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
itemsToReturn++
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemsToReturn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and store parameters for later use (in post-transaction).
|
||||||
|
fun cacheParameters(txId: Long, data: Parcel, isBatchMode: Boolean): ListEntriesParams? {
|
||||||
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
|
||||||
|
val domain = data.readInt()
|
||||||
|
val namespace = data.readLong()
|
||||||
|
val startPastAlias = if (isBatchMode) data.readString() else null
|
||||||
|
|
||||||
|
// List entries is only supported for Domain::APP and Domain::SELINUX.
|
||||||
|
// See AOSP function `get_key_descriptor_for_lookup` in service.rs.
|
||||||
|
// Note that all generated keys belong to Domain::APP.
|
||||||
|
if (domain == Domain.APP) {
|
||||||
|
val params = ListEntriesParams(domain, namespace, startPastAlias)
|
||||||
|
SystemLogger.debug("[TX_ID: $txId] Cached $params.")
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge software-backed keys with hardware-backed keys in the reply parcel.
|
||||||
|
fun injectGeneratedKeys(
|
||||||
|
txId: Long,
|
||||||
|
callingUid: Int,
|
||||||
|
params: ListEntriesParams,
|
||||||
|
reply: Parcel,
|
||||||
|
): Array<KeyDescriptor> {
|
||||||
|
// By default we use the calling uid as namespace if domain is Domain::APP.
|
||||||
|
// The namespace parameter is thus ignored for non-privileged applications.
|
||||||
|
// See AOSP function `get_key_descriptor_for_lookup` in service.rs.
|
||||||
|
val keysToInject =
|
||||||
|
extractGeneratedKeyDescriptors(callingUid, callingUid.toLong(), params.startPastAlias)
|
||||||
|
val originalList = reply.createTypedArray(KeyDescriptor.CREATOR)!!
|
||||||
|
val mergedArray = mergeKeyDescriptors(originalList, keysToInject)
|
||||||
|
|
||||||
|
// Limit response size to avoid binder buffer overflow.
|
||||||
|
// See AOSP function `list_key_entries` in utils.rs.
|
||||||
|
val safeAmountToReturn = estimateSafeAmountToReturn(mergedArray, RESPONSE_SIZE_LIMIT)
|
||||||
|
|
||||||
|
return if (safeAmountToReturn < mergedArray.size) {
|
||||||
|
SystemLogger.debug(
|
||||||
|
"[TX_ID: $txId] Listing entries are truncated [${mergedArray.size} -> $safeAmountToReturn] to avoid transaction overflow."
|
||||||
|
)
|
||||||
|
mergedArray.copyOfRange(0, safeAmountToReturn)
|
||||||
|
} else {
|
||||||
|
SystemLogger.debug(
|
||||||
|
"[TX_ID: $txId] Listing entries returns ${mergedArray.size} [injected: ${keysToInject.size}] keys."
|
||||||
|
)
|
||||||
|
mergedArray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge hardware and software key descriptors into a single sorted array.
|
||||||
|
private fun mergeKeyDescriptors(
|
||||||
|
hardwareKeys: Array<KeyDescriptor>,
|
||||||
|
keysToInject: List<KeyDescriptor>,
|
||||||
|
): Array<KeyDescriptor> {
|
||||||
|
// Uses TreeMap to ensure alphabetical ordering and uniqueness (prefer injected keys).
|
||||||
|
val combinedMap = TreeMap<String, KeyDescriptor>()
|
||||||
|
hardwareKeys.forEach { key -> key.alias?.let { combinedMap[it] = key } }
|
||||||
|
keysToInject.forEach { key -> key.alias?.let { combinedMap[it] = key } }
|
||||||
|
return combinedMap.values.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based on AOSP function `list_past_alias` in database.rs
|
||||||
|
private fun extractGeneratedKeyDescriptors(
|
||||||
|
uid: Int,
|
||||||
|
namespace: Long,
|
||||||
|
startPastAlias: String?,
|
||||||
|
): List<KeyDescriptor> {
|
||||||
|
return KeyMintSecurityLevelInterceptor.generatedKeys.keys
|
||||||
|
.filter { it.uid == uid && (startPastAlias == null || it.alias > startPastAlias) }
|
||||||
|
.map { keyId ->
|
||||||
|
KeyDescriptor().apply {
|
||||||
|
this.domain = Domain.APP
|
||||||
|
this.nspace = namespace
|
||||||
|
this.alias = keyId.alias
|
||||||
|
this.blob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameters of AOSP function `list_key_entries` in utils.rs.
|
||||||
|
data class ListEntriesParams(val domain: Int, val namespace: Long, val startPastAlias: String?)
|
||||||
+263
@@ -0,0 +1,263 @@
|
|||||||
|
package org.matrix.TEESimulator.interception.keystore.shim
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.DataInputStream
|
||||||
|
import java.io.DataOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.security.KeyPair
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.cert.Certificate
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
|
||||||
|
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
|
||||||
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
|
||||||
|
|
||||||
|
data class PersistedKeyData(
|
||||||
|
val uid: Int,
|
||||||
|
val alias: String,
|
||||||
|
val nspace: Long,
|
||||||
|
val securityLevel: Int,
|
||||||
|
val isAttestationKey: Boolean,
|
||||||
|
val algorithm: Int,
|
||||||
|
val keySize: Int,
|
||||||
|
val ecCurve: Int,
|
||||||
|
val purposes: List<Int>,
|
||||||
|
val digests: List<Int>,
|
||||||
|
val privateKeyBytes: ByteArray,
|
||||||
|
val certChainBytes: List<ByteArray>,
|
||||||
|
)
|
||||||
|
|
||||||
|
object GeneratedKeyPersistence {
|
||||||
|
|
||||||
|
private const val FORMAT_VERSION = 1
|
||||||
|
private val PERSISTENCE_DIR = File(CONFIG_PATH, "persistent_keys")
|
||||||
|
|
||||||
|
// Per-filename locks to prevent concurrent writes to the same key file
|
||||||
|
private val fileLocks = ConcurrentHashMap<String, ReentrantLock>()
|
||||||
|
|
||||||
|
private fun getLockForKey(filename: String): ReentrantLock {
|
||||||
|
return fileLocks.computeIfAbsent(filename) { ReentrantLock() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(
|
||||||
|
keyId: KeyIdentifier,
|
||||||
|
keyPair: KeyPair,
|
||||||
|
nspace: Long,
|
||||||
|
securityLevel: Int,
|
||||||
|
certChain: List<Certificate>,
|
||||||
|
algorithm: Int,
|
||||||
|
keySize: Int,
|
||||||
|
ecCurve: Int,
|
||||||
|
purposes: List<Int>,
|
||||||
|
digests: List<Int>,
|
||||||
|
isAttestationKey: Boolean,
|
||||||
|
) {
|
||||||
|
val filename = keyFileName(keyId.uid, keyId.alias)
|
||||||
|
val lock = getLockForKey(filename)
|
||||||
|
SystemLogger.debug("[Persistence] Acquiring lock for $filename")
|
||||||
|
lock.lock()
|
||||||
|
try {
|
||||||
|
SystemLogger.debug("[Persistence] Lock acquired for $filename")
|
||||||
|
runCatching {
|
||||||
|
PERSISTENCE_DIR.mkdirs()
|
||||||
|
val finalFile = File(PERSISTENCE_DIR, filename)
|
||||||
|
val tmpFile = File(PERSISTENCE_DIR, "$filename.tmp")
|
||||||
|
|
||||||
|
try {
|
||||||
|
DataOutputStream(BufferedOutputStream(FileOutputStream(tmpFile))).use { out ->
|
||||||
|
out.writeInt(FORMAT_VERSION)
|
||||||
|
out.writeInt(securityLevel)
|
||||||
|
out.writeInt(keyId.uid)
|
||||||
|
out.writeUTF(keyId.alias)
|
||||||
|
out.writeLong(nspace)
|
||||||
|
out.writeBoolean(isAttestationKey)
|
||||||
|
out.writeInt(algorithm)
|
||||||
|
out.writeInt(keySize)
|
||||||
|
out.writeInt(ecCurve)
|
||||||
|
|
||||||
|
out.writeInt(purposes.size)
|
||||||
|
purposes.forEach { out.writeInt(it) }
|
||||||
|
|
||||||
|
out.writeInt(digests.size)
|
||||||
|
digests.forEach { out.writeInt(it) }
|
||||||
|
|
||||||
|
val pkBytes = keyPair.private.encoded
|
||||||
|
out.writeInt(pkBytes.size)
|
||||||
|
out.write(pkBytes)
|
||||||
|
|
||||||
|
out.writeInt(certChain.size)
|
||||||
|
certChain.forEach { cert ->
|
||||||
|
val encoded = cert.encoded
|
||||||
|
out.writeInt(encoded.size)
|
||||||
|
out.write(encoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
tmpFile.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomic rename — if this fails the tmp is left behind and cleaned on next deleteAll
|
||||||
|
if (!tmpFile.renameTo(finalFile)) {
|
||||||
|
tmpFile.delete()
|
||||||
|
throw IllegalStateException("Failed to atomically rename $tmpFile -> $finalFile")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify write succeeded - catches disk-full or filesystem errors
|
||||||
|
if (!finalFile.exists() || finalFile.length() < 20) {
|
||||||
|
throw IOException("File write verification failed - possible disk full")
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemLogger.debug("Persisted key: $keyId")
|
||||||
|
}.onFailure { e ->
|
||||||
|
SystemLogger.error("Failed to persist key $keyId", e)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lock.unlock()
|
||||||
|
SystemLogger.debug("[Persistence] Lock released for $filename")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete(keyId: KeyIdentifier) {
|
||||||
|
runCatching {
|
||||||
|
val file = File(PERSISTENCE_DIR, keyFileName(keyId.uid, keyId.alias))
|
||||||
|
if (file.exists()) {
|
||||||
|
if (file.delete()) {
|
||||||
|
fileLocks.remove(keyFileName(keyId.uid, keyId.alias))
|
||||||
|
SystemLogger.debug("Deleted persisted key: $keyId")
|
||||||
|
} else {
|
||||||
|
SystemLogger.warning("Failed to delete persisted key file: ${file.name}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SystemLogger.debug("No persisted file to delete for: $keyId")
|
||||||
|
}
|
||||||
|
}.onFailure { e ->
|
||||||
|
SystemLogger.error("Failed to delete persisted key $keyId", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteAll() {
|
||||||
|
runCatching {
|
||||||
|
if (!PERSISTENCE_DIR.exists()) {
|
||||||
|
SystemLogger.debug("No persistent_keys directory, nothing to delete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val files = PERSISTENCE_DIR.listFiles()
|
||||||
|
if (files == null) {
|
||||||
|
SystemLogger.warning("Cannot list persistent_keys directory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var count = 0
|
||||||
|
files.forEach { file ->
|
||||||
|
if (file.name.endsWith(".bin") || file.name.endsWith(".tmp")) {
|
||||||
|
if (file.delete()) count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileLocks.clear()
|
||||||
|
SystemLogger.info("Deleted $count persisted key files")
|
||||||
|
}.onFailure { e ->
|
||||||
|
SystemLogger.error("Failed to delete all persisted keys", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadAll(securityLevel: Int): List<PersistedKeyData> {
|
||||||
|
if (!PERSISTENCE_DIR.exists()) {
|
||||||
|
SystemLogger.debug("No persistent_keys directory, nothing to load")
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
val files = PERSISTENCE_DIR.listFiles { _, name -> name.endsWith(".bin") }
|
||||||
|
if (files == null) {
|
||||||
|
SystemLogger.warning("Cannot read persistent_keys directory")
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
if (files.isEmpty()) {
|
||||||
|
SystemLogger.debug("No persisted key files found")
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
SystemLogger.info("Found ${files.size} persisted key files to process")
|
||||||
|
|
||||||
|
val result = mutableListOf<PersistedKeyData>()
|
||||||
|
|
||||||
|
for (file in files) {
|
||||||
|
runCatching {
|
||||||
|
DataInputStream(BufferedInputStream(FileInputStream(file))).use { input ->
|
||||||
|
val version = input.readInt()
|
||||||
|
if (version != FORMAT_VERSION) {
|
||||||
|
SystemLogger.warning(
|
||||||
|
"Skipping ${file.name}: unknown format version $version"
|
||||||
|
)
|
||||||
|
return@runCatching
|
||||||
|
}
|
||||||
|
|
||||||
|
val storedSecLevel = input.readInt()
|
||||||
|
val uid = input.readInt()
|
||||||
|
val alias = input.readUTF()
|
||||||
|
val nspace = input.readLong()
|
||||||
|
val isAttestKey = input.readBoolean()
|
||||||
|
val algo = input.readInt()
|
||||||
|
val kSize = input.readInt()
|
||||||
|
val curve = input.readInt()
|
||||||
|
|
||||||
|
val purposeCount = requireBounds(input.readInt(), 64, "purposeCount")
|
||||||
|
val purposes = (0 until purposeCount).map { input.readInt() }
|
||||||
|
|
||||||
|
val digestCount = requireBounds(input.readInt(), 64, "digestCount")
|
||||||
|
val digests = (0 until digestCount).map { input.readInt() }
|
||||||
|
|
||||||
|
val pkLen = requireBounds(input.readInt(), 8192, "pkLen")
|
||||||
|
val pkBytes = ByteArray(pkLen)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storedSecLevel == securityLevel) {
|
||||||
|
result.add(
|
||||||
|
PersistedKeyData(
|
||||||
|
uid = uid,
|
||||||
|
alias = alias,
|
||||||
|
nspace = nspace,
|
||||||
|
securityLevel = storedSecLevel,
|
||||||
|
isAttestationKey = isAttestKey,
|
||||||
|
algorithm = algo,
|
||||||
|
keySize = kSize,
|
||||||
|
ecCurve = curve,
|
||||||
|
purposes = purposes,
|
||||||
|
digests = digests,
|
||||||
|
privateKeyBytes = pkBytes,
|
||||||
|
certChainBytes = certChainBytes,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onFailure { e ->
|
||||||
|
SystemLogger.warning("Skipping corrupted persisted key file: ${file.name}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemLogger.info("Loaded ${result.size} persisted keys for security level $securityLevel")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requireBounds(value: Int, max: Int, name: String): Int {
|
||||||
|
require(value in 0..max) { "$name out of bounds: $value (max $max)" }
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun keyFileName(uid: Int, alias: String): String {
|
||||||
|
val digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
.digest("$uid:$alias".toByteArray(Charsets.UTF_8))
|
||||||
|
return digest.joinToString("") { "%02x".format(it) } + ".bin"
|
||||||
|
}
|
||||||
|
}
|
||||||
+1066
-84
File diff suppressed because it is too large
Load Diff
+61
@@ -0,0 +1,61 @@
|
|||||||
|
package org.matrix.TEESimulator.interception.keystore.shim
|
||||||
|
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.system.keystore2.IKeystoreOperation
|
||||||
|
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||||
|
import org.matrix.TEESimulator.interception.keystore.InterceptorUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intercepts calls to an `IKeystoreOperation` service. This is used to log the data manipulation
|
||||||
|
* methods of a cryptographic operation.
|
||||||
|
*/
|
||||||
|
class OperationInterceptor(
|
||||||
|
private val original: IKeystoreOperation,
|
||||||
|
private val backdoor: IBinder,
|
||||||
|
) : BinderInterceptor() {
|
||||||
|
|
||||||
|
override fun onPreTransact(
|
||||||
|
txId: Long,
|
||||||
|
target: IBinder,
|
||||||
|
code: Int,
|
||||||
|
flags: Int,
|
||||||
|
callingUid: Int,
|
||||||
|
callingPid: Int,
|
||||||
|
data: Parcel,
|
||||||
|
): TransactionResult {
|
||||||
|
val methodName = transactionNames[code] ?: "unknown code=$code"
|
||||||
|
logTransaction(txId, methodName, callingUid, callingPid, true)
|
||||||
|
|
||||||
|
if (code == FINISH_TRANSACTION || code == ABORT_TRANSACTION) {
|
||||||
|
KeyMintSecurityLevelInterceptor.removeOperationInterceptor(target, backdoor)
|
||||||
|
}
|
||||||
|
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val UPDATE_AAD_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "updateAad")
|
||||||
|
private val UPDATE_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "update")
|
||||||
|
private val FINISH_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "finish")
|
||||||
|
private val ABORT_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort")
|
||||||
|
|
||||||
|
/** Only intercept finish/abort for cleanup. Other ops pass through without round-trip. */
|
||||||
|
val INTERCEPTED_CODES = intArrayOf(FINISH_TRANSACTION, ABORT_TRANSACTION)
|
||||||
|
|
||||||
|
private val transactionNames: Map<Int, String> by lazy {
|
||||||
|
IKeystoreOperation.Stub::class
|
||||||
|
.java
|
||||||
|
.declaredFields
|
||||||
|
.filter {
|
||||||
|
it.isAccessible = true
|
||||||
|
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
|
||||||
|
}
|
||||||
|
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+415
@@ -0,0 +1,415 @@
|
|||||||
|
package org.matrix.TEESimulator.interception.keystore.shim
|
||||||
|
|
||||||
|
import android.hardware.security.keymint.Algorithm
|
||||||
|
import android.hardware.security.keymint.BlockMode
|
||||||
|
import android.hardware.security.keymint.Digest
|
||||||
|
import android.hardware.security.keymint.KeyParameter
|
||||||
|
import android.hardware.security.keymint.KeyParameterValue
|
||||||
|
import android.hardware.security.keymint.KeyPurpose
|
||||||
|
import android.hardware.security.keymint.PaddingMode
|
||||||
|
import android.hardware.security.keymint.Tag
|
||||||
|
import android.os.RemoteException
|
||||||
|
import android.os.ServiceSpecificException
|
||||||
|
import android.system.keystore2.IKeystoreOperation
|
||||||
|
import android.system.keystore2.KeyParameters
|
||||||
|
import java.security.KeyPair
|
||||||
|
import java.security.Signature
|
||||||
|
import java.security.SignatureException
|
||||||
|
import java.util.concurrent.locks.LockSupport
|
||||||
|
import javax.crypto.BadPaddingException
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.IllegalBlockSizeException
|
||||||
|
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||||
|
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||||
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
|
||||||
|
internal object KeystoreErrorCode {
|
||||||
|
val INVALID_OPERATION_HANDLE: Int by lazy { resolve("ErrorCode", "INVALID_OPERATION_HANDLE", -28) }
|
||||||
|
val VERIFICATION_FAILED: Int by lazy { resolve("ErrorCode", "VERIFICATION_FAILED", -30) }
|
||||||
|
val UNSUPPORTED_PURPOSE: Int by lazy { resolve("ErrorCode", "UNSUPPORTED_PURPOSE", -2) }
|
||||||
|
val INCOMPATIBLE_PURPOSE: Int by lazy { resolve("ErrorCode", "INCOMPATIBLE_PURPOSE", -3) }
|
||||||
|
val INVALID_ARGUMENT: Int by lazy { resolve("ErrorCode", "INVALID_ARGUMENT", -38) }
|
||||||
|
val INVALID_TAG: Int by lazy { resolve("ErrorCode", "INVALID_TAG", -40) }
|
||||||
|
val INVALID_INPUT_LENGTH: Int by lazy { resolve("ErrorCode", "INVALID_INPUT_LENGTH", -21) }
|
||||||
|
val INCOMPATIBLE_KEY: Int by lazy { resolve("ErrorCode", "INCOMPATIBLE_KEY", -31) }
|
||||||
|
val INCOMPATIBLE_ALGORITHM: Int by lazy { resolve("ErrorCode", "INCOMPATIBLE_ALGORITHM", -18) }
|
||||||
|
val KEY_EXPIRED: Int by lazy { resolve("ErrorCode", "KEY_EXPIRED", -25) }
|
||||||
|
val KEY_NOT_YET_VALID: Int by lazy { resolve("ErrorCode", "KEY_NOT_YET_VALID", -24) }
|
||||||
|
val CALLER_NONCE_PROHIBITED: Int by lazy { resolve("ErrorCode", "CALLER_NONCE_PROHIBITED", -55) }
|
||||||
|
val UNKNOWN_ERROR: Int by lazy { resolve("ErrorCode", "UNKNOWN_ERROR", -1000) }
|
||||||
|
val SYSTEM_ERROR: Int by lazy { resolve("ResponseCode", "SYSTEM_ERROR", 4, keystore = true) }
|
||||||
|
val TOO_MUCH_DATA: Int by lazy { resolve("ResponseCode", "TOO_MUCH_DATA", 21, keystore = true) }
|
||||||
|
val PERMISSION_DENIED: Int by lazy { resolve("ResponseCode", "PERMISSION_DENIED", 6, keystore = true) }
|
||||||
|
val KEY_NOT_FOUND: Int by lazy { resolve("ResponseCode", "KEY_NOT_FOUND", 7, keystore = true) }
|
||||||
|
|
||||||
|
private fun resolve(enumName: String, field: String, fallback: Int, keystore: Boolean = false): Int {
|
||||||
|
val pkg = if (keystore) "android.system.keystore2" else "android.hardware.security.keymint"
|
||||||
|
return runCatching { Class.forName("$pkg.$enumName").getField(field).getInt(null) }
|
||||||
|
.getOrDefault(fallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A sealed interface to represent the different cryptographic operations we can perform.
|
||||||
|
private sealed interface CryptoPrimitive {
|
||||||
|
fun updateAad(data: ByteArray?)
|
||||||
|
|
||||||
|
fun update(data: ByteArray?): ByteArray?
|
||||||
|
|
||||||
|
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
|
||||||
|
|
||||||
|
fun abort()
|
||||||
|
|
||||||
|
/** Returns parameters from the begin phase (e.g. GCM nonce), or null if none. */
|
||||||
|
fun getBeginParameters(): Array<KeyParameter>? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper object to map KeyMint constants to JCA algorithm strings.
|
||||||
|
private object JcaAlgorithmMapper {
|
||||||
|
fun mapSignatureAlgorithm(params: KeyMintAttestation): String {
|
||||||
|
val digest =
|
||||||
|
when (params.digest.firstOrNull()) {
|
||||||
|
Digest.SHA_2_256 -> "SHA256"
|
||||||
|
Digest.SHA_2_384 -> "SHA384"
|
||||||
|
Digest.SHA_2_512 -> "SHA512"
|
||||||
|
else -> "NONE"
|
||||||
|
}
|
||||||
|
val keyAlgo =
|
||||||
|
when (params.algorithm) {
|
||||||
|
Algorithm.EC -> "ECDSA"
|
||||||
|
Algorithm.RSA -> "RSA"
|
||||||
|
else ->
|
||||||
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCode.SYSTEM_ERROR,
|
||||||
|
"Unsupported signature algorithm: ${params.algorithm}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return "${digest}with${keyAlgo}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
|
||||||
|
val keyAlgo =
|
||||||
|
when (params.algorithm) {
|
||||||
|
Algorithm.RSA -> "RSA"
|
||||||
|
Algorithm.AES -> "AES"
|
||||||
|
else ->
|
||||||
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCode.SYSTEM_ERROR,
|
||||||
|
"Unsupported cipher algorithm: ${params.algorithm}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val blockMode =
|
||||||
|
when (params.blockMode.firstOrNull()) {
|
||||||
|
BlockMode.ECB -> "ECB"
|
||||||
|
BlockMode.CBC -> "CBC"
|
||||||
|
BlockMode.CTR -> "CTR"
|
||||||
|
BlockMode.GCM -> "GCM"
|
||||||
|
else -> "ECB"
|
||||||
|
}
|
||||||
|
val padding =
|
||||||
|
when (params.padding.firstOrNull()) {
|
||||||
|
PaddingMode.NONE -> "NoPadding"
|
||||||
|
PaddingMode.PKCS7 -> "PKCS7Padding"
|
||||||
|
PaddingMode.RSA_PKCS1_1_5_ENCRYPT -> "PKCS1Padding"
|
||||||
|
PaddingMode.RSA_OAEP -> "OAEPPadding"
|
||||||
|
else -> "NoPadding" // Default for GCM
|
||||||
|
}
|
||||||
|
return "$keyAlgo/$blockMode/$padding"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concrete implementation for Signing.
|
||||||
|
private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
|
||||||
|
private val signature: Signature =
|
||||||
|
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
|
||||||
|
initSign(keyPair.private)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateAad(data: ByteArray?) {
|
||||||
|
throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(data: ByteArray?): ByteArray? {
|
||||||
|
if (data != null) signature.update(data)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray {
|
||||||
|
if (data != null) update(data)
|
||||||
|
return this.signature.sign()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun abort() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concrete implementation for Verification.
|
||||||
|
private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
|
||||||
|
private val signature: Signature =
|
||||||
|
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
|
||||||
|
initVerify(keyPair.public)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateAad(data: ByteArray?) {
|
||||||
|
throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(data: ByteArray?): ByteArray? {
|
||||||
|
if (data != null) signature.update(data)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
|
if (data != null) update(data)
|
||||||
|
if (signature == null)
|
||||||
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCode.VERIFICATION_FAILED,
|
||||||
|
"Signature to verify is null",
|
||||||
|
)
|
||||||
|
if (!this.signature.verify(signature)) {
|
||||||
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCode.VERIFICATION_FAILED,
|
||||||
|
"Signature/MAC verification failed",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun abort() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concrete implementation for Encryption/Decryption.
|
||||||
|
private class CipherPrimitive(
|
||||||
|
cryptoKey: java.security.Key,
|
||||||
|
params: KeyMintAttestation,
|
||||||
|
private val opMode: Int,
|
||||||
|
) : CryptoPrimitive {
|
||||||
|
private val cipher: Cipher =
|
||||||
|
Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply {
|
||||||
|
init(opMode, cryptoKey)
|
||||||
|
}
|
||||||
|
private val isAead = params.blockMode.firstOrNull() == BlockMode.GCM
|
||||||
|
|
||||||
|
override fun updateAad(data: ByteArray?) {
|
||||||
|
if (!isAead) throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG)
|
||||||
|
if (data != null) cipher.updateAAD(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(data: ByteArray?): ByteArray? =
|
||||||
|
if (data != null) cipher.update(data) else null
|
||||||
|
|
||||||
|
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? =
|
||||||
|
if (data != null) cipher.doFinal(data) else cipher.doFinal()
|
||||||
|
|
||||||
|
override fun abort() {}
|
||||||
|
|
||||||
|
/** Returns the cipher IV as a NONCE parameter for GCM operations. */
|
||||||
|
override fun getBeginParameters(): Array<KeyParameter>? {
|
||||||
|
val iv = cipher.iv ?: return null
|
||||||
|
return arrayOf(
|
||||||
|
KeyParameter().apply {
|
||||||
|
tag = Tag.NONCE
|
||||||
|
value = KeyParameterValue.blob(iv)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concrete implementation for ECDH Key Agreement.
|
||||||
|
private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive {
|
||||||
|
private val agreement: javax.crypto.KeyAgreement =
|
||||||
|
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 finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
|
if (data == null)
|
||||||
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCode.INVALID_ARGUMENT,
|
||||||
|
"Peer public key required for key agreement",
|
||||||
|
)
|
||||||
|
val peerKey =
|
||||||
|
java.security.KeyFactory.getInstance("EC")
|
||||||
|
.generatePublic(java.security.spec.X509EncodedKeySpec(data))
|
||||||
|
agreement.doPhase(peerKey, true)
|
||||||
|
return agreement.generateSecret()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun abort() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
||||||
|
private val txId: Long,
|
||||||
|
keyPair: KeyPair?,
|
||||||
|
secretKey: javax.crypto.SecretKey?,
|
||||||
|
params: KeyMintAttestation,
|
||||||
|
private val latencyFloorMs: Long = 0L,
|
||||||
|
var onFinishCallback: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
private val primitive: CryptoPrimitive
|
||||||
|
|
||||||
|
@Volatile var isFinalized = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
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?
|
||||||
|
get() {
|
||||||
|
val params = primitive.getBeginParameters() ?: return null
|
||||||
|
if (params.isEmpty()) return null
|
||||||
|
return KeyParameters().apply { keyParameter = params }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkActive() {
|
||||||
|
if (isFinalized)
|
||||||
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCode.INVALID_OPERATION_HANDLE,
|
||||||
|
"Operation already finalized.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateAad(data: ByteArray?) {
|
||||||
|
checkActive()
|
||||||
|
try {
|
||||||
|
primitive.updateAad(data)
|
||||||
|
} catch (e: ServiceSpecificException) {
|
||||||
|
isFinalized = true
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
isFinalized = true
|
||||||
|
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to updateAad.", e)
|
||||||
|
throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(data: ByteArray?): ByteArray? {
|
||||||
|
checkActive()
|
||||||
|
try {
|
||||||
|
return primitive.update(data)
|
||||||
|
} catch (e: ServiceSpecificException) {
|
||||||
|
isFinalized = true
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
isFinalized = true
|
||||||
|
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
|
||||||
|
throw mapToServiceSpecificException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
|
checkActive()
|
||||||
|
val startNs = if (latencyFloorMs > 0) System.nanoTime() else 0L
|
||||||
|
try {
|
||||||
|
val result = primitive.finish(data, signature)
|
||||||
|
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
|
||||||
|
if (latencyFloorMs > 0) {
|
||||||
|
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
|
||||||
|
val delayMs = latencyFloorMs - elapsedMs
|
||||||
|
if (delayMs > 0) LockSupport.parkNanos(delayMs * 1_000_000)
|
||||||
|
}
|
||||||
|
onFinishCallback?.invoke()
|
||||||
|
return result
|
||||||
|
} catch (e: ServiceSpecificException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
|
||||||
|
throw mapToServiceSpecificException(e)
|
||||||
|
} finally {
|
||||||
|
isFinalized = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapToServiceSpecificException(e: Exception): ServiceSpecificException = when (e) {
|
||||||
|
is ServiceSpecificException -> e
|
||||||
|
is SignatureException -> ServiceSpecificException(KeystoreErrorCode.VERIFICATION_FAILED, e.message)
|
||||||
|
is BadPaddingException -> ServiceSpecificException(KeystoreErrorCode.INVALID_ARGUMENT, e.message)
|
||||||
|
is IllegalBlockSizeException -> ServiceSpecificException(KeystoreErrorCode.INVALID_INPUT_LENGTH, e.message)
|
||||||
|
is java.security.InvalidKeyException -> ServiceSpecificException(KeystoreErrorCode.INCOMPATIBLE_KEY, e.message)
|
||||||
|
else -> ServiceSpecificException(KeystoreErrorCode.UNKNOWN_ERROR, e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun abort() {
|
||||||
|
checkActive()
|
||||||
|
isFinalized = true
|
||||||
|
primitive.abort()
|
||||||
|
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Binder interface for [SoftwareOperation]. Synchronized and input-length validated. */
|
||||||
|
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
||||||
|
IKeystoreOperation.Stub() {
|
||||||
|
|
||||||
|
private fun checkInputLength(data: ByteArray?) {
|
||||||
|
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 {
|
||||||
|
private const val MAX_RECEIVE_DATA = 0x8000
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
package org.matrix.TEESimulator.logging
|
package org.matrix.TEESimulator.logging
|
||||||
|
|
||||||
import android.hardware.security.keymint.Algorithm
|
import android.hardware.security.keymint.*
|
||||||
import android.hardware.security.keymint.Digest
|
|
||||||
import android.hardware.security.keymint.EcCurve
|
|
||||||
import android.hardware.security.keymint.KeyParameter
|
|
||||||
import android.hardware.security.keymint.KeyPurpose
|
|
||||||
import android.hardware.security.keymint.Tag
|
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -34,7 +29,39 @@ object KeyMintParameterLogger {
|
|||||||
.associate { field -> (field.get(null) as Int) to field.name }
|
.associate { field -> (field.get(null) as Int) to field.name }
|
||||||
}
|
}
|
||||||
|
|
||||||
private val purposeNames: Map<Int, String> by lazy {
|
val blockModeNames: Map<Int, String> by lazy {
|
||||||
|
BlockMode::class
|
||||||
|
.java
|
||||||
|
.fields
|
||||||
|
.filter { it.type == Int::class.java }
|
||||||
|
.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 {
|
||||||
|
PaddingMode::class
|
||||||
|
.java
|
||||||
|
.fields
|
||||||
|
.filter { it.type == Int::class.java }
|
||||||
|
.associate { field -> (field.get(null) as Int) to field.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
val purposeNames: Map<Int, String> by lazy {
|
||||||
KeyPurpose::class
|
KeyPurpose::class
|
||||||
.java
|
.java
|
||||||
.fields
|
.fields
|
||||||
@@ -69,21 +96,34 @@ object KeyMintParameterLogger {
|
|||||||
val formattedValue: String =
|
val formattedValue: String =
|
||||||
when (param.tag) {
|
when (param.tag) {
|
||||||
Tag.ALGORITHM -> algorithmNames[value.algorithm]
|
Tag.ALGORITHM -> algorithmNames[value.algorithm]
|
||||||
Tag.EC_CURVE -> ecCurveNames[value.ecCurve]
|
Tag.BLOCK_MODE -> blockModeNames[value.blockMode]
|
||||||
Tag.PURPOSE -> purposeNames[value.keyPurpose]
|
|
||||||
Tag.DIGEST -> digestNames[value.digest]
|
Tag.DIGEST -> digestNames[value.digest]
|
||||||
|
Tag.EC_CURVE -> ecCurveNames[value.ecCurve]
|
||||||
|
Tag.ORIGIN -> keyOriginNames[value.origin]
|
||||||
|
Tag.PADDING -> paddingNames[value.paddingMode]
|
||||||
|
Tag.PURPOSE -> purposeNames[value.keyPurpose]
|
||||||
|
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,6 +1,7 @@
|
|||||||
package org.matrix.TEESimulator.logging
|
package org.matrix.TEESimulator.logging
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
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
|
||||||
@@ -10,12 +11,15 @@ object SystemLogger {
|
|||||||
// The tag used for all log messages from this application.
|
// The tag used for all log messages from this application.
|
||||||
private const val TAG = "TEESimulator"
|
private const val TAG = "TEESimulator"
|
||||||
|
|
||||||
|
private val isDebugBuild = BuildConfig.DEBUG
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
* @param message The message to log.
|
||||||
*/
|
*/
|
||||||
fun debug(message: String) {
|
fun debug(message: String) {
|
||||||
|
if (!isDebugBuild) return
|
||||||
Log.d(TAG, message)
|
Log.d(TAG, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +68,7 @@ object SystemLogger {
|
|||||||
* @param message The message to log.
|
* @param message The message to log.
|
||||||
*/
|
*/
|
||||||
fun verbose(message: String) {
|
fun verbose(message: String) {
|
||||||
|
if (!isDebugBuild) return
|
||||||
Log.v(TAG, message)
|
Log.v(TAG, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
package org.matrix.TEESimulator.pki
|
package org.matrix.TEESimulator.pki
|
||||||
|
|
||||||
import android.hardware.security.keymint.Algorithm
|
import android.hardware.security.keymint.Algorithm
|
||||||
|
import android.hardware.security.keymint.KeyPurpose
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Pair
|
import android.util.Pair
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
import java.security.Security
|
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
import java.security.cert.X509Certificate
|
|
||||||
import java.security.spec.ECGenParameterSpec
|
import java.security.spec.ECGenParameterSpec
|
||||||
import java.security.spec.RSAKeyGenParameterSpec
|
import java.security.spec.RSAKeyGenParameterSpec
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -21,6 +20,7 @@ import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
|||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||||
import org.matrix.TEESimulator.attestation.AttestationBuilder
|
import org.matrix.TEESimulator.attestation.AttestationBuilder
|
||||||
|
import org.matrix.TEESimulator.attestation.AttestationConstants
|
||||||
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.KeyIdentifier
|
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
|
||||||
@@ -35,13 +35,8 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
|||||||
*/
|
*/
|
||||||
object CertificateGenerator {
|
object CertificateGenerator {
|
||||||
|
|
||||||
init {
|
// RFC 5280 GeneralizedTime maximum: 9999-12-31T23:59:59 UTC (millis since epoch).
|
||||||
// Android ships with a stripped-down Bouncy Castle provider under the name "BC".
|
private const val UNDEFINED_NOT_AFTER = 253402300799000L
|
||||||
// We must remove the system provider first to ensure the full Bouncy Castle library
|
|
||||||
// (packaged with the app) is used.
|
|
||||||
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
|
||||||
Security.addProvider(BouncyCastleProvider())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a software-based cryptographic key pair.
|
* Generates a software-based cryptographic key pair.
|
||||||
@@ -56,7 +51,10 @@ object CertificateGenerator {
|
|||||||
Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName)
|
Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName)
|
||||||
Algorithm.RSA ->
|
Algorithm.RSA ->
|
||||||
"RSA" to
|
"RSA" to
|
||||||
RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent)
|
RSAKeyGenParameterSpec(
|
||||||
|
params.keySize,
|
||||||
|
params.rsaPublicExponent ?: RSAKeyGenParameterSpec.F4,
|
||||||
|
)
|
||||||
else ->
|
else ->
|
||||||
throw IllegalArgumentException(
|
throw IllegalArgumentException(
|
||||||
"Unsupported algorithm: ${params.algorithm}"
|
"Unsupported algorithm: ${params.algorithm}"
|
||||||
@@ -72,36 +70,32 @@ object CertificateGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new key pair and a corresponding certificate chain containing a simulated
|
* Generates a certificate chain for a given key pair. This is the primary function for creating
|
||||||
* attestation.
|
* attested certificates.
|
||||||
*
|
*
|
||||||
* @param uid The UID of the application requesting the key.
|
* @param uid The UID of the application requesting the key.
|
||||||
* @param alias The alias for the new key.
|
* @param subjectKeyPair The key pair for which the certificate will be generated.
|
||||||
* @param attestKeyAlias Optional alias of a key to use for attestation signing.
|
* @param attestKeyAlias Optional alias of a key to use for attestation signing.
|
||||||
* @param params The parameters for the new key and its attestation.
|
* @param params The parameters for the new key and its attestation.
|
||||||
* @param securityLevel The security level to embed in the attestation.
|
* @param securityLevel The security level to embed in the attestation.
|
||||||
* @return A [Pair] containing the new [KeyPair] and its certificate chain, or `null` on
|
* @return A [List] of [Certificate] forming the new chain, or `null` on failure.
|
||||||
* failure.
|
|
||||||
*/
|
*/
|
||||||
fun generateAttestedKeyPair(
|
fun generateCertificateChain(
|
||||||
uid: Int,
|
uid: Int,
|
||||||
alias: String,
|
subjectKeyPair: KeyPair,
|
||||||
attestKeyAlias: String?,
|
attestKeyAlias: String?,
|
||||||
params: KeyMintAttestation,
|
params: KeyMintAttestation,
|
||||||
securityLevel: Int,
|
securityLevel: Int,
|
||||||
): Pair<KeyPair, List<Certificate>>? {
|
): List<Certificate>? {
|
||||||
return runCatching {
|
val challenge = params.attestationChallenge
|
||||||
SystemLogger.info(
|
if (challenge != null && challenge.size > AttestationConstants.CHALLENGE_LENGTH_LIMIT)
|
||||||
"Generating new attested key pair for alias: '$alias' (UID: $uid)"
|
throw IllegalArgumentException(
|
||||||
)
|
"Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})"
|
||||||
val newKeyPair =
|
)
|
||||||
generateSoftwareKeyPair(params)
|
|
||||||
?: throw Exception("Failed to generate underlying software key pair.")
|
|
||||||
|
|
||||||
|
return try {
|
||||||
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
|
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
|
||||||
|
|
||||||
// Determine the signing key and issuer. If an attestKey is provided, use it.
|
|
||||||
// Otherwise, fall back to the root key from the keybox.
|
|
||||||
val (signingKey, issuer) =
|
val (signingKey, issuer) =
|
||||||
if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second }
|
getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second }
|
||||||
@@ -110,31 +104,58 @@ object CertificateGenerator {
|
|||||||
keybox.keyPair to getIssuerFromKeybox(keybox)
|
keybox.keyPair to getIssuerFromKeybox(keybox)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the new leaf certificate with the simulated attestation.
|
|
||||||
val leafCert =
|
val leafCert =
|
||||||
buildCertificate(newKeyPair, signingKey, issuer, params, securityLevel)
|
buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel)
|
||||||
|
|
||||||
|
if (attestKeyAlias != null) {
|
||||||
|
listOf(leafCert)
|
||||||
|
} else {
|
||||||
|
listOf(leafCert) + keybox.certificates
|
||||||
|
}
|
||||||
|
} catch (e: android.os.ServiceSpecificException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("Failed to generate certificate chain.", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A convenience function that combines key pair generation and certificate chain generation.
|
||||||
|
* Primarily used by the modern Keystore2 interceptor where generation is a single step.
|
||||||
|
*/
|
||||||
|
fun generateAttestedKeyPair(
|
||||||
|
uid: Int,
|
||||||
|
alias: String,
|
||||||
|
attestKeyAlias: String?,
|
||||||
|
params: KeyMintAttestation,
|
||||||
|
securityLevel: Int,
|
||||||
|
): Pair<KeyPair, List<Certificate>>? {
|
||||||
|
return try {
|
||||||
|
SystemLogger.info(
|
||||||
|
"Generating new attested key pair for alias: '$alias' (UID: $uid)"
|
||||||
|
)
|
||||||
|
val newKeyPair =
|
||||||
|
generateSoftwareKeyPair(params)
|
||||||
|
?: throw Exception("Failed to generate underlying software key pair.")
|
||||||
|
|
||||||
// If not self-attesting, the chain is just the leaf. Otherwise, append the keybox
|
|
||||||
// chain.
|
|
||||||
val chain =
|
val chain =
|
||||||
if (attestKeyAlias != null) {
|
generateCertificateChain(uid, newKeyPair, attestKeyAlias, params, securityLevel)
|
||||||
listOf(leafCert)
|
?: throw Exception("Failed to generate certificate chain for new key pair.")
|
||||||
} else {
|
|
||||||
listOf(leafCert) + keybox.certificates
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"Successfully generated new certificate chain for alias: '$alias'."
|
"Successfully generated new certificate chain for alias: '$alias'."
|
||||||
)
|
)
|
||||||
Pair(newKeyPair, chain)
|
Pair(newKeyPair, chain)
|
||||||
|
} catch (e: android.os.ServiceSpecificException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", e)
|
||||||
|
null
|
||||||
}
|
}
|
||||||
.onFailure {
|
|
||||||
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", it)
|
|
||||||
}
|
|
||||||
.getOrNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getIssuerFromKeybox(keybox: KeyBox) =
|
fun getIssuerFromKeybox(keybox: KeyBox) =
|
||||||
X509CertificateHolder(keybox.certificates[0].encoded).subject
|
X509CertificateHolder(keybox.certificates[0].encoded).subject
|
||||||
|
|
||||||
private fun getKeyboxForAlgorithm(uid: Int, algorithm: Int): KeyBox {
|
private fun getKeyboxForAlgorithm(uid: Int, algorithm: Int): KeyBox {
|
||||||
@@ -146,7 +167,10 @@ object CertificateGenerator {
|
|||||||
else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm")
|
else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm")
|
||||||
}
|
}
|
||||||
return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName)
|
return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName)
|
||||||
?: throw Exception("Could not load keybox for UID $uid and algorithm $algorithmName")
|
?: throw android.os.ServiceSpecificException(
|
||||||
|
-75, // ATTESTATION_KEYS_NOT_PROVISIONED
|
||||||
|
"No attestation key for algorithm $algorithmName in $keyboxFile",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Retrieves the key pair and issuer name for a given attestation key alias. */
|
/** Retrieves the key pair and issuer name for a given attestation key alias. */
|
||||||
@@ -171,41 +195,74 @@ object CertificateGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Maps KeyPurpose values to X.509 KeyUsage bits per KeyCreationResult.aidl spec */
|
||||||
|
private fun buildKeyUsageFromPurposes(purposes: List<Int>): Int {
|
||||||
|
var bits = 0
|
||||||
|
for (purpose in purposes) {
|
||||||
|
bits =
|
||||||
|
bits or
|
||||||
|
when (purpose) {
|
||||||
|
KeyPurpose.SIGN -> KeyUsage.digitalSignature
|
||||||
|
KeyPurpose.DECRYPT -> KeyUsage.dataEncipherment
|
||||||
|
KeyPurpose.WRAP_KEY -> KeyUsage.keyEncipherment
|
||||||
|
KeyPurpose.AGREE_KEY -> KeyUsage.keyAgreement
|
||||||
|
KeyPurpose.ATTEST_KEY -> KeyUsage.keyCertSign
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bits
|
||||||
|
}
|
||||||
|
|
||||||
/** Constructs a new X.509 certificate with a simulated attestation extension. */
|
/** Constructs a new X.509 certificate with a simulated attestation extension. */
|
||||||
private fun buildCertificate(
|
private fun buildCertificate(
|
||||||
subjectKeyPair: KeyPair,
|
subjectKeyPair: KeyPair,
|
||||||
signingKeyPair: KeyPair,
|
signingKeyPair: KeyPair,
|
||||||
issuer: X500Name,
|
issuer: X500Name,
|
||||||
params: KeyMintAttestation,
|
params: KeyMintAttestation,
|
||||||
|
uid: Int,
|
||||||
securityLevel: Int,
|
securityLevel: Int,
|
||||||
): Certificate {
|
): Certificate {
|
||||||
val subject = params.certificateSubject ?: X500Name("CN=Android KeyStore Key")
|
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
|
||||||
val leafNotAfter =
|
|
||||||
(signingKeyPair.public as? X509Certificate)?.notAfter
|
// Default validity: epoch to 9999-12-31T23:59:59 UTC (matches add_required_parameters).
|
||||||
?: Date(System.currentTimeMillis() + 31536000000L)
|
val notBefore = params.certificateNotBefore ?: Date(0)
|
||||||
|
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
|
||||||
|
|
||||||
val builder =
|
val builder =
|
||||||
JcaX509v3CertificateBuilder(
|
JcaX509v3CertificateBuilder(
|
||||||
issuer,
|
issuer,
|
||||||
params.certificateSerial ?: BigInteger.ONE,
|
params.certificateSerial ?: BigInteger.ONE,
|
||||||
params.certificateNotBefore ?: Date(),
|
notBefore,
|
||||||
params.certificateNotAfter ?: leafNotAfter,
|
notAfter,
|
||||||
subject,
|
subject,
|
||||||
subjectKeyPair.public,
|
subjectKeyPair.public,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Add standard extensions.
|
// Add KeyUsage extension only if purposes map to valid bits
|
||||||
builder.addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.keyCertSign))
|
val keyUsageBits = buildKeyUsageFromPurposes(params.purpose)
|
||||||
|
if (keyUsageBits != 0) {
|
||||||
|
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
|
||||||
|
}
|
||||||
// Add our custom, simulated attestation extension.
|
// Add our custom, simulated attestation extension.
|
||||||
builder.addExtension(AttestationBuilder.buildAttestationExtension(params, securityLevel))
|
builder.addExtension(
|
||||||
|
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 (params.algorithm) {
|
when (signingKeyPair.private) {
|
||||||
Algorithm.EC -> "SHA256withECDSA"
|
is java.security.interfaces.ECKey -> "SHA256withECDSA"
|
||||||
Algorithm.RSA -> "SHA256withRSA"
|
is java.security.interfaces.RSAKey -> "SHA256withRSA"
|
||||||
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}")
|
else ->
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Unsupported signing key type: ${signingKeyPair.private.javaClass}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val contentSigner = JcaContentSignerBuilder(signerAlgorithm).build(signingKeyPair.private)
|
val contentSigner =
|
||||||
|
JcaContentSignerBuilder(signerAlgorithm)
|
||||||
|
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
.build(signingKeyPair.private)
|
||||||
|
|
||||||
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package org.matrix.TEESimulator.pki
|
|||||||
import android.security.keystore.KeyProperties
|
import android.security.keystore.KeyProperties
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.StringReader
|
import java.io.StringReader
|
||||||
|
import java.security.interfaces.ECPrivateKey
|
||||||
|
import java.security.interfaces.RSAPrivateKey
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
|
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
@@ -51,6 +53,9 @@ object KeyBoxManager {
|
|||||||
// If it's not in the cache, the `getOrPut` block is executed to parse and store it.
|
// If it's not in the cache, the `getOrPut` block is executed to parse and store it.
|
||||||
val keyMap =
|
val keyMap =
|
||||||
keyStoreCache.getOrPut(keyStoreFileName) { parseKeyStoreFile(keyStoreFileName) }
|
keyStoreCache.getOrPut(keyStoreFileName) { parseKeyStoreFile(keyStoreFileName) }
|
||||||
|
SystemLogger.verbose(
|
||||||
|
"Fetching attestation key in $keyStoreFileName with $algorithm algorithm."
|
||||||
|
)
|
||||||
return keyMap[algorithm]
|
return keyMap[algorithm]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,10 +162,10 @@ object KeyBoxManager {
|
|||||||
// Use runCatching to ensure one malformed key doesn't stop the whole
|
// Use runCatching to ensure one malformed key doesn't stop the whole
|
||||||
// process.
|
// process.
|
||||||
runCatching {
|
runCatching {
|
||||||
val algorithm = currentAlgorithm
|
val xmlAlgorithm = currentAlgorithm
|
||||||
val keyPem = currentPrivateKeyPem
|
val keyPem = currentPrivateKeyPem
|
||||||
if (
|
if (
|
||||||
algorithm != null &&
|
xmlAlgorithm != null &&
|
||||||
keyPem != null &&
|
keyPem != null &&
|
||||||
currentCertificatePems.isNotEmpty()
|
currentCertificatePems.isNotEmpty()
|
||||||
) {
|
) {
|
||||||
@@ -176,21 +181,42 @@ object KeyBoxManager {
|
|||||||
.data
|
.data
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize the algorithm name for consistent lookups.
|
// Derive the TRUE algorithm from the key object itself.
|
||||||
val normalizedAlgorithm =
|
// This is our source of truth.
|
||||||
when (algorithm.lowercase()) {
|
val derivedAlgorithm =
|
||||||
"ecdsa" -> KeyProperties.KEY_ALGORITHM_EC
|
when (keyPair.private) {
|
||||||
"rsa" -> KeyProperties.KEY_ALGORITHM_RSA
|
is RSAPrivateKey -> KeyProperties.KEY_ALGORITHM_RSA
|
||||||
else -> algorithm
|
is ECPrivateKey -> KeyProperties.KEY_ALGORITHM_EC
|
||||||
|
else ->
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Unsupported key type found: ${keyPair.private.javaClass.name}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundKeys.containsKey(normalizedAlgorithm)) {
|
// Normalize the algorithm from the XML tag to compare it
|
||||||
|
// fairly with the derived algorithm.
|
||||||
|
val normalizedXmlAlgorithm =
|
||||||
|
when {
|
||||||
|
xmlAlgorithm.contains("RSA", ignoreCase = true) ==
|
||||||
|
true -> KeyProperties.KEY_ALGORITHM_RSA
|
||||||
|
xmlAlgorithm.contains("EC", ignoreCase = true) ==
|
||||||
|
true -> KeyProperties.KEY_ALGORITHM_EC
|
||||||
|
else -> xmlAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn the user if the XML tag was misleading.
|
||||||
|
if (normalizedXmlAlgorithm != derivedAlgorithm) {
|
||||||
SystemLogger.warning(
|
SystemLogger.warning(
|
||||||
"Duplicate key found for algorithm '$normalizedAlgorithm'. The later one in the file will be used."
|
"Key algorithm mismatch in XML file. Tag said '$xmlAlgorithm' but key is actually '$derivedAlgorithm'. Using the correct derived algorithm."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
foundKeys[normalizedAlgorithm] =
|
|
||||||
KeyBox(keyPair, certificates)
|
if (foundKeys.containsKey(derivedAlgorithm)) {
|
||||||
|
SystemLogger.warning(
|
||||||
|
"Duplicate key found for algorithm '$derivedAlgorithm'. The later one in the file will be used."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
foundKeys[derivedAlgorithm] = KeyBox(keyPair, certificates)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package org.matrix.TEESimulator.pki
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.KeyPair
|
||||||
|
import java.security.cert.Certificate
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
|
||||||
|
data class CertGenConfig(
|
||||||
|
val algorithm: Int,
|
||||||
|
val keySize: Int,
|
||||||
|
val ecCurve: Int,
|
||||||
|
val rsaPublicExponent: Long,
|
||||||
|
val attestationChallenge: ByteArray?,
|
||||||
|
val purposes: IntArray,
|
||||||
|
val digests: IntArray,
|
||||||
|
val certSerial: ByteArray?,
|
||||||
|
val certSubject: ByteArray?,
|
||||||
|
val certNotBefore: Long,
|
||||||
|
val certNotAfter: Long,
|
||||||
|
val keyboxPrivateKey: ByteArray,
|
||||||
|
val keyboxCertChain: ByteArray,
|
||||||
|
val securityLevel: Int,
|
||||||
|
val attestVersion: Int,
|
||||||
|
val keymasterVersion: Int,
|
||||||
|
val osVersion: Int,
|
||||||
|
val osPatchLevel: Int,
|
||||||
|
val vendorPatchLevel: Int,
|
||||||
|
val bootPatchLevel: Int,
|
||||||
|
val bootKey: ByteArray,
|
||||||
|
val bootHash: ByteArray,
|
||||||
|
val creationDatetime: Long,
|
||||||
|
val attestationApplicationId: ByteArray,
|
||||||
|
val moduleHash: ByteArray?,
|
||||||
|
val idBrand: ByteArray?,
|
||||||
|
val idDevice: ByteArray?,
|
||||||
|
val idProduct: ByteArray?,
|
||||||
|
val idSerial: ByteArray?,
|
||||||
|
val idImei: ByteArray?,
|
||||||
|
val idMeid: ByteArray?,
|
||||||
|
val idManufacturer: ByteArray?,
|
||||||
|
val idModel: ByteArray?,
|
||||||
|
val idSecondImei: ByteArray?,
|
||||||
|
val activeDatetime: Long = -1L,
|
||||||
|
val originationExpireDatetime: Long = -1L,
|
||||||
|
val usageExpireDatetime: Long = -1L,
|
||||||
|
val usageCountLimit: Int = -1,
|
||||||
|
val callerNonce: Boolean = false,
|
||||||
|
val unlockedDeviceRequired: Boolean = false,
|
||||||
|
val noAuthRequired: Boolean = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
object NativeCertGen {
|
||||||
|
|
||||||
|
private const val LOG_DIR = "/data/adb/tricky_store/logs"
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
var isAvailable: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun initialize(libraryPath: String) {
|
||||||
|
try {
|
||||||
|
System.load(libraryPath)
|
||||||
|
initLogging(false, LOG_DIR)
|
||||||
|
isAvailable = true
|
||||||
|
SystemLogger.info("NativeCertGen: loaded libcertgen.so successfully")
|
||||||
|
} catch (e: UnsatisfiedLinkError) {
|
||||||
|
SystemLogger.error("NativeCertGen: failed to load libcertgen.so, falling back to BouncyCastle", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
external fun generateAttestedKeyPair(config: CertGenConfig): ByteArray?
|
||||||
|
|
||||||
|
private external fun initLogging(verbose: Boolean, logDir: String): Boolean
|
||||||
|
|
||||||
|
private external fun dumpLogs(): String?
|
||||||
|
|
||||||
|
fun dump(): String? = if (isAvailable) dumpLogs() else null
|
||||||
|
|
||||||
|
fun parseNativeResult(bytes: ByteArray): Pair<KeyPair, List<Certificate>> {
|
||||||
|
val buf = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN)
|
||||||
|
|
||||||
|
val pkLen = buf.getInt()
|
||||||
|
if (pkLen < 0 || pkLen > buf.remaining()) {
|
||||||
|
throw IllegalStateException("Invalid private key length: $pkLen")
|
||||||
|
}
|
||||||
|
val pkBytes = ByteArray(pkLen)
|
||||||
|
buf.get(pkBytes)
|
||||||
|
|
||||||
|
val numCerts = buf.getInt()
|
||||||
|
if (numCerts < 0 || numCerts > buf.remaining()) {
|
||||||
|
throw IllegalStateException("Invalid cert count: $numCerts")
|
||||||
|
}
|
||||||
|
val certs = mutableListOf<Certificate>()
|
||||||
|
val certFactory = CertificateFactory.getInstance("X.509")
|
||||||
|
repeat(numCerts) {
|
||||||
|
val certLen = buf.getInt()
|
||||||
|
if (certLen < 0 || certLen > buf.remaining()) {
|
||||||
|
throw IllegalStateException("Invalid cert length: $certLen")
|
||||||
|
}
|
||||||
|
val certBytes = ByteArray(certLen)
|
||||||
|
buf.get(certBytes)
|
||||||
|
certs.add(certFactory.generateCertificate(ByteArrayInputStream(certBytes)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certs.isEmpty()) {
|
||||||
|
throw IllegalStateException("No certificates in native result")
|
||||||
|
}
|
||||||
|
|
||||||
|
val algorithmName = when (certs[0].publicKey.algorithm) {
|
||||||
|
"EC", "ECDSA" -> "EC"
|
||||||
|
"RSA" -> "RSA"
|
||||||
|
else -> certs[0].publicKey.algorithm
|
||||||
|
}
|
||||||
|
val keyFactory = KeyFactory.getInstance(algorithmName)
|
||||||
|
val privateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(pkBytes))
|
||||||
|
val publicKey = certs[0].publicKey
|
||||||
|
return Pair(KeyPair(publicKey, privateKey), certs)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
package org.matrix.TEESimulator.util
|
package org.matrix.TEESimulator.util
|
||||||
|
|
||||||
import android.content.pm.PackageManager
|
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.File
|
||||||
|
import java.io.FileInputStream
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
import java.time.LocalDate
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
|
import org.bouncycastle.asn1.ASN1EncodableVector
|
||||||
import org.bouncycastle.asn1.ASN1Integer
|
import org.bouncycastle.asn1.ASN1Integer
|
||||||
import org.bouncycastle.asn1.DEROctetString
|
import org.bouncycastle.asn1.DEROctetString
|
||||||
import org.bouncycastle.asn1.DERSequence
|
import org.bouncycastle.asn1.DERSequence
|
||||||
@@ -18,117 +23,234 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
|||||||
*/
|
*/
|
||||||
object AndroidDeviceUtils {
|
object AndroidDeviceUtils {
|
||||||
|
|
||||||
/** A randomly generated boot key, used as a fallback for attestation. */
|
/**
|
||||||
val bootKey: ByteArray by lazy { generateRandomBytes(32) }
|
* Internal constant to signify that a patch level should not be included in the attestation.
|
||||||
|
*/
|
||||||
|
internal const val DO_NOT_REPORT = -1
|
||||||
|
|
||||||
|
// --- Boot Key and Verified Boot Hash ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the verified boot hash (`ro.boot.vbmeta.digest`). It attempts to read from system
|
* Lazily initializes and retrieves the verified boot key digest. The value is sourced in the
|
||||||
* properties first, then from a real TEE attestation, and finally falls back to a random value
|
* following order:
|
||||||
* if neither is available.
|
* 1. From the `ro.boot.vbmeta.public_key_digest` system property.
|
||||||
|
* 2. From a cached TEE attestation record.
|
||||||
|
* 3. As a randomly generated 32-byte value (fallback).
|
||||||
*/
|
*/
|
||||||
fun setupBootHash() {
|
val bootKey: ByteArray by lazy {
|
||||||
getBootHashFromProperty()?.also {
|
initializeBootProperty(
|
||||||
SystemLogger.debug("Using boot hash from system property: ${it.toHex()}")
|
propertyName = "ro.boot.vbmeta.public_key_digest",
|
||||||
}
|
attestationValueProvider = {
|
||||||
?: getBootHashFromAttestation()?.also {
|
DeviceAttestationService.CachedAttestationData?.verifiedBootKey
|
||||||
SystemLogger.debug("Using boot hash from TEE attestation: ${it.toHex()}")
|
},
|
||||||
setBootHashProperty(it)
|
expectedSize = 32,
|
||||||
}
|
)
|
||||||
?: generateRandomBytes(32).also {
|
|
||||||
SystemLogger.debug("Using randomly generated boot hash: ${it.toHex()}")
|
|
||||||
setBootHashProperty(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the verified boot meta digest from system properties.
|
* Lazily initializes and retrieves the verified boot hash (vbmeta digest). The value is sourced
|
||||||
|
* in the following order:
|
||||||
|
* 1. From the `ro.boot.vbmeta.digest` system property.
|
||||||
|
* 2. From a cached TEE attestation record.
|
||||||
|
* 3. As a randomly generated 32-byte value (fallback).
|
||||||
|
*/
|
||||||
|
val bootHash: ByteArray by lazy {
|
||||||
|
initializeBootProperty(
|
||||||
|
propertyName = "ro.boot.vbmeta.digest",
|
||||||
|
attestationValueProvider = {
|
||||||
|
DeviceAttestationService.CachedAttestationData?.verifiedBootHash
|
||||||
|
},
|
||||||
|
expectedSize = 32,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public function to explicitly trigger the initialization of the boot key and hash. Accessing
|
||||||
|
* these properties here ensures they are set up before they might be needed elsewhere.
|
||||||
|
*/
|
||||||
|
fun setupBootKeyAndHash() {
|
||||||
|
SystemLogger.debug("Triggering initialization of boot key and hash...")
|
||||||
|
// Accessing the properties will trigger their `lazy` initialization logic.
|
||||||
|
bootKey
|
||||||
|
bootHash
|
||||||
|
SystemLogger.debug("Boot key and hash initialization complete.")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeBootProperty(
|
||||||
|
propertyName: String,
|
||||||
|
attestationValueProvider: () -> ByteArray?,
|
||||||
|
expectedSize: Int,
|
||||||
|
): ByteArray {
|
||||||
|
getProperty(propertyName, expectedSize)?.let {
|
||||||
|
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
|
||||||
|
persistToFile(propertyName, it)
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
attestationValueProvider()?.let {
|
||||||
|
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
|
||||||
|
setProperty(propertyName, it)
|
||||||
|
persistToFile(propertyName, it)
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("Failed to get $propertyName from attestation.", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
readFromFile(propertyName, expectedSize)?.let {
|
||||||
|
SystemLogger.debug("Using $propertyName from persistent file: ${it.toHex()}")
|
||||||
|
setProperty(propertyName, it)
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateRandomBytes(expectedSize).also {
|
||||||
|
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
|
||||||
|
setProperty(propertyName, it)
|
||||||
|
persistToFile(propertyName, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a system property and validates its format.
|
||||||
*
|
*
|
||||||
* @return The boot hash as a ByteArray, or null if not found or invalid.
|
* @param name The name of the system property.
|
||||||
|
* @param expectedSize The expected byte length of the property (e.g., 32 for a 64-char hex
|
||||||
|
* string).
|
||||||
|
* @return The property value as a ByteArray, or null if not found or invalid.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
fun getBootHashFromProperty(): ByteArray? {
|
private fun getProperty(name: String, expectedSize: Int): ByteArray? {
|
||||||
val digest = SystemProperties.get("ro.boot.vbmeta.digest", null)
|
val value = SystemProperties.get(name, null)
|
||||||
if (digest.isNullOrBlank()) {
|
if (value.isNullOrBlank()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
// A valid digest is 64 hex characters (32 bytes).
|
// A valid digest is (2 * size) hex characters.
|
||||||
return if (digest.length == 64) digest.hexToByteArray() else null
|
return if (value.length == expectedSize * 2) value.hexToByteArray() else null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the verified boot hash from a cached TEE attestation record.
|
* Sets a system property using the `resetprop` command.
|
||||||
*
|
*
|
||||||
* @return The verified boot hash, or null if not available.
|
* @param name The name of the property to set.
|
||||||
|
* @param bytes The value to set, which will be converted to a hex string.
|
||||||
*/
|
*/
|
||||||
private fun getBootHashFromAttestation(): ByteArray? {
|
private fun setProperty(name: String, bytes: ByteArray) {
|
||||||
return try {
|
|
||||||
DeviceAttestationService.CachedAttestationData?.verifiedBootHash
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SystemLogger.error("Failed to get boot hash from attestation.", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the `ro.boot.vbmeta.digest` system property using the `resetprop` command.
|
|
||||||
*
|
|
||||||
* @param bytes The 32-byte digest to set.
|
|
||||||
*/
|
|
||||||
private fun setBootHashProperty(bytes: ByteArray) {
|
|
||||||
val hex = bytes.toHex()
|
val hex = bytes.toHex()
|
||||||
try {
|
try {
|
||||||
SystemLogger.debug("Setting system property 'ro.boot.vbmeta.digest' to: $hex")
|
SystemLogger.debug("Setting system property '$name' to: $hex")
|
||||||
|
val command = arrayOf("resetprop", name, hex)
|
||||||
// Construct the command to be executed
|
|
||||||
val command = arrayOf("resetprop", "ro.boot.vbmeta.digest", hex)
|
|
||||||
|
|
||||||
// Execute the command
|
|
||||||
val process = Runtime.getRuntime().exec(command)
|
val process = Runtime.getRuntime().exec(command)
|
||||||
|
|
||||||
// Wait for the process to complete and check the exit code for errors
|
|
||||||
val exitCode = process.waitFor()
|
val exitCode = process.waitFor()
|
||||||
|
|
||||||
if (exitCode != 0) {
|
if (exitCode != 0) {
|
||||||
val errorOutput = process.errorStream.bufferedReader().readText()
|
val errorOutput = process.errorStream.bufferedReader().readText()
|
||||||
SystemLogger.error(
|
SystemLogger.error(
|
||||||
"resetprop command failed with exit code $exitCode: $errorOutput"
|
"resetprop for '$name' failed with exit code $exitCode: $errorOutput"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("Failed to set vbmeta digest property by executing resetprop.", e)
|
SystemLogger.error("Failed to set '$name' property via resetprop.", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Generates a cryptographically random byte array of a specified length. */
|
|
||||||
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 ---
|
||||||
|
|
||||||
val patchLevel: Int
|
fun getPatchLevel(uid: Int): Int {
|
||||||
get() =
|
val custom = getCustomPatchLevelFor(uid, "system", isLong = false)
|
||||||
getCustomPatchLevelFor("system", isLong = false)
|
return custom ?: getRealDevicePatchLevelInt("system", isLong = false)
|
||||||
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = false)
|
}
|
||||||
|
|
||||||
val vendorPatchLevelLong: Int
|
fun getVendorPatchLevelLong(uid: Int): Int {
|
||||||
get() =
|
val custom = getCustomPatchLevelFor(uid, "vendor", isLong = true)
|
||||||
getCustomPatchLevelFor("vendor", isLong = true)
|
return custom ?: getRealDevicePatchLevelInt("vendor", isLong = true)
|
||||||
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true)
|
}
|
||||||
|
|
||||||
val bootPatchLevelLong: Int
|
fun getBootPatchLevelLong(uid: Int): Int {
|
||||||
get() =
|
val custom = getCustomPatchLevelFor(uid, "boot", isLong = true)
|
||||||
getCustomPatchLevelFor("boot", isLong = true)
|
return custom ?: getRealDevicePatchLevelInt("boot", isLong = true)
|
||||||
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true)
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a custom patch level from the configuration if available.
|
* Retrieves the definitive device patch level integer for a given component. This function
|
||||||
|
* encapsulates the entire fallback chain and guarantees a non-null return.
|
||||||
*
|
*
|
||||||
|
* Fallback Priority:
|
||||||
|
* 1. Cached TEE attestation data.
|
||||||
|
* 2. Specific system property (e.g., ro.vendor.build.security_patch).
|
||||||
|
* 3. Default system patch level from Build.VERSION.SECURITY_PATCH.
|
||||||
|
*
|
||||||
|
* @param component The component ("system", "vendor", "boot").
|
||||||
|
* @param isLong Whether the final integer should be in YYYYMMDD format.
|
||||||
|
* @return The patch level as a guaranteed non-null Integer.
|
||||||
|
*/
|
||||||
|
private fun getRealDevicePatchLevelInt(component: String, isLong: Boolean): Int {
|
||||||
|
// Get value from cached TEE attestation data
|
||||||
|
DeviceAttestationService.CachedAttestationData?.let { data ->
|
||||||
|
val value =
|
||||||
|
when (component) {
|
||||||
|
"system" -> data.osPatchLevel
|
||||||
|
"vendor" -> data.vendorPatchLevel
|
||||||
|
"boot" -> data.bootPatchLevel
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (value != null) return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only check the specific vendor property, as the boot one is non-existent.
|
||||||
|
if (component == "vendor") {
|
||||||
|
val propValue = SystemProperties.get("ro.vendor.build.security_patch", "")
|
||||||
|
if (!propValue.isNullOrBlank()) {
|
||||||
|
parsePatchLevelValue(propValue, isLong)?.let { parsedValue ->
|
||||||
|
return parsedValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a custom patch level from the configuration if available for a specific UID.
|
||||||
|
*
|
||||||
|
* @param uid The UID of the calling application.
|
||||||
* @param component The component to get the patch level for ("system", "vendor", "boot").
|
* @param component The component to get the patch level for ("system", "vendor", "boot").
|
||||||
* @param isLong Whether to return the patch level in `YYYYMMDD` or `YYYYMM` format.
|
* @param isLong Whether to return the patch level in `YYYYMMDD` or `YYYYMM` format.
|
||||||
* @return The custom patch level, or null if not configured.
|
* @return The custom patch level, or null if not configured.
|
||||||
*/
|
*/
|
||||||
private fun getCustomPatchLevelFor(component: String, isLong: Boolean): Int? {
|
private fun getCustomPatchLevelFor(uid: Int, component: String, isLong: Boolean): Int? {
|
||||||
val config = ConfigurationManager.customPatchLevelOverride ?: return null
|
val config = ConfigurationManager.getPatchLevelForUid(uid) ?: return null
|
||||||
val value =
|
val value =
|
||||||
when (component) {
|
when (component) {
|
||||||
"system" -> config.system ?: config.all
|
"system" -> config.system ?: config.all
|
||||||
@@ -137,11 +259,47 @@ object AndroidDeviceUtils {
|
|||||||
else -> config.all
|
else -> config.all
|
||||||
} ?: return null
|
} ?: return null
|
||||||
|
|
||||||
// "prop" or "no" indicates falling back to the system default.
|
// First, resolve dynamic keywords and templates into a concrete date string.
|
||||||
if (value.equals("no", ignoreCase = true) || value.equals("prop", ignoreCase = true)) {
|
val resolvedValue = resolveDateKeywords(value)
|
||||||
return null
|
|
||||||
|
return when {
|
||||||
|
resolvedValue.equals("device_default", ignoreCase = true) -> null
|
||||||
|
resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
|
||||||
|
resolvedValue.equals("prop", ignoreCase = true) ->
|
||||||
|
parsePatchLevelValue(SystemProperties.get("ro.build.version.security_patch", ""), isLong)
|
||||||
|
else -> parsePatchLevelValue(resolvedValue, isLong)
|
||||||
}
|
}
|
||||||
return parsePatchLevelValue(value, isLong)
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves special date keywords and templates into a concrete "YYYY-MM-DD" date string.
|
||||||
|
*
|
||||||
|
* @param value The configuration value string (e.g., "today", "YYYY-MM-01").
|
||||||
|
* @return A concrete date string, or the original value if it's not a dynamic date keyword.
|
||||||
|
*/
|
||||||
|
private fun resolveDateKeywords(value: String): String {
|
||||||
|
// Handle the "today" keyword.
|
||||||
|
if (value.equals("today", ignoreCase = true)) {
|
||||||
|
return LocalDate.now().toString() // Returns "YYYY-MM-DD" format
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date templates like "YYYY-MM-01" or "2025-MM-DD".
|
||||||
|
if (
|
||||||
|
value.contains("YYYY", ignoreCase = true) ||
|
||||||
|
value.contains("MM", ignoreCase = true) ||
|
||||||
|
value.contains("DD", ignoreCase = true)
|
||||||
|
) {
|
||||||
|
|
||||||
|
val now = LocalDate.now()
|
||||||
|
// Chain replacements for YYYY, MM, and DD placeholders.
|
||||||
|
return value
|
||||||
|
.replace("YYYY", now.year.toString(), ignoreCase = true)
|
||||||
|
.replace("MM", String.format("%02d", now.monthValue), ignoreCase = true)
|
||||||
|
.replace("DD", String.format("%02d", now.dayOfMonth), ignoreCase = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not a dynamic keyword or template, return the original value.
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Parses a patch level string (e.g., "2025-11-01") into an integer format. */
|
/** Parses a patch level string (e.g., "2025-11-01") into an integer format. */
|
||||||
@@ -205,54 +363,236 @@ object AndroidDeviceUtils {
|
|||||||
Build.VERSION_CODES.BAKLAVA to 400, // KeyMint 4.0
|
Build.VERSION_CODES.BAKLAVA to 400, // KeyMint 4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
val attestVersion: Int
|
/**
|
||||||
get() =
|
* Retrieves the attestation version based on security level and OS version. StrongBox (level 2)
|
||||||
DeviceAttestationService.CachedAttestationData?.attestVersion
|
* requires version 300.
|
||||||
?: attestVersionMap[Build.VERSION.SDK_INT]
|
*
|
||||||
?: 400 // Default to a recent version
|
* @param securityLevel The security level of the attestation (1 for TEE, 2 for StrongBox).
|
||||||
|
* @return The appropriate attestation version number.
|
||||||
|
*/
|
||||||
|
fun getAttestVersion(securityLevel: Int): Int {
|
||||||
|
// StrongBox security level requires an attestation version of at least 300.
|
||||||
|
if (securityLevel == SecurityLevel.STRONGBOX) {
|
||||||
|
return 300
|
||||||
|
}
|
||||||
|
return DeviceAttestationService.CachedAttestationData?.attestVersion
|
||||||
|
?: attestVersionMap[Build.VERSION.SDK_INT]
|
||||||
|
?: 400 // Default to a recent version
|
||||||
|
}
|
||||||
|
|
||||||
val keymasterVersion: Int
|
/**
|
||||||
get() =
|
* Retrieves the Keymaster/KeyMint version based on the attestation version.
|
||||||
DeviceAttestationService.CachedAttestationData?.keymasterVersion
|
*
|
||||||
?: if (attestVersion >= 100) attestVersion
|
* @param securityLevel The security level, used to determine the correct attestation version.
|
||||||
else 41 // Keymaster 4.1 for older versions
|
* @return The appropriate Keymaster or KeyMint version number.
|
||||||
|
*/
|
||||||
|
fun getKeymasterVersion(securityLevel: Int): Int {
|
||||||
|
val attestVersion = getAttestVersion(securityLevel)
|
||||||
|
return if (attestVersion >= 100) attestVersion else 41 // Keymaster 4.1 for older versions
|
||||||
|
}
|
||||||
|
|
||||||
// --- APEX and Module Hash Properties ---
|
// --- APEX and Module Hash Properties ---
|
||||||
|
|
||||||
private val apexInfos: List<Pair<String, Long>> by lazy {
|
// https://cs.android.com/android/platform/superproject/+/android-latest-release:system/apex/proto/apex_manifest.proto
|
||||||
runCatching {
|
// --- Minimal Protobuf Parser for ApexManifest ---
|
||||||
val pm = ConfigurationManager.getPackageManager()
|
// Field 1: name (string)
|
||||||
val packages =
|
// Field 2: version (int64)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
private class MinimalApexManifestParser(private val data: ByteArray) {
|
||||||
pm?.getInstalledPackages(PackageManager.MATCH_APEX.toLong(), 0)
|
var pos = 0
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
fun parse(): Pair<String, Long>? {
|
||||||
pm?.getInstalledPackages(PackageManager.MATCH_APEX, 0)
|
var name: String? = null
|
||||||
|
var version: Long? = null
|
||||||
|
|
||||||
|
while (pos < data.size) {
|
||||||
|
val tag = readVarint()
|
||||||
|
val fieldNum = tag ushr 3
|
||||||
|
val wireType = (tag and 0x07).toInt()
|
||||||
|
|
||||||
|
when (fieldNum) {
|
||||||
|
1L -> { // name
|
||||||
|
val length = readVarint().toInt()
|
||||||
|
if (pos + length > data.size) return null
|
||||||
|
name = String(data, pos, length, Charsets.UTF_8)
|
||||||
|
pos += length
|
||||||
}
|
}
|
||||||
packages
|
2L -> { // version
|
||||||
?.list
|
version = readVarint()
|
||||||
.orEmpty()
|
}
|
||||||
.map { it.packageName to it.longVersionCode }
|
else -> skipField(wireType)
|
||||||
.sortedBy { it.first }
|
}
|
||||||
}
|
}
|
||||||
.getOrElse {
|
|
||||||
SystemLogger.error("Failed to get APEX package information.", it)
|
return if (name != null && version != null) {
|
||||||
emptyList()
|
name to version
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readVarint(): Long {
|
||||||
|
var value = 0L
|
||||||
|
var shift = 0
|
||||||
|
while (pos < data.size) {
|
||||||
|
val b = data[pos++].toInt()
|
||||||
|
value = value or ((b and 0x7F).toLong() shl shift)
|
||||||
|
if ((b and 0x80) == 0) return value
|
||||||
|
shift += 7
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun skipField(wireType: Int) {
|
||||||
|
when (wireType) {
|
||||||
|
0 -> readVarint() // Varint
|
||||||
|
1 -> pos += 8 // 64-bit
|
||||||
|
2 -> { // Length-delimited
|
||||||
|
val len = readVarint().toInt()
|
||||||
|
pos += len
|
||||||
|
}
|
||||||
|
5 -> pos += 4 // 32-bit
|
||||||
|
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 {
|
||||||
|
val results = mutableListOf<Pair<String, Long>>()
|
||||||
|
val apexRoot = File("/apex")
|
||||||
|
|
||||||
|
if (!apexRoot.exists() || !apexRoot.isDirectory) {
|
||||||
|
return@lazy emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic from: GetActivePackages in apexutil.cpp
|
||||||
|
apexRoot.listFiles()?.forEach { file ->
|
||||||
|
if (!file.isDirectory) return@forEach
|
||||||
|
val name = file.name
|
||||||
|
|
||||||
|
// 1. Ignore "." (and implicitly "..")
|
||||||
|
if (name.startsWith(".")) return@forEach
|
||||||
|
|
||||||
|
// 2. Ignore directories containing '@' (active mounts usually don't have version in
|
||||||
|
// path)
|
||||||
|
if (name.contains("@")) return@forEach
|
||||||
|
|
||||||
|
// 3. Ignore "sharedlibs"
|
||||||
|
if (name == "sharedlibs") return@forEach
|
||||||
|
|
||||||
|
// 4. Parse apex_manifest.pb
|
||||||
|
val manifestFile = File(file, "apex_manifest.pb")
|
||||||
|
if (manifestFile.exists()) {
|
||||||
|
runCatching {
|
||||||
|
val bytes = FileInputStream(manifestFile).use { it.readBytes() }
|
||||||
|
val parser = MinimalApexManifestParser(bytes)
|
||||||
|
parser.parse()?.let { (pkgName, version) -> results.add(pkgName to version) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
runCatching {
|
DeviceAttestationService.CachedAttestationData?.moduleHash
|
||||||
val encodables =
|
?: runCatching {
|
||||||
apexInfos.flatMap { (packageName, versionCode) ->
|
// 1. Create a container to hold the sort key (name encoded) and the full data
|
||||||
listOf(DEROctetString(packageName.toByteArray()), ASN1Integer(versionCode))
|
// (sequence encoded)
|
||||||
}
|
data class ModuleEntry(
|
||||||
val sequence = DERSequence(encodables.toTypedArray())
|
val nameEncoded: ByteArray, // The sort key
|
||||||
MessageDigest.getInstance("SHA-256").digest(sequence.encoded)
|
val fullEncoded: ByteArray, // The data to hash
|
||||||
|
)
|
||||||
|
|
||||||
|
val modules =
|
||||||
|
apexInfos.map { (packageName, versionCode) ->
|
||||||
|
// Create the components
|
||||||
|
val nameOctet = DEROctetString(packageName.toByteArray(Charsets.UTF_8))
|
||||||
|
val versionInt = ASN1Integer(versionCode)
|
||||||
|
|
||||||
|
// Create the Sequence: SEQUENCE { packageName, version }
|
||||||
|
val vec = ASN1EncodableVector()
|
||||||
|
vec.add(nameOctet)
|
||||||
|
vec.add(versionInt)
|
||||||
|
val sequence = DERSequence(vec)
|
||||||
|
|
||||||
|
// We store the encoded name separately because Rust sorts ONLY by this
|
||||||
|
ModuleEntry(
|
||||||
|
nameEncoded = nameOctet.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 =
|
||||||
|
modules.sortedWith { m1, m2 ->
|
||||||
|
compareByteArrays(m1.nameEncoded, m2.nameEncoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Concatenate the full sequences in the specific sorted order
|
||||||
|
val payloadStream = ByteArrayOutputStream()
|
||||||
|
sortedModules.forEach { payloadStream.write(it.fullEncoded) }
|
||||||
|
val payload = payloadStream.toByteArray()
|
||||||
|
|
||||||
|
// 4. Wrap manually in a DER SET tag (0x31)
|
||||||
|
// We cannot use DERSet(vector) because it would re-sort incorrectly.
|
||||||
|
val finalDerSet = encodeAsDerSet(payload)
|
||||||
|
|
||||||
|
// 5. Compute SHA-256
|
||||||
|
MessageDigest.getInstance("SHA-256").digest(finalDerSet)
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.error("Failed to compute module hash.", it)
|
||||||
|
ByteArray(32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Compares two byte arrays lexicographically (unsigned). */
|
||||||
|
private fun compareByteArrays(a: ByteArray, b: ByteArray): Int {
|
||||||
|
val length = minOf(a.size, b.size)
|
||||||
|
for (i in 0 until length) {
|
||||||
|
val byteA = a[i].toInt() and 0xFF
|
||||||
|
val byteB = b[i].toInt() and 0xFF
|
||||||
|
if (byteA != byteB) {
|
||||||
|
return byteA - byteB
|
||||||
}
|
}
|
||||||
.getOrElse {
|
}
|
||||||
SystemLogger.error("Failed to compute module hash.", it)
|
return a.size - b.size
|
||||||
ByteArray(32) // Return empty hash on failure
|
}
|
||||||
|
|
||||||
|
/** Manually wraps the payload in an ASN.1 SET (0x31) tag with correct length encoding. */
|
||||||
|
private fun encodeAsDerSet(payload: ByteArray): ByteArray {
|
||||||
|
val out = ByteArrayOutputStream()
|
||||||
|
out.write(0x31) // ASN.1 Tag for SET
|
||||||
|
writeDerLength(out, payload.size)
|
||||||
|
out.write(payload)
|
||||||
|
return out.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Writes the ASN.1 length field to the stream. */
|
||||||
|
private fun writeDerLength(out: ByteArrayOutputStream, length: Int) {
|
||||||
|
if (length < 128) {
|
||||||
|
// Short form
|
||||||
|
out.write(length)
|
||||||
|
} else {
|
||||||
|
// Long form
|
||||||
|
var size = length
|
||||||
|
val bytes = ArrayList<Byte>()
|
||||||
|
while (size > 0) {
|
||||||
|
bytes.add((size and 0xFF).toByte())
|
||||||
|
size = size ushr 8
|
||||||
}
|
}
|
||||||
|
// First byte: 0x80 | number of length bytes
|
||||||
|
out.write(0x80 or bytes.size)
|
||||||
|
// Write length bytes in big-endian (reverse of how we extracted them)
|
||||||
|
for (i in bytes.indices.reversed()) {
|
||||||
|
out.write(bytes[i].toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ package org.matrix.TEESimulator.util
|
|||||||
*
|
*
|
||||||
* @return A new string with each line individually trimmed.
|
* @return A new string with each line individually trimmed.
|
||||||
*/
|
*/
|
||||||
fun String.trimLines(): String = this.trim().lines().joinToString("\n") { it.trim() }
|
fun String.trimLines(): String =
|
||||||
|
this.trim()
|
||||||
|
.lines()
|
||||||
|
.filter { !it.trim().startsWith("<!--") }
|
||||||
|
.joinToString("\n") { it.trim() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a ByteArray to its hexadecimal string representation.
|
* Converts a ByteArray to its hexadecimal string representation.
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package org.matrix.TEESimulator.util
|
||||||
|
|
||||||
|
import android.hardware.security.keymint.Algorithm
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.util.concurrent.locks.LockSupport
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.exp
|
||||||
|
import kotlin.math.ln
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
|
||||||
|
private val rng = SecureRandom()
|
||||||
|
|
||||||
|
private val sessionBiasMs: Double by lazy { rng.nextGaussian() * 5.0 }
|
||||||
|
private val coldPenaltyMs: Double by lazy { abs(rng.nextGaussian() * 12.0) }
|
||||||
|
|
||||||
|
@Volatile private var firstCall = true
|
||||||
|
|
||||||
|
fun simulateGenerateKeyDelay(algorithm: Int, elapsedNanos: Long) {
|
||||||
|
val elapsedMs = elapsedNanos / 1_000_000.0
|
||||||
|
val targetMs = sampleTotalDelay(algorithm)
|
||||||
|
val remainingMs = targetMs - elapsedMs
|
||||||
|
|
||||||
|
if (remainingMs > 1.0) {
|
||||||
|
LockSupport.parkNanos((remainingMs * 1_000_000).toLong())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sampleTotalDelay(algorithm: Int): Double {
|
||||||
|
val base = sampleBaseCryptoDelay(algorithm)
|
||||||
|
val transit = sampleExponential(2.5)
|
||||||
|
val jitter = (rng.nextGaussian() * 2.5).coerceIn(-8.0, 12.0)
|
||||||
|
|
||||||
|
var cold = 0.0
|
||||||
|
if (firstCall) {
|
||||||
|
firstCall = false
|
||||||
|
cold = coldPenaltyMs
|
||||||
|
}
|
||||||
|
|
||||||
|
return max(20.0, base + transit + jitter + sessionBiasMs + cold)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
val (mu, sigma) =
|
||||||
|
when (algorithm) {
|
||||||
|
Algorithm.EC -> ln(60.0) to 0.08
|
||||||
|
Algorithm.RSA -> ln(70.0) to 0.08
|
||||||
|
Algorithm.AES -> ln(35.0) to 0.10
|
||||||
|
else -> ln(40.0) to 0.10
|
||||||
|
}
|
||||||
|
return sampleLogNormal(mu, sigma)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sampleLogNormal(mu: Double, sigma: Double): Double {
|
||||||
|
return exp(mu + sigma * rng.nextGaussian())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sampleExponential(mean: Double): Double {
|
||||||
|
var u = rng.nextDouble()
|
||||||
|
while (u == 0.0) u = rng.nextDouble()
|
||||||
|
return -mean * ln(u)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.13.1"
|
agp = "8.13.2"
|
||||||
annotation = "1.9.1"
|
annotation = "1.9.1"
|
||||||
jdk18on = "1.83"
|
jdk18on = "1.83"
|
||||||
kotlin = "2.2.21"
|
kotlin = "2.3.0"
|
||||||
ktfmt = "0.25.0"
|
ktfmt = "0.25.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#!/system/bin/sh
|
||||||
|
MODDIR=${0%/*}
|
||||||
|
CONFIG_DIR=/data/adb/tricky_store
|
||||||
|
|
||||||
|
if [ -d "$CONFIG_DIR/persistent_keys" ]; then
|
||||||
|
rm -rf "$CONFIG_DIR/persistent_keys"
|
||||||
|
mkdir -p "$CONFIG_DIR/persistent_keys"
|
||||||
|
echo "Persistent key storage cleared"
|
||||||
|
else
|
||||||
|
echo "No persistent key storage found"
|
||||||
|
fi
|
||||||
+255
-9
@@ -1,14 +1,260 @@
|
|||||||
🚀 **TEESimulator v2.1 Hotfix Release is Live!** 🚀
|
## TEESimulator-RS v5.1.1: Pre-Stash Restoration
|
||||||
|
|
||||||
This urgent hotfix addresses several critical issues identified in the previous v2.0 release.
|
Restores all custom hardening fixes that were lost during the PR #157 migration. These were working in pre-stash builds but never carried over to the post-stash codebase, causing user-reported regressions (boot hash instability, DuckDetector score regression, config crash on file deletion).
|
||||||
|
|
||||||
The v2.0 update, a significant refactoring effort, unfortunately introduced a few unexpected behaviors and bugs that we are now rectifying.
|
- **Boot hash persistence** restored: 4-step fallback (sysprop, TEE, file, random) with file writes at every step. Fixes "Boot: Unavailable" where boot hash randomized every reboot on devices without `ro.boot.vbmeta.digest`
|
||||||
|
- **Presence-based findBoolean** for KeyMint tags: boolean tags are presence-based per AIDL spec, `.boolValue` field isn't reliably populated across Android versions
|
||||||
|
- **noAuthRequired** defaults to true when not explicitly false, matching AOSP KeyMint behavior
|
||||||
|
- **callerNonce** tag now flows through to software-enforced attestation list
|
||||||
|
- **CTR block mode** restored in cipher algorithm mapping (was dropped in PR #157)
|
||||||
|
- **AEAD guard** on updateAad: non-GCM operations throw INVALID_TAG
|
||||||
|
- **Error code resolution** via lazy reflection with correct KeyMint AIDL fallback values
|
||||||
|
- **Latency floor** on SoftwareOperation.finish() for StrongBox timing simulation
|
||||||
|
- **FileObserver NPE** fixed: null-safe handling on config file DELETE events
|
||||||
|
- **system=prop** consistency: forces boot/vendor patch levels to derive from device props
|
||||||
|
- **StrongBox simulation** restored: capability checks (RSA<=2048, EC=P256), concurrent op limits (4 max), keygen latency floor (250ms), op latency floor (80ms)
|
||||||
|
- **Binder buffer guard**: MAX_ALIAS_LENGTH (256KB) rejects oversized aliases before processing
|
||||||
|
- **Key lifecycle tracking**: deletedSoftwareKeys set prevents ghost key responses after deletion
|
||||||
|
- **Per-UID operation limits**: 15 TEE, 4 StrongBox with LRU eviction
|
||||||
|
- **EC+DECRYPT rejection** in createOperation, matching AOSP unsupported purpose check
|
||||||
|
- **Attest key nspace update** aligned with upstream PR #169
|
||||||
|
|
||||||
**Key fixes in this release include:**
|
---
|
||||||
1. **Google Play Integrity:** Resolved an issue preventing the attainment of STRONG integrity for Google Play verdicts, caused by an incorrect vendor patch level format. ✅
|
|
||||||
2. **Application Stability:** Fixed a critical crash related to an incorrect signature for the `SystemProperties.set` stub method. 🐛
|
|
||||||
3. **Stealth Enhancement:** Implemented a fix to bypass detection by the `Android Native Detector`. 👻
|
|
||||||
|
|
||||||
🔬 We are actively investigating a recent detection method to further enhance stealth capabilities.
|
## TEESimulator-RS v5.1: Interception Architecture Rewrite
|
||||||
|
|
||||||
🙏 Support for TEE-broken devices and Android 10/11 remains an area of ongoing improvement. We highly encourage you to submit any issues you encounter to help us refine these aspects! 🤝
|
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.
|
||||||
|
|
||||||
|
### Interception Layer Rewrite
|
||||||
|
- KeyMintSecurityLevelInterceptor completely restructured: GeneratedKeyInfo now carries full KeyMintAttestation instead of nullable stub, eliminating scattered null checks across every operation path
|
||||||
|
- SoftwareOperation rewritten with sealed CryptoPrimitive interface separating Signer, Verifier, Encryptor, and Decryptor into isolated implementations with proper JCA algorithm mapping
|
||||||
|
- 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
|
||||||
|
|
||||||
|
### Software Crypto Operations
|
||||||
|
- GCM nonce returned in CreateOperationResponse.parameters for encrypt operations, matching real KeyMint HAL behavior
|
||||||
|
- updateAad correctly throws INVALID_TAG on non-AEAD operations instead of silently succeeding
|
||||||
|
- Cipher algorithm mapping cleaned up: dropped CTR block mode and RSA_PKCS1_1_5_SIGN padding that caused JCA provider mismatches
|
||||||
|
- All crypto exceptions wrapped as ServiceSpecificException with correct KeyMint error codes instead of raw exceptions
|
||||||
|
|
||||||
|
### Attestation & Certificate Generation
|
||||||
|
- CertificateGenerator rewritten with clean Kotlin Pair return type instead of Android's util.Pair
|
||||||
|
- AttestationBuilder field ordering aligned with AOSP KeyDescription ASN.1 schema
|
||||||
|
- 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
|
||||||
|
|
||||||
|
### Configuration & Device Properties
|
||||||
|
- ConfigurationManager target package parsing refactored: mode/package extraction deduplicated across GENERATE/PATCH/AUTO branches
|
||||||
|
- 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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v5.0: AOSP Compliance Overhaul
|
||||||
|
|
||||||
|
Major release integrating 30+ AOSP compliance improvements from upstream PR #157 analysis, layered on top of our StrongBox hardening and native cert gen architecture.
|
||||||
|
|
||||||
|
### Attestation Extension Alignment
|
||||||
|
- 17 enforcement tags added to KeyMintAttestation (ACTIVE_DATETIME, ORIGINATION_EXPIRE, USAGE_EXPIRE, USAGE_COUNT_LIMIT, CALLER_NONCE, UNLOCKED_DEVICE_REQUIRED, INCLUDE_UNIQUE_ID, ROLLBACK_RESISTANCE, EARLY_BOOT_ONLY, ALLOW_WHILE_ON_BODY, TRUSTED_USER_PRESENCE_REQUIRED, TRUSTED_CONFIRMATION_REQUIRED, NO_AUTH_REQUIRED, MAX_USES_PER_BOOT, MAX_BOOT_LEVEL, MIN_MAC_LENGTH, RSA_OAEP_MGF_DIGEST)
|
||||||
|
- BLOCK_MODE encoded as SET OF INTEGER per AOSP attestation_record.h
|
||||||
|
- Version-guarded tags (RSA_OAEP_MGF_DIGEST >=100, ROLLBACK_RESISTANCE >=3, EARLY_BOOT_ONLY >=4)
|
||||||
|
- INCLUDE_UNIQUE_ID computed via HMAC-SHA256 per KeyMint HAL spec using device HBK
|
||||||
|
- AAID gated on attestation challenge presence
|
||||||
|
- Certificate validity defaults aligned with AOSP (epoch notBefore, 9999-12-31 notAfter)
|
||||||
|
|
||||||
|
### Binder Infrastructure
|
||||||
|
- Native transaction code filtering at C++ level, skipping JNI for non-intercepted codes
|
||||||
|
- getNumberOfEntries includes software-generated key count
|
||||||
|
- deleteKey resolves KEY_ID domain via generatedKeys lookup
|
||||||
|
- patchAuthorizations for OS/VENDOR/BOOT patch levels in authorization arrays
|
||||||
|
|
||||||
|
### Software Operation AOSP Conformance
|
||||||
|
- updateAad on non-AEAD operations returns INVALID_TAG (-76), matching AOSP operation.rs
|
||||||
|
- All crypto exceptions wrapped as ServiceSpecificException with correct KeyMint error codes
|
||||||
|
- GCM IV returned in CreateOperationResponse.parameters for encrypt operations
|
||||||
|
- SoftwareOperationBinder methods @Synchronized, matching AOSP Mutex per operation
|
||||||
|
- authorize_create enforcement: PURPOSE validation, algorithm-purpose compatibility, temporal constraints, CALLER_NONCE prohibition, WRAP_KEY rejection
|
||||||
|
|
||||||
|
### Security and Configuration
|
||||||
|
- SELinux permission checks via /proc/pid/attr/current
|
||||||
|
- Per-UID permission verification through IPackageManager.checkPermission
|
||||||
|
- Imported key tracking prevents stale attest-key overrides in getKeyEntry
|
||||||
|
- nspace consistency fix in attest-key override path
|
||||||
|
- TeeLatencySimulator with log-normal distribution matching real hardware profiles
|
||||||
|
- Device-unique HBK seed generated on install (32 bytes from /dev/random)
|
||||||
|
|
||||||
|
### Preserved from v4.8
|
||||||
|
- StrongBox op limits (4 concurrent max, TOO_MANY_OPERATIONS rejection)
|
||||||
|
- LRU operation pruning per security level
|
||||||
|
- Hardware keygen rate limiting (2/30s sliding window, 2 concurrent cap)
|
||||||
|
- Native Rust cert generation with BouncyCastle fallback
|
||||||
|
- Key persistence across reboots
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v4.8.1: StrongBox Op Rejection Fix
|
||||||
|
|
||||||
|
- **StrongBox op limit gate fix** — `trackAndEnforceOpLimit` was only called in the `Domain.KEY_ID` not-found path, so software-generated keys (found via `Domain.APP`) bypassed `STRONGBOX_MAX_CONCURRENT_OPS=4` entirely. DuckDetector's concurrent signing handles test created 24+ operations that all succeeded via LRU pruning instead of being rejected with `TOO_MANY_OPERATIONS (-29)`. Now enforced for all StrongBox createOperation paths.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v4.8: StrongBox Hardening & LRU Pruning
|
||||||
|
|
||||||
|
Tested against DuckDetector on OnePlus (Android 16, KSU). Tamper score dropped from 32 to 8.
|
||||||
|
|
||||||
|
- **LRU operation pruning** — Concurrent software operations capped at 15 per UID (TEE) and 4 per UID (StrongBox), with oldest-first eviction. Pruned operations return `INVALID_OPERATION_HANDLE (-28)`, matching AOSP keystore2 malus-based pruning.
|
||||||
|
- **StrongBox param guard** — Unsupported StrongBox params (RSA >2048-bit, non-P256 EC curves) forwarded to real HAL for proper rejection instead of generating in software.
|
||||||
|
- **StrongBox timing** — Key generation floors at 250ms, signing at 80ms on StrongBox security level to match real secure element latency.
|
||||||
|
- **StrongBox op limit** — Sliding-window enforcer caps concurrent StrongBox operations for both software and hardware key paths, returning `TOO_MANY_OPERATIONS (-29)` when exceeded.
|
||||||
|
- **ECDSA algorithm alias** — Accept "ECDSA" in addition to "EC" as JCA private key algorithm name. Fixes SIGSEGV crash on Android 10 devices where the provider reports EC keys as "ECDSA". Closes #4.
|
||||||
|
- **createOperation domain handling** — Software-generated keys now found via both `Domain.APP` (alias) and `Domain.KEY_ID` (nspace) lookup paths.
|
||||||
|
- **Permission guards** — Device ID attestation tags (IMEI, MEID, serial) require caller permission checks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v4.7: Operation & Attestation Fixes
|
||||||
|
|
||||||
|
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) and [Key Attestation](https://github.com/nickel-lang/nickel) on OnePlus (Android 16) and Xiaomi Redmi 14C (Android 14).
|
||||||
|
|
||||||
|
- **PADDING encoding** — Fixed ASN.1 encoding of PADDING tag in attestation extension from individual `[6] INTEGER` entries to `[6] SET OF INTEGER`, matching AOSP `attestation_record.h` schema. Broke all RSA key attestation since v4.6.
|
||||||
|
- **Operation error-path conformance** — Software operations now track finalized state and return `INVALID_OPERATION_HANDLE (-28)` on post-abort calls. Input length guard (32KB) returns `TOO_MUCH_DATA` matching AOSP `operation.rs`. Passes KeyDetector's OperationErrorPathChecker.
|
||||||
|
- **updateAad support** — Added `updateAad` to `SoftwareOperationBinder`, fixing `AbstractMethodError` on Android 16 where the runtime Stub declares it abstract.
|
||||||
|
- **Algorithm inference** — `createOperation` now infers algorithm from the stored key pair when operation params omit the ALGORITHM tag, matching AOSP behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v4.6: Rebrand & Detection Fix
|
||||||
|
|
||||||
|
- **RTT normalization rework** — Replaced Gaussian sleep (mean=55ms) with a 15ms floor fence. The old approach triggered Chunqiu Native Check 2.8 timing analysis; the floor-only approach satisfies the minimum RTT threshold without creating a detectable delay pattern.
|
||||||
|
- **Cross-algorithm attestation** — Signing algorithm now derived from the attestation key's actual type, not the generated key's algorithm. Fixes BouncyCastle crash when signing RSA keys with EC attestation keys (Shizuku attestation flow).
|
||||||
|
- **Device ID attestation** — Serial/IMEI/MEID/secondImei tags now flow through to software cert gen instead of blanket rejection. Only DEVICE_UNIQUE_ATTESTATION is rejected, matching AOSP keystore2 policy.
|
||||||
|
- **Rebrand to TEESimulator-RS** — Distinguishes this fork from upstream. Version scheme simplified to v{major}.{minor}-{commitCount}.
|
||||||
|
- **CI streamlined** — Release pipeline uses Gradle-generated filenames directly, eliminating the rename step.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator v4.5: Detection Hardening
|
||||||
|
|
||||||
|
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) (23-check attestation validator). All keystore-level checks now pass.
|
||||||
|
|
||||||
|
- **Key deletion consistency** — After deleting a software-generated key, `getKeyEntry` now correctly returns `KEY_NOT_FOUND` instead of falling through to a stale live-patch fallback. Fixes binder consistency checks that detect ghost key responses.
|
||||||
|
- **generateKey timing normalization** — Software key generation RTT now matches real TEE latency profile (Gaussian distribution, mean=55ms, floor=15ms). Previously completed in ~4ms, which is an immediate timing side-channel.
|
||||||
|
- **Delete cleanup scope** — `deleteKey` now clears all cached state (patched chains, attestation keys) regardless of whether the key was software or hardware-generated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator v4.4: AOSP Conformance
|
||||||
|
|
||||||
|
- **Binder error reply format** — Aligned EX_SERVICE_SPECIFIC wire layout with AOSP Status.cpp, including the remote stack trace header field.
|
||||||
|
- **Key enumeration** — Corrected list_past_alias pagination order to match AOSP database.rs semantics.
|
||||||
|
- **KeyMetadata fields** — Generated key responses now include modificationTimeMs, Tag.ORIGIN, and normalized KeyDescriptor fields per AOSP Keystore2.
|
||||||
|
- **Parcel handling** — hasException() preserves reply position for downstream consumers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator v4.3: Performance & Reliability
|
||||||
|
|
||||||
|
- **Debug log gating** — `SystemLogger.debug()` now skipped entirely in release builds, eliminating unnecessary logcat syscalls on every intercepted transaction.
|
||||||
|
- **Supervisor backoff** — Exponential restart delay (500ms → 30s cap) prevents CPU spin if the daemon crashes repeatedly. Resets automatically once stable.
|
||||||
|
- **Process priority** — Daemon runs at nice=10, yielding CPU to foreground apps on constrained devices.
|
||||||
|
- **Map eviction** — Rate limiter and file lock maps now evict stale entries instead of growing unbounded.
|
||||||
|
- **CI pipeline** — Single-trigger build→release pipeline with proper changelog extraction and correctly sized artifacts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator v4.2: Detection Evasion Hardening
|
||||||
|
|
||||||
|
Fixes 6 detection vectors flagged by attestation validator apps.
|
||||||
|
|
||||||
|
### Attestation Policy Enforcement
|
||||||
|
|
||||||
|
Replicate AOSP keystore2's `add_required_parameters()` validation that our software keygen path was bypassing:
|
||||||
|
|
||||||
|
- **CREATION_DATETIME** — Reject caller-provided input with `INVALID_ARGUMENT (20)`, matching `security_level.rs:424`. Our cert gen still adds its own timestamp, same as real keystore2.
|
||||||
|
- **Device ID attestation** — Reject ATTESTATION_ID_SERIAL, IMEI, MEID, SECOND_IMEI, and DEVICE_UNIQUE_ATTESTATION with `CANNOT_ATTEST_IDS (-66)`. No consumer app has READ_PRIVILEGED_PHONE_STATE.
|
||||||
|
- **Error reply format** — Fixed AIDL ServiceSpecificException parcel write order (was errorCode→message, now message→errorCode).
|
||||||
|
|
||||||
|
### Certificate Fix
|
||||||
|
|
||||||
|
Leaf certificate Subject CN corrected from "Android KeyStore Key" to "Android Keystore Key" (lowercase s), matching AOSP `KeyGenParameterSpec.java:282`. Both Kotlin and Rust paths.
|
||||||
|
|
||||||
|
### Binder Timing
|
||||||
|
|
||||||
|
Skip interception for system transaction codes (PING, INTERFACE, DUMP) above LAST_CALL_TRANSACTION. Eliminates the JNI round-trip that inflated binder ping ratio to 3.85x (detector threshold: 3.0x).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator v4.1: Boot Identity Persistence
|
||||||
|
|
||||||
|
Bugfix release. The vbmeta boot key digest was randomizing on every reboot, producing a different RootOfTrust in attestation certificates each boot.
|
||||||
|
|
||||||
|
On devices where the kernel doesn't set `ro.boot.vbmeta.public_key_digest`, the fallback chain hit random generation every boot because `resetprop` overrides for `ro.boot.*` props don't survive reboots. Added file-based persistence (`boot_hash.bin`, `boot_key.bin`) between the TEE cache and random fallback. Once determined, boot identity values persist across reboots.
|
||||||
|
|
||||||
|
Verified on Redmi 14C: second boot reads from persistent file instead of regenerating.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator v4.0: Native Rust Cert Generation
|
||||||
|
|
||||||
|
Major release. Certificate chain generation rebuilt from the ground up in Rust, replacing the BouncyCastle Java path for EC and RSA keys. Hardened against every known detector app.
|
||||||
|
|
||||||
|
### Native Cert Generation
|
||||||
|
|
||||||
|
The headline feature. `libcertgen.so` generates X.509 certificate chains using `ring` (EC-P256/P384) and `rsa` (RSA-2048/4096) with manual DER assembly. No more BouncyCastle quirks — issuer/subject DN bytes are injected directly from the keybox, ensuring byte-perfect chain linkage. BouncyCastle remains as fallback for unsupported curves (P-224, P-521, Curve25519).
|
||||||
|
|
||||||
|
### Anti-Detection Hardening
|
||||||
|
|
||||||
|
- **Challenge validation** — Oversized attestation challenges (>128 bytes) now return `INVALID_INPUT_LENGTH (-21)`, matching real KeyMint behavior. Previously accepted silently — DuckDetector exploited this.
|
||||||
|
- **Per-UID rate limiter** — 2 hardware keygens per 30s burst, 2 concurrent max. Overflow falls back to software certs. Blocks DuckDetector-style keygen flooding that starves GMS.
|
||||||
|
- **importKey eviction guard** — Retained patch chains prevent generate-then-import attacks that evict cached attestation data.
|
||||||
|
- **256KB native payload cap** — Oversized binder payloads bypass interception cleanly instead of stalling threads.
|
||||||
|
- **Alias size rejection** — Oversized key aliases rejected before they hit the binder buffer.
|
||||||
|
|
||||||
|
### Key Persistence
|
||||||
|
|
||||||
|
Generated keys now survive reboots. File-backed storage with file-level locking, preserved across keybox rotations. Banking and biometric apps that cache attestation keys no longer break after restart.
|
||||||
|
|
||||||
|
### Attestation Fixes
|
||||||
|
|
||||||
|
- Null out all-zero `verifiedBootHash` from TEE cache (fingerprinting vector)
|
||||||
|
- Correct `module_hash` field to match AOSP Keystore2 format
|
||||||
|
- Override pre-existing attest keys instead of skipping them
|
||||||
|
- Strip HTML comments from PEM blocks in keybox parsing
|
||||||
|
- Security patch consistency — `system=prop` forces boot/vendor to match
|
||||||
|
|
||||||
|
### Module Lifecycle
|
||||||
|
|
||||||
|
- Supervisor daemon keeps the interceptor alive
|
||||||
|
- KSU Action button clears persistent key cache
|
||||||
|
- Clean uninstall removes all traces (persistent keys, TEE status, daemon)
|
||||||
|
|
||||||
|
### Stability
|
||||||
|
|
||||||
|
- FileObserver NPE on config deletion fixed
|
||||||
|
- Global uncaught exception handler — daemon stays alive on unexpected errors
|
||||||
|
- PEM parsing hardened against malformed keybox files
|
||||||
|
|
||||||
|
### Tested Against
|
||||||
|
|
||||||
|
DuckDetector, Luna, Play Integrity, Key Attestation Demo — all passing on Redmi 14C (Android 14, Beanpod KeyMaster, KSU).
|
||||||
|
|||||||
+13
-2
@@ -15,7 +15,7 @@ fi
|
|||||||
|
|
||||||
# --- Version Info ---
|
# --- Version Info ---
|
||||||
VERSION=$(grep_prop version "${TMPDIR}/module.prop")
|
VERSION=$(grep_prop version "${TMPDIR}/module.prop")
|
||||||
ui_print "- Installing TEESimulator $VERSION"
|
ui_print "- Installing TEESimulator-RS $VERSION"
|
||||||
ui_print ""
|
ui_print ""
|
||||||
|
|
||||||
# --- Architecture Handling ---
|
# --- Architecture Handling ---
|
||||||
@@ -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; 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
|
||||||
|
|
||||||
@@ -67,10 +67,14 @@ ui_print ""
|
|||||||
ui_print "- Extracting $ARCH libraries"
|
ui_print "- Extracting $ARCH libraries"
|
||||||
install_file "lib/$ABI_DIR/libTEESimulator.so" "$MODPATH"
|
install_file "lib/$ABI_DIR/libTEESimulator.so" "$MODPATH"
|
||||||
install_file "lib/$ABI_DIR/libinject.so" "$MODPATH"
|
install_file "lib/$ABI_DIR/libinject.so" "$MODPATH"
|
||||||
|
install_file "lib/$ABI_DIR/libsupervisor.so" "$MODPATH"
|
||||||
|
install_file "lib/$ABI_DIR/libcertgen.so" "$MODPATH"
|
||||||
ui_print ""
|
ui_print ""
|
||||||
|
|
||||||
mv "$MODPATH/libinject.so" "$MODPATH/inject"
|
mv "$MODPATH/libinject.so" "$MODPATH/inject"
|
||||||
|
mv "$MODPATH/libsupervisor.so" "$MODPATH/supervisor"
|
||||||
chmod 755 "$MODPATH/inject"
|
chmod 755 "$MODPATH/inject"
|
||||||
|
chmod 755 "$MODPATH/supervisor"
|
||||||
|
|
||||||
# --- Configuration Files ---
|
# --- Configuration Files ---
|
||||||
if [ ! -d "$CONFIG_DIR" ]; then
|
if [ ! -d "$CONFIG_DIR" ]; then
|
||||||
@@ -87,3 +91,10 @@ if [ ! -f "$CONFIG_DIR/target.txt" ]; then
|
|||||||
ui_print "- Adding default target scope"
|
ui_print "- Adding default target scope"
|
||||||
install_file "target.txt" "$CONFIG_DIR"
|
install_file "target.txt" "$CONFIG_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
rm -f "$CONFIG_DIR/tee_status.txt"
|
||||||
|
|
||||||
|
if [ ! -f "$CONFIG_DIR/hbk" ]; then
|
||||||
|
ui_print "- Generating device-unique hardware-bound key seed"
|
||||||
|
head -c 32 /dev/random > "$CONFIG_DIR/hbk"
|
||||||
|
fi
|
||||||
|
|||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
id=tricky_store
|
id=tricky_store
|
||||||
name=TEESimulator
|
name=TEESimulator-RS
|
||||||
version=${REPLACEMEVER}
|
version=${REPLACEMEVER}
|
||||||
versionCode=${REPLACEMEVERCODE}
|
versionCode=${REPLACEMEVERCODE}
|
||||||
author=JingMatrix
|
author=JingMatrix, Enginex0
|
||||||
description=Software simulation for Android hardware-backed key pairs with key attestation
|
description=Software simulation for Android hardware-backed key pairs with key attestation
|
||||||
updateJson=https://raw.githubusercontent.com/JingMatrix/TEESimulator/main/module/update.json
|
updateJson=https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/update.json
|
||||||
|
|||||||
@@ -1,4 +1,2 @@
|
|||||||
allow keystore system_file unix_dgram_socket *
|
allow keystore {adb_data_file shell_data_file} file *
|
||||||
allow system_file keystore unix_dgram_socket *
|
|
||||||
allow keystore system_file file *
|
|
||||||
allow crash_dump keystore process *
|
allow crash_dump keystore process *
|
||||||
|
|||||||
+2
-8
@@ -1,11 +1,5 @@
|
|||||||
DEBUG=false
|
|
||||||
|
|
||||||
MODDIR=${0%/*}
|
MODDIR=${0%/*}
|
||||||
|
|
||||||
cd $MODDIR
|
cd $MODDIR
|
||||||
|
|
||||||
while true; do
|
# Fork-based supervisor for instant restart
|
||||||
./daemon "$MODDIR" || exit 1
|
./supervisor ./daemon "$MODDIR" &
|
||||||
# ensure keystore initialized
|
|
||||||
sleep 2
|
|
||||||
done &
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#!/system/bin/sh
|
||||||
|
MODDIR=${0%/*}
|
||||||
|
CONFIG_DIR=/data/adb/tricky_store
|
||||||
|
|
||||||
|
# Kill daemon and supervisor
|
||||||
|
for pid in $(pidof TEESimulator) $(pidof supervisor) $(pidof daemon); do
|
||||||
|
kill -9 "$pid" 2>/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
rm -rf "$CONFIG_DIR/persistent_keys"
|
||||||
|
rm -f "$CONFIG_DIR/tee_status.txt"
|
||||||
|
rm -f "$CONFIG_DIR/boot_hash.bin" "$CONFIG_DIR/boot_key.bin"
|
||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "v2.1",
|
"version": "v4.5",
|
||||||
"versionCode": 20,
|
"versionCode": 111,
|
||||||
"zipUrl": "https://github.com/JingMatrix/TEESimulator/releases/download/v2.1/TEESimulator-v2.1-20-release.zip",
|
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.5/TEESimulator-v4.5-Release.zip",
|
||||||
"changelog": "https://raw.githubusercontent.com/JingMatrix/TEESimulator/main/module/changelog.md"
|
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[target.aarch64-linux-android]
|
||||||
|
linker = "aarch64-linux-android29-clang"
|
||||||
|
|
||||||
|
[target.armv7-linux-androideabi]
|
||||||
|
linker = "armv7a-linux-androideabi29-clang"
|
||||||
|
|
||||||
|
[target.i686-linux-android]
|
||||||
|
linker = "i686-linux-android29-clang"
|
||||||
|
|
||||||
|
[target.x86_64-linux-android]
|
||||||
|
linker = "x86_64-linux-android29-clang"
|
||||||
Generated
+1166
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
|||||||
|
[package]
|
||||||
|
name = "certgen"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
jni = { version = "0.21.1", default-features = false }
|
||||||
|
ring = "0.17.14"
|
||||||
|
rsa = { version = "0.9", features = ["sha2"] }
|
||||||
|
pkcs8 = { version = "0.10", features = ["alloc"] }
|
||||||
|
rand = "0.8"
|
||||||
|
der = { version = "0.7.10", features = ["alloc", "oid"] }
|
||||||
|
const-oid = "0.9.6"
|
||||||
|
x509-cert = { version = "0.2.5", features = ["pem"] }
|
||||||
|
time = { version = "0.3", features = ["std"] }
|
||||||
|
anyhow = "1.0"
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
libc = "0.2"
|
||||||
|
zip = { version = "2.2", default-features = false, features = ["deflate"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "z"
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
strip = "symbols"
|
||||||
|
panic = "abort"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "stable"
|
||||||
|
targets = [
|
||||||
|
"aarch64-linux-android",
|
||||||
|
"armv7-linux-androideabi",
|
||||||
|
"i686-linux-android",
|
||||||
|
"x86_64-linux-android",
|
||||||
|
]
|
||||||
@@ -0,0 +1,721 @@
|
|||||||
|
use crate::error::Result;
|
||||||
|
use crate::types::CertGenParams;
|
||||||
|
|
||||||
|
const DO_NOT_REPORT: i32 = -1;
|
||||||
|
|
||||||
|
pub fn build_attestation_extension(params: &CertGenParams) -> Result<Vec<u8>> {
|
||||||
|
let sw = build_software_enforced(params)?;
|
||||||
|
let tee = build_tee_enforced(params)?;
|
||||||
|
|
||||||
|
let mut inner = Vec::new();
|
||||||
|
// attestationVersion — INTEGER
|
||||||
|
inner.extend_from_slice(&enc_integer(params.attest_version as i64));
|
||||||
|
// attestationSecurityLevel — ENUMERATED, not INTEGER
|
||||||
|
inner.extend_from_slice(&enc_enumerated(params.security_level));
|
||||||
|
// keymintVersion — INTEGER
|
||||||
|
inner.extend_from_slice(&enc_integer(params.keymaster_version as i64));
|
||||||
|
// keymintSecurityLevel — ENUMERATED, not INTEGER
|
||||||
|
inner.extend_from_slice(&enc_enumerated(params.security_level));
|
||||||
|
// attestationChallenge — OCTET STRING
|
||||||
|
inner.extend_from_slice(&enc_octet_string(
|
||||||
|
params.attestation_challenge.as_deref().unwrap_or(&[]),
|
||||||
|
));
|
||||||
|
// uniqueId — OCTET STRING (always empty)
|
||||||
|
inner.extend_from_slice(&enc_octet_string(&[]));
|
||||||
|
// softwareEnforced
|
||||||
|
inner.extend_from_slice(&sw);
|
||||||
|
// teeEnforced
|
||||||
|
inner.extend_from_slice(&tee);
|
||||||
|
|
||||||
|
Ok(enc_sequence(&inner))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_software_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
|
||||||
|
let mut fields: Vec<(u32, Vec<u8>)> = Vec::new();
|
||||||
|
|
||||||
|
// Tag 303: CALLER_NONCE — NULL (presence = true)
|
||||||
|
if params.caller_nonce {
|
||||||
|
fields.push((303, enc_null()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 400: ACTIVE_DATETIME — INTEGER (milliseconds)
|
||||||
|
if params.active_datetime >= 0 {
|
||||||
|
fields.push((400, enc_integer(params.active_datetime)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 401: ORIGINATION_EXPIRE_DATETIME — INTEGER (milliseconds)
|
||||||
|
if params.origination_expire_datetime >= 0 {
|
||||||
|
fields.push((401, enc_integer(params.origination_expire_datetime)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 402: USAGE_EXPIRE_DATETIME — INTEGER (milliseconds)
|
||||||
|
if params.usage_expire_datetime >= 0 {
|
||||||
|
fields.push((402, enc_integer(params.usage_expire_datetime)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 405: USAGE_COUNT_LIMIT — INTEGER
|
||||||
|
if params.usage_count_limit >= 0 {
|
||||||
|
fields.push((405, enc_integer(params.usage_count_limit as i64)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 509: UNLOCKED_DEVICE_REQUIRED — NULL
|
||||||
|
if params.unlocked_device_required {
|
||||||
|
fields.push((509, enc_null()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 701: CREATION_DATETIME — INTEGER (milliseconds)
|
||||||
|
fields.push((701, enc_integer(params.creation_datetime)));
|
||||||
|
|
||||||
|
// Tag 709: ATTESTATION_APPLICATION_ID — OCTET STRING
|
||||||
|
// The bytes are already the DER-encoded AttestationApplicationId wrapped in OCTET STRING
|
||||||
|
// by the Kotlin layer. We wrap them in an EXPLICIT tag.
|
||||||
|
if !params.attestation_application_id.is_empty() {
|
||||||
|
fields.push((709, enc_octet_string(¶ms.attestation_application_id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 724: MODULE_HASH — OCTET STRING (only if attestVersion >= 400)
|
||||||
|
if params.attest_version >= 400 {
|
||||||
|
if let Some(ref hash) = params.module_hash {
|
||||||
|
fields.push((724, enc_octet_string(hash)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(build_authorization_list(&mut fields))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_tee_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
|
||||||
|
let mut fields: Vec<(u32, Vec<u8>)> = Vec::new();
|
||||||
|
|
||||||
|
// Tag 1: PURPOSE — SET OF INTEGER
|
||||||
|
if !params.purposes.is_empty() {
|
||||||
|
fields.push((1, build_set_of_integer(¶ms.purposes)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 2: ALGORITHM — INTEGER
|
||||||
|
fields.push((2, enc_integer(params.algorithm as i32 as i64)));
|
||||||
|
|
||||||
|
// Tag 3: KEY_SIZE — INTEGER
|
||||||
|
fields.push((3, enc_integer(params.key_size as i64)));
|
||||||
|
|
||||||
|
// Tag 5: DIGEST — SET OF INTEGER
|
||||||
|
if !params.digests.is_empty() {
|
||||||
|
fields.push((5, build_set_of_integer(¶ms.digests)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 10: EC_CURVE — INTEGER (only for EC keys)
|
||||||
|
if let Some(curve) = params.ec_curve {
|
||||||
|
fields.push((10, enc_integer(curve as i32 as i64)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 503: NO_AUTH_REQUIRED — NULL (conditional)
|
||||||
|
if params.no_auth_required {
|
||||||
|
fields.push((503, enc_null()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 702: ORIGIN — INTEGER 0 (GENERATED)
|
||||||
|
fields.push((702, enc_integer(0)));
|
||||||
|
|
||||||
|
// Tag 704: ROOT_OF_TRUST — SEQUENCE
|
||||||
|
fields.push((704, build_root_of_trust(params)));
|
||||||
|
|
||||||
|
// Tag 705: OS_VERSION — INTEGER
|
||||||
|
if params.os_version != DO_NOT_REPORT {
|
||||||
|
fields.push((705, enc_integer(params.os_version as i64)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 706: OS_PATCHLEVEL — INTEGER
|
||||||
|
if params.os_patch_level != DO_NOT_REPORT {
|
||||||
|
fields.push((706, enc_integer(params.os_patch_level as i64)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags 710-717: ATTESTATION_ID_* — OCTET STRING (optional)
|
||||||
|
if let Some(ref v) = params.id_brand {
|
||||||
|
fields.push((710, enc_octet_string(v)));
|
||||||
|
}
|
||||||
|
if let Some(ref v) = params.id_device {
|
||||||
|
fields.push((711, enc_octet_string(v)));
|
||||||
|
}
|
||||||
|
if let Some(ref v) = params.id_product {
|
||||||
|
fields.push((712, enc_octet_string(v)));
|
||||||
|
}
|
||||||
|
if let Some(ref v) = params.id_serial {
|
||||||
|
fields.push((713, enc_octet_string(v)));
|
||||||
|
}
|
||||||
|
if let Some(ref v) = params.id_imei {
|
||||||
|
fields.push((714, enc_octet_string(v)));
|
||||||
|
}
|
||||||
|
if let Some(ref v) = params.id_meid {
|
||||||
|
fields.push((715, enc_octet_string(v)));
|
||||||
|
}
|
||||||
|
if let Some(ref v) = params.id_manufacturer {
|
||||||
|
fields.push((716, enc_octet_string(v)));
|
||||||
|
}
|
||||||
|
if let Some(ref v) = params.id_model {
|
||||||
|
fields.push((717, enc_octet_string(v)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 718: VENDOR_PATCHLEVEL — INTEGER
|
||||||
|
if params.vendor_patch_level != DO_NOT_REPORT {
|
||||||
|
fields.push((718, enc_integer(params.vendor_patch_level as i64)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 719: BOOT_PATCHLEVEL — INTEGER
|
||||||
|
if params.boot_patch_level != DO_NOT_REPORT {
|
||||||
|
fields.push((719, enc_integer(params.boot_patch_level as i64)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag 723: ATTESTATION_ID_SECOND_IMEI — OCTET STRING (only if attestVersion >= 300)
|
||||||
|
if params.attest_version >= 300 {
|
||||||
|
if let Some(ref v) = params.id_second_imei {
|
||||||
|
fields.push((723, enc_octet_string(v)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(build_authorization_list(&mut fields))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_root_of_trust(params: &CertGenParams) -> Vec<u8> {
|
||||||
|
let mut inner = Vec::new();
|
||||||
|
// verifiedBootKey — OCTET STRING (32 bytes)
|
||||||
|
inner.extend_from_slice(&enc_octet_string(¶ms.boot_key));
|
||||||
|
// deviceLocked — BOOLEAN TRUE (0xFF, not 0x01)
|
||||||
|
inner.extend_from_slice(&enc_boolean(true));
|
||||||
|
// verifiedBootState — ENUMERATED 0 (Verified), not INTEGER
|
||||||
|
inner.extend_from_slice(&enc_enumerated(0));
|
||||||
|
// verifiedBootHash — OCTET STRING (32 bytes)
|
||||||
|
inner.extend_from_slice(&enc_octet_string(¶ms.boot_hash));
|
||||||
|
enc_sequence(&inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_authorization_list(fields: &mut Vec<(u32, Vec<u8>)>) -> Vec<u8> {
|
||||||
|
fields.sort_by_key(|(tag, _)| *tag);
|
||||||
|
let mut inner = Vec::new();
|
||||||
|
for (tag, value) in fields.iter() {
|
||||||
|
inner.extend_from_slice(&enc_explicit_tag(*tag, value));
|
||||||
|
}
|
||||||
|
enc_sequence(&inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_set_of_integer(values: &[i32]) -> Vec<u8> {
|
||||||
|
// DER SET OF: elements sorted by encoded byte value
|
||||||
|
let mut encoded: Vec<Vec<u8>> = values.iter().map(|v| enc_integer(*v as i64)).collect();
|
||||||
|
encoded.sort();
|
||||||
|
let mut inner = Vec::new();
|
||||||
|
for e in &encoded {
|
||||||
|
inner.extend_from_slice(e);
|
||||||
|
}
|
||||||
|
enc_set(&inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DER primitives ---
|
||||||
|
|
||||||
|
fn enc_length(len: usize) -> Vec<u8> {
|
||||||
|
if len < 0x80 {
|
||||||
|
vec![len as u8]
|
||||||
|
} else if len <= 0xFF {
|
||||||
|
vec![0x81, len as u8]
|
||||||
|
} else if len <= 0xFFFF {
|
||||||
|
vec![0x82, (len >> 8) as u8, len as u8]
|
||||||
|
} else if len <= 0xFF_FFFF {
|
||||||
|
vec![0x83, (len >> 16) as u8, (len >> 8) as u8, len as u8]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
0x84,
|
||||||
|
(len >> 24) as u8,
|
||||||
|
(len >> 16) as u8,
|
||||||
|
(len >> 8) as u8,
|
||||||
|
len as u8,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enc_integer(value: i64) -> Vec<u8> {
|
||||||
|
// DER INTEGER: tag 0x02, minimal two's complement big-endian
|
||||||
|
let bytes = integer_bytes(value);
|
||||||
|
let mut out = vec![0x02];
|
||||||
|
out.extend_from_slice(&enc_length(bytes.len()));
|
||||||
|
out.extend_from_slice(&bytes);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn integer_bytes(value: i64) -> Vec<u8> {
|
||||||
|
if value == 0 {
|
||||||
|
return vec![0x00];
|
||||||
|
}
|
||||||
|
let raw = value.to_be_bytes();
|
||||||
|
// Find first significant byte
|
||||||
|
let mut start = 0;
|
||||||
|
if value > 0 {
|
||||||
|
while start < 7 && raw[start] == 0x00 {
|
||||||
|
start += 1;
|
||||||
|
}
|
||||||
|
// If high bit set, need leading 0x00 to keep positive
|
||||||
|
if raw[start] & 0x80 != 0 {
|
||||||
|
let mut out = vec![0x00];
|
||||||
|
out.extend_from_slice(&raw[start..]);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while start < 7 && raw[start] == 0xFF {
|
||||||
|
start += 1;
|
||||||
|
}
|
||||||
|
// If high bit clear, need leading 0xFF to keep negative
|
||||||
|
if raw[start] & 0x80 == 0 {
|
||||||
|
let mut out = vec![0xFF];
|
||||||
|
out.extend_from_slice(&raw[start..]);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raw[start..].to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enc_enumerated(value: i32) -> Vec<u8> {
|
||||||
|
// DER ENUMERATED: tag 0x0A, same value encoding as INTEGER
|
||||||
|
let bytes = integer_bytes(value as i64);
|
||||||
|
let mut out = vec![0x0A];
|
||||||
|
out.extend_from_slice(&enc_length(bytes.len()));
|
||||||
|
out.extend_from_slice(&bytes);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enc_octet_string(data: &[u8]) -> Vec<u8> {
|
||||||
|
let mut out = vec![0x04];
|
||||||
|
out.extend_from_slice(&enc_length(data.len()));
|
||||||
|
out.extend_from_slice(data);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enc_null() -> Vec<u8> {
|
||||||
|
vec![0x05, 0x00]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enc_boolean(value: bool) -> Vec<u8> {
|
||||||
|
// DER BOOLEAN: TRUE = 0xFF, FALSE = 0x00
|
||||||
|
vec![0x01, 0x01, if value { 0xFF } else { 0x00 }]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enc_sequence(contents: &[u8]) -> Vec<u8> {
|
||||||
|
let mut out = vec![0x30];
|
||||||
|
out.extend_from_slice(&enc_length(contents.len()));
|
||||||
|
out.extend_from_slice(contents);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enc_set(contents: &[u8]) -> Vec<u8> {
|
||||||
|
let mut out = vec![0x31];
|
||||||
|
out.extend_from_slice(&enc_length(contents.len()));
|
||||||
|
out.extend_from_slice(contents);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enc_explicit_tag(tag_number: u32, inner: &[u8]) -> Vec<u8> {
|
||||||
|
// EXPLICIT context-specific constructed tag
|
||||||
|
let mut out = Vec::new();
|
||||||
|
if tag_number < 31 {
|
||||||
|
// Short form: single byte 0xA0 | tag_number
|
||||||
|
out.push(0xA0 | tag_number as u8);
|
||||||
|
} else {
|
||||||
|
// Long form: 0xBF followed by base-128 encoding of tag number
|
||||||
|
out.push(0xBF);
|
||||||
|
enc_base128_tag(&mut out, tag_number);
|
||||||
|
}
|
||||||
|
out.extend_from_slice(&enc_length(inner.len()));
|
||||||
|
out.extend_from_slice(inner);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enc_base128_tag(out: &mut Vec<u8>, tag: u32) {
|
||||||
|
// Base-128 with continuation bits: MSB first, bit 7 set on all but last byte
|
||||||
|
let mut digits = Vec::new();
|
||||||
|
let mut val = tag;
|
||||||
|
digits.push((val & 0x7F) as u8);
|
||||||
|
val >>= 7;
|
||||||
|
while val > 0 {
|
||||||
|
digits.push((val & 0x7F) as u8 | 0x80);
|
||||||
|
val >>= 7;
|
||||||
|
}
|
||||||
|
// Written MSB first
|
||||||
|
for b in digits.iter().rev() {
|
||||||
|
out.push(*b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::types::{Algorithm, EcCurve};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enc_integer_zero() {
|
||||||
|
assert_eq!(enc_integer(0), vec![0x02, 0x01, 0x00]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enc_integer_small_positive() {
|
||||||
|
assert_eq!(enc_integer(3), vec![0x02, 0x01, 0x03]);
|
||||||
|
assert_eq!(enc_integer(127), vec![0x02, 0x01, 0x7F]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enc_integer_needs_leading_zero() {
|
||||||
|
// 128 = 0x80, high bit set so needs 0x00 prefix
|
||||||
|
assert_eq!(enc_integer(128), vec![0x02, 0x02, 0x00, 0x80]);
|
||||||
|
assert_eq!(enc_integer(256), vec![0x02, 0x02, 0x01, 0x00]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enc_integer_multi_byte() {
|
||||||
|
// 140000 = 0x02_22_E0
|
||||||
|
assert_eq!(enc_integer(140000), vec![0x02, 0x03, 0x02, 0x22, 0xE0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enc_integer_large() {
|
||||||
|
// 20250301 = 0x01_34_FE_BD
|
||||||
|
assert_eq!(
|
||||||
|
enc_integer(20250301),
|
||||||
|
vec![0x02, 0x04, 0x01, 0x34, 0xFE, 0xBD]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enc_enumerated() {
|
||||||
|
// SecurityLevel TEE = 1
|
||||||
|
assert_eq!(enc_enumerated(1), vec![0x0A, 0x01, 0x01]);
|
||||||
|
// VerifiedBootState Verified = 0
|
||||||
|
assert_eq!(enc_enumerated(0), vec![0x0A, 0x01, 0x00]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enc_boolean_true() {
|
||||||
|
// DER: TRUE = 0xFF
|
||||||
|
assert_eq!(enc_boolean(true), vec![0x01, 0x01, 0xFF]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enc_null() {
|
||||||
|
assert_eq!(enc_null(), vec![0x05, 0x00]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enc_octet_string_empty() {
|
||||||
|
assert_eq!(enc_octet_string(&[]), vec![0x04, 0x00]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enc_explicit_tag_short() {
|
||||||
|
// Tag 1 wrapping INTEGER 2: A1 03 02 01 02
|
||||||
|
let inner = enc_integer(2);
|
||||||
|
let tagged = enc_explicit_tag(1, &inner);
|
||||||
|
assert_eq!(tagged, vec![0xA1, 0x03, 0x02, 0x01, 0x02]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enc_explicit_tag_10() {
|
||||||
|
// Tag 10: 0xAA
|
||||||
|
let inner = enc_integer(1);
|
||||||
|
let tagged = enc_explicit_tag(10, &inner);
|
||||||
|
assert_eq!(tagged[0], 0xAA);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enc_explicit_tag_503() {
|
||||||
|
// Tag 503: 0xBF 0x83 0x77
|
||||||
|
// 503 = 3*128 + 119 => 0x83 0x77
|
||||||
|
let inner = enc_null();
|
||||||
|
let tagged = enc_explicit_tag(503, &inner);
|
||||||
|
assert_eq!(&tagged[..3], &[0xBF, 0x83, 0x77]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enc_explicit_tag_704() {
|
||||||
|
// Tag 704: 0xBF 0x85 0x40
|
||||||
|
// 704 = 5*128 + 64 => 0x85 0x40
|
||||||
|
let inner = enc_sequence(&[]);
|
||||||
|
let tagged = enc_explicit_tag(704, &inner);
|
||||||
|
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x40]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enc_explicit_tag_718() {
|
||||||
|
// Tag 718: 0xBF 0x85 0x4E
|
||||||
|
let inner = enc_integer(20250301);
|
||||||
|
let tagged = enc_explicit_tag(718, &inner);
|
||||||
|
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x4E]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enc_explicit_tag_719() {
|
||||||
|
// Tag 719: 0xBF 0x85 0x4F
|
||||||
|
let inner = enc_integer(20250301);
|
||||||
|
let tagged = enc_explicit_tag(719, &inner);
|
||||||
|
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x4F]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enc_explicit_tag_701() {
|
||||||
|
// Tag 701: 0xBF 0x85 0x3D
|
||||||
|
let inner = enc_integer(1000);
|
||||||
|
let tagged = enc_explicit_tag(701, &inner);
|
||||||
|
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x3D]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enc_explicit_tag_709() {
|
||||||
|
// Tag 709: 0xBF 0x85 0x45
|
||||||
|
let inner = enc_octet_string(&[0x01]);
|
||||||
|
let tagged = enc_explicit_tag(709, &inner);
|
||||||
|
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x45]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_set_of_integer_sorted() {
|
||||||
|
// SET OF INTEGER must sort by encoded bytes
|
||||||
|
let result = build_set_of_integer(&[3, 2]);
|
||||||
|
// Expect sorted: INTEGER 2 before INTEGER 3
|
||||||
|
let expected = enc_set(&[0x02, 0x01, 0x02, 0x02, 0x01, 0x03]);
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_root_of_trust_structure() {
|
||||||
|
let params = make_test_params();
|
||||||
|
let rot = build_root_of_trust(¶ms);
|
||||||
|
// Should be a SEQUENCE (0x30)
|
||||||
|
assert_eq!(rot[0], 0x30);
|
||||||
|
// Find BOOLEAN TRUE inside
|
||||||
|
let rot_inner = &rot[2..]; // skip tag+length
|
||||||
|
// First: OCTET STRING (32 bytes boot key)
|
||||||
|
assert_eq!(rot_inner[0], 0x04);
|
||||||
|
assert_eq!(rot_inner[1], 0x20); // 32 bytes
|
||||||
|
// After boot key (34 bytes): BOOLEAN TRUE
|
||||||
|
assert_eq!(rot_inner[34], 0x01); // BOOLEAN tag
|
||||||
|
assert_eq!(rot_inner[35], 0x01); // length 1
|
||||||
|
assert_eq!(rot_inner[36], 0xFF); // TRUE = 0xFF
|
||||||
|
// Then ENUMERATED 0 (verifiedBootState)
|
||||||
|
assert_eq!(rot_inner[37], 0x0A); // ENUMERATED tag, not 0x02
|
||||||
|
assert_eq!(rot_inner[38], 0x01);
|
||||||
|
assert_eq!(rot_inner[39], 0x00);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_do_not_report_omits_fields() {
|
||||||
|
let mut params = make_test_params();
|
||||||
|
params.os_patch_level = DO_NOT_REPORT;
|
||||||
|
params.vendor_patch_level = DO_NOT_REPORT;
|
||||||
|
params.boot_patch_level = DO_NOT_REPORT;
|
||||||
|
let tee = build_tee_enforced(¶ms).unwrap();
|
||||||
|
let hex = hex_string(&tee);
|
||||||
|
// Tags 706, 718, 719 should not appear
|
||||||
|
// Tag 706 = BF 85 42, 718 = BF 85 4E, 719 = BF 85 4F
|
||||||
|
assert!(!hex.contains("bf8542"), "os_patch_level should be omitted");
|
||||||
|
assert!(
|
||||||
|
!hex.contains("bf854e"),
|
||||||
|
"vendor_patch_level should be omitted"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!hex.contains("bf854f"),
|
||||||
|
"boot_patch_level should be omitted"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_key_description_security_level_is_enumerated() {
|
||||||
|
let params = make_test_params();
|
||||||
|
let ext = build_attestation_extension(¶ms).unwrap();
|
||||||
|
// KeyDescription is a SEQUENCE: 0x30 ...
|
||||||
|
assert_eq!(ext[0], 0x30);
|
||||||
|
// Skip SEQUENCE tag + length to get to inner fields
|
||||||
|
let inner = skip_tlv_header(&ext);
|
||||||
|
// Field 0: attestationVersion — INTEGER (0x02)
|
||||||
|
assert_eq!(inner[0], 0x02);
|
||||||
|
let (_, rest) = skip_one_tlv(inner);
|
||||||
|
// Field 1: attestationSecurityLevel — ENUMERATED (0x0A)
|
||||||
|
assert_eq!(rest[0], 0x0A, "attestationSecurityLevel must be ENUMERATED");
|
||||||
|
let (_, rest) = skip_one_tlv(rest);
|
||||||
|
// Field 2: keymintVersion — INTEGER (0x02)
|
||||||
|
assert_eq!(rest[0], 0x02);
|
||||||
|
let (_, rest) = skip_one_tlv(rest);
|
||||||
|
// Field 3: keymintSecurityLevel — ENUMERATED (0x0A)
|
||||||
|
assert_eq!(rest[0], 0x0A, "keymintSecurityLevel must be ENUMERATED");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_authorization_list_sorted_by_tag() {
|
||||||
|
let params = make_test_params();
|
||||||
|
let tee = build_tee_enforced(¶ms).unwrap();
|
||||||
|
let inner = skip_tlv_header(&tee);
|
||||||
|
let tags = extract_tag_numbers(inner);
|
||||||
|
let mut sorted = tags.clone();
|
||||||
|
sorted.sort();
|
||||||
|
assert_eq!(tags, sorted, "AuthorizationList fields must be sorted by tag number");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enforcement_tags_in_software_enforced() {
|
||||||
|
let mut params = make_test_params();
|
||||||
|
params.usage_count_limit = 3;
|
||||||
|
params.unlocked_device_required = true;
|
||||||
|
params.caller_nonce = true;
|
||||||
|
params.active_datetime = 1709913600000;
|
||||||
|
let sw = build_software_enforced(¶ms).unwrap();
|
||||||
|
let inner = skip_tlv_header(&sw);
|
||||||
|
let tags = extract_tag_numbers(inner);
|
||||||
|
assert!(tags.contains(&303), "CALLER_NONCE (303) must be in softwareEnforced");
|
||||||
|
assert!(tags.contains(&400), "ACTIVE_DATETIME (400) must be in softwareEnforced");
|
||||||
|
assert!(tags.contains(&405), "USAGE_COUNT_LIMIT (405) must be in softwareEnforced");
|
||||||
|
assert!(tags.contains(&509), "UNLOCKED_DEVICE_REQUIRED (509) must be in softwareEnforced");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_auth_required_conditional() {
|
||||||
|
let mut params = make_test_params();
|
||||||
|
params.no_auth_required = false;
|
||||||
|
let tee = build_tee_enforced(¶ms).unwrap();
|
||||||
|
let inner = skip_tlv_header(&tee);
|
||||||
|
let tags = extract_tag_numbers(inner);
|
||||||
|
assert!(!tags.contains(&503), "NO_AUTH_REQUIRED (503) must be absent when false");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enforcement_tags_omitted_when_unset() {
|
||||||
|
let params = make_test_params();
|
||||||
|
let sw = build_software_enforced(¶ms).unwrap();
|
||||||
|
let inner = skip_tlv_header(&sw);
|
||||||
|
let tags = extract_tag_numbers(inner);
|
||||||
|
assert!(!tags.contains(&303), "CALLER_NONCE should be absent when false");
|
||||||
|
assert!(!tags.contains(&400), "ACTIVE_DATETIME should be absent when -1");
|
||||||
|
assert!(!tags.contains(&405), "USAGE_COUNT_LIMIT should be absent when -1");
|
||||||
|
assert!(!tags.contains(&509), "UNLOCKED_DEVICE_REQUIRED should be absent when false");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_full_extension_roundtrip() {
|
||||||
|
let params = make_test_params();
|
||||||
|
let ext = build_attestation_extension(¶ms).unwrap();
|
||||||
|
// Must be valid DER: starts with SEQUENCE tag
|
||||||
|
assert_eq!(ext[0], 0x30);
|
||||||
|
// Length must account for all inner bytes
|
||||||
|
let (header_len, total_content_len) = parse_tlv_lengths(&ext);
|
||||||
|
assert_eq!(ext.len(), header_len + total_content_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- test helpers ---
|
||||||
|
|
||||||
|
fn make_test_params() -> CertGenParams {
|
||||||
|
CertGenParams {
|
||||||
|
algorithm: Algorithm::Ec,
|
||||||
|
key_size: 256,
|
||||||
|
ec_curve: Some(EcCurve::P256),
|
||||||
|
rsa_public_exponent: 0,
|
||||||
|
attestation_challenge: Some(vec![0xAB; 32]),
|
||||||
|
purposes: vec![2, 3],
|
||||||
|
digests: vec![4],
|
||||||
|
cert_serial: None,
|
||||||
|
cert_subject: None,
|
||||||
|
cert_not_before: -1,
|
||||||
|
cert_not_after: -1,
|
||||||
|
keybox_private_key: vec![],
|
||||||
|
keybox_cert_chain: vec![],
|
||||||
|
security_level: 1,
|
||||||
|
attest_version: 200,
|
||||||
|
keymaster_version: 200,
|
||||||
|
os_version: 140000,
|
||||||
|
os_patch_level: 202503,
|
||||||
|
vendor_patch_level: 20250301,
|
||||||
|
boot_patch_level: 20250301,
|
||||||
|
boot_key: vec![0x01; 32],
|
||||||
|
boot_hash: vec![0x02; 32],
|
||||||
|
creation_datetime: 1709913600000,
|
||||||
|
attestation_application_id: vec![0xDE, 0xAD],
|
||||||
|
module_hash: None,
|
||||||
|
id_brand: None,
|
||||||
|
id_device: None,
|
||||||
|
id_product: None,
|
||||||
|
id_serial: None,
|
||||||
|
id_imei: None,
|
||||||
|
id_meid: None,
|
||||||
|
id_manufacturer: None,
|
||||||
|
id_model: None,
|
||||||
|
id_second_imei: None,
|
||||||
|
active_datetime: -1,
|
||||||
|
origination_expire_datetime: -1,
|
||||||
|
usage_expire_datetime: -1,
|
||||||
|
usage_count_limit: -1,
|
||||||
|
caller_nonce: false,
|
||||||
|
unlocked_device_required: false,
|
||||||
|
no_auth_required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_string(data: &[u8]) -> String {
|
||||||
|
data.iter().map(|b| format!("{:02x}", b)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skip_tlv_header(data: &[u8]) -> &[u8] {
|
||||||
|
let (header_len, _) = parse_tlv_lengths(data);
|
||||||
|
&data[header_len..]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skip_one_tlv(data: &[u8]) -> (usize, &[u8]) {
|
||||||
|
let (header_len, content_len) = parse_tlv_lengths(data);
|
||||||
|
let total = header_len + content_len;
|
||||||
|
(total, &data[total..])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_tlv_lengths(data: &[u8]) -> (usize, usize) {
|
||||||
|
// Returns (header_bytes, content_bytes)
|
||||||
|
let tag_len = tag_byte_len(data);
|
||||||
|
let len_start = tag_len;
|
||||||
|
if data[len_start] < 0x80 {
|
||||||
|
(len_start + 1, data[len_start] as usize)
|
||||||
|
} else {
|
||||||
|
let num_len_bytes = (data[len_start] & 0x7F) as usize;
|
||||||
|
let mut content_len = 0usize;
|
||||||
|
for i in 0..num_len_bytes {
|
||||||
|
content_len = (content_len << 8) | data[len_start + 1 + i] as usize;
|
||||||
|
}
|
||||||
|
(len_start + 1 + num_len_bytes, content_len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tag_byte_len(data: &[u8]) -> usize {
|
||||||
|
if data[0] & 0x1F != 0x1F {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
let mut i = 1;
|
||||||
|
while data[i] & 0x80 != 0 {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_tag_numbers(mut data: &[u8]) -> Vec<u32> {
|
||||||
|
let mut tags = Vec::new();
|
||||||
|
while !data.is_empty() {
|
||||||
|
let tag = read_tag_number(data);
|
||||||
|
tags.push(tag);
|
||||||
|
let (_, rest) = skip_one_tlv(data);
|
||||||
|
data = rest;
|
||||||
|
}
|
||||||
|
tags
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_tag_number(data: &[u8]) -> u32 {
|
||||||
|
if data[0] & 0x1F != 0x1F {
|
||||||
|
(data[0] & 0x1F) as u32
|
||||||
|
} else {
|
||||||
|
let mut val = 0u32;
|
||||||
|
let mut i = 1;
|
||||||
|
loop {
|
||||||
|
val = (val << 7) | (data[i] & 0x7F) as u32;
|
||||||
|
if data[i] & 0x80 == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,523 @@
|
|||||||
|
use crate::error::{CertGenError, Result};
|
||||||
|
use crate::keybox::ParsedKeybox;
|
||||||
|
use crate::types::{Algorithm, CertGenParams, GeneratedKeyPair};
|
||||||
|
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
const ATTESTATION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 11129, 2, 1, 17];
|
||||||
|
|
||||||
|
// Signature algorithm OIDs
|
||||||
|
const OID_SHA256_WITH_ECDSA: &[u64] = &[1, 2, 840, 10045, 4, 3, 2];
|
||||||
|
const OID_SHA384_WITH_ECDSA: &[u64] = &[1, 2, 840, 10045, 4, 3, 3];
|
||||||
|
const OID_SHA256_WITH_RSA: &[u64] = &[1, 2, 840, 113549, 1, 1, 11];
|
||||||
|
|
||||||
|
// Extension OIDs
|
||||||
|
const OID_KEY_USAGE: &[u64] = &[2, 5, 29, 15];
|
||||||
|
|
||||||
|
pub fn build_certificate_chain(
|
||||||
|
key_pair: &GeneratedKeyPair,
|
||||||
|
attestation_ext_der: &[u8],
|
||||||
|
keybox: &ParsedKeybox,
|
||||||
|
params: &CertGenParams,
|
||||||
|
) -> Result<Vec<Vec<u8>>> {
|
||||||
|
let leaf_der = build_leaf_cert(key_pair, attestation_ext_der, keybox, params)?;
|
||||||
|
|
||||||
|
let mut chain = Vec::with_capacity(1 + keybox.cert_chain_ders.len());
|
||||||
|
chain.push(leaf_der);
|
||||||
|
for cert_der in &keybox.cert_chain_ders {
|
||||||
|
chain.push(cert_der.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(chain)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_leaf_cert(
|
||||||
|
key_pair: &GeneratedKeyPair,
|
||||||
|
attestation_ext_der: &[u8],
|
||||||
|
keybox: &ParsedKeybox,
|
||||||
|
params: &CertGenParams,
|
||||||
|
) -> Result<Vec<u8>> {
|
||||||
|
let spki_der = extract_spki_from_pkcs8(&key_pair.private_key_pkcs8)?;
|
||||||
|
let sig_alg_der = signature_algorithm_for_signing_key(&keybox.signing_key_der, params.algorithm)?;
|
||||||
|
|
||||||
|
// Serial number
|
||||||
|
let serial_bytes = if let Some(ref serial) = params.cert_serial {
|
||||||
|
serial.clone()
|
||||||
|
} else {
|
||||||
|
vec![1u8]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subject DN
|
||||||
|
let subject_dn_der = if let Some(ref subject) = params.cert_subject {
|
||||||
|
subject.clone()
|
||||||
|
} else {
|
||||||
|
encode_simple_cn_dn("Android Keystore Key")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validity
|
||||||
|
let not_before = timestamp_to_datetime(params.cert_not_before)?;
|
||||||
|
let not_after = if params.cert_not_after == -1 {
|
||||||
|
OffsetDateTime::from_unix_timestamp(keybox.leaf_not_after)
|
||||||
|
.unwrap_or_else(|_| OffsetDateTime::now_utc() + time::Duration::days(365))
|
||||||
|
} else {
|
||||||
|
timestamp_to_datetime(params.cert_not_after)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extensions
|
||||||
|
let extensions_der = build_extensions(attestation_ext_der, ¶ms.purposes)?;
|
||||||
|
|
||||||
|
// TBS Certificate
|
||||||
|
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);
|
||||||
|
|
||||||
|
let tbs_der = encode_der_sequence(&[
|
||||||
|
&version_der,
|
||||||
|
&serial_der,
|
||||||
|
&sig_alg_der,
|
||||||
|
&keybox.issuer_dn_der, // RAW bytes — no re-encoding
|
||||||
|
&validity_der,
|
||||||
|
&subject_dn_der,
|
||||||
|
&spki_der,
|
||||||
|
&extensions_tagged,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sign the TBS
|
||||||
|
let signature_bytes = sign_tbs(&tbs_der, &keybox.signing_key_der, params.algorithm)?;
|
||||||
|
let signature_bit_string = encode_der_bit_string(&signature_bytes);
|
||||||
|
|
||||||
|
// Final certificate: SEQUENCE { TBS, sigAlgorithm, signature }
|
||||||
|
let cert_der = encode_der_sequence(&[
|
||||||
|
&tbs_der,
|
||||||
|
&sig_alg_der,
|
||||||
|
&signature_bit_string,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Ok(cert_der)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_tbs(tbs_der: &[u8], signing_key_der: &[u8], algorithm: Algorithm) -> Result<Vec<u8>> {
|
||||||
|
match algorithm {
|
||||||
|
Algorithm::Ec => sign_tbs_ec(tbs_der, signing_key_der),
|
||||||
|
Algorithm::Rsa => sign_tbs_rsa(tbs_der, signing_key_der),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_tbs_ec(tbs_der: &[u8], signing_key_der: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
// Determine EC curve from the signing key's PKCS8 AlgorithmIdentifier
|
||||||
|
let alg = detect_ec_signing_algorithm(signing_key_der)?;
|
||||||
|
|
||||||
|
let key_pair = ring::signature::EcdsaKeyPair::from_pkcs8(alg, signing_key_der, &ring::rand::SystemRandom::new())
|
||||||
|
.map_err(|e| CertGenError::SigningFailed(format!("EC key parse: {e}")))?;
|
||||||
|
|
||||||
|
let rng = ring::rand::SystemRandom::new();
|
||||||
|
let sig = key_pair.sign(&rng, tbs_der)
|
||||||
|
.map_err(|e| CertGenError::SigningFailed(format!("EC sign: {e}")))?;
|
||||||
|
|
||||||
|
Ok(sig.as_ref().to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_ec_signing_algorithm(pkcs8_der: &[u8]) -> Result<&'static ring::signature::EcdsaSigningAlgorithm> {
|
||||||
|
use der::Decode;
|
||||||
|
let info = pkcs8::PrivateKeyInfo::from_der(pkcs8_der)
|
||||||
|
.map_err(|e| CertGenError::SigningFailed(format!("PKCS8 parse: {e}")))?;
|
||||||
|
|
||||||
|
let params_oid = info.algorithm.parameters_oid()
|
||||||
|
.map_err(|e| CertGenError::SigningFailed(format!("EC curve OID: {e}")))?;
|
||||||
|
|
||||||
|
let p256_oid: const_oid::ObjectIdentifier = "1.2.840.10045.3.1.7".parse()
|
||||||
|
.map_err(|_| CertGenError::SigningFailed("OID parse".into()))?;
|
||||||
|
let p384_oid: const_oid::ObjectIdentifier = "1.3.132.0.34".parse()
|
||||||
|
.map_err(|_| CertGenError::SigningFailed("OID parse".into()))?;
|
||||||
|
|
||||||
|
if params_oid == p256_oid {
|
||||||
|
Ok(&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING)
|
||||||
|
} else if params_oid == p384_oid {
|
||||||
|
Ok(&ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING)
|
||||||
|
} else {
|
||||||
|
Err(CertGenError::SigningFailed(format!("unsupported EC curve OID: {params_oid}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_tbs_rsa(tbs_der: &[u8], signing_key_der: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
use rsa::pkcs8::DecodePrivateKey;
|
||||||
|
use rsa::signature::{SignatureEncoding, SignerMut};
|
||||||
|
use rsa::pkcs1v15::SigningKey;
|
||||||
|
use rsa::sha2::Sha256;
|
||||||
|
|
||||||
|
let private_key = rsa::RsaPrivateKey::from_pkcs8_der(signing_key_der)
|
||||||
|
.map_err(|e| CertGenError::SigningFailed(format!("RSA key parse: {e}")))?;
|
||||||
|
|
||||||
|
let mut signing_key = SigningKey::<Sha256>::new(private_key);
|
||||||
|
let signature = signing_key.sign(tbs_der);
|
||||||
|
|
||||||
|
Ok(signature.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature_algorithm_for_signing_key(signing_key_der: &[u8], algorithm: Algorithm) -> Result<Vec<u8>> {
|
||||||
|
match algorithm {
|
||||||
|
Algorithm::Ec => {
|
||||||
|
let ring_alg = detect_ec_signing_algorithm(signing_key_der)?;
|
||||||
|
// Determine OID from the algorithm used
|
||||||
|
let oid = if std::ptr::eq(ring_alg, &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING) {
|
||||||
|
OID_SHA384_WITH_ECDSA
|
||||||
|
} else {
|
||||||
|
OID_SHA256_WITH_ECDSA
|
||||||
|
};
|
||||||
|
let oid_der = encode_der_oid(oid);
|
||||||
|
Ok(encode_der_sequence(&[&oid_der]))
|
||||||
|
}
|
||||||
|
Algorithm::Rsa => {
|
||||||
|
let oid_der = encode_der_oid(OID_SHA256_WITH_RSA);
|
||||||
|
let null_der = vec![0x05, 0x00];
|
||||||
|
Ok(encode_der_sequence(&[&oid_der, &null_der]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_spki_from_pkcs8(pkcs8_der: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
use der::Decode;
|
||||||
|
|
||||||
|
let info = pkcs8::PrivateKeyInfo::from_der(pkcs8_der)
|
||||||
|
.map_err(|e| CertGenError::CertBuildFailed(format!("PKCS8 parse for SPKI: {e}")))?;
|
||||||
|
|
||||||
|
// Reconstruct SPKI from AlgorithmIdentifier + public key
|
||||||
|
// For EC: derive public key from private key via ring
|
||||||
|
// For RSA: derive from rsa crate
|
||||||
|
let alg_id_oid = info.algorithm.oid;
|
||||||
|
let ec_oid: const_oid::ObjectIdentifier = "1.2.840.10045.2.1".parse()
|
||||||
|
.map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?;
|
||||||
|
|
||||||
|
if alg_id_oid == ec_oid {
|
||||||
|
extract_ec_spki(pkcs8_der, &info)
|
||||||
|
} else {
|
||||||
|
extract_rsa_spki(pkcs8_der)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_ec_spki(pkcs8_der: &[u8], info: &pkcs8::PrivateKeyInfo) -> Result<Vec<u8>> {
|
||||||
|
use ring::signature::KeyPair as _;
|
||||||
|
let params_oid = info.algorithm.parameters_oid()
|
||||||
|
.map_err(|e| CertGenError::CertBuildFailed(format!("EC curve OID: {e}")))?;
|
||||||
|
|
||||||
|
let p256_oid: const_oid::ObjectIdentifier = "1.2.840.10045.3.1.7".parse()
|
||||||
|
.map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?;
|
||||||
|
let p384_oid: const_oid::ObjectIdentifier = "1.3.132.0.34".parse()
|
||||||
|
.map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?;
|
||||||
|
|
||||||
|
let (ring_alg, curve_oid_der): (&ring::signature::EcdsaSigningAlgorithm, Vec<u8>) = if params_oid == p256_oid {
|
||||||
|
(&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, encode_der_oid(&[1, 2, 840, 10045, 3, 1, 7]))
|
||||||
|
} else if params_oid == p384_oid {
|
||||||
|
(&ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING, encode_der_oid(&[1, 3, 132, 0, 34]))
|
||||||
|
} else {
|
||||||
|
return Err(CertGenError::CertBuildFailed(format!("unsupported EC curve: {params_oid}")));
|
||||||
|
};
|
||||||
|
|
||||||
|
let kp = ring::signature::EcdsaKeyPair::from_pkcs8(
|
||||||
|
ring_alg,
|
||||||
|
pkcs8_der,
|
||||||
|
&ring::rand::SystemRandom::new(),
|
||||||
|
).map_err(|e| CertGenError::CertBuildFailed(format!("EC key parse: {e}")))?;
|
||||||
|
let ec_kp = kp.public_key().as_ref().to_vec();
|
||||||
|
|
||||||
|
// SPKI = SEQUENCE { AlgorithmIdentifier, BIT STRING (public key) }
|
||||||
|
// AlgorithmIdentifier = SEQUENCE { ecPublicKey OID, curve OID }
|
||||||
|
let ec_oid_der = encode_der_oid(&[1, 2, 840, 10045, 2, 1]);
|
||||||
|
let alg_id = encode_der_sequence(&[&ec_oid_der, &curve_oid_der]);
|
||||||
|
let pub_key_bits = encode_der_bit_string(&ec_kp);
|
||||||
|
|
||||||
|
Ok(encode_der_sequence(&[&alg_id, &pub_key_bits]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_rsa_spki(pkcs8_der: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
use rsa::pkcs8::DecodePrivateKey;
|
||||||
|
|
||||||
|
let private_key = rsa::RsaPrivateKey::from_pkcs8_der(pkcs8_der)
|
||||||
|
.map_err(|e| CertGenError::CertBuildFailed(format!("RSA key parse: {e}")))?;
|
||||||
|
|
||||||
|
let public_key = rsa::RsaPublicKey::from(&private_key);
|
||||||
|
|
||||||
|
// Encode RSA public key as DER: SEQUENCE { n INTEGER, e INTEGER }
|
||||||
|
use rsa::traits::PublicKeyParts;
|
||||||
|
let n_bytes = public_key.n().to_bytes_be();
|
||||||
|
let e_bytes = public_key.e().to_bytes_be();
|
||||||
|
let rsa_pub_der = encode_der_sequence(&[
|
||||||
|
&encode_der_integer(&n_bytes),
|
||||||
|
&encode_der_integer(&e_bytes),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// SPKI = SEQUENCE { AlgorithmIdentifier, BIT STRING (DER-encoded RSAPublicKey) }
|
||||||
|
let rsa_oid_der = encode_der_oid(&[1, 2, 840, 113549, 1, 1, 1]);
|
||||||
|
let null_der = vec![0x05, 0x00];
|
||||||
|
let alg_id = encode_der_sequence(&[&rsa_oid_der, &null_der]);
|
||||||
|
let pub_key_bits = encode_der_bit_string(&rsa_pub_der);
|
||||||
|
|
||||||
|
Ok(encode_der_sequence(&[&alg_id, &pub_key_bits]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_extensions(attestation_ext_der: &[u8], purposes: &[i32]) -> Result<Vec<u8>> {
|
||||||
|
let mut extensions: Vec<Vec<u8>> = Vec::new();
|
||||||
|
|
||||||
|
// KeyUsage extension (critical)
|
||||||
|
let ku_byte = map_key_usage_byte(purposes);
|
||||||
|
if ku_byte != 0 {
|
||||||
|
let ku_ext = build_key_usage_extension(ku_byte);
|
||||||
|
extensions.push(ku_ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attestation extension (non-critical)
|
||||||
|
let attest_ext = build_extension(&encode_der_oid(ATTESTATION_OID), false, attestation_ext_der);
|
||||||
|
extensions.push(attest_ext);
|
||||||
|
|
||||||
|
Ok(encode_der_sequence_of(&extensions))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_extension(oid_der: &[u8], critical: bool, value_der: &[u8]) -> Vec<u8> {
|
||||||
|
let value_octet_string = encode_der_octet_string(value_der);
|
||||||
|
if critical {
|
||||||
|
let critical_der = encode_der_boolean(true);
|
||||||
|
encode_der_sequence(&[oid_der, &critical_der, &value_octet_string])
|
||||||
|
} else {
|
||||||
|
encode_der_sequence(&[oid_der, &value_octet_string])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_key_usage_extension(ku_byte: u8) -> Vec<u8> {
|
||||||
|
// DER BIT STRING: minimal encoding requires trimming trailing zero bits
|
||||||
|
let unused_bits = ku_byte.trailing_zeros().min(7) as u8;
|
||||||
|
|
||||||
|
// BIT STRING = tag (0x03) + length(2) + unused_bits + byte
|
||||||
|
let bit_string = vec![0x03, 0x02, unused_bits, ku_byte];
|
||||||
|
|
||||||
|
let oid_der = encode_der_oid(OID_KEY_USAGE);
|
||||||
|
let value_octet_string = encode_der_octet_string(&bit_string);
|
||||||
|
let critical_der = encode_der_boolean(true);
|
||||||
|
|
||||||
|
encode_der_sequence(&[&oid_der, &critical_der, &value_octet_string])
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyUsage BIT STRING byte layout (RFC 5280):
|
||||||
|
// byte[0] bit 7 = digitalSignature (0x80)
|
||||||
|
// byte[0] bit 6 = nonRepudiation (0x40)
|
||||||
|
// byte[0] bit 5 = keyEncipherment (0x20)
|
||||||
|
// byte[0] bit 4 = dataEncipherment (0x10)
|
||||||
|
// byte[0] bit 3 = keyAgreement (0x08)
|
||||||
|
// byte[0] bit 2 = keyCertSign (0x04)
|
||||||
|
// byte[0] bit 1 = cRLSign (0x02)
|
||||||
|
// byte[0] bit 0 = encipherOnly (0x01)
|
||||||
|
// byte[1] bit 7 = decipherOnly (0x80)
|
||||||
|
fn map_key_usage_byte(purposes: &[i32]) -> u8 {
|
||||||
|
let mut bits: u8 = 0;
|
||||||
|
for &purpose in purposes {
|
||||||
|
match purpose {
|
||||||
|
2 => bits |= 0x80, // SIGN -> digitalSignature
|
||||||
|
1 => bits |= 0x10, // DECRYPT -> dataEncipherment
|
||||||
|
5 => bits |= 0x20, // WRAP_KEY -> keyEncipherment
|
||||||
|
6 => bits |= 0x08, // AGREE_KEY -> keyAgreement
|
||||||
|
7 => bits |= 0x04, // ATTEST_KEY -> keyCertSign
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bits
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_validity(not_before: &OffsetDateTime, not_after: &OffsetDateTime) -> Vec<u8> {
|
||||||
|
let nb = encode_time(not_before);
|
||||||
|
let na = encode_time(not_after);
|
||||||
|
encode_der_sequence(&[&nb, &na])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_time(dt: &OffsetDateTime) -> Vec<u8> {
|
||||||
|
let year = dt.year();
|
||||||
|
if (1950..2050).contains(&year) {
|
||||||
|
encode_utctime(dt)
|
||||||
|
} else {
|
||||||
|
encode_gentime(dt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_utctime(dt: &OffsetDateTime) -> Vec<u8> {
|
||||||
|
// UTCTime: YYMMDDHHMMSSZ
|
||||||
|
let year = dt.year() % 100;
|
||||||
|
let s = format!(
|
||||||
|
"{:02}{:02}{:02}{:02}{:02}{:02}Z",
|
||||||
|
year, dt.month() as u8, dt.day(), dt.hour(), dt.minute(), dt.second()
|
||||||
|
);
|
||||||
|
let mut out = Vec::with_capacity(2 + s.len());
|
||||||
|
out.push(0x17); // UTCTime tag
|
||||||
|
out.extend_from_slice(&encode_der_length_bytes(s.len()));
|
||||||
|
out.extend_from_slice(s.as_bytes());
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_gentime(dt: &OffsetDateTime) -> Vec<u8> {
|
||||||
|
// GeneralizedTime: YYYYMMDDHHMMSSZ
|
||||||
|
let s = format!(
|
||||||
|
"{:04}{:02}{:02}{:02}{:02}{:02}Z",
|
||||||
|
dt.year(), dt.month() as u8, dt.day(), dt.hour(), dt.minute(), dt.second()
|
||||||
|
);
|
||||||
|
let mut out = Vec::with_capacity(2 + s.len());
|
||||||
|
out.push(0x18); // GeneralizedTime tag
|
||||||
|
out.extend_from_slice(&encode_der_length_bytes(s.len()));
|
||||||
|
out.extend_from_slice(s.as_bytes());
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_simple_cn_dn(cn: &str) -> Vec<u8> {
|
||||||
|
// Name = SEQUENCE OF RelativeDistinguishedName
|
||||||
|
// RDN = SET OF AttributeTypeAndValue
|
||||||
|
// ATV = SEQUENCE { OID, UTF8String }
|
||||||
|
let cn_oid = encode_der_oid(&[2, 5, 4, 3]);
|
||||||
|
let cn_value = encode_der_utf8string(cn);
|
||||||
|
let atv = encode_der_sequence(&[&cn_oid, &cn_value]);
|
||||||
|
let rdn = encode_der_set(&[&atv]);
|
||||||
|
encode_der_sequence(&[&rdn])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timestamp_to_datetime(ts: i64) -> Result<OffsetDateTime> {
|
||||||
|
if ts == -1 {
|
||||||
|
return Ok(OffsetDateTime::now_utc());
|
||||||
|
}
|
||||||
|
OffsetDateTime::from_unix_timestamp(ts / 1000)
|
||||||
|
.map_err(|e| CertGenError::CertBuildFailed(format!("invalid timestamp {ts}: {e}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// DER encoding primitives
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn encode_der_length_bytes(len: usize) -> Vec<u8> {
|
||||||
|
if len < 0x80 {
|
||||||
|
vec![len as u8]
|
||||||
|
} else if len <= 0xFF {
|
||||||
|
vec![0x81, len as u8]
|
||||||
|
} else if len <= 0xFFFF {
|
||||||
|
vec![0x82, (len >> 8) as u8, len as u8]
|
||||||
|
} else if len <= 0xFF_FFFF {
|
||||||
|
vec![0x83, (len >> 16) as u8, (len >> 8) as u8, len as u8]
|
||||||
|
} else {
|
||||||
|
vec![0x84, (len >> 24) as u8, (len >> 16) as u8, (len >> 8) as u8, len as u8]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_der_tag_length_value(tag: u8, content: &[u8]) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(1 + 4 + content.len());
|
||||||
|
out.push(tag);
|
||||||
|
out.extend_from_slice(&encode_der_length_bytes(content.len()));
|
||||||
|
out.extend_from_slice(content);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_der_sequence(items: &[&[u8]]) -> Vec<u8> {
|
||||||
|
let total: usize = items.iter().map(|i| i.len()).sum();
|
||||||
|
let mut content = Vec::with_capacity(total);
|
||||||
|
for item in items {
|
||||||
|
content.extend_from_slice(item);
|
||||||
|
}
|
||||||
|
encode_der_tag_length_value(0x30, &content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_der_sequence_of(items: &[Vec<u8>]) -> Vec<u8> {
|
||||||
|
let total: usize = items.iter().map(|i| i.len()).sum();
|
||||||
|
let mut content = Vec::with_capacity(total);
|
||||||
|
for item in items {
|
||||||
|
content.extend_from_slice(item);
|
||||||
|
}
|
||||||
|
encode_der_tag_length_value(0x30, &content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_der_set(items: &[&[u8]]) -> Vec<u8> {
|
||||||
|
let total: usize = items.iter().map(|i| i.len()).sum();
|
||||||
|
let mut content = Vec::with_capacity(total);
|
||||||
|
for item in items {
|
||||||
|
content.extend_from_slice(item);
|
||||||
|
}
|
||||||
|
encode_der_tag_length_value(0x31, &content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_der_explicit_tag(tag_num: u8, content: &[u8]) -> Vec<u8> {
|
||||||
|
encode_der_tag_length_value(0xA0 | tag_num, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_der_integer(value: &[u8]) -> Vec<u8> {
|
||||||
|
// DER INTEGER must have minimal encoding and leading 0x00 if high bit set
|
||||||
|
if value.is_empty() {
|
||||||
|
return encode_der_tag_length_value(0x02, &[0x00]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip leading zeros (but keep at least one byte)
|
||||||
|
let mut start = 0;
|
||||||
|
while start < value.len() - 1 && value[start] == 0 {
|
||||||
|
start += 1;
|
||||||
|
}
|
||||||
|
let trimmed = &value[start..];
|
||||||
|
|
||||||
|
// Add leading 0x00 if high bit is set (positive integer)
|
||||||
|
if trimmed[0] & 0x80 != 0 {
|
||||||
|
let mut padded = Vec::with_capacity(1 + trimmed.len());
|
||||||
|
padded.push(0x00);
|
||||||
|
padded.extend_from_slice(trimmed);
|
||||||
|
encode_der_tag_length_value(0x02, &padded)
|
||||||
|
} else {
|
||||||
|
encode_der_tag_length_value(0x02, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_der_bit_string(bits: &[u8]) -> Vec<u8> {
|
||||||
|
// BIT STRING: tag 0x03, length, unused_bits (0), content
|
||||||
|
let mut content = Vec::with_capacity(1 + bits.len());
|
||||||
|
content.push(0x00); // 0 unused bits
|
||||||
|
content.extend_from_slice(bits);
|
||||||
|
encode_der_tag_length_value(0x03, &content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_der_octet_string(content: &[u8]) -> Vec<u8> {
|
||||||
|
encode_der_tag_length_value(0x04, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_der_utf8string(s: &str) -> Vec<u8> {
|
||||||
|
encode_der_tag_length_value(0x0C, s.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_der_boolean(val: bool) -> Vec<u8> {
|
||||||
|
encode_der_tag_length_value(0x01, &[if val { 0xFF } else { 0x00 }])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_der_oid(components: &[u64]) -> Vec<u8> {
|
||||||
|
if components.len() < 2 {
|
||||||
|
return encode_der_tag_length_value(0x06, &[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut content = Vec::new();
|
||||||
|
// First two components encoded as 40 * c[0] + c[1]
|
||||||
|
content.push((components[0] * 40 + components[1]) as u8);
|
||||||
|
|
||||||
|
for &c in &components[2..] {
|
||||||
|
encode_oid_subidentifier(&mut content, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
encode_der_tag_length_value(0x06, &content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_oid_subidentifier(buf: &mut Vec<u8>, mut value: u64) {
|
||||||
|
if value == 0 {
|
||||||
|
buf.push(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode in base-128 with continuation bits
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
while value > 0 {
|
||||||
|
bytes.push((value & 0x7F) as u8);
|
||||||
|
value >>= 7;
|
||||||
|
}
|
||||||
|
bytes.reverse();
|
||||||
|
|
||||||
|
// Set high bit on all but the last byte
|
||||||
|
for i in 0..bytes.len() - 1 {
|
||||||
|
bytes[i] |= 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.extend_from_slice(&bytes);
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CertGenError {
|
||||||
|
Jni(String),
|
||||||
|
NullParam(&'static str),
|
||||||
|
UnsupportedAlgorithm(i32),
|
||||||
|
UnsupportedEcCurve(i32),
|
||||||
|
KeyGenFailed(String),
|
||||||
|
CertBuildFailed(String),
|
||||||
|
KeyboxParseFailed(String),
|
||||||
|
AttestationBuildFailed(String),
|
||||||
|
DerError(der::Error),
|
||||||
|
EmptyKeyboxChain,
|
||||||
|
ChallengeTooLong(usize),
|
||||||
|
InvalidParameter(String),
|
||||||
|
SigningFailed(String),
|
||||||
|
SerializationFailed(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CertGenError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Jni(msg) => write!(f, "JNI error: {}", msg),
|
||||||
|
Self::NullParam(name) => write!(f, "null required parameter: {}", name),
|
||||||
|
Self::UnsupportedAlgorithm(v) => write!(f, "unsupported algorithm: {}", v),
|
||||||
|
Self::UnsupportedEcCurve(v) => write!(f, "unsupported EC curve: {}", v),
|
||||||
|
Self::KeyGenFailed(msg) => write!(f, "key generation failed: {}", msg),
|
||||||
|
Self::CertBuildFailed(msg) => write!(f, "certificate build failed: {}", msg),
|
||||||
|
Self::KeyboxParseFailed(msg) => write!(f, "keybox parse failed: {}", msg),
|
||||||
|
Self::AttestationBuildFailed(msg) => write!(f, "attestation build failed: {}", msg),
|
||||||
|
Self::DerError(e) => write!(f, "DER error: {}", e),
|
||||||
|
Self::EmptyKeyboxChain => write!(f, "keybox certificate chain is empty"),
|
||||||
|
Self::ChallengeTooLong(len) => write!(f, "attestation challenge too long: {} bytes (max 128)", len),
|
||||||
|
Self::InvalidParameter(msg) => write!(f, "invalid parameter: {}", msg),
|
||||||
|
Self::SigningFailed(msg) => write!(f, "signing failed: {}", msg),
|
||||||
|
Self::SerializationFailed(msg) => write!(f, "serialization failed: {}", msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for CertGenError {}
|
||||||
|
|
||||||
|
impl From<jni::errors::Error> for CertGenError {
|
||||||
|
fn from(e: jni::errors::Error) -> Self {
|
||||||
|
Self::Jni(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<der::Error> for CertGenError {
|
||||||
|
fn from(e: der::Error) -> Self {
|
||||||
|
Self::DerError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ring::error::Unspecified> for CertGenError {
|
||||||
|
fn from(e: ring::error::Unspecified) -> Self {
|
||||||
|
Self::KeyGenFailed(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ring::error::KeyRejected> for CertGenError {
|
||||||
|
fn from(e: ring::error::KeyRejected) -> Self {
|
||||||
|
Self::KeyGenFailed(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rsa::Error> for CertGenError {
|
||||||
|
fn from(e: rsa::Error) -> Self {
|
||||||
|
Self::KeyGenFailed(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, CertGenError>;
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
use crate::error::{CertGenError, Result};
|
||||||
|
use der::{Decode, Encode};
|
||||||
|
use x509_cert::Certificate;
|
||||||
|
|
||||||
|
pub struct ParsedKeybox {
|
||||||
|
pub signing_key_der: Vec<u8>,
|
||||||
|
pub issuer_dn_der: Vec<u8>,
|
||||||
|
pub cert_chain_ders: Vec<Vec<u8>>,
|
||||||
|
pub leaf_not_after: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_keybox(cert_chain_bytes: &[u8], private_key_bytes: &[u8]) -> Result<ParsedKeybox> {
|
||||||
|
let certs = split_der_certificates(cert_chain_bytes)?;
|
||||||
|
if certs.is_empty() {
|
||||||
|
return Err(CertGenError::KeyboxParseFailed("no certificates found".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let leaf = Certificate::from_der(&certs[0])
|
||||||
|
.map_err(|e| CertGenError::KeyboxParseFailed(format!("leaf cert parse: {e}")))?;
|
||||||
|
|
||||||
|
let issuer_dn_der = leaf.tbs_certificate.subject.to_der()
|
||||||
|
.map_err(|e| CertGenError::KeyboxParseFailed(format!("subject DN encode: {e}")))?;
|
||||||
|
|
||||||
|
let not_after = leaf.tbs_certificate.validity.not_after;
|
||||||
|
let leaf_not_after = not_after.to_unix_duration().as_secs() as i64;
|
||||||
|
|
||||||
|
Ok(ParsedKeybox {
|
||||||
|
signing_key_der: private_key_bytes.to_vec(),
|
||||||
|
issuer_dn_der,
|
||||||
|
cert_chain_ders: certs,
|
||||||
|
leaf_not_after,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_der_certificates(data: &[u8]) -> Result<Vec<Vec<u8>>> {
|
||||||
|
let mut certs = Vec::new();
|
||||||
|
let mut offset = 0;
|
||||||
|
|
||||||
|
while offset < data.len() {
|
||||||
|
if data[offset] != 0x30 {
|
||||||
|
return Err(CertGenError::KeyboxParseFailed(
|
||||||
|
format!("expected SEQUENCE tag 0x30 at offset {offset}, got 0x{:02x}", data[offset])
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (content_len, header_len) = parse_der_length(&data[offset + 1..])?;
|
||||||
|
let total_len = 1 + header_len + content_len;
|
||||||
|
|
||||||
|
if offset + total_len > data.len() {
|
||||||
|
return Err(CertGenError::KeyboxParseFailed(
|
||||||
|
format!("cert at offset {offset} extends beyond buffer: need {total_len}, have {}", data.len() - offset)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
certs.push(data[offset..offset + total_len].to_vec());
|
||||||
|
offset += total_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
if certs.is_empty() {
|
||||||
|
return Err(CertGenError::KeyboxParseFailed("no certificates in chain".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(certs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns (content_length, number_of_length_bytes_consumed)
|
||||||
|
fn parse_der_length(data: &[u8]) -> Result<(usize, usize)> {
|
||||||
|
if data.is_empty() {
|
||||||
|
return Err(CertGenError::KeyboxParseFailed("truncated DER length".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = data[0];
|
||||||
|
|
||||||
|
if first < 0x80 {
|
||||||
|
// Short form: length is the byte itself
|
||||||
|
return Ok((first as usize, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Long form: low 7 bits = number of subsequent length bytes
|
||||||
|
let num_bytes = (first & 0x7f) as usize;
|
||||||
|
if num_bytes == 0 || num_bytes > 4 {
|
||||||
|
return Err(CertGenError::KeyboxParseFailed(
|
||||||
|
format!("unsupported DER length encoding: 0x{first:02x}")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if 1 + num_bytes > data.len() {
|
||||||
|
return Err(CertGenError::KeyboxParseFailed("truncated multi-byte DER length".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut len: usize = 0;
|
||||||
|
for i in 0..num_bytes {
|
||||||
|
len = (len << 8) | (data[1 + i] as usize);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((len, 1 + num_bytes))
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
use crate::error::{CertGenError, Result};
|
||||||
|
use crate::types::{Algorithm, EcCurve, GeneratedKeyPair};
|
||||||
|
|
||||||
|
pub fn generate_key_pair(
|
||||||
|
algorithm: Algorithm,
|
||||||
|
key_size: u32,
|
||||||
|
ec_curve: Option<EcCurve>,
|
||||||
|
rsa_public_exponent: u64,
|
||||||
|
) -> Result<GeneratedKeyPair> {
|
||||||
|
match algorithm {
|
||||||
|
Algorithm::Ec => {
|
||||||
|
let curve = ec_curve.ok_or_else(|| CertGenError::InvalidParameter("ec_curve required for EC".into()))?;
|
||||||
|
generate_ec_key_pair(curve)
|
||||||
|
}
|
||||||
|
Algorithm::Rsa => generate_rsa_key_pair(key_size, rsa_public_exponent),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_ec_key_pair(curve: EcCurve) -> Result<GeneratedKeyPair> {
|
||||||
|
let alg = match curve {
|
||||||
|
EcCurve::P256 => &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
|
||||||
|
EcCurve::P384 => &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING,
|
||||||
|
_ => return Err(CertGenError::UnsupportedEcCurve(curve as i32)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let rng = ring::rand::SystemRandom::new();
|
||||||
|
let pkcs8_doc = ring::signature::EcdsaKeyPair::generate_pkcs8(alg, &rng)?;
|
||||||
|
|
||||||
|
Ok(GeneratedKeyPair {
|
||||||
|
private_key_pkcs8: pkcs8_doc.as_ref().to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_rsa_key_pair(key_size: u32, rsa_public_exponent: u64) -> Result<GeneratedKeyPair> {
|
||||||
|
use pkcs8::EncodePrivateKey;
|
||||||
|
|
||||||
|
if !matches!(key_size, 2048 | 3072 | 4096) {
|
||||||
|
return Err(CertGenError::InvalidParameter(
|
||||||
|
format!("RSA key size must be 2048, 3072, or 4096; got {key_size}")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let exp = if rsa_public_exponent == 0 {
|
||||||
|
rsa::BigUint::from(65537u64)
|
||||||
|
} else {
|
||||||
|
rsa::BigUint::from(rsa_public_exponent)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let private_key = rsa::RsaPrivateKey::new_with_exp(&mut rng, key_size as usize, &exp)
|
||||||
|
.map_err(|e| CertGenError::KeyGenFailed(e.to_string()))?;
|
||||||
|
|
||||||
|
let pkcs8_der = private_key.to_pkcs8_der()
|
||||||
|
.map_err(|e| CertGenError::SerializationFailed(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(GeneratedKeyPair {
|
||||||
|
private_key_pkcs8: pkcs8_der.as_bytes().to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod types;
|
||||||
|
mod keygen;
|
||||||
|
pub mod keybox;
|
||||||
|
pub mod attestation;
|
||||||
|
pub mod certbuilder;
|
||||||
|
pub mod logging;
|
||||||
|
|
||||||
|
use jni::objects::{JByteArray, JClass, JIntArray, JObject, JString};
|
||||||
|
use jni::sys::{jboolean, jbyteArray, jstring};
|
||||||
|
use jni::JNIEnv;
|
||||||
|
|
||||||
|
use crate::error::{CertGenError, Result};
|
||||||
|
use crate::types::{Algorithm, CertGenParams, EcCurve};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// JNI entry: generateAttestedKeyPair
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_org_matrix_TEESimulator_pki_NativeCertGen_generateAttestedKeyPair(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
config: JObject,
|
||||||
|
) -> jbyteArray {
|
||||||
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
|
generate_attested_inner(&mut env, &config)
|
||||||
|
}));
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(Ok(raw)) => raw,
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
tracing::error!(%e, "generateAttestedKeyPair failed");
|
||||||
|
let _ = env.throw_new(
|
||||||
|
"java/lang/RuntimeException",
|
||||||
|
format!("NativeCertGen: {e}"),
|
||||||
|
);
|
||||||
|
std::ptr::null_mut()
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
tracing::error!("generateAttestedKeyPair panicked");
|
||||||
|
let _ = env.throw_new(
|
||||||
|
"java/lang/RuntimeException",
|
||||||
|
"NativeCertGen: internal panic",
|
||||||
|
);
|
||||||
|
std::ptr::null_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_attested_inner(env: &mut JNIEnv, config: &JObject) -> Result<jbyteArray> {
|
||||||
|
let params = extract_config(env, config)?;
|
||||||
|
|
||||||
|
let key_pair = keygen::generate_key_pair(
|
||||||
|
params.algorithm,
|
||||||
|
params.key_size,
|
||||||
|
params.ec_curve,
|
||||||
|
params.rsa_public_exponent,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let keybox = keybox::parse_keybox(¶ms.keybox_cert_chain, ¶ms.keybox_private_key)?;
|
||||||
|
|
||||||
|
let attest_ext = attestation::build_attestation_extension(¶ms)?;
|
||||||
|
|
||||||
|
let cert_chain = certbuilder::build_certificate_chain(
|
||||||
|
&key_pair,
|
||||||
|
&attest_ext,
|
||||||
|
&keybox,
|
||||||
|
¶ms,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain);
|
||||||
|
|
||||||
|
let out = env.byte_array_from_slice(&blob)?;
|
||||||
|
Ok(out.into_raw())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// JNI entry: initLogging
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_org_matrix_TEESimulator_pki_NativeCertGen_initLogging(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
verbose: jboolean,
|
||||||
|
log_dir: JString,
|
||||||
|
) -> jboolean {
|
||||||
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
|
init_logging_inner(&mut env, verbose, &log_dir)
|
||||||
|
}));
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(Ok(())) => 1,
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
let _ = env.throw_new(
|
||||||
|
"java/lang/RuntimeException",
|
||||||
|
format!("NativeCertGen initLogging: {e}"),
|
||||||
|
);
|
||||||
|
0
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
let _ = env.throw_new(
|
||||||
|
"java/lang/RuntimeException",
|
||||||
|
"NativeCertGen initLogging: internal panic",
|
||||||
|
);
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_logging_inner(env: &mut JNIEnv, verbose: jboolean, log_dir: &JString) -> Result<()> {
|
||||||
|
let dir: String = env.get_string(log_dir)?.into();
|
||||||
|
logging::init(verbose != 0, &dir, 2, 3)
|
||||||
|
.map_err(|e| CertGenError::Jni(format!("logging init failed: {e}")))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// JNI entry: dumpLogs
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_org_matrix_TEESimulator_pki_NativeCertGen_dumpLogs(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
) -> jstring {
|
||||||
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
|
dump_logs_inner(&mut env)
|
||||||
|
}));
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(Ok(raw)) => raw,
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
tracing::error!(%e, "dumpLogs failed");
|
||||||
|
std::ptr::null_mut()
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
tracing::error!("dumpLogs panicked");
|
||||||
|
std::ptr::null_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dump_logs_inner(env: &mut JNIEnv) -> Result<jstring> {
|
||||||
|
logging::dump::execute_dump()
|
||||||
|
.map_err(|e| CertGenError::Jni(format!("dump failed: {e}")))?;
|
||||||
|
|
||||||
|
// Read the dump path written by execute_dump
|
||||||
|
let path = std::fs::read_to_string("/data/adb/tricky_store/.dump_path")
|
||||||
|
.map_err(|e| CertGenError::Jni(format!("read dump path: {e}")))?;
|
||||||
|
|
||||||
|
let jpath = env.new_string(&path)?;
|
||||||
|
Ok(jpath.into_raw())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Config extraction from Java CertGenConfig object
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn extract_config(env: &mut JNIEnv, config: &JObject) -> Result<CertGenParams> {
|
||||||
|
let algorithm = get_int(env, config, "algorithm")?;
|
||||||
|
let key_size = get_int(env, config, "keySize")?;
|
||||||
|
let ec_curve_raw = get_int(env, config, "ecCurve")?;
|
||||||
|
let rsa_pub_exp = get_long(env, config, "rsaPublicExponent")?;
|
||||||
|
|
||||||
|
let cert_not_before = get_long(env, config, "certNotBefore")?;
|
||||||
|
let cert_not_after = get_long(env, config, "certNotAfter")?;
|
||||||
|
let security_level = get_int(env, config, "securityLevel")?;
|
||||||
|
let attest_version = get_int(env, config, "attestVersion")?;
|
||||||
|
let keymaster_version = get_int(env, config, "keymasterVersion")?;
|
||||||
|
let os_version = get_int(env, config, "osVersion")?;
|
||||||
|
let os_patch_level = get_int(env, config, "osPatchLevel")?;
|
||||||
|
let vendor_patch_level = get_int(env, config, "vendorPatchLevel")?;
|
||||||
|
let boot_patch_level = get_int(env, config, "bootPatchLevel")?;
|
||||||
|
let creation_datetime = get_long(env, config, "creationDatetime")?;
|
||||||
|
|
||||||
|
let attestation_challenge = get_nullable_byte_array(env, config, "attestationChallenge")?;
|
||||||
|
let purposes = get_int_array(env, config, "purposes")?;
|
||||||
|
let digests = get_int_array(env, config, "digests")?;
|
||||||
|
let cert_serial = get_nullable_byte_array(env, config, "certSerial")?;
|
||||||
|
let cert_subject = get_nullable_byte_array(env, config, "certSubject")?;
|
||||||
|
let keybox_private_key = get_byte_array(env, config, "keyboxPrivateKey")?;
|
||||||
|
let keybox_cert_chain = get_byte_array(env, config, "keyboxCertChain")?;
|
||||||
|
let boot_key = get_byte_array(env, config, "bootKey")?;
|
||||||
|
let boot_hash = get_byte_array(env, config, "bootHash")?;
|
||||||
|
let attestation_app_id = get_byte_array(env, config, "attestationApplicationId")?;
|
||||||
|
let module_hash = get_nullable_byte_array(env, config, "moduleHash")?;
|
||||||
|
|
||||||
|
let id_brand = get_nullable_byte_array(env, config, "idBrand")?;
|
||||||
|
let id_device = get_nullable_byte_array(env, config, "idDevice")?;
|
||||||
|
let id_product = get_nullable_byte_array(env, config, "idProduct")?;
|
||||||
|
let id_serial = get_nullable_byte_array(env, config, "idSerial")?;
|
||||||
|
let id_imei = get_nullable_byte_array(env, config, "idImei")?;
|
||||||
|
let id_meid = get_nullable_byte_array(env, config, "idMeid")?;
|
||||||
|
let id_manufacturer = get_nullable_byte_array(env, config, "idManufacturer")?;
|
||||||
|
let id_model = get_nullable_byte_array(env, config, "idModel")?;
|
||||||
|
let id_second_imei = get_nullable_byte_array(env, config, "idSecondImei")?;
|
||||||
|
|
||||||
|
let active_datetime = get_long(env, config, "activeDatetime")?;
|
||||||
|
let origination_expire_datetime = get_long(env, config, "originationExpireDatetime")?;
|
||||||
|
let usage_expire_datetime = get_long(env, config, "usageExpireDatetime")?;
|
||||||
|
let usage_count_limit = get_int(env, config, "usageCountLimit")?;
|
||||||
|
let caller_nonce = get_boolean(env, config, "callerNonce")?;
|
||||||
|
let unlocked_device_required = get_boolean(env, config, "unlockedDeviceRequired")?;
|
||||||
|
let no_auth_required = get_boolean(env, config, "noAuthRequired")?;
|
||||||
|
|
||||||
|
Ok(CertGenParams {
|
||||||
|
algorithm: Algorithm::try_from(algorithm)?,
|
||||||
|
key_size: key_size as u32,
|
||||||
|
ec_curve: if algorithm == 3 {
|
||||||
|
Some(EcCurve::try_from(ec_curve_raw)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
rsa_public_exponent: rsa_pub_exp as u64,
|
||||||
|
attestation_challenge,
|
||||||
|
purposes,
|
||||||
|
digests,
|
||||||
|
cert_serial,
|
||||||
|
cert_subject,
|
||||||
|
cert_not_before,
|
||||||
|
cert_not_after,
|
||||||
|
keybox_private_key,
|
||||||
|
keybox_cert_chain,
|
||||||
|
security_level,
|
||||||
|
attest_version,
|
||||||
|
keymaster_version,
|
||||||
|
os_version,
|
||||||
|
os_patch_level,
|
||||||
|
vendor_patch_level,
|
||||||
|
boot_patch_level,
|
||||||
|
boot_key,
|
||||||
|
boot_hash,
|
||||||
|
creation_datetime,
|
||||||
|
attestation_application_id: attestation_app_id,
|
||||||
|
module_hash,
|
||||||
|
id_brand,
|
||||||
|
id_device,
|
||||||
|
id_product,
|
||||||
|
id_serial,
|
||||||
|
id_imei,
|
||||||
|
id_meid,
|
||||||
|
id_manufacturer,
|
||||||
|
id_model,
|
||||||
|
id_second_imei,
|
||||||
|
active_datetime,
|
||||||
|
origination_expire_datetime,
|
||||||
|
usage_expire_datetime,
|
||||||
|
usage_count_limit,
|
||||||
|
caller_nonce,
|
||||||
|
unlocked_device_required,
|
||||||
|
no_auth_required,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// JNI field accessor helpers — called 35+ times, justifies the abstraction
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn get_int(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<i32> {
|
||||||
|
Ok(env.get_field(obj, name, "I")?.i()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_long(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<i64> {
|
||||||
|
Ok(env.get_field(obj, name, "J")?.j()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_boolean(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<bool> {
|
||||||
|
Ok(env.get_field(obj, name, "Z")?.z()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_byte_array(env: &mut JNIEnv, obj: &JObject, name: &'static str) -> Result<Vec<u8>> {
|
||||||
|
let field = env.get_field(obj, name, "[B")?.l()?;
|
||||||
|
if field.is_null() {
|
||||||
|
return Err(CertGenError::NullParam(name));
|
||||||
|
}
|
||||||
|
let arr: JByteArray = field.into();
|
||||||
|
let len = env.get_array_length(&arr)?;
|
||||||
|
let mut buf = vec![0i8; len as usize];
|
||||||
|
env.get_byte_array_region(&arr, 0, &mut buf)?;
|
||||||
|
env.delete_local_ref(arr)?;
|
||||||
|
Ok(buf.into_iter().map(|b| b as u8).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_nullable_byte_array(
|
||||||
|
env: &mut JNIEnv,
|
||||||
|
obj: &JObject,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<Option<Vec<u8>>> {
|
||||||
|
let field = env.get_field(obj, name, "[B")?.l()?;
|
||||||
|
if field.is_null() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let arr: JByteArray = field.into();
|
||||||
|
let len = env.get_array_length(&arr)?;
|
||||||
|
let mut buf = vec![0i8; len as usize];
|
||||||
|
env.get_byte_array_region(&arr, 0, &mut buf)?;
|
||||||
|
env.delete_local_ref(arr)?;
|
||||||
|
Ok(Some(buf.into_iter().map(|b| b as u8).collect()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_int_array(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<Vec<i32>> {
|
||||||
|
let field = env.get_field(obj, name, "[I")?.l()?;
|
||||||
|
if field.is_null() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
let arr: JIntArray = field.into();
|
||||||
|
let len = env.get_array_length(&arr)?;
|
||||||
|
let mut buf = vec![0i32; len as usize];
|
||||||
|
env.get_int_array_region(&arr, 0, &mut buf)?;
|
||||||
|
env.delete_local_ref(arr)?;
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Binary result assembly (doc 09 section 4.1)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn assemble_result(private_key: &[u8], cert_chain: &[Vec<u8>]) -> Vec<u8> {
|
||||||
|
let total = 4 + private_key.len()
|
||||||
|
+ 4
|
||||||
|
+ cert_chain.iter().map(|c| 4 + c.len()).sum::<usize>();
|
||||||
|
|
||||||
|
let mut buf = Vec::with_capacity(total);
|
||||||
|
|
||||||
|
// Private key segment
|
||||||
|
buf.extend_from_slice(&(private_key.len() as u32).to_be_bytes());
|
||||||
|
buf.extend_from_slice(private_key);
|
||||||
|
|
||||||
|
// Cert count
|
||||||
|
buf.extend_from_slice(&(cert_chain.len() as u32).to_be_bytes());
|
||||||
|
|
||||||
|
// Each cert: length-prefixed DER
|
||||||
|
for cert in cert_chain {
|
||||||
|
buf.extend_from_slice(&(cert.len() as u32).to_be_bytes());
|
||||||
|
buf.extend_from_slice(cert);
|
||||||
|
}
|
||||||
|
|
||||||
|
buf
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
const DUMP_DIR: &str = "/sdcard/Download";
|
||||||
|
const LOCK_PATH: &str = "/data/adb/tricky_store/.dump_lock";
|
||||||
|
const DUMP_PATH_FILE: &str = "/data/adb/tricky_store/.dump_path";
|
||||||
|
const LOG_DIR: &str = "/data/adb/tricky_store/logs";
|
||||||
|
const BASE_DIR: &str = "/data/adb/tricky_store";
|
||||||
|
const LOGCAT_SIZE_LIMIT: usize = 2 * 1024 * 1024;
|
||||||
|
|
||||||
|
struct FlockGuard {
|
||||||
|
_file: File,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlockGuard {
|
||||||
|
fn acquire() -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
if let Some(parent) = Path::new(LOCK_PATH).parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let file = File::create(LOCK_PATH)?;
|
||||||
|
let fd = {
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
file.as_raw_fd()
|
||||||
|
};
|
||||||
|
let ret = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
|
||||||
|
if ret != 0 {
|
||||||
|
return Err("dump already in progress".into());
|
||||||
|
}
|
||||||
|
Ok(Self { _file: file })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for FlockGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// flock released automatically when file descriptor closes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn random_name(len: usize) -> String {
|
||||||
|
use rand::Rng;
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
(0..len)
|
||||||
|
.map(|_| {
|
||||||
|
let idx = rng.gen_range(0..36u8);
|
||||||
|
if idx < 10 {
|
||||||
|
(b'0' + idx) as char
|
||||||
|
} else {
|
||||||
|
(b'a' + idx - 10) as char
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_logcat(tag: &str) -> Vec<u8> {
|
||||||
|
let output = Command::new("logcat")
|
||||||
|
.args(["-d", "-s", tag])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(o) => {
|
||||||
|
let mut data = o.stdout;
|
||||||
|
data.truncate(LOGCAT_SIZE_LIMIT);
|
||||||
|
data
|
||||||
|
}
|
||||||
|
Err(_) => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_device_info() -> String {
|
||||||
|
let mut info = String::new();
|
||||||
|
|
||||||
|
if let Ok(output) = Command::new("uname").arg("-a").output() {
|
||||||
|
info.push_str(&format!(
|
||||||
|
"uname={}\n",
|
||||||
|
String::from_utf8_lossy(&output.stdout).trim()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, prop) in [
|
||||||
|
("device", "ro.product.device"),
|
||||||
|
("build", "ro.build.display.id"),
|
||||||
|
("android", "ro.build.version.release"),
|
||||||
|
] {
|
||||||
|
if let Ok(output) = Command::new("getprop").arg(prop).output() {
|
||||||
|
info.push_str(&format!(
|
||||||
|
"{}={}\n",
|
||||||
|
key,
|
||||||
|
String::from_utf8_lossy(&output.stdout).trim()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KSU version
|
||||||
|
if let Ok(ver) = fs::read_to_string("/data/adb/ksu/version") {
|
||||||
|
info.push_str(&format!("ksu={}\n", ver.trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module version from module.prop
|
||||||
|
if let Ok(prop) = fs::read_to_string("/data/adb/modules/tricky_store/module.prop") {
|
||||||
|
for line in prop.lines() {
|
||||||
|
if let Some(ver) = line.strip_prefix("version=") {
|
||||||
|
info.push_str(&format!("module={}\n", ver.trim()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_file_bytes(path: &str) -> Option<Vec<u8>> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
File::open(path).ok()?.read_to_end(&mut buf).ok()?;
|
||||||
|
Some(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn epoch_millis() -> u64 {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_millis() as u64)
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute_dump() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let _lock = FlockGuard::acquire()?;
|
||||||
|
|
||||||
|
let _ = fs::create_dir_all(DUMP_DIR);
|
||||||
|
let zip_name = format!("{}.zip", random_name(8));
|
||||||
|
let zip_path = format!("{}/{}", DUMP_DIR, zip_name);
|
||||||
|
|
||||||
|
let zip_file = File::create(&zip_path)?;
|
||||||
|
let mut zip = zip::ZipWriter::new(zip_file);
|
||||||
|
let options =
|
||||||
|
zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
|
||||||
|
|
||||||
|
let mut file_count = 0u32;
|
||||||
|
|
||||||
|
// Log files
|
||||||
|
let log_files = [
|
||||||
|
"certgen.log",
|
||||||
|
"certgen.log.1",
|
||||||
|
"certgen.log.2",
|
||||||
|
"certgen.log.3",
|
||||||
|
"certgen.log.4",
|
||||||
|
];
|
||||||
|
for name in &log_files {
|
||||||
|
let path = format!("{}/{}", LOG_DIR, name);
|
||||||
|
if let Some(data) = read_file_bytes(&path) {
|
||||||
|
zip.start_file(*name, options)?;
|
||||||
|
zip.write_all(&data)?;
|
||||||
|
file_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logcat
|
||||||
|
let logcat = collect_logcat("TEESimulator");
|
||||||
|
if !logcat.is_empty() {
|
||||||
|
zip.start_file("logcat-teesimulator.log", options)?;
|
||||||
|
zip.write_all(&logcat)?;
|
||||||
|
file_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config files
|
||||||
|
for name in ["tee_status.txt", "security_patch.txt"] {
|
||||||
|
let path = format!("{}/{}", BASE_DIR, name);
|
||||||
|
if let Some(data) = read_file_bytes(&path) {
|
||||||
|
zip.start_file(name, options)?;
|
||||||
|
zip.write_all(&data)?;
|
||||||
|
file_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device info
|
||||||
|
let device_info = collect_device_info();
|
||||||
|
if !device_info.is_empty() {
|
||||||
|
zip.start_file("device-info.txt", options)?;
|
||||||
|
zip.write_all(device_info.as_bytes())?;
|
||||||
|
file_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manifest
|
||||||
|
let manifest = serde_json::json!({
|
||||||
|
"timestamp": epoch_millis(),
|
||||||
|
"version": env!("CARGO_PKG_VERSION"),
|
||||||
|
"files": file_count,
|
||||||
|
});
|
||||||
|
zip.start_file("manifest.json", options)?;
|
||||||
|
zip.write_all(manifest.to_string().as_bytes())?;
|
||||||
|
|
||||||
|
zip.finish()?;
|
||||||
|
|
||||||
|
let zip_size = fs::metadata(&zip_path).map(|m| m.len()).unwrap_or(0);
|
||||||
|
fs::write(DUMP_PATH_FILE, &zip_path)?;
|
||||||
|
|
||||||
|
let result = serde_json::json!({
|
||||||
|
"zip": zip_path,
|
||||||
|
"size": zip_size,
|
||||||
|
"files": file_count + 1, // +1 for manifest
|
||||||
|
});
|
||||||
|
println!("{}", result);
|
||||||
|
|
||||||
|
tracing::info!(path = %zip_path, size = zip_size, "diagnostic dump created");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
use std::fs::{File, OpenOptions};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use tracing::field::{Field, Visit};
|
||||||
|
use tracing::{Event, Level, Subscriber};
|
||||||
|
use tracing_subscriber::layer::Context;
|
||||||
|
use tracing_subscriber::Layer;
|
||||||
|
|
||||||
|
const KMSG_PATH: &str = "/dev/kmsg";
|
||||||
|
const TAG: &str = "TEESimulator";
|
||||||
|
|
||||||
|
pub struct KmsgLayer {
|
||||||
|
writer: Mutex<Option<File>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KmsgLayer {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let file = OpenOptions::new().write(true).open(KMSG_PATH).ok();
|
||||||
|
Self {
|
||||||
|
writer: Mutex::new(file),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn syslog_priority(level: &Level) -> u8 {
|
||||||
|
match *level {
|
||||||
|
Level::ERROR => 3,
|
||||||
|
Level::WARN => 4,
|
||||||
|
Level::INFO => 6,
|
||||||
|
Level::DEBUG | Level::TRACE => 7,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MessageVisitor {
|
||||||
|
message: String,
|
||||||
|
fields: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageVisitor {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
message: String::new(),
|
||||||
|
fields: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Visit for MessageVisitor {
|
||||||
|
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
|
||||||
|
if field.name() == "message" {
|
||||||
|
let raw = format!("{:?}", value);
|
||||||
|
// Strip surrounding debug quotes if present
|
||||||
|
self.message = raw
|
||||||
|
.strip_prefix('"')
|
||||||
|
.and_then(|s| s.strip_suffix('"'))
|
||||||
|
.unwrap_or(&raw)
|
||||||
|
.to_string();
|
||||||
|
} else {
|
||||||
|
if !self.fields.is_empty() {
|
||||||
|
self.fields.push(' ');
|
||||||
|
}
|
||||||
|
self.fields.push_str(&format!("{}={:?}", field.name(), value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Subscriber> Layer<S> for KmsgLayer {
|
||||||
|
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
|
||||||
|
let mut guard = match self.writer.lock() {
|
||||||
|
Ok(g) => g,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
let file = match guard.as_mut() {
|
||||||
|
Some(f) => f,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let priority = syslog_priority(event.metadata().level());
|
||||||
|
let mut visitor = MessageVisitor::new();
|
||||||
|
event.record(&mut visitor);
|
||||||
|
|
||||||
|
let line = if visitor.fields.is_empty() {
|
||||||
|
format!("<{}>{}: {}\n", priority, TAG, visitor.message)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"<{}>{}: {} {}\n",
|
||||||
|
priority, TAG, visitor.message, visitor.fields
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = file.write_all(line.as_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
mod kmsg;
|
||||||
|
mod rotating;
|
||||||
|
pub mod sysfs;
|
||||||
|
pub mod dump;
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||||
|
|
||||||
|
const VERBOSE_MARKER: &str = "/data/adb/tricky_store/.verbose";
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
verbose_flag: bool,
|
||||||
|
log_dir: &str,
|
||||||
|
max_size_mb: u64,
|
||||||
|
max_files: usize,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let verbose = verbose_flag || Path::new(VERBOSE_MARKER).exists();
|
||||||
|
|
||||||
|
let (max_size, max_files) = if verbose {
|
||||||
|
(5 * 1024 * 1024, 5)
|
||||||
|
} else {
|
||||||
|
(max_size_mb * 1024 * 1024, max_files)
|
||||||
|
};
|
||||||
|
|
||||||
|
let level = if verbose { "trace" } else { "info" };
|
||||||
|
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level));
|
||||||
|
|
||||||
|
let kmsg_layer = kmsg::KmsgLayer::new();
|
||||||
|
let rotating_layer = rotating::RotatingFileLayer::new(log_dir, max_size, max_files);
|
||||||
|
let stderr_layer = tracing_subscriber::fmt::layer().with_writer(std::io::stderr);
|
||||||
|
|
||||||
|
// Idempotent — second call returns Ok instead of propagating SetGlobalDefaultError
|
||||||
|
let _ = tracing_subscriber::registry()
|
||||||
|
.with(filter)
|
||||||
|
.with(kmsg_layer)
|
||||||
|
.with(rotating_layer)
|
||||||
|
.with(stderr_layer)
|
||||||
|
.try_init();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
use std::fs::{self, File, OpenOptions};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use tracing::field::{Field, Visit};
|
||||||
|
use tracing::{Event, Level, Subscriber};
|
||||||
|
use tracing_subscriber::layer::Context;
|
||||||
|
use tracing_subscriber::Layer;
|
||||||
|
|
||||||
|
struct RotatingState {
|
||||||
|
dir: PathBuf,
|
||||||
|
current: Option<File>,
|
||||||
|
current_size: u64,
|
||||||
|
max_size: u64,
|
||||||
|
max_files: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RotatingFileLayer {
|
||||||
|
state: Mutex<RotatingState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RotatingFileLayer {
|
||||||
|
pub fn new(dir: &str, max_size: u64, max_files: usize) -> Self {
|
||||||
|
let dir = PathBuf::from(dir);
|
||||||
|
let _ = fs::create_dir_all(&dir);
|
||||||
|
let (file, size) = open_current_log(&dir);
|
||||||
|
Self {
|
||||||
|
state: Mutex::new(RotatingState {
|
||||||
|
dir,
|
||||||
|
current: file,
|
||||||
|
current_size: size,
|
||||||
|
max_size,
|
||||||
|
max_files,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_current_log(dir: &Path) -> (Option<File>, u64) {
|
||||||
|
let path = dir.join("certgen.log");
|
||||||
|
let size = fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(&path)
|
||||||
|
.ok();
|
||||||
|
(file, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rotate(state: &mut RotatingState) {
|
||||||
|
// Close current handle before renaming
|
||||||
|
state.current.take();
|
||||||
|
|
||||||
|
let dir = &state.dir;
|
||||||
|
// Delete the oldest rotated file before shifting
|
||||||
|
let oldest = dir.join(format!("certgen.log.{}", state.max_files));
|
||||||
|
if oldest.exists() {
|
||||||
|
let _ = fs::remove_file(&oldest);
|
||||||
|
}
|
||||||
|
// Shift older files up: .{N} -> .{N+1}
|
||||||
|
for i in (1..state.max_files).rev() {
|
||||||
|
let from = dir.join(format!("certgen.log.{}", i));
|
||||||
|
let to = dir.join(format!("certgen.log.{}", i + 1));
|
||||||
|
if from.exists() {
|
||||||
|
let _ = fs::rename(&from, &to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Current -> .1
|
||||||
|
let current_path = dir.join("certgen.log");
|
||||||
|
let first_rotated = dir.join("certgen.log.1");
|
||||||
|
if current_path.exists() {
|
||||||
|
let _ = fs::rename(¤t_path, &first_rotated);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (file, size) = open_current_log(dir);
|
||||||
|
state.current = file;
|
||||||
|
state.current_size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn epoch_secs() -> u64 {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn level_str(level: &Level) -> &'static str {
|
||||||
|
match *level {
|
||||||
|
Level::ERROR => "ERROR",
|
||||||
|
Level::WARN => "WARN",
|
||||||
|
Level::INFO => "INFO",
|
||||||
|
Level::DEBUG => "DEBUG",
|
||||||
|
Level::TRACE => "TRACE",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LogVisitor {
|
||||||
|
message: String,
|
||||||
|
fields: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogVisitor {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
message: String::new(),
|
||||||
|
fields: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Visit for LogVisitor {
|
||||||
|
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
|
||||||
|
if field.name() == "message" {
|
||||||
|
let raw = format!("{:?}", value);
|
||||||
|
self.message = raw
|
||||||
|
.strip_prefix('"')
|
||||||
|
.and_then(|s| s.strip_suffix('"'))
|
||||||
|
.unwrap_or(&raw)
|
||||||
|
.to_string();
|
||||||
|
} else {
|
||||||
|
if !self.fields.is_empty() {
|
||||||
|
self.fields.push(' ');
|
||||||
|
}
|
||||||
|
self.fields.push_str(&format!("{}={:?}", field.name(), value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Subscriber> Layer<S> for RotatingFileLayer {
|
||||||
|
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
|
||||||
|
let mut state = match self.state.lock() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
if state.current_size >= state.max_size {
|
||||||
|
rotate(&mut state);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = match state.current.as_mut() {
|
||||||
|
Some(f) => f,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let ts = epoch_secs();
|
||||||
|
let lvl = level_str(event.metadata().level());
|
||||||
|
let target = event.metadata().target();
|
||||||
|
|
||||||
|
let mut visitor = LogVisitor::new();
|
||||||
|
event.record(&mut visitor);
|
||||||
|
|
||||||
|
let line = if visitor.fields.is_empty() {
|
||||||
|
format!("{} [{}] {}: {}\n", ts, lvl, target, visitor.message)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{} [{}] {}: {} {}\n",
|
||||||
|
ts, lvl, target, visitor.message, visitor.fields
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if file.write_all(line.as_bytes()).is_ok() {
|
||||||
|
state.current_size += line.len() as u64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
const VERBOSE_MARKER: &str = "/data/adb/tricky_store/.verbose";
|
||||||
|
|
||||||
|
pub fn is_verbose() -> bool {
|
||||||
|
Path::new(VERBOSE_MARKER).exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_verbose_marker(enabled: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if enabled {
|
||||||
|
if let Some(parent) = Path::new(VERBOSE_MARKER).parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
fs::write(VERBOSE_MARKER, "")?;
|
||||||
|
} else if Path::new(VERBOSE_MARKER).exists() {
|
||||||
|
fs::remove_file(VERBOSE_MARKER)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
set_verbose_marker(true)?;
|
||||||
|
tracing::info!("verbose logging enabled via marker file");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disable() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
set_verbose_marker(false)?;
|
||||||
|
tracing::info!("verbose logging disabled, marker file removed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn status() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let state = if is_verbose() { "enabled" } else { "disabled" };
|
||||||
|
tracing::info!(verbose = state, "verbose marker status");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
use crate::error::CertGenError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum Algorithm {
|
||||||
|
Rsa = 1,
|
||||||
|
Ec = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<i32> for Algorithm {
|
||||||
|
type Error = CertGenError;
|
||||||
|
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
1 => Ok(Self::Rsa),
|
||||||
|
3 => Ok(Self::Ec),
|
||||||
|
_ => Err(CertGenError::UnsupportedAlgorithm(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum EcCurve {
|
||||||
|
P224 = 0,
|
||||||
|
P256 = 1,
|
||||||
|
P384 = 2,
|
||||||
|
P521 = 3,
|
||||||
|
Curve25519 = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<i32> for EcCurve {
|
||||||
|
type Error = CertGenError;
|
||||||
|
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
0 => Ok(Self::P224),
|
||||||
|
1 => Ok(Self::P256),
|
||||||
|
2 => Ok(Self::P384),
|
||||||
|
3 => Ok(Self::P521),
|
||||||
|
4 => Ok(Self::Curve25519),
|
||||||
|
_ => Err(CertGenError::UnsupportedEcCurve(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CertGenParams {
|
||||||
|
pub algorithm: Algorithm,
|
||||||
|
pub key_size: u32,
|
||||||
|
pub ec_curve: Option<EcCurve>,
|
||||||
|
pub rsa_public_exponent: u64,
|
||||||
|
|
||||||
|
pub attestation_challenge: Option<Vec<u8>>,
|
||||||
|
pub purposes: Vec<i32>,
|
||||||
|
pub digests: Vec<i32>,
|
||||||
|
|
||||||
|
pub cert_serial: Option<Vec<u8>>,
|
||||||
|
pub cert_subject: Option<Vec<u8>>,
|
||||||
|
pub cert_not_before: i64,
|
||||||
|
pub cert_not_after: i64,
|
||||||
|
|
||||||
|
pub keybox_private_key: Vec<u8>,
|
||||||
|
pub keybox_cert_chain: Vec<u8>,
|
||||||
|
|
||||||
|
pub security_level: i32,
|
||||||
|
pub attest_version: i32,
|
||||||
|
pub keymaster_version: i32,
|
||||||
|
|
||||||
|
pub os_version: i32,
|
||||||
|
pub os_patch_level: i32,
|
||||||
|
pub vendor_patch_level: i32,
|
||||||
|
pub boot_patch_level: i32,
|
||||||
|
|
||||||
|
pub boot_key: Vec<u8>,
|
||||||
|
pub boot_hash: Vec<u8>,
|
||||||
|
|
||||||
|
pub creation_datetime: i64,
|
||||||
|
pub attestation_application_id: Vec<u8>,
|
||||||
|
pub module_hash: Option<Vec<u8>>,
|
||||||
|
|
||||||
|
pub id_brand: Option<Vec<u8>>,
|
||||||
|
pub id_device: Option<Vec<u8>>,
|
||||||
|
pub id_product: Option<Vec<u8>>,
|
||||||
|
pub id_serial: Option<Vec<u8>>,
|
||||||
|
pub id_imei: Option<Vec<u8>>,
|
||||||
|
pub id_meid: Option<Vec<u8>>,
|
||||||
|
pub id_manufacturer: Option<Vec<u8>>,
|
||||||
|
pub id_model: Option<Vec<u8>>,
|
||||||
|
pub id_second_imei: Option<Vec<u8>>,
|
||||||
|
|
||||||
|
pub active_datetime: i64,
|
||||||
|
pub origination_expire_datetime: i64,
|
||||||
|
pub usage_expire_datetime: i64,
|
||||||
|
pub usage_count_limit: i32,
|
||||||
|
pub caller_nonce: bool,
|
||||||
|
pub unlocked_device_required: bool,
|
||||||
|
pub no_auth_required: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GeneratedKeyPair {
|
||||||
|
pub private_key_pkcs8: Vec<u8>,
|
||||||
|
}
|
||||||
Executable
+262
@@ -0,0 +1,262 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build, package, deploy, and verify TEESimulator module ZIPs.
|
||||||
|
# Usage: ./scripts/package.sh [flags]
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# ./scripts/package.sh --release # build release ZIP
|
||||||
|
# ./scripts/package.sh --all --clean # clean build, both variants
|
||||||
|
# ./scripts/package.sh --release --deploy --reboot # build, push, install, reboot
|
||||||
|
# ./scripts/package.sh --deploy --verify # deploy latest ZIP + verify via logcat
|
||||||
|
# ./scripts/package.sh --rust --release # build Rust crate first, then release
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
OUT_DIR="$PROJECT_ROOT/out"
|
||||||
|
|
||||||
|
VARIANT=""
|
||||||
|
CLEAN=false
|
||||||
|
DEPLOY=false
|
||||||
|
REBOOT=false
|
||||||
|
VERIFY=false
|
||||||
|
BUILD_RUST=false
|
||||||
|
CLEAR_KEYS=false
|
||||||
|
TRACE=false
|
||||||
|
ROOT_PROVIDER="ksu"
|
||||||
|
|
||||||
|
red() { printf '\033[0;31m%s\033[0m\n' "$*"; }
|
||||||
|
green() { printf '\033[0;32m%s\033[0m\n' "$*"; }
|
||||||
|
yellow() { printf '\033[0;33m%s\033[0m\n' "$*"; }
|
||||||
|
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $(basename "$0") [options]
|
||||||
|
|
||||||
|
Build variants (pick one, or --all):
|
||||||
|
--release Build release variant (default if none specified)
|
||||||
|
--debug Build debug variant
|
||||||
|
--all Build both debug and release
|
||||||
|
|
||||||
|
Build options:
|
||||||
|
--clean Run gradle clean before building
|
||||||
|
--rust Build native-certgen Rust crate before Gradle
|
||||||
|
|
||||||
|
Deploy options:
|
||||||
|
--deploy Push ZIP to device and install
|
||||||
|
--reboot Reboot device after install
|
||||||
|
--clear-keys Clear persistent_keys before deploy
|
||||||
|
--verify Run logcat verification after deploy
|
||||||
|
--root PROVIDER Root provider: ksu (default), magisk, apatch
|
||||||
|
|
||||||
|
Misc:
|
||||||
|
-v, --verbose Print every command as it runs (set -x)
|
||||||
|
--help Show this help
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--release) VARIANT="release"; shift ;;
|
||||||
|
--debug) VARIANT="debug"; shift ;;
|
||||||
|
--all) VARIANT="all"; shift ;;
|
||||||
|
--clean) CLEAN=true; shift ;;
|
||||||
|
--deploy) DEPLOY=true; shift ;;
|
||||||
|
--reboot) REBOOT=true; shift ;;
|
||||||
|
--verify) VERIFY=true; shift ;;
|
||||||
|
--rust) BUILD_RUST=true; shift ;;
|
||||||
|
--clear-keys) CLEAR_KEYS=true; shift ;;
|
||||||
|
-v|--verbose) TRACE=true; shift ;;
|
||||||
|
--root) ROOT_PROVIDER="$2"; shift 2 ;;
|
||||||
|
--help|-h) usage ;;
|
||||||
|
*) red "Unknown flag: $1"; usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$VARIANT" ]] && VARIANT="release"
|
||||||
|
[[ "$TRACE" == true ]] && set -x
|
||||||
|
|
||||||
|
case "$ROOT_PROVIDER" in
|
||||||
|
ksu) INSTALL_CMD="ksud module install" ;;
|
||||||
|
magisk) INSTALL_CMD="magisk --install-module" ;;
|
||||||
|
apatch) INSTALL_CMD="/data/adb/apd module install" ;;
|
||||||
|
*) red "Unknown root provider: $ROOT_PROVIDER"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
build_rust() {
|
||||||
|
local cargo_toml="$PROJECT_ROOT/native-certgen/Cargo.toml"
|
||||||
|
if [[ ! -f "$cargo_toml" ]]; then
|
||||||
|
red "native-certgen/Cargo.toml not found — skipping Rust build"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
bold "==> Building native-certgen (aarch64)"
|
||||||
|
|
||||||
|
if ! command -v cargo-ndk &>/dev/null; then
|
||||||
|
red "cargo-ndk not found. Install: cargo install cargo-ndk"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
(cd "$PROJECT_ROOT/native-certgen" && \
|
||||||
|
cargo ndk -t arm64-v8a --platform 29 -- build --release)
|
||||||
|
|
||||||
|
local so="$PROJECT_ROOT/native-certgen/target/aarch64-linux-android/release/libcertgen.so"
|
||||||
|
if [[ -f "$so" ]]; then
|
||||||
|
local size
|
||||||
|
size=$(du -h "$so" | cut -f1)
|
||||||
|
green " libcertgen.so built ($size)"
|
||||||
|
else
|
||||||
|
red " libcertgen.so not found after build"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
gradle_build() {
|
||||||
|
local tasks=()
|
||||||
|
|
||||||
|
[[ "$CLEAN" == true ]] && tasks+=(clean)
|
||||||
|
|
||||||
|
case "$VARIANT" in
|
||||||
|
release) tasks+=(zipRelease) ;;
|
||||||
|
debug) tasks+=(zipDebug) ;;
|
||||||
|
all) tasks+=(zipDebug zipRelease) ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
bold "==> Gradle: ${tasks[*]}"
|
||||||
|
(cd "$PROJECT_ROOT" && ./gradlew "${tasks[@]}")
|
||||||
|
}
|
||||||
|
|
||||||
|
find_latest_zip() {
|
||||||
|
local pattern="$1"
|
||||||
|
ls -t "$OUT_DIR"/$pattern 2>/dev/null | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy_zip() {
|
||||||
|
local zip="$1"
|
||||||
|
local name
|
||||||
|
name=$(basename "$zip")
|
||||||
|
|
||||||
|
if ! adb get-state &>/dev/null; then
|
||||||
|
red "No ADB device connected"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$CLEAR_KEYS" == true ]]; then
|
||||||
|
bold "==> Clearing persistent_keys"
|
||||||
|
adb shell "rm -rf /data/adb/tricky_store/persistent_keys/*" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
bold "==> Deploying $name"
|
||||||
|
adb push "$zip" /data/local/tmp/module.zip
|
||||||
|
adb shell "su -c '$INSTALL_CMD /data/local/tmp/module.zip'"
|
||||||
|
green " Installed via $ROOT_PROVIDER"
|
||||||
|
|
||||||
|
if [[ "$REBOOT" == true ]]; then
|
||||||
|
bold "==> Rebooting"
|
||||||
|
adb reboot
|
||||||
|
echo " Waiting for device..."
|
||||||
|
adb wait-for-device
|
||||||
|
sleep 10
|
||||||
|
local pid
|
||||||
|
pid=$(adb shell "pidof TEESimulator" 2>/dev/null || true)
|
||||||
|
if [[ -n "$pid" ]]; then
|
||||||
|
green " Daemon alive (PID $pid)"
|
||||||
|
else
|
||||||
|
yellow " Daemon not yet started — check logcat"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_device() {
|
||||||
|
bold "==> Verification"
|
||||||
|
|
||||||
|
if ! adb get-state &>/dev/null; then
|
||||||
|
red "No ADB device connected"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local pid
|
||||||
|
pid=$(adb shell "pidof TEESimulator" 2>/dev/null || true)
|
||||||
|
if [[ -n "$pid" ]]; then
|
||||||
|
green " Daemon: running (PID $pid)"
|
||||||
|
else
|
||||||
|
red " Daemon: not running"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tee_status
|
||||||
|
tee_status=$(adb shell "cat /data/adb/tricky_store/tee_status.txt" 2>/dev/null || echo "N/A")
|
||||||
|
echo " TEE status: $tee_status"
|
||||||
|
|
||||||
|
local sec_patch
|
||||||
|
sec_patch=$(adb shell "cat /data/adb/tricky_store/security_patch.txt" 2>/dev/null || echo "N/A")
|
||||||
|
echo " Security patch config: $(echo "$sec_patch" | head -1)"
|
||||||
|
|
||||||
|
local errors
|
||||||
|
errors=$(adb logcat -d -s TEESimulator 2>/dev/null | \
|
||||||
|
grep -iE "error|exception" | \
|
||||||
|
grep -v "StrongBox\|SurfaceRuntime\|ClassLoader\|HARDWARE_TYPE_UNAVAILABLE" | \
|
||||||
|
wc -l)
|
||||||
|
if [[ "$errors" -eq 0 ]]; then
|
||||||
|
green " Logcat errors: 0"
|
||||||
|
else
|
||||||
|
yellow " Logcat errors: $errors (run: adb logcat -d -s TEESimulator | grep -iE 'error|exception')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local throttle_events
|
||||||
|
throttle_events=$(adb logcat -d -s TEESimulator 2>/dev/null | \
|
||||||
|
grep -cE "RATE_LIMITED|CONCURRENT_LIMITED" || true)
|
||||||
|
echo " Rate limit events: $throttle_events"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_summary() {
|
||||||
|
echo ""
|
||||||
|
bold "==> Build Summary"
|
||||||
|
|
||||||
|
local variants=()
|
||||||
|
case "$VARIANT" in
|
||||||
|
release) variants=(Release) ;;
|
||||||
|
debug) variants=(Debug) ;;
|
||||||
|
all) variants=(Debug Release) ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
for v in "${variants[@]}"; do
|
||||||
|
local zip
|
||||||
|
zip=$(find_latest_zip "*-${v}.zip")
|
||||||
|
if [[ -n "$zip" ]]; then
|
||||||
|
local size
|
||||||
|
size=$(du -h "$zip" | cut -f1)
|
||||||
|
green " $v: $(basename "$zip") ($size)"
|
||||||
|
else
|
||||||
|
red " $v: ZIP not found"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Main ---
|
||||||
|
echo ""
|
||||||
|
bold "TEESimulator-RS package pipeline"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
[[ "$BUILD_RUST" == true ]] && build_rust
|
||||||
|
|
||||||
|
gradle_build
|
||||||
|
print_summary
|
||||||
|
|
||||||
|
if [[ "$DEPLOY" == true ]]; then
|
||||||
|
local_variant="$VARIANT"
|
||||||
|
[[ "$local_variant" == "all" ]] && local_variant="release"
|
||||||
|
|
||||||
|
cap="${local_variant^}"
|
||||||
|
zip=$(find_latest_zip "*-${cap}.zip")
|
||||||
|
if [[ -z "$zip" ]]; then
|
||||||
|
red "No $cap ZIP found to deploy"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
deploy_zip "$zip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$VERIFY" == true ]] && verify_device
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
green "Done."
|
||||||
+1
-1
@@ -14,7 +14,7 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "TEESimulator"
|
rootProject.name = "TEESimulator-RS"
|
||||||
|
|
||||||
include(":stub")
|
include(":stub")
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,12 @@ public class ActivityThread {
|
|||||||
public static void initializeMainlineModules() {
|
public static void initializeMainlineModules() {
|
||||||
throw new UnsupportedOperationException("STUB!");
|
throw new UnsupportedOperationException("STUB!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ActivityThread systemMain() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContextImpl getSystemContext() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package android.app;
|
||||||
|
|
||||||
|
public class ContextImpl {
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ public interface IPackageManager {
|
|||||||
|
|
||||||
ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId);
|
ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId);
|
||||||
|
|
||||||
|
int checkPermission(String permName, String pkgName, int userId);
|
||||||
|
|
||||||
class Stub {
|
class Stub {
|
||||||
public static IPackageManager asInterface(IBinder binder) {
|
public static IPackageManager asInterface(IBinder binder) {
|
||||||
throw new UnsupportedOperationException("STUB!");
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package android.hardware.security.keymint;
|
||||||
|
|
||||||
|
public @interface BlockMode {
|
||||||
|
public static final int ECB = 1;
|
||||||
|
public static final int CBC = 2;
|
||||||
|
public static final int CTR = 3;
|
||||||
|
public static final int GCM = 32;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package android.hardware.security.keymint;
|
||||||
|
|
||||||
|
public @interface HardwareAuthenticatorType {
|
||||||
|
int NONE = 0;
|
||||||
|
int PASSWORD = 1;
|
||||||
|
int FINGERPRINT = 2;
|
||||||
|
int ANY = -1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package android.hardware.security.keymint;
|
||||||
|
|
||||||
|
public @interface PaddingMode {
|
||||||
|
public static final int NONE = 1;
|
||||||
|
public static final int RSA_OAEP = 2;
|
||||||
|
public static final int RSA_PSS = 3;
|
||||||
|
public static final int RSA_PKCS1_1_5_ENCRYPT = 4;
|
||||||
|
public static final int RSA_PKCS1_1_5_SIGN = 5;
|
||||||
|
public static final int PKCS7 = 64;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package android.os;
|
||||||
|
|
||||||
|
/** Stub for android.os.SELinux. */
|
||||||
|
public class SELinux {
|
||||||
|
public static boolean checkSELinuxAccess(
|
||||||
|
String scon, String tcon, String tclass, String perm) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,10 @@ public class ServiceManager {
|
|||||||
throw new UnsupportedOperationException("STUB!");
|
throw new UnsupportedOperationException("STUB!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isDeclared(String name) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
public static String[] listServices() {
|
public static String[] listServices() {
|
||||||
throw new UnsupportedOperationException("STUB!");
|
throw new UnsupportedOperationException("STUB!");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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 final int errorCode;
|
||||||
|
|
||||||
|
public ServiceSpecificException(int errorCode, String message) {
|
||||||
|
super(message);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceSpecificException(int errorCode) {
|
||||||
|
this(errorCode, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package android.security.keymaster;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class ExportResult implements Parcelable {
|
||||||
|
public final byte[] exportData;
|
||||||
|
public final int resultCode;
|
||||||
|
|
||||||
|
public ExportResult(int resultCode) {
|
||||||
|
this.resultCode = resultCode;
|
||||||
|
this.exportData = new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<ExportResult> CREATOR = new Creator<ExportResult>() {
|
||||||
|
@Override
|
||||||
|
public ExportResult createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExportResult[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package android.security.keymaster;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class KeyCharacteristics implements Parcelable {
|
||||||
|
public KeymasterArguments hwEnforced;
|
||||||
|
public KeymasterArguments swEnforced;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<KeyCharacteristics> CREATOR = new Creator<KeyCharacteristics>() {
|
||||||
|
@Override
|
||||||
|
public KeyCharacteristics createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyCharacteristics[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package android.security.keymaster;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
abstract class KeymasterArgument implements Parcelable {
|
||||||
|
public final int tag;
|
||||||
|
|
||||||
|
protected KeymasterArgument(int tag) {
|
||||||
|
this.tag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<KeymasterArgument> CREATOR = new Creator<KeymasterArgument>() {
|
||||||
|
@Override
|
||||||
|
public KeymasterArgument createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeymasterArgument[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package android.security.keymaster;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class KeymasterArguments implements Parcelable {
|
||||||
|
|
||||||
|
private static final long UINT32_RANGE = 1L << 32;
|
||||||
|
public static final long UINT32_MAX_VALUE = UINT32_RANGE - 1;
|
||||||
|
|
||||||
|
private static final BigInteger UINT64_RANGE = BigInteger.ONE.shiftLeft(64);
|
||||||
|
public static final BigInteger UINT64_MAX_VALUE = UINT64_RANGE.subtract(BigInteger.ONE);
|
||||||
|
|
||||||
|
private List<KeymasterArgument> mArguments;
|
||||||
|
|
||||||
|
public static final @NonNull Parcelable.Creator<KeymasterArguments> CREATOR = new Parcelable.Creator<KeymasterArguments>() {
|
||||||
|
@Override
|
||||||
|
public KeymasterArguments createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeymasterArguments[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public KeymasterArguments() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeymasterArguments(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addEnum(int tag, int value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addEnums(int tag, int... values) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getEnum(int tag, int defaultValue) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Integer> getEnums(int tag) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addEnumTag(int tag, int value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getEnumTagValue(KeymasterArgument arg) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addUnsignedInt(int tag, long value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getUnsignedInt(int tag, long defaultValue) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addUnsignedLong(int tag, BigInteger value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<BigInteger> getUnsignedLongs(int tag) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addLongTag(int tag, BigInteger value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigInteger getLongTagValue(KeymasterArgument arg) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addBoolean(int tag) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getBoolean(int tag) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addBytes(int tag, byte[] value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getBytes(int tag, byte[] defaultValue) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDate(int tag, Date value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDateIfNotNull(int tag, Date value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getDate(int tag, Date defaultValue) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeymasterArgument getArgumentByTag(int tag) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean containsTag(int tag) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel out, int flags) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BigInteger toUint64(long value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package android.security.keymaster;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class KeymasterCertificateChain implements Parcelable {
|
||||||
|
private List<byte[]> mCertificates;
|
||||||
|
|
||||||
|
public KeymasterCertificateChain() {
|
||||||
|
this.mCertificates = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeymasterCertificateChain(List<byte[]> mCertificates) {
|
||||||
|
this.mCertificates = mCertificates;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<KeymasterCertificateChain> CREATOR = new Creator<KeymasterCertificateChain>() {
|
||||||
|
@Override
|
||||||
|
public KeymasterCertificateChain createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeymasterCertificateChain[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
package android.security.keymaster;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class KeymasterDefs {
|
||||||
|
|
||||||
|
private KeymasterDefs() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag types.
|
||||||
|
public static final int KM_INVALID = 0 << 28;
|
||||||
|
public static final int KM_ENUM = 1 << 28;
|
||||||
|
public static final int KM_ENUM_REP = 2 << 28;
|
||||||
|
public static final int KM_UINT = 3 << 28;
|
||||||
|
public static final int KM_UINT_REP = 4 << 28;
|
||||||
|
public static final int KM_ULONG = 5 << 28;
|
||||||
|
public static final int KM_DATE = 6 << 28;
|
||||||
|
public static final int KM_BOOL = 7 << 28;
|
||||||
|
public static final int KM_BIGNUM = 8 << 28;
|
||||||
|
public static final int KM_BYTES = 9 << 28;
|
||||||
|
public static final int KM_ULONG_REP = 10 << 28;
|
||||||
|
|
||||||
|
// Tag values.
|
||||||
|
public static final int KM_TAG_INVALID = KM_INVALID | 0;
|
||||||
|
public static final int KM_TAG_PURPOSE = KM_ENUM_REP | 1;
|
||||||
|
public static final int KM_TAG_ALGORITHM = KM_ENUM | 2;
|
||||||
|
public static final int KM_TAG_KEY_SIZE = KM_UINT | 3;
|
||||||
|
public static final int KM_TAG_BLOCK_MODE = KM_ENUM_REP | 4;
|
||||||
|
public static final int KM_TAG_DIGEST = KM_ENUM_REP | 5;
|
||||||
|
public static final int KM_TAG_PADDING = KM_ENUM_REP | 6;
|
||||||
|
public static final int KM_TAG_CALLER_NONCE = KM_BOOL | 7;
|
||||||
|
public static final int KM_TAG_MIN_MAC_LENGTH = KM_UINT | 8;
|
||||||
|
|
||||||
|
public static final int KM_TAG_RESCOPING_ADD = KM_ENUM_REP | 101;
|
||||||
|
public static final int KM_TAG_RESCOPING_DEL = KM_ENUM_REP | 102;
|
||||||
|
public static final int KM_TAG_BLOB_USAGE_REQUIREMENTS = KM_ENUM | 705;
|
||||||
|
|
||||||
|
public static final int KM_TAG_RSA_PUBLIC_EXPONENT = KM_ULONG | 200;
|
||||||
|
public static final int KM_TAG_INCLUDE_UNIQUE_ID = KM_BOOL | 202;
|
||||||
|
|
||||||
|
public static final int KM_TAG_ACTIVE_DATETIME = KM_DATE | 400;
|
||||||
|
public static final int KM_TAG_ORIGINATION_EXPIRE_DATETIME = KM_DATE | 401;
|
||||||
|
public static final int KM_TAG_USAGE_EXPIRE_DATETIME = KM_DATE | 402;
|
||||||
|
public static final int KM_TAG_MIN_SECONDS_BETWEEN_OPS = KM_UINT | 403;
|
||||||
|
public static final int KM_TAG_MAX_USES_PER_BOOT = KM_UINT | 404;
|
||||||
|
|
||||||
|
public static final int KM_TAG_ALL_USERS = KM_BOOL | 500;
|
||||||
|
public static final int KM_TAG_USER_ID = KM_UINT | 501;
|
||||||
|
public static final int KM_TAG_USER_SECURE_ID = KM_ULONG_REP | 502;
|
||||||
|
public static final int KM_TAG_NO_AUTH_REQUIRED = KM_BOOL | 503;
|
||||||
|
public static final int KM_TAG_USER_AUTH_TYPE = KM_ENUM | 504;
|
||||||
|
public static final int KM_TAG_AUTH_TIMEOUT = KM_UINT | 505;
|
||||||
|
public static final int KM_TAG_ALLOW_WHILE_ON_BODY = KM_BOOL | 506;
|
||||||
|
public static final int KM_TAG_TRUSTED_USER_PRESENCE_REQUIRED = KM_BOOL | 507;
|
||||||
|
public static final int KM_TAG_TRUSTED_CONFIRMATION_REQUIRED = KM_BOOL | 508;
|
||||||
|
public static final int KM_TAG_UNLOCKED_DEVICE_REQUIRED = KM_BOOL | 509;
|
||||||
|
|
||||||
|
public static final int KM_TAG_ALL_APPLICATIONS = KM_BOOL | 600;
|
||||||
|
public static final int KM_TAG_APPLICATION_ID = KM_BYTES | 601;
|
||||||
|
|
||||||
|
public static final int KM_TAG_CREATION_DATETIME = KM_DATE | 701;
|
||||||
|
public static final int KM_TAG_ORIGIN = KM_ENUM | 702;
|
||||||
|
public static final int KM_TAG_ROLLBACK_RESISTANT = KM_BOOL | 703;
|
||||||
|
public static final int KM_TAG_ROOT_OF_TRUST = KM_BYTES | 704;
|
||||||
|
public static final int KM_TAG_UNIQUE_ID = KM_BYTES | 707;
|
||||||
|
public static final int KM_TAG_ATTESTATION_CHALLENGE = KM_BYTES | 708;
|
||||||
|
public static final int KM_TAG_ATTESTATION_ID_BRAND = KM_BYTES | 710;
|
||||||
|
public static final int KM_TAG_ATTESTATION_ID_DEVICE = KM_BYTES | 711;
|
||||||
|
public static final int KM_TAG_ATTESTATION_ID_PRODUCT = KM_BYTES | 712;
|
||||||
|
public static final int KM_TAG_ATTESTATION_ID_SERIAL = KM_BYTES | 713;
|
||||||
|
public static final int KM_TAG_ATTESTATION_ID_IMEI = KM_BYTES | 714;
|
||||||
|
public static final int KM_TAG_ATTESTATION_ID_MEID = KM_BYTES | 715;
|
||||||
|
public static final int KM_TAG_ATTESTATION_ID_MANUFACTURER = KM_BYTES | 716;
|
||||||
|
public static final int KM_TAG_ATTESTATION_ID_MODEL = KM_BYTES | 717;
|
||||||
|
public static final int KM_TAG_DEVICE_UNIQUE_ATTESTATION = KM_BOOL | 720;
|
||||||
|
|
||||||
|
public static final int KM_TAG_ASSOCIATED_DATA = KM_BYTES | 1000;
|
||||||
|
public static final int KM_TAG_NONCE = KM_BYTES | 1001;
|
||||||
|
public static final int KM_TAG_AUTH_TOKEN = KM_BYTES | 1002;
|
||||||
|
public static final int KM_TAG_MAC_LENGTH = KM_UINT | 1003;
|
||||||
|
|
||||||
|
// Algorithm values.
|
||||||
|
public static final int KM_ALGORITHM_RSA = 1;
|
||||||
|
public static final int KM_ALGORITHM_EC = 3;
|
||||||
|
public static final int KM_ALGORITHM_AES = 32;
|
||||||
|
public static final int KM_ALGORITHM_3DES = 33;
|
||||||
|
public static final int KM_ALGORITHM_HMAC = 128;
|
||||||
|
|
||||||
|
// Block modes.
|
||||||
|
public static final int KM_MODE_ECB = 1;
|
||||||
|
public static final int KM_MODE_CBC = 2;
|
||||||
|
public static final int KM_MODE_CTR = 3;
|
||||||
|
public static final int KM_MODE_GCM = 32;
|
||||||
|
|
||||||
|
// Padding modes.
|
||||||
|
public static final int KM_PAD_NONE = 1;
|
||||||
|
public static final int KM_PAD_RSA_OAEP = 2;
|
||||||
|
public static final int KM_PAD_RSA_PSS = 3;
|
||||||
|
public static final int KM_PAD_RSA_PKCS1_1_5_ENCRYPT = 4;
|
||||||
|
public static final int KM_PAD_RSA_PKCS1_1_5_SIGN = 5;
|
||||||
|
public static final int KM_PAD_PKCS7 = 64;
|
||||||
|
|
||||||
|
// Digest modes.
|
||||||
|
public static final int KM_DIGEST_NONE = 0;
|
||||||
|
public static final int KM_DIGEST_MD5 = 1;
|
||||||
|
public static final int KM_DIGEST_SHA1 = 2;
|
||||||
|
public static final int KM_DIGEST_SHA_2_224 = 3;
|
||||||
|
public static final int KM_DIGEST_SHA_2_256 = 4;
|
||||||
|
public static final int KM_DIGEST_SHA_2_384 = 5;
|
||||||
|
public static final int KM_DIGEST_SHA_2_512 = 6;
|
||||||
|
|
||||||
|
// Key origins.
|
||||||
|
public static final int KM_ORIGIN_GENERATED = 0;
|
||||||
|
public static final int KM_ORIGIN_IMPORTED = 2;
|
||||||
|
public static final int KM_ORIGIN_UNKNOWN = 3;
|
||||||
|
public static final int KM_ORIGIN_SECURELY_IMPORTED = 4;
|
||||||
|
|
||||||
|
// Key usability requirements.
|
||||||
|
public static final int KM_BLOB_STANDALONE = 0;
|
||||||
|
public static final int KM_BLOB_REQUIRES_FILE_SYSTEM = 1;
|
||||||
|
|
||||||
|
// Operation Purposes.
|
||||||
|
public static final int KM_PURPOSE_ENCRYPT = 0;
|
||||||
|
public static final int KM_PURPOSE_DECRYPT = 1;
|
||||||
|
public static final int KM_PURPOSE_SIGN = 2;
|
||||||
|
public static final int KM_PURPOSE_VERIFY = 3;
|
||||||
|
public static final int KM_PURPOSE_WRAP = 5;
|
||||||
|
|
||||||
|
// Key formats.
|
||||||
|
public static final int KM_KEY_FORMAT_X509 = 0;
|
||||||
|
public static final int KM_KEY_FORMAT_PKCS8 = 1;
|
||||||
|
public static final int KM_KEY_FORMAT_RAW = 3;
|
||||||
|
|
||||||
|
// User authenticators.
|
||||||
|
public static final int HW_AUTH_PASSWORD = 1 << 0;
|
||||||
|
public static final int HW_AUTH_BIOMETRIC = 1 << 1;
|
||||||
|
|
||||||
|
// Error codes.
|
||||||
|
public static final int KM_ERROR_OK = 0;
|
||||||
|
public static final int KM_ERROR_ROOT_OF_TRUST_ALREADY_SET = -1;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_PURPOSE = -2;
|
||||||
|
public static final int KM_ERROR_INCOMPATIBLE_PURPOSE = -3;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_ALGORITHM = -4;
|
||||||
|
public static final int KM_ERROR_INCOMPATIBLE_ALGORITHM = -5;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_KEY_SIZE = -6;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_BLOCK_MODE = -7;
|
||||||
|
public static final int KM_ERROR_INCOMPATIBLE_BLOCK_MODE = -8;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_MAC_LENGTH = -9;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_PADDING_MODE = -10;
|
||||||
|
public static final int KM_ERROR_INCOMPATIBLE_PADDING_MODE = -11;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_DIGEST = -12;
|
||||||
|
public static final int KM_ERROR_INCOMPATIBLE_DIGEST = -13;
|
||||||
|
public static final int KM_ERROR_INVALID_EXPIRATION_TIME = -14;
|
||||||
|
public static final int KM_ERROR_INVALID_USER_ID = -15;
|
||||||
|
public static final int KM_ERROR_INVALID_AUTHORIZATION_TIMEOUT = -16;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_KEY_FORMAT = -17;
|
||||||
|
public static final int KM_ERROR_INCOMPATIBLE_KEY_FORMAT = -18;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_KEY_ENCRYPTION_ALGORITHM = -19;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_KEY_VERIFICATION_ALGORITHM = -20;
|
||||||
|
public static final int KM_ERROR_INVALID_INPUT_LENGTH = -21;
|
||||||
|
public static final int KM_ERROR_KEY_EXPORT_OPTIONS_INVALID = -22;
|
||||||
|
public static final int KM_ERROR_DELEGATION_NOT_ALLOWED = -23;
|
||||||
|
public static final int KM_ERROR_KEY_NOT_YET_VALID = -24;
|
||||||
|
public static final int KM_ERROR_KEY_EXPIRED = -25;
|
||||||
|
public static final int KM_ERROR_KEY_USER_NOT_AUTHENTICATED = -26;
|
||||||
|
public static final int KM_ERROR_OUTPUT_PARAMETER_NULL = -27;
|
||||||
|
public static final int KM_ERROR_INVALID_OPERATION_HANDLE = -28;
|
||||||
|
public static final int KM_ERROR_INSUFFICIENT_BUFFER_SPACE = -29;
|
||||||
|
public static final int KM_ERROR_VERIFICATION_FAILED = -30;
|
||||||
|
public static final int KM_ERROR_TOO_MANY_OPERATIONS = -31;
|
||||||
|
public static final int KM_ERROR_UNEXPECTED_NULL_POINTER = -32;
|
||||||
|
public static final int KM_ERROR_INVALID_KEY_BLOB = -33;
|
||||||
|
public static final int KM_ERROR_IMPORTED_KEY_NOT_ENCRYPTED = -34;
|
||||||
|
public static final int KM_ERROR_IMPORTED_KEY_DECRYPTION_FAILED = -35;
|
||||||
|
public static final int KM_ERROR_IMPORTED_KEY_NOT_SIGNED = -36;
|
||||||
|
public static final int KM_ERROR_IMPORTED_KEY_VERIFICATION_FAILED = -37;
|
||||||
|
public static final int KM_ERROR_INVALID_ARGUMENT = -38;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_TAG = -39;
|
||||||
|
public static final int KM_ERROR_INVALID_TAG = -40;
|
||||||
|
public static final int KM_ERROR_MEMORY_ALLOCATION_FAILED = -41;
|
||||||
|
public static final int KM_ERROR_INVALID_RESCOPING = -42;
|
||||||
|
public static final int KM_ERROR_IMPORT_PARAMETER_MISMATCH = -44;
|
||||||
|
public static final int KM_ERROR_SECURE_HW_ACCESS_DENIED = -45;
|
||||||
|
public static final int KM_ERROR_OPERATION_CANCELLED = -46;
|
||||||
|
public static final int KM_ERROR_CONCURRENT_ACCESS_CONFLICT = -47;
|
||||||
|
public static final int KM_ERROR_SECURE_HW_BUSY = -48;
|
||||||
|
public static final int KM_ERROR_SECURE_HW_COMMUNICATION_FAILED = -49;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_EC_FIELD = -50;
|
||||||
|
public static final int KM_ERROR_MISSING_NONCE = -51;
|
||||||
|
public static final int KM_ERROR_INVALID_NONCE = -52;
|
||||||
|
public static final int KM_ERROR_MISSING_MAC_LENGTH = -53;
|
||||||
|
public static final int KM_ERROR_KEY_RATE_LIMIT_EXCEEDED = -54;
|
||||||
|
public static final int KM_ERROR_CALLER_NONCE_PROHIBITED = -55;
|
||||||
|
public static final int KM_ERROR_KEY_MAX_OPS_EXCEEDED = -56;
|
||||||
|
public static final int KM_ERROR_INVALID_MAC_LENGTH = -57;
|
||||||
|
public static final int KM_ERROR_MISSING_MIN_MAC_LENGTH = -58;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_MIN_MAC_LENGTH = -59;
|
||||||
|
public static final int KM_ERROR_CANNOT_ATTEST_IDS = -66;
|
||||||
|
public static final int KM_ERROR_DEVICE_LOCKED = -72;
|
||||||
|
public static final int KM_ERROR_UNIMPLEMENTED = -100;
|
||||||
|
public static final int KM_ERROR_VERSION_MISMATCH = -101;
|
||||||
|
public static final int KM_ERROR_UNKNOWN_ERROR = -1000;
|
||||||
|
|
||||||
|
public static final Map<Integer, String> sErrorCodeToString = new HashMap<Integer, String>();
|
||||||
|
static {
|
||||||
|
sErrorCodeToString.put(KM_ERROR_OK, "OK");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_PURPOSE, "Unsupported purpose");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_PURPOSE, "Incompatible purpose");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_ALGORITHM, "Unsupported algorithm");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_ALGORITHM, "Incompatible algorithm");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_KEY_SIZE, "Unsupported key size");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_BLOCK_MODE, "Unsupported block mode");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_BLOCK_MODE, "Incompatible block mode");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_MAC_LENGTH,
|
||||||
|
"Unsupported MAC or authentication tag length");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_PADDING_MODE, "Unsupported padding mode");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_PADDING_MODE, "Incompatible padding mode");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_DIGEST, "Unsupported digest");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_DIGEST, "Incompatible digest");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_EXPIRATION_TIME, "Invalid expiration time");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_USER_ID, "Invalid user ID");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_AUTHORIZATION_TIMEOUT,
|
||||||
|
"Invalid user authorization timeout");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_KEY_FORMAT, "Unsupported key format");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_KEY_FORMAT, "Incompatible key format");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_INPUT_LENGTH, "Invalid input length");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_KEY_NOT_YET_VALID, "Key not yet valid");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_KEY_EXPIRED, "Key expired");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_KEY_USER_NOT_AUTHENTICATED, "Key user not authenticated");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_OPERATION_HANDLE, "Invalid operation handle");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_VERIFICATION_FAILED, "Signature/MAC verification failed");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_TOO_MANY_OPERATIONS, "Too many operations");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_KEY_BLOB, "Invalid key blob");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_ARGUMENT, "Invalid argument");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_TAG, "Unsupported tag");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_TAG, "Invalid tag");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_MEMORY_ALLOCATION_FAILED, "Memory allocation failed");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_EC_FIELD, "Unsupported EC field");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_MISSING_NONCE, "Required IV missing");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_NONCE, "Invalid IV");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_CALLER_NONCE_PROHIBITED,
|
||||||
|
"Caller-provided IV not permitted");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_MAC_LENGTH,
|
||||||
|
"Invalid MAC or authentication tag length");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_CANNOT_ATTEST_IDS, "Unable to attest device ids");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_DEVICE_LOCKED, "Device locked");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNIMPLEMENTED, "Not implemented");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNKNOWN_ERROR, "Unknown error");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getTagType(int tag) {
|
||||||
|
return tag & (0xF << 28);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getErrorMessage(int errorCode) {
|
||||||
|
String result = sErrorCodeToString.get(errorCode);
|
||||||
|
if (result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return String.valueOf(errorCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package android.security.keystore;
|
||||||
|
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.security.keymaster.KeymasterCertificateChain;
|
||||||
|
|
||||||
|
public interface IKeystoreCertificateChainCallback {
|
||||||
|
void onFinished(KeystoreResponse keystoreResponse, KeymasterCertificateChain keymasterCertificateChain)
|
||||||
|
throws RemoteException;
|
||||||
|
|
||||||
|
public static abstract class Stub {
|
||||||
|
public static IKeystoreCertificateChainCallback asInterface(IBinder b) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package android.security.keystore;
|
||||||
|
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.security.keymaster.ExportResult;
|
||||||
|
|
||||||
|
public interface IKeystoreExportKeyCallback {
|
||||||
|
void onFinished(ExportResult exportResult) throws RemoteException;
|
||||||
|
|
||||||
|
public static abstract class Stub {
|
||||||
|
public static IKeystoreExportKeyCallback asInterface(IBinder b) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package android.security.keystore;
|
||||||
|
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.IInterface;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.security.keymaster.KeyCharacteristics;
|
||||||
|
|
||||||
|
public interface IKeystoreKeyCharacteristicsCallback extends IInterface {
|
||||||
|
void onFinished(KeystoreResponse keystoreResponse, KeyCharacteristics keyCharacteristics) throws RemoteException;
|
||||||
|
|
||||||
|
public static abstract class Stub {
|
||||||
|
public static IKeystoreKeyCharacteristicsCallback asInterface(IBinder b) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
package android.security.keystore;
|
package android.security.keystore;
|
||||||
|
|
||||||
|
import java.lang.String;
|
||||||
|
|
||||||
public interface IKeystoreService {
|
public interface IKeystoreService {
|
||||||
|
public static final String DESCRIPTOR = "android.security.keystore.IKeystoreService";
|
||||||
|
|
||||||
class Stub {
|
class Stub {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package android.security.keystore;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class KeystoreResponse implements Parcelable {
|
||||||
|
public final int error_code_;
|
||||||
|
public final String error_msg_;
|
||||||
|
|
||||||
|
protected KeystoreResponse(int error_code, String error_msg) {
|
||||||
|
this.error_code_ = error_code;
|
||||||
|
this.error_msg_ = error_msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<KeystoreResponse> CREATOR = new Creator<KeystoreResponse>() {
|
||||||
|
@Override
|
||||||
|
public KeystoreResponse createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeystoreResponse[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package android.system.keystore2;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class CreateOperationResponse implements Parcelable {
|
||||||
|
public IKeystoreOperation iOperation;
|
||||||
|
|
||||||
|
public OperationChallenge operationChallenge;
|
||||||
|
|
||||||
|
public KeyParameters parameters;
|
||||||
|
|
||||||
|
public byte[] upgradedBlob;
|
||||||
|
|
||||||
|
public static final Creator<CreateOperationResponse> CREATOR = new Creator<CreateOperationResponse>() {
|
||||||
|
@Override
|
||||||
|
public CreateOperationResponse createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CreateOperationResponse[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull Parcel parcel, int i) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package android.system.keystore2;
|
||||||
|
|
||||||
|
public @interface Domain {
|
||||||
|
public static final int APP = 0;
|
||||||
|
public static final int GRANT = 1;
|
||||||
|
public static final int SELINUX = 2;
|
||||||
|
public static final int BLOB = 3;
|
||||||
|
public static final int KEY_ID = 4;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package android.system.keystore2;
|
||||||
|
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.IInterface;
|
||||||
|
|
||||||
|
public interface IKeystoreOperation extends IInterface {
|
||||||
|
public static final java.lang.String DESCRIPTOR = "android.system.keystore2.IKeystoreOperation";
|
||||||
|
|
||||||
|
public void updateAad(byte[] aadInput);
|
||||||
|
|
||||||
|
public byte[] update(byte[] input);
|
||||||
|
|
||||||
|
public byte[] finish(byte[] input, byte[] signature);
|
||||||
|
|
||||||
|
public void abort() throws android.os.RemoteException;
|
||||||
|
|
||||||
|
abstract class Stub extends Binder implements IKeystoreOperation {
|
||||||
|
public static IKeystoreOperation asInterface(IBinder b) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder asBinder() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateAad(byte[] aadInput) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package android.system.keystore2;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.hardware.security.keymint.KeyParameter;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class KeyParameters implements Parcelable {
|
||||||
|
public KeyParameter[] keyParameter;
|
||||||
|
|
||||||
|
public static final Creator<KeyParameters> CREATOR = new Creator<KeyParameters>() {
|
||||||
|
@Override
|
||||||
|
public KeyParameters createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyParameters[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull Parcel parcel, int i) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package android.system.keystore2;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class OperationChallenge implements Parcelable {
|
||||||
|
public long challenge = 0L;
|
||||||
|
|
||||||
|
public static final Creator<OperationChallenge> CREATOR = new Creator<OperationChallenge>() {
|
||||||
|
@Override
|
||||||
|
public OperationChallenge createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OperationChallenge[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull Parcel parcel, int i) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user