Compare commits
210
Commits
v3.0
...
v6.0.1-282
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b40754fd8b | ||
|
|
0f32194a89 | ||
|
|
64723b2564 | ||
|
|
42842e22c0 | ||
|
|
77fc96db37 | ||
|
|
b27a33b444 | ||
|
|
bae65ac47c | ||
|
|
217a5dc7f3 | ||
|
|
0229368c04 | ||
|
|
0f61bb841a | ||
|
|
50f2e98375 | ||
|
|
f4e2619eba | ||
|
|
649136ab4e | ||
|
|
d155a0ded6 | ||
|
|
edac284972 | ||
|
|
cbb73a0b0e | ||
|
|
3140ff5e96 | ||
|
|
8544aac260 | ||
|
|
134d5111ad | ||
|
|
4c801f2089 | ||
|
|
afc5caeb1b | ||
|
|
0e9ea10b50 | ||
|
|
6ae5ea391c | ||
|
|
f554b36416 | ||
|
|
684542f4b1 | ||
|
|
240728f98d | ||
|
|
95b8c27a9f | ||
|
|
66a8c7fbf8 | ||
|
|
36c93decc6 | ||
|
|
55e39c7f01 | ||
|
|
44816c1a8d | ||
|
|
60b6ec64c2 | ||
|
|
2f21cd57a0 | ||
|
|
fb7f0ca098 | ||
|
|
b323f41b08 | ||
|
|
c46aaa34f8 | ||
|
|
ca86148633 | ||
|
|
0fbdf42e9d | ||
|
|
91ce9485fe | ||
|
|
69fbdc112d | ||
|
|
1ee66be05a | ||
|
|
0b2c34ff8c | ||
|
|
2704eff797 | ||
|
|
22d1972bc7 | ||
|
|
5f72acb1e7 | ||
|
|
d7dc5e0b63 | ||
|
|
59836e143c | ||
|
|
a7e7e454e7 | ||
|
|
bba4a9ebfa | ||
|
|
58b98fd308 | ||
|
|
0617297b22 | ||
|
|
aef80c3105 | ||
|
|
032c87d50e | ||
|
|
62d666fd63 | ||
|
|
39b3811dc3 | ||
|
|
1446090da9 | ||
|
|
52ff39d130 | ||
|
|
3dea767058 | ||
|
|
17b359e94d | ||
|
|
57035b2c94 | ||
|
|
e8d12c4165 | ||
|
|
d048174402 | ||
|
|
21fb3ba879 | ||
|
|
ca56928add | ||
|
|
521e28cece | ||
|
|
372001e8de | ||
|
|
6fa10d7111 | ||
|
|
7c58e2f039 | ||
|
|
6dc7755658 | ||
|
|
921edecb86 | ||
|
|
756aa2efb2 | ||
|
|
0ebfef55b6 | ||
|
|
94c7d00fb5 | ||
|
|
fe21106151 | ||
|
|
3d8d193a44 | ||
|
|
85eef8054d | ||
|
|
051a003b33 | ||
|
|
29b2a85e9f | ||
|
|
890f47009b | ||
|
|
b85b3dea48 | ||
|
|
c8b3e9e528 | ||
|
|
a5375f7426 | ||
|
|
9e1b459b74 | ||
|
|
6476216aa3 | ||
|
|
bc7b11a380 | ||
|
|
22cadc125e | ||
|
|
5267c9dd00 | ||
|
|
dfc8aac920 | ||
|
|
bdb460411a | ||
|
|
ea792c7b78 | ||
|
|
18724cf40d | ||
|
|
1b7800345d | ||
|
|
54c12a9fd5 | ||
|
|
1bc47840d5 | ||
|
|
c8fadb07ae | ||
|
|
c0b14eeeb1 | ||
|
|
47ab0225e1 | ||
|
|
ebb6336281 | ||
|
|
784373c8b5 | ||
|
|
2241bfb13d | ||
|
|
954478b89b | ||
|
|
191085087b | ||
|
|
f870598e77 | ||
|
|
8fdc59a142 | ||
|
|
b6f9d7b486 | ||
|
|
99e6b0b5ea | ||
|
|
397bb6338b | ||
|
|
cb81116701 | ||
|
|
45477a7898 | ||
|
|
634a1293c1 | ||
|
|
f115eda2dc | ||
|
|
217edf61fe | ||
|
|
ee5bf2e1a7 | ||
|
|
2181157cb6 | ||
|
|
aa4917e623 | ||
|
|
f06cb30b40 | ||
|
|
03c71bd202 | ||
|
|
7e2fc0b288 | ||
|
|
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 |
+110
-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,73 +64,106 @@ jobs:
|
|||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: |
|
run: |
|
||||||
chmod +x ./gradlew
|
chmod +x ./gradlew
|
||||||
|
|
||||||
./gradlew zipRelease zipDebug -Porg.gradle.parallel=true -Porg.gradle.vfs.watch=true -Dorg.gradle.jvmargs=-Xmx2048m
|
./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 }}
|
||||||
|
|
||||||
|
- name: Bump update.json
|
||||||
|
run: |
|
||||||
|
COUNT=$(git rev-list HEAD --count)
|
||||||
|
RELEASE_NAME=$(basename zips/*Release*.zip)
|
||||||
|
ZIP_URL="https://github.com/${{ github.repository }}/releases/download/${VER}/${RELEASE_NAME}"
|
||||||
|
jq ".versionCode = $COUNT | .zipUrl = \"$ZIP_URL\"" module/update.json > /tmp/update.json
|
||||||
|
mv /tmp/update.json module/update.json
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add module/update.json
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore(release): bump update.json to $VER [skip ci]"
|
||||||
|
git push origin HEAD:main
|
||||||
|
}
|
||||||
|
env:
|
||||||
|
VER: ${{ steps.ver.outputs.version }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -1 +1,10 @@
|
|||||||
out
|
out
|
||||||
|
.gradle
|
||||||
|
.kotlin
|
||||||
|
app/build
|
||||||
|
build
|
||||||
|
native-certgen/target
|
||||||
|
app/src/main/jniLibs
|
||||||
|
|
||||||
|
# Internal planning/strategy plane, never publish
|
||||||
|
.omc/
|
||||||
|
|||||||
@@ -1,140 +1,139 @@
|
|||||||
# TEESimulator – A Full TEE Emulation Framework
|
<p align="center">
|
||||||
|
<h1 align="center">TEESimulator-RS</h1>
|
||||||
|
<p align="center"><b>Full TEE Emulation for Rooted Android</b></p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/Enginex0/TEESimulator-RS/actions/workflows/build.yml"><img src="https://github.com/Enginex0/TEESimulator-RS/actions/workflows/build.yml/badge.svg" alt="Build"></a>
|
||||||
|
<img src="https://img.shields.io/badge/Android-10%2B-green?logo=android" alt="Android 10+">
|
||||||
|
<a href="https://t.me/superpowers9"><img src="https://img.shields.io/badge/Telegram-community-blue?logo=telegram" alt="Telegram"></a>
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
|
||||||
**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]
|
||||||
|
> Fork of [JingMatrix/TEESimulator](https://github.com/JingMatrix/TEESimulator) with native Rust certificate generation, key persistence, and AOSP-compliant attestation behavior. For the upstream project, see the original repo.
|
||||||
|
|
||||||
## ✨ Core Principles
|
## What It Does
|
||||||
|
|
||||||
* **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.
|
TEESimulator intercepts Binder IPC at the `ioctl` level inside the `keystore2` process and generates entire certificate chains from scratch, signed by your keybox, with correct attestation extensions. Apps that verify hardware attestation see a legitimate device.
|
||||||
* **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
|
This is not TrickyStore. TEESimulator replaces TrickyStore and its forks entirely. It shares the same config paths for drop-in compatibility, but the internals are different: native Rust cert generation, binder-level interception via `lsplt`, per-UID rate limiting, key persistence, and AOSP-spec attestation behavior.
|
||||||
- Android 10 or above
|
|
||||||
|
|
||||||
## 📦 Installation & Configuration
|
## Requirements
|
||||||
|
|
||||||
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.
|
> [!IMPORTANT]
|
||||||
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.
|
> A valid `keybox.xml` is required for hardware-level attestation. Without one, the module generates software-level certificates that won't pass strict hardware checks.
|
||||||
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.**
|
1. Android 10+
|
||||||
|
2. Root manager: KernelSU, Magisk, or APatch
|
||||||
|
3. `keybox.xml` at `/data/adb/tricky_store/keybox.xml`
|
||||||
|
|
||||||
### The `keybox.xml` Root of Trust
|
## Quick Start
|
||||||
|
|
||||||
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.
|
1. Download the latest ZIP from [Releases](https://github.com/Enginex0/TEESimulator-RS/releases)
|
||||||
|
2. Install via your root manager and reboot
|
||||||
|
3. Place your keybox at `/data/adb/tricky_store/keybox.xml`
|
||||||
|
4. Configure targets in `/data/adb/tricky_store/target.txt`
|
||||||
|
5. Verify with Play Integrity or Key Attestation Demo
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**Native Cert Generation** — `libcertgen.so` generates X.509 chains in Rust using `ring` and manual DER encoding. BouncyCastle fallback for unsupported curves (P-224, P-521, Curve25519).
|
||||||
|
|
||||||
|
**Binder Interception** — PLT hook on `ioctl()` in `libc.so` via `lsplt` inside `keystore2`. Intercepts `generateKey`, `importKey`, and `getKeyEntry` transactions.
|
||||||
|
|
||||||
|
**AOSP Compliance** — Self-signed certs for non-attested keys (matching `ta/src/keys.rs`), correct AuthorizationList tag ordering, version-guarded extension fields, `authorize_create` enforcement.
|
||||||
|
|
||||||
|
**Key Persistence** — Generated keys survive reboots. File-backed with file-level locking.
|
||||||
|
|
||||||
|
**Rate Limiting** — Per-UID hardware keygen cap (2/30s window, 2 concurrent). Overflow falls to software certs.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
All config files live at `/data/adb/tricky_store/` and are hot-reloaded via `FileObserver`.
|
||||||
|
|
||||||
|
### target.txt
|
||||||
|
|
||||||
|
Controls which apps get intercepted and the simulation mode.
|
||||||
|
|
||||||
|
| Suffix | Mode |
|
||||||
|
|--------|------|
|
||||||
|
| `!` | Force software key generation |
|
||||||
|
| `?` | Force leaf certificate patching (real TEE key, patched cert) |
|
||||||
|
| *(none)* | Automatic selection |
|
||||||
|
|
||||||
|
Multi-keybox support via `[filename.xml]` headers:
|
||||||
|
|
||||||
```xml
|
|
||||||
<?xml version="1.0"?>
|
|
||||||
<AndroidAttestation>
|
|
||||||
<Keybox DeviceID="...">
|
|
||||||
<Key algorithm="ecdsa|rsa">
|
|
||||||
<PrivateKey format="pem">...</PrivateKey>
|
|
||||||
<CertificateChain>...</CertificateChain>
|
|
||||||
</Key>
|
|
||||||
</Keybox>
|
|
||||||
</AndroidAttestation>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Mode and Keybox Configuration (`target.txt`)
|
|
||||||
|
|
||||||
TEESimulator currently operates in two primary modes as it transitions towards full emulation.
|
|
||||||
You can control the simulation mode and the specific keybox.xml file used on a per-package basis.
|
|
||||||
|
|
||||||
#### Mode Suffixes
|
|
||||||
|
|
||||||
* **`!` → Force Generation Mode:** Creates a complete, software-based virtual key. This is the foundation of the 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.
|
|
||||||
* **No symbol → Automatic Mode:** The module selects the most appropriate mode for the device.
|
|
||||||
|
|
||||||
#### Multi-Keybox Configuration
|
|
||||||
|
|
||||||
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]).
|
|
||||||
|
|
||||||
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
|
|
||||||
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.
|
|
||||||
# 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.
|
|
||||||
# The file must be located at /data/adb/tricky_store/demo_keybox.xml
|
|
||||||
[demo_keybox.xml]
|
|
||||||
org.matrix.demo
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Security Patch Level (`security_patch.txt`)
|
### security_patch.txt
|
||||||
|
|
||||||
This file allows you to configure the `osPatchLevel`, `vendorPatchLevel`, and `bootPatchLevel` that the simulator will report in its patched or forged attestation certificates.
|
Override patch levels reported in attestation certificates. Global defaults at top, per-package overrides with `[package.name]`.
|
||||||
|
|
||||||
**Note:** This only affects the Key Attestation data generated by the simulator. It does not change the actual system properties of your device.
|
| Key | Scope |
|
||||||
|
|-----|-------|
|
||||||
|
| `system` | OS patch level |
|
||||||
|
| `vendor` | Vendor patch level |
|
||||||
|
| `boot` | Boot/kernel patch level |
|
||||||
|
| `all` | Sets all three |
|
||||||
|
|
||||||
#### Global and Per-Package Configuration
|
Special values: `today`, `YYYY-MM-DD` templates, `no` (omit tag), `device_default`, `prop` (read from system property).
|
||||||
|
|
||||||
You can set a global patch level that applies to all applications, and you can also override these settings for specific packages. The syntax is hierarchical:
|
|
||||||
|
|
||||||
* Settings defined at the top of the file, before any `[package.name]` line, are **global** and serve as the default for all apps.
|
|
||||||
* To create a specific configuration for an application, add its package name in square brackets (e.g., `[com.google.android.gms]`). All settings following this line will apply *only* to that package until a new package context is declared.
|
|
||||||
|
|
||||||
#### Configuration Keys and Values
|
|
||||||
|
|
||||||
You can specify the patch level for the following components using a `key=value` format:
|
|
||||||
|
|
||||||
* `system`: The main OS patch level.
|
|
||||||
* `vendor`: The vendor patch level.
|
|
||||||
* `boot`: The boot/kernel patch level.
|
|
||||||
* `all`: A convenient shorthand to set the same date for `system`, `vendor`, and `boot` simultaneously. Any individual key can still be used to override the value set by `all`.
|
|
||||||
|
|
||||||
Dates should be provided in `YYYY-MM-DD` format (e.g., `2025-11-05`).
|
|
||||||
|
|
||||||
#### Special Keywords
|
|
||||||
|
|
||||||
In addition to static dates, several special keywords provide advanced, dynamic control:
|
|
||||||
|
|
||||||
* **`today`**: Dynamically uses the current date every time an attestation is generated. This ensures the device always appears up-to-date without needing manual edits.
|
|
||||||
|
|
||||||
* **Date Templates**: You can create semi-dynamic dates using `YYYY`, `MM`, and `DD` as placeholders for the current year, month, and day. For example, `YYYY-MM-05` will always resolve to the 5th of the current month and year.
|
|
||||||
|
|
||||||
* **`no`**: This keyword instructs the simulator to **completely omit** the corresponding patch level tag from the generated attestation.
|
|
||||||
|
|
||||||
* **`device_default`**: This keyword forces the simulator to fall back and use the device's **real hardware value** for that specific patch level. This is essential for creating exceptions to a global override or an `all` rule.
|
|
||||||
|
|
||||||
#### Example Configuration
|
|
||||||
|
|
||||||
This example demonstrates how to combine global settings, per-package overrides, and special keywords for fine-grained control.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
# --- Global Configuration ---
|
|
||||||
# This is the default for all apps unless specified otherwise.
|
|
||||||
# - Forge a recent system patch level, the 5th of the current month (a common patch date).
|
|
||||||
# - Use the device's real vendor patch level.
|
|
||||||
# - Do not report a boot patch level at all.
|
|
||||||
system=YYYY-MM-05
|
system=YYYY-MM-05
|
||||||
vendor=device_default
|
vendor=device_default
|
||||||
boot=no
|
boot=no
|
||||||
|
|
||||||
# --- Per-Package Override for Google Play Services ---
|
|
||||||
# This app will report an older, specific date for its system patch.
|
|
||||||
# It will inherit the global settings for vendor (device_default) and boot (no).
|
|
||||||
[com.google.android.gms]
|
[com.google.android.gms]
|
||||||
system=2024-10-01
|
system=2025-10-01
|
||||||
|
|
||||||
# --- Per-Package Override for a Demo App ---
|
|
||||||
# This app gets a completely custom configuration.
|
|
||||||
[org.matrix.demo]
|
|
||||||
# Set a base date for all patch levels...
|
|
||||||
all=2025-09-15
|
|
||||||
# ...but make an exception: use the real boot patch level instead of the one from 'all'.
|
|
||||||
boot=device_default
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Building from Source
|
||||||
|
|
||||||
|
Prerequisites: JDK 21, Android SDK/NDK 27, Rust stable with `aarch64-linux-android` target, `cargo-ndk`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --recursive https://github.com/Enginex0/TEESimulator-RS.git
|
||||||
|
cd TEESimulator-RS
|
||||||
|
./gradlew zipRelease zipDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
Output ZIPs in `out/`. Gradle invokes `cargo ndk` automatically to cross-compile `libcertgen.so`.
|
||||||
|
|
||||||
|
Push to `main` or use **Actions > Build > Run workflow** to trigger CI.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
| Root Manager | Status |
|
||||||
|
|---|---|
|
||||||
|
| KernelSU | Tested (Action button + lifecycle scripts) |
|
||||||
|
| Magisk | Supported |
|
||||||
|
| APatch | Supported |
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://t.me/superpowers9">
|
||||||
|
<img src="https://img.shields.io/badge/SuperPowers_Telegram-Join-blue?style=for-the-badge&logo=telegram" alt="Telegram">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- [JingMatrix](https://github.com/JingMatrix/TEESimulator) — original TEESimulator and interception architecture
|
||||||
|
- [5ec1cff](https://github.com/5ec1cff/TrickyStore) — TrickyStore, the project that pioneered keystore interception
|
||||||
|
- [LSPlt](https://github.com/LSPosed/LSPlt) — PLT hook library
|
||||||
|
- [ring](https://github.com/briansmith/ring) — Rust cryptography library
|
||||||
|
- [MhmRdd](https://github.com/MhmRdd) — AOSP compliance work via upstream [PR #157](https://github.com/JingMatrix/TEESimulator/pull/157)
|
||||||
|
- [fatalcoder524](https://github.com/fatalcoder524) — contributor and collaborator
|
||||||
|
- [huguangares](https://github.com/huguangares) — collaborator and tester
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[GNU General Public License v3.0](LICENSE)
|
||||||
|
|||||||
+84
-17
@@ -2,6 +2,7 @@ import com.android.build.api.artifact.SingleArtifact
|
|||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.gradle.process.ExecOperations
|
import org.gradle.process.ExecOperations
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
@@ -29,7 +30,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 = "v3.0"
|
val verName = "v6.0.1"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "org.matrix.TEESimulator"
|
namespace = "org.matrix.TEESimulator"
|
||||||
@@ -65,12 +66,76 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(JvmTarget.JVM_21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(project(":stub"))
|
compileOnly(project(":stub"))
|
||||||
compileOnly(libs.annotation)
|
compileOnly(libs.annotation)
|
||||||
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)
|
||||||
|
environment("PATH", "${System.getProperty("user.home")}/.cargo/bin:${System.getenv("PATH") ?: ""}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AGP auto-detects jniLibs/ as an input to mergeJniLibFolders — wire the dependency
|
||||||
|
tasks.configureEach {
|
||||||
|
if (name.endsWith("JniLibFolders") && name.startsWith("merge")) {
|
||||||
|
dependsOn(buildRustCertgen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-rewrite module/update.json on every packaging build so versionCode and
|
||||||
|
// zipUrl track gitCommitCount automatically, matching module.prop.
|
||||||
|
val refreshUpdateJson by tasks.registering {
|
||||||
|
group = "TEESimulator-RS Module Packaging"
|
||||||
|
description = "Rewrite module/update.json to match current verName and gitCommitCount."
|
||||||
|
|
||||||
|
val updateJsonFile = rootProject.projectDir.resolve("module/update.json")
|
||||||
|
val capturedVerName = verName
|
||||||
|
val capturedCount = gitCommitCount
|
||||||
|
|
||||||
|
inputs.property("verName", capturedVerName)
|
||||||
|
inputs.property("gitCommitCount", capturedCount)
|
||||||
|
outputs.file(updateJsonFile)
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
val fullVer = "$capturedVerName-$capturedCount"
|
||||||
|
updateJsonFile.writeText(
|
||||||
|
"""{
|
||||||
|
"version": "$fullVer",
|
||||||
|
"versionCode": $capturedCount,
|
||||||
|
"zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/download/$fullVer/TEESimulator-RS-$fullVer-Release.zip",
|
||||||
|
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/changelog.md"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
androidComponents {
|
androidComponents {
|
||||||
onVariants(selector().all()) { variant ->
|
onVariants(selector().all()) { variant ->
|
||||||
val capitalized = variant.name.replaceFirstChar { it.uppercase() }
|
val capitalized = variant.name.replaceFirstChar { it.uppercase() }
|
||||||
@@ -79,21 +144,23 @@ 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) {
|
||||||
dependsOn("package${capitalized}")
|
dependsOn("package${capitalized}")
|
||||||
} else {
|
} else {
|
||||||
dependsOn("minify${capitalized}WithR8")
|
dependsOn("minify${capitalized}WithR8")
|
||||||
|
dependsOn("strip${capitalized}DebugSymbols")
|
||||||
}
|
}
|
||||||
dependsOn("strip${capitalized}DebugSymbols")
|
dependsOn(buildRustCertgen)
|
||||||
|
dependsOn(refreshUpdateJson)
|
||||||
|
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
from(variant.artifacts.get(SingleArtifact.APK)) {
|
from(variant.artifacts.get(SingleArtifact.APK)) {
|
||||||
@@ -110,13 +177,14 @@ androidComponents {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
from(
|
val nativeLibsDir = if (isDebug) {
|
||||||
project.layout.buildDirectory.dir(
|
"intermediates/merged_native_libs/${variant.name}/merge${capitalized}NativeLibs/out/lib"
|
||||||
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
|
} else {
|
||||||
)
|
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
|
||||||
) {
|
}
|
||||||
into("lib") // Place them in the 'lib' subfolder of the staging directory.
|
from(project.layout.buildDirectory.dir(nativeLibsDir)) {
|
||||||
include("**/libinject.so", "**/libTEESimulator.so")
|
into("lib")
|
||||||
|
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.
|
||||||
@@ -131,8 +199,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})",
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +210,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)
|
||||||
|
|
||||||
@@ -156,7 +223,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)
|
||||||
@@ -170,7 +237,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(
|
||||||
@@ -183,7 +250,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)
|
||||||
@@ -12,7 +13,7 @@ add_subdirectory(external/LSPlt/lsplt/src/main/jni)
|
|||||||
|
|
||||||
add_compile_definitions(BINDER_DISABLE_NATIVE_HANDLE)
|
add_compile_definitions(BINDER_DISABLE_NATIVE_HANDLE)
|
||||||
add_library(utils SHARED stub/stub_utils.cpp)
|
add_library(utils SHARED stub/stub_utils.cpp)
|
||||||
target_include_directories(utils PUBLIC external/AOSP/include compat)
|
target_include_directories(utils PUBLIC external/AOSP/include)
|
||||||
|
|
||||||
add_library(binder SHARED stub/stub_binder.cpp)
|
add_library(binder SHARED stub/stub_binder.cpp)
|
||||||
target_include_directories(binder PUBLIC external/AOSP/include)
|
target_include_directories(binder PUBLIC external/AOSP/include)
|
||||||
@@ -22,7 +23,10 @@ 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_library(${CMAKE_PROJECT_NAME} SHARED binder_interceptor.cpp compat/refbase_compat.cpp)
|
add_executable(libsupervisor.so supervisor.cpp)
|
||||||
|
target_link_libraries(libsupervisor.so PRIVATE log)
|
||||||
|
|
||||||
|
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,19 +235,21 @@ class BinderInterceptor : public BBinder {
|
|||||||
struct RegistrationEntry {
|
struct RegistrationEntry {
|
||||||
wp<IBinder> target;
|
wp<IBinder> target;
|
||||||
sp<IBinder> callback_interface;
|
sp<IBinder> callback_interface;
|
||||||
|
std::vector<uint32_t> filtered_codes;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reader-Writer lock for the registry to allow concurrent reads (lookups)
|
|
||||||
mutable std::shared_mutex registry_mutex_;
|
mutable std::shared_mutex registry_mutex_;
|
||||||
std::map<wp<IBinder>, RegistrationEntry> registry_;
|
std::map<wp<IBinder>, RegistrationEntry> registry_;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
BinderInterceptor() = default;
|
BinderInterceptor() = default;
|
||||||
|
|
||||||
// Checks if a specific Binder instance is currently registered for interception
|
bool shouldIntercept(const wp<BBinder> &target, uint32_t code) const {
|
||||||
bool isBinderIntercepted(const wp<BBinder> &target) const {
|
|
||||||
std::shared_lock lock(registry_mutex_);
|
std::shared_lock lock(registry_mutex_);
|
||||||
return registry_.find(target) != registry_.end();
|
auto it = registry_.find(target);
|
||||||
|
if (it == registry_.end()) return false;
|
||||||
|
const auto &codes = it->second.filtered_codes;
|
||||||
|
return codes.empty() || std::find(codes.begin(), codes.end(), code) != codes.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main entry point for processing the "Man-in-the-Middle" logic
|
// Main entry point for processing the "Man-in-the-Middle" logic
|
||||||
@@ -276,6 +278,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 +350,16 @@ 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;
|
||||||
|
|
||||||
|
// AIDL methods use codes in [FIRST_CALL_TRANSACTION, LAST_CALL_TRANSACTION] (1..0x00ffffff).
|
||||||
|
// System transactions (PING, INTERFACE, DUMP, SHELL_COMMAND) use codes above that range.
|
||||||
|
// Skip those — intercepting a ping adds measurable latency that timing detectors flag.
|
||||||
|
if (txn_data->code > 0x00ffffffu && txn_data->code != intercept::kBackdoorCode)
|
||||||
|
return;
|
||||||
|
|
||||||
bool hijack = false;
|
bool hijack = false;
|
||||||
ThreadTransactionInfo info;
|
ThreadTransactionInfo info;
|
||||||
|
|
||||||
@@ -359,9 +368,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 +385,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,44 +428,29 @@ void processBinderReadBuffer(const binder_write_read &bwr) {
|
|||||||
uintptr_t ptr = bwr.read_buffer;
|
uintptr_t ptr = bwr.read_buffer;
|
||||||
uintptr_t end = ptr + bwr.read_consumed;
|
uintptr_t end = ptr + bwr.read_consumed;
|
||||||
|
|
||||||
LOGV("[Hook] Processing Read Buffer: Size=%llu, Consumed=%llu", bwr.read_size, bwr.read_consumed);
|
|
||||||
|
|
||||||
while (ptr < end) {
|
while (ptr < end) {
|
||||||
// Ensure we can read at least the command header
|
|
||||||
if (end - ptr < sizeof(uint32_t))
|
if (end - ptr < sizeof(uint32_t))
|
||||||
break;
|
break;
|
||||||
|
|
||||||
uint32_t cmd = *reinterpret_cast<const uint32_t *>(ptr);
|
uint32_t cmd = *reinterpret_cast<const uint32_t *>(ptr);
|
||||||
ptr += sizeof(uint32_t);
|
ptr += sizeof(uint32_t);
|
||||||
|
|
||||||
// Calculate payload size from the ioctl command code
|
|
||||||
size_t cmd_size = _IOC_SIZE(cmd);
|
size_t cmd_size = _IOC_SIZE(cmd);
|
||||||
|
|
||||||
// Log the command using our generated to-string function
|
|
||||||
LOGV("[Driver -> User] Command: %s (0x%x), DataSize: %zu", getBinderReturnCommandName(cmd), cmd, cmd_size);
|
|
||||||
|
|
||||||
// Safety check: ensure the command's data does not exceed the buffer
|
|
||||||
if (ptr + cmd_size > end) {
|
if (ptr + cmd_size > end) {
|
||||||
LOGE("[Hook] Buffer overflow detected while parsing command %s", getBinderReturnCommandName(cmd));
|
LOGE("[Hook] Buffer overrun parsing command 0x%x", cmd);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We are primarily interested in BR_TRANSACTION commands to intercept
|
if (__builtin_expect(cmd == BR_TRANSACTION || cmd == BR_TRANSACTION_SEC_CTX, 0)) {
|
||||||
if (cmd == BR_TRANSACTION || cmd == BR_TRANSACTION_SEC_CTX) {
|
binder_transaction_data *txn;
|
||||||
binder_transaction_data *txn = nullptr;
|
|
||||||
|
|
||||||
if (cmd == BR_TRANSACTION_SEC_CTX) {
|
if (cmd == BR_TRANSACTION_SEC_CTX) {
|
||||||
// The data is wrapped in a secctx struct
|
txn = &reinterpret_cast<binder_transaction_data_secctx *>(ptr)->transaction_data;
|
||||||
auto *wrapper = reinterpret_cast<binder_transaction_data_secctx *>(ptr);
|
|
||||||
txn = &wrapper->transaction_data;
|
|
||||||
} else {
|
} else {
|
||||||
txn = reinterpret_cast<binder_transaction_data *>(ptr);
|
txn = reinterpret_cast<binder_transaction_data *>(ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
inspectAndRewriteTransaction(txn);
|
inspectAndRewriteTransaction(txn);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance pointer to the next command
|
|
||||||
ptr += cmd_size;
|
ptr += cmd_size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -471,13 +470,17 @@ int intercepted_ioctl(int fd, int request, ...) {
|
|||||||
// 1. Call original kernel ioctl to let the driver do its work
|
// 1. Call original kernel ioctl to let the driver do its work
|
||||||
int result = g_original_ioctl(fd, request, arg);
|
int result = g_original_ioctl(fd, request, arg);
|
||||||
|
|
||||||
// 2. After the call returns, check if it was a BINDER_WRITE_READ and if it succeeded
|
|
||||||
if (result >= 0 && request == BINDER_WRITE_READ && arg != nullptr) {
|
if (result >= 0 && request == BINDER_WRITE_READ && arg != nullptr) {
|
||||||
const auto *bwr = static_cast<const binder_write_read *>(arg);
|
const auto *bwr = static_cast<const binder_write_read *>(arg);
|
||||||
|
// Fast reject: only enter the parser if the buffer could contain a BR_TRANSACTION.
|
||||||
// We only care about data read FROM the driver (i.e., incoming commands)
|
// Pings, ref ops, and looper management never produce BR_TRANSACTION, so scanning
|
||||||
if (bwr->read_consumed > 0) {
|
// their buffers is pure overhead (~2-5us per ioctl in debug builds).
|
||||||
processBinderReadBuffer(*bwr);
|
if (bwr->read_consumed >= sizeof(uint32_t)) {
|
||||||
|
uint32_t first_cmd = *reinterpret_cast<const uint32_t *>(bwr->read_buffer);
|
||||||
|
if (first_cmd == BR_TRANSACTION || first_cmd == BR_TRANSACTION_SEC_CTX
|
||||||
|
|| bwr->read_consumed > sizeof(uint32_t) + _IOC_SIZE(first_cmd)) {
|
||||||
|
processBinderReadBuffer(*bwr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,18 +523,29 @@ status_t BinderInterceptor::handleRegister(const Parcel &data) {
|
|||||||
if (data.readStrongBinder(&callback) != OK || !callback)
|
if (data.readStrongBinder(&callback) != OK || !callback)
|
||||||
return BAD_VALUE;
|
return BAD_VALUE;
|
||||||
|
|
||||||
// We can only intercept local Binders (BBinder), not remote proxies (BpBinder)
|
|
||||||
if (target->localBinder() == nullptr) {
|
if (target->localBinder() == nullptr) {
|
||||||
LOGE("Cannot intercept remote binder proxies.");
|
LOGE("Cannot intercept remote binder proxies.");
|
||||||
return BAD_TYPE;
|
return BAD_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<uint32_t> codes;
|
||||||
|
int32_t code_count = 0;
|
||||||
|
if (data.dataAvail() >= sizeof(int32_t) && data.readInt32(&code_count) == OK && code_count > 0) {
|
||||||
|
codes.reserve(code_count);
|
||||||
|
for (int32_t i = 0; i < code_count; i++) {
|
||||||
|
uint32_t c = 0;
|
||||||
|
if (data.readUint32(&c) == OK) codes.push_back(c);
|
||||||
|
}
|
||||||
|
LOGI("Interceptor registered for binder %p with %zu filtered codes", target.get(), codes.size());
|
||||||
|
} else {
|
||||||
|
LOGI("Interceptor registered for binder %p (all codes)", target.get());
|
||||||
|
}
|
||||||
|
|
||||||
wp<IBinder> weak_target = target;
|
wp<IBinder> weak_target = target;
|
||||||
|
|
||||||
std::unique_lock lock(registry_mutex_);
|
std::unique_lock lock(registry_mutex_);
|
||||||
registry_[weak_target] = {weak_target, callback};
|
registry_[weak_target] = {weak_target, callback, std::move(codes)};
|
||||||
|
|
||||||
LOGI("Interceptor registered for binder %p", target.get());
|
|
||||||
return OK;
|
return OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,9 +595,16 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
|
|||||||
Parcel pre_req, pre_resp;
|
Parcel pre_req, pre_resp;
|
||||||
writeTransactionData(pre_req, tx_id, target, code, flags, request);
|
writeTransactionData(pre_req, tx_id, target, code, flags, request);
|
||||||
|
|
||||||
if (callback->transact(intercept::kPreTransact, pre_req, &pre_resp) != OK) {
|
status_t pre_status = callback->transact(intercept::kPreTransact, pre_req, &pre_resp);
|
||||||
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed. Forwarding original call.", tx_id);
|
if (pre_status != OK) {
|
||||||
return false; // Callback failed, proceed as if not intercepted
|
// Block when interceptor is dead to prevent privacy leak to third-party apps
|
||||||
|
if (callback->pingBinder() != OK) {
|
||||||
|
LOGE("[TX_ID: %" PRIu64 "] Interceptor DEAD. Blocking to prevent attestation leak.", tx_id);
|
||||||
|
result = DEAD_OBJECT;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed (not dead). Forwarding.", tx_id);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t action = pre_resp.readInt32();
|
int32_t action = pre_resp.readInt32();
|
||||||
@@ -636,7 +657,8 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
|
|||||||
VALIDATE_STATUS(tx_id, post_req.appendFrom(reply, 0, reply_size));
|
VALIDATE_STATUS(tx_id, post_req.appendFrom(reply, 0, reply_size));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (callback->transact(intercept::kPostTransact, post_req, &post_resp) == OK) {
|
status_t post_status = callback->transact(intercept::kPostTransact, post_req, &post_resp);
|
||||||
|
if (post_status == OK) {
|
||||||
int32_t post_action = post_resp.readInt32();
|
int32_t post_action = post_resp.readInt32();
|
||||||
if (post_action == intercept::kActionOverrideReply && reply) {
|
if (post_action == intercept::kActionOverrideReply && reply) {
|
||||||
result = post_resp.readInt32(); // Read new status
|
result = post_resp.readInt32(); // Read new status
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
#include "refbase_compat.h"
|
|
||||||
#include "utils/RefBase.h"
|
|
||||||
#include <atomic>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstring> // For memcpy
|
|
||||||
#include <dlfcn.h>
|
|
||||||
#include <mutex>
|
|
||||||
#include <sys/system_properties.h>
|
|
||||||
|
|
||||||
namespace android {
|
|
||||||
|
|
||||||
// Helper function to get the Android API level at runtime.
|
|
||||||
// It caches the result for performance.
|
|
||||||
int32_t get_android_api_level() {
|
|
||||||
static std::atomic<int32_t> api_level = -1;
|
|
||||||
if (api_level.load(std::memory_order_relaxed) == -1) {
|
|
||||||
char sdk_version_str[PROP_VALUE_MAX];
|
|
||||||
if (__system_property_get("ro.build.version.sdk", sdk_version_str) > 0) {
|
|
||||||
api_level.store(atoi(sdk_version_str), std::memory_order_relaxed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return api_level.load(std::memory_order_relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define the function pointer type for the const member function
|
|
||||||
// RefBase::incStrongRequireStrong.
|
|
||||||
using incStrongRequireStrong_t = void (RefBase::*)(const void *) const;
|
|
||||||
|
|
||||||
// This is the implementation of our compatibility wrapper.
|
|
||||||
void incStrongFromExisting(const RefBase *ref, const void *id) {
|
|
||||||
// Only attempt to use the new function on Android 12 (API 31) or higher.
|
|
||||||
if (get_android_api_level() >= 31) {
|
|
||||||
static incStrongRequireStrong_t sIncStrongRequireStrong = nullptr;
|
|
||||||
static std::once_flag sFlag;
|
|
||||||
|
|
||||||
// Thread-safe, one-time initialization.
|
|
||||||
std::call_once(sFlag, []() {
|
|
||||||
// Find the symbol in the already loaded libraries.
|
|
||||||
// The mangled symbol is _ZNK7android7RefBase22incStrongRequireStrongEPKv
|
|
||||||
void *sym = dlsym(RTLD_DEFAULT,
|
|
||||||
"_ZNK7android7RefBase22incStrongRequireStrongEPKv");
|
|
||||||
if (sym) {
|
|
||||||
// Safely cast the void* symbol to our member function pointer.
|
|
||||||
memcpy(&sIncStrongRequireStrong, &sym, sizeof(void *));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (sIncStrongRequireStrong) {
|
|
||||||
// If the symbol was found, call it as member function.
|
|
||||||
(ref->*sIncStrongRequireStrong)(id);
|
|
||||||
return; // Success, we are done.
|
|
||||||
}
|
|
||||||
// If dlsym failed for any reason, we fall through to the old method.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback for older Android versions or if dlsym failed.
|
|
||||||
// This calls the universally available incStrong method.
|
|
||||||
ref->incStrong(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace android
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
namespace android {
|
|
||||||
|
|
||||||
// Forward-declare the RefBase class.
|
|
||||||
class RefBase;
|
|
||||||
|
|
||||||
// Declares our compatibility function.
|
|
||||||
void incStrongFromExisting(const RefBase *ref, const void *id);
|
|
||||||
|
|
||||||
} // namespace android
|
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
#ifndef ANDROID_STRONG_POINTER_H
|
#ifndef ANDROID_STRONG_POINTER_H
|
||||||
#define ANDROID_STRONG_POINTER_H
|
#define ANDROID_STRONG_POINTER_H
|
||||||
|
|
||||||
#include "refbase_compat.h"
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <type_traits> // for common_type.
|
#include <type_traits> // for common_type.
|
||||||
|
|
||||||
@@ -213,7 +212,7 @@ sp<T> sp<T>::make(Args&&... args) {
|
|||||||
template <typename T>
|
template <typename T>
|
||||||
sp<T> sp<T>::fromExisting(T* other) {
|
sp<T> sp<T>::fromExisting(T* other) {
|
||||||
if (other) {
|
if (other) {
|
||||||
incStrongFromExisting(other, other);
|
other->incStrongRequireStrong(other);
|
||||||
sp<T> result;
|
sp<T> result;
|
||||||
result.m_ptr = other;
|
result.m_ptr = other;
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -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,13 +1,21 @@
|
|||||||
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.io.File
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
|
import org.matrix.TEESimulator.config.BootStateManager
|
||||||
import org.matrix.TEESimulator.config.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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,8 +25,6 @@ import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
|||||||
object App {
|
object App {
|
||||||
// The delay in milliseconds before retrying to initialize the interceptor.
|
// The delay in milliseconds before retrying to initialize the interceptor.
|
||||||
private const val RETRY_DELAY_MS = 1000L
|
private const val RETRY_DELAY_MS = 1000L
|
||||||
// The sleep duration in milliseconds for the main service loop to keep the process alive.
|
|
||||||
private const val SERVICE_SLEEP_MS = 1000000L
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main entry point of the TEESimulator application.
|
* The main entry point of the TEESimulator application.
|
||||||
@@ -29,14 +35,26 @@ 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 {
|
||||||
|
purgeDebugDiagnostics()
|
||||||
|
prepareEnvironment()
|
||||||
|
|
||||||
|
// Spoof boot-state props before any hook attaches, so keystore2's
|
||||||
|
// cached snapshot reflects the spoofed values.
|
||||||
|
BootStateManager.apply()
|
||||||
|
|
||||||
// Load the package configuration.
|
// Load the package configuration.
|
||||||
ConfigurationManager.initialize()
|
ConfigurationManager.initialize()
|
||||||
// Set up the device's boot key and hash, which are crucial for attestation.
|
|
||||||
AndroidDeviceUtils.setupBootKeyAndHash()
|
|
||||||
// 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.
|
|
||||||
|
// Set up the device's boot key and hash, which are crucial for attestation.
|
||||||
|
AndroidDeviceUtils.setupBootKeyAndHash()
|
||||||
|
|
||||||
// Android ships with a stripped-down Bouncy Castle provider under the name "BC".
|
// Android ships with a stripped-down Bouncy Castle provider under the name "BC".
|
||||||
// We must remove the system provider first to ensure the full Bouncy Castle library
|
// We must remove the system provider first to ensure the full Bouncy Castle library
|
||||||
@@ -44,13 +62,64 @@ object App {
|
|||||||
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
Security.addProvider(BouncyCastleProvider())
|
Security.addProvider(BouncyCastleProvider())
|
||||||
|
|
||||||
maintainService()
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release builds never emit diagnostics. Sweep any `.bin` dumps a prior
|
||||||
|
* debug install left in the world-readable temp dir so they can't act as a
|
||||||
|
* detection artifact for apps that probe /data/local/tmp.
|
||||||
|
*/
|
||||||
|
private fun purgeDebugDiagnostics() {
|
||||||
|
if (SystemLogger.isDebugBuild) return
|
||||||
|
val stale =
|
||||||
|
File("/data/local/tmp").listFiles { _, name ->
|
||||||
|
name.startsWith("teesim-") && name.endsWith(".bin")
|
||||||
|
} ?: return
|
||||||
|
stale.forEach { runCatching { it.delete() } }
|
||||||
|
if (stale.isNotEmpty()) {
|
||||||
|
// warning() bypasses the rate limiter, so this once-per-boot audit
|
||||||
|
// line survives the noisy startup window.
|
||||||
|
SystemLogger.warning("Purged ${stale.size} stale debug diagnostic(s) from /data/local/tmp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Initializes the necessary Android framework internals to satisfy KeyStore requirements. */
|
||||||
|
private fun prepareEnvironment() {
|
||||||
|
// 1. Prepare Main Looper
|
||||||
|
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.
|
||||||
@@ -79,6 +148,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.
|
||||||
@@ -86,18 +156,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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ package org.matrix.TEESimulator.attestation
|
|||||||
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import java.nio.ByteBuffer
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
import org.bouncycastle.asn1.ASN1Boolean
|
import org.bouncycastle.asn1.ASN1Boolean
|
||||||
import org.bouncycastle.asn1.ASN1Encodable
|
import org.bouncycastle.asn1.ASN1Encodable
|
||||||
import org.bouncycastle.asn1.ASN1Enumerated
|
import org.bouncycastle.asn1.ASN1Enumerated
|
||||||
@@ -40,11 +43,12 @@ object AttestationBuilder {
|
|||||||
securityLevel: Int,
|
securityLevel: Int,
|
||||||
): Extension {
|
): Extension {
|
||||||
val keyDescription = buildKeyDescription(params, uid, securityLevel)
|
val keyDescription = buildKeyDescription(params, uid, securityLevel)
|
||||||
var formattedString =
|
SystemLogger.verbose {
|
||||||
keyDescription.joinToString(separator = ", ") {
|
val formattedString = keyDescription.joinToString(separator = ", ") {
|
||||||
AttestationPatcher.formatAsn1Primitive(it)
|
AttestationPatcher.formatAsn1Primitive(it)
|
||||||
}
|
}
|
||||||
SystemLogger.verbose("Forged attestation data: ${formattedString}")
|
"Forged attestation data: $formattedString"
|
||||||
|
}
|
||||||
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
|
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +116,7 @@ object AttestationBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
|
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
|
||||||
|
SystemLogger.info("Attestation patch levels for uid=$uid: os=$osPatch, vendor=$vendorPatch, boot=$bootPatch")
|
||||||
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
|
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
|
||||||
if (bootPatch != DO_NOT_REPORT) {
|
if (bootPatch != DO_NOT_REPORT) {
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
@@ -126,33 +131,59 @@ object AttestationBuilder {
|
|||||||
return properties
|
return properties
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */
|
|
||||||
private fun buildKeyDescription(
|
private fun buildKeyDescription(
|
||||||
params: KeyMintAttestation,
|
params: KeyMintAttestation,
|
||||||
uid: Int,
|
uid: Int,
|
||||||
securityLevel: Int,
|
securityLevel: Int,
|
||||||
): ASN1Sequence {
|
): ASN1Sequence {
|
||||||
|
val creationTime = System.currentTimeMillis()
|
||||||
val teeEnforced = buildTeeEnforcedList(params, uid, securityLevel)
|
val teeEnforced = buildTeeEnforcedList(params, uid, securityLevel)
|
||||||
val softwareEnforced = buildSoftwareEnforcedList(uid, securityLevel)
|
val softwareEnforced = buildSoftwareEnforcedList(params, uid, securityLevel, creationTime)
|
||||||
|
|
||||||
|
val uniqueId =
|
||||||
|
if (params.includeUniqueId == true && params.attestationChallenge != null) {
|
||||||
|
computeUniqueId(creationTime, createApplicationId(uid).octets)
|
||||||
|
} else {
|
||||||
|
ByteArray(0)
|
||||||
|
}
|
||||||
|
|
||||||
val fields =
|
val fields =
|
||||||
arrayOf(
|
arrayOf(
|
||||||
ASN1Integer(
|
ASN1Integer(AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()),
|
||||||
AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()
|
ASN1Enumerated(securityLevel),
|
||||||
), // attestationVersion
|
ASN1Integer(AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()),
|
||||||
ASN1Enumerated(securityLevel), // attestationSecurityLevel
|
ASN1Enumerated(securityLevel),
|
||||||
ASN1Integer(
|
DEROctetString(params.attestationChallenge ?: ByteArray(0)),
|
||||||
AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()
|
DEROctetString(uniqueId),
|
||||||
), // keymasterVersion
|
|
||||||
ASN1Enumerated(securityLevel), // keymasterSecurityLevel
|
|
||||||
DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge
|
|
||||||
DEROctetString(ByteArray(0)), // uniqueId
|
|
||||||
softwareEnforced,
|
softwareEnforced,
|
||||||
teeEnforced,
|
teeEnforced,
|
||||||
)
|
)
|
||||||
return DERSequence(fields)
|
return DERSequence(fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun computeUniqueId(creationTimeMs: Long, aaidDer: ByteArray): ByteArray {
|
||||||
|
val temporalCounter = creationTimeMs / 2592000000L
|
||||||
|
val message =
|
||||||
|
ByteBuffer.allocate(8 + aaidDer.size + 1)
|
||||||
|
.putLong(temporalCounter)
|
||||||
|
.put(aaidDer)
|
||||||
|
.put(0x00)
|
||||||
|
.array()
|
||||||
|
val mac = Mac.getInstance("HmacSHA256")
|
||||||
|
mac.init(SecretKeySpec(hbk, "HmacSHA256"))
|
||||||
|
return mac.doFinal(message).copyOf(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val hbk: ByteArray by lazy {
|
||||||
|
val file = java.io.File(ConfigurationManager.CONFIG_PATH, "hbk")
|
||||||
|
if (file.exists() && file.length() == 32L) {
|
||||||
|
file.readBytes()
|
||||||
|
} else {
|
||||||
|
SystemLogger.warning("hbk not found, generating ephemeral HBK.")
|
||||||
|
ByteArray(32).also { java.security.SecureRandom().nextBytes(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */
|
/** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */
|
||||||
private fun buildTeeEnforcedList(
|
private fun buildTeeEnforcedList(
|
||||||
params: KeyMintAttestation,
|
params: KeyMintAttestation,
|
||||||
@@ -181,23 +212,110 @@ 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),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels.
|
// Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels.
|
||||||
val simulatedProperties = getSimulatedHardwareProperties(uid)
|
val simulatedProperties = getSimulatedHardwareProperties(uid)
|
||||||
@@ -294,20 +412,32 @@ object AttestationBuilder {
|
|||||||
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
|
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
|
||||||
* Keystore.
|
* Keystore.
|
||||||
*/
|
*/
|
||||||
private fun buildSoftwareEnforcedList(uid: Int, securityLevel: Int): DERSequence {
|
private fun buildSoftwareEnforcedList(
|
||||||
val list =
|
params: KeyMintAttestation,
|
||||||
mutableListOf<ASN1Encodable>(
|
uid: Int,
|
||||||
DERTaggedObject(
|
securityLevel: Int,
|
||||||
true,
|
creationTimeMs: Long = System.currentTimeMillis(),
|
||||||
AttestationConstants.TAG_CREATION_DATETIME,
|
): DERSequence {
|
||||||
ASN1Integer(System.currentTimeMillis()),
|
val list = mutableListOf<ASN1Encodable>()
|
||||||
),
|
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_CREATION_DATETIME,
|
||||||
|
ASN1Integer(creationTimeMs),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (params.attestationChallenge != null) {
|
||||||
|
list.add(
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
true,
|
true,
|
||||||
AttestationConstants.TAG_ATTESTATION_APPLICATION_ID,
|
AttestationConstants.TAG_ATTESTATION_APPLICATION_ID,
|
||||||
createApplicationId(uid),
|
createApplicationId(uid),
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) {
|
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
@@ -317,7 +447,39 @@ object AttestationBuilder {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return DERSequence(list.toTypedArray())
|
|
||||||
|
if (params.callerNonce == true) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_CALLER_NONCE, DERNull.INSTANCE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
params.activeDateTime?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
params.originationExpireDateTime?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_ORIGINATION_EXPIRE_DATETIME, ASN1Integer(it.time))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
params.usageExpireDateTime?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_USAGE_EXPIRE_DATETIME, ASN1Integer(it.time))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
params.usageCountLimit?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_USAGE_COUNT_LIMIT, ASN1Integer(it.toLong()))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (params.unlockedDeviceRequired == true) {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(true, AttestationConstants.TAG_UNLOCKED_DEVICE_REQUIRED, DERNull.INSTANCE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -344,7 +506,12 @@ object AttestationBuilder {
|
|||||||
* retrieved.
|
* retrieved.
|
||||||
*/
|
*/
|
||||||
@Throws(Throwable::class)
|
@Throws(Throwable::class)
|
||||||
private fun createApplicationId(uid: Int): DEROctetString {
|
internal fun createApplicationId(uid: Int): DEROctetString {
|
||||||
|
val appUid = uid % 100000
|
||||||
|
if (appUid == 0 || appUid == 1000) {
|
||||||
|
return buildApplicationIdDer(listOf("AndroidSystem" to 1L), emptySet())
|
||||||
|
}
|
||||||
|
|
||||||
val pm =
|
val pm =
|
||||||
ConfigurationManager.getPackageManager()
|
ConfigurationManager.getPackageManager()
|
||||||
?: throw IllegalStateException("PackageManager not found!")
|
?: throw IllegalStateException("PackageManager not found!")
|
||||||
@@ -352,12 +519,11 @@ object AttestationBuilder {
|
|||||||
pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid")
|
pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid")
|
||||||
|
|
||||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||||
val packageInfoList = mutableListOf<DERSequence>()
|
val packageInfoList = mutableListOf<Pair<String, Long>>()
|
||||||
val signatureDigests = mutableSetOf<Digest>()
|
val signatureDigests = mutableSetOf<Digest>()
|
||||||
|
|
||||||
// Process all packages associated with the UID in a single loop.
|
val userId = uid / 100000
|
||||||
packages.forEach { packageName ->
|
packages.forEach { packageName ->
|
||||||
val userId = uid / 100000
|
|
||||||
val packageInfo =
|
val packageInfo =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
pm.getPackageInfo(
|
pm.getPackageInfo(
|
||||||
@@ -370,34 +536,36 @@ object AttestationBuilder {
|
|||||||
pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId)
|
pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add package information (name and version code) to our list.
|
packageInfoList.add(packageInfo.packageName to packageInfo.longVersionCode)
|
||||||
packageInfoList.add(
|
|
||||||
DERSequence(
|
|
||||||
arrayOf(
|
|
||||||
DEROctetString(packageInfo.packageName.toByteArray(StandardCharsets.UTF_8)),
|
|
||||||
ASN1Integer(packageInfo.longVersionCode),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Collect unique signature digests from the signing history.
|
|
||||||
packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature ->
|
packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature ->
|
||||||
val digest = sha256.digest(signature.toByteArray())
|
signatureDigests.add(Digest(sha256.digest(signature.toByteArray())))
|
||||||
signatureDigests.add(Digest(digest))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The application ID is a sequence of two sets:
|
return buildApplicationIdDer(packageInfoList, signatureDigests)
|
||||||
// 1. A set of package information (name and version).
|
}
|
||||||
// 2. A set of SHA-256 digests of the signing certificates.
|
|
||||||
|
private fun buildApplicationIdDer(
|
||||||
|
packages: List<Pair<String, Long>>,
|
||||||
|
digests: Set<Digest>,
|
||||||
|
): DEROctetString {
|
||||||
|
val packageInfoList =
|
||||||
|
packages.map { (name, version) ->
|
||||||
|
DERSequence(
|
||||||
|
arrayOf(
|
||||||
|
DEROctetString(name.toByteArray(StandardCharsets.UTF_8)),
|
||||||
|
ASN1Integer(version),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
val applicationIdSequence =
|
val applicationIdSequence =
|
||||||
DERSequence(
|
DERSequence(
|
||||||
arrayOf(
|
arrayOf(
|
||||||
DERSet(packageInfoList.toTypedArray()),
|
DERSet(packageInfoList.toTypedArray()),
|
||||||
DERSet(signatureDigests.map { DEROctetString(it.digest) }.toTypedArray()),
|
DERSet(digests.map { DEROctetString(it.digest) }.toTypedArray()),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return DEROctetString(applicationIdSequence.encoded)
|
return DEROctetString(applicationIdSequence.encoded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
|||||||
import org.matrix.TEESimulator.pki.KeyBox
|
import org.matrix.TEESimulator.pki.KeyBox
|
||||||
import org.matrix.TEESimulator.pki.KeyBoxManager
|
import org.matrix.TEESimulator.pki.KeyBoxManager
|
||||||
import org.matrix.TEESimulator.util.toHex
|
import org.matrix.TEESimulator.util.toHex
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the modification (patching) of Android Key Attestation extensions within certificates.
|
* Handles the modification (patching) of Android Key Attestation extensions within certificates.
|
||||||
@@ -36,7 +37,12 @@ object AttestationPatcher {
|
|||||||
* @return A new, cryptographically valid, patched certificate chain. Returns the original chain
|
* @return A new, cryptographically valid, patched certificate chain. Returns the original chain
|
||||||
* on any failure.
|
* on any failure.
|
||||||
*/
|
*/
|
||||||
fun patchCertificateChain(originalChain: Array<Certificate>?, uid: Int): Array<Certificate> {
|
fun patchCertificateChain(
|
||||||
|
originalChain: Array<Certificate>?,
|
||||||
|
uid: Int,
|
||||||
|
notBefore: Date? = null,
|
||||||
|
notAfter: Date? = null,
|
||||||
|
): Array<Certificate> {
|
||||||
if (originalChain.isNullOrEmpty()) {
|
if (originalChain.isNullOrEmpty()) {
|
||||||
SystemLogger.error("Attempted to patch a null or empty certificate chain for UID $uid.")
|
SystemLogger.error("Attempted to patch a null or empty certificate chain for UID $uid.")
|
||||||
return originalChain ?: emptyArray()
|
return originalChain ?: emptyArray()
|
||||||
@@ -63,6 +69,8 @@ object AttestationPatcher {
|
|||||||
keybox,
|
keybox,
|
||||||
originalLeaf.sigAlgName,
|
originalLeaf.sigAlgName,
|
||||||
uid,
|
uid,
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
)
|
)
|
||||||
|
|
||||||
// 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's
|
// 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's
|
||||||
@@ -83,6 +91,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.
|
||||||
*
|
*
|
||||||
@@ -101,17 +119,27 @@ object AttestationPatcher {
|
|||||||
keybox: KeyBox,
|
keybox: KeyBox,
|
||||||
sigAlgName: String,
|
sigAlgName: String,
|
||||||
uid: Int,
|
uid: Int,
|
||||||
|
notBefore: Date? = null,
|
||||||
|
notAfter: Date? = null,
|
||||||
): Certificate {
|
): Certificate {
|
||||||
// The issuer of our new leaf is the subject of the first certificate in our custom keybox
|
// The issuer of our new leaf is the subject of the first certificate in our custom keybox
|
||||||
// chain.
|
// chain.
|
||||||
val newIssuer = X509CertificateHolder(keybox.certificates[0].encoded).subject
|
val newIssuer = X509CertificateHolder(keybox.certificates[0].encoded).subject
|
||||||
|
|
||||||
|
val effectiveNotBefore = notBefore ?: originalLeafHolder.notBefore
|
||||||
|
val effectiveNotAfter = notAfter ?: originalLeafHolder.notAfter
|
||||||
|
if (notBefore != null || notAfter != null) {
|
||||||
|
SystemLogger.debug(
|
||||||
|
"Overriding cert dates: notBefore=${effectiveNotBefore} (was ${originalLeafHolder.notBefore}), notAfter=${effectiveNotAfter} (was ${originalLeafHolder.notAfter})"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val builder =
|
val builder =
|
||||||
X509v3CertificateBuilder(
|
X509v3CertificateBuilder(
|
||||||
newIssuer,
|
newIssuer,
|
||||||
originalLeafHolder.serialNumber,
|
originalLeafHolder.serialNumber,
|
||||||
originalLeafHolder.notBefore,
|
effectiveNotBefore,
|
||||||
originalLeafHolder.notAfter,
|
effectiveNotAfter,
|
||||||
originalLeafHolder.subject,
|
originalLeafHolder.subject,
|
||||||
originalLeafHolder.subjectPublicKeyInfo,
|
originalLeafHolder.subjectPublicKeyInfo,
|
||||||
)
|
)
|
||||||
@@ -128,7 +156,7 @@ object AttestationPatcher {
|
|||||||
|
|
||||||
// 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 =
|
val signer =
|
||||||
JcaContentSignerBuilder(sigAlgName)
|
JcaContentSignerBuilder(normalizeSignatureAlgorithm(sigAlgName))
|
||||||
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
.build(keybox.keyPair.private)
|
.build(keybox.keyPair.private)
|
||||||
val newCertificate = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
val newCertificate = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||||
@@ -136,7 +164,7 @@ object AttestationPatcher {
|
|||||||
// Log the signature of the newly created certificate to observe its non-deterministic
|
// Log the signature of the newly created certificate to observe its non-deterministic
|
||||||
// nature.
|
// nature.
|
||||||
val signatureBytes = (newCertificate as X509Certificate).signature
|
val signatureBytes = (newCertificate as X509Certificate).signature
|
||||||
SystemLogger.verbose("Signature of patched leaf cert: ${signatureBytes.toHex()}")
|
SystemLogger.verbose { "Signature of patched leaf cert: ${signatureBytes.toHex()}" }
|
||||||
|
|
||||||
return newCertificate
|
return newCertificate
|
||||||
}
|
}
|
||||||
@@ -206,11 +234,37 @@ object AttestationPatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
@@ -232,8 +286,10 @@ object AttestationPatcher {
|
|||||||
private fun createPatchedAttestationExtension(parsed: ParsedAttestation, uid: Int): Extension {
|
private fun createPatchedAttestationExtension(parsed: ParsedAttestation, uid: Int): Extension {
|
||||||
val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed
|
val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed
|
||||||
|
|
||||||
var formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
SystemLogger.verbose {
|
||||||
SystemLogger.verbose("Original attestation data: ${formattedString}")
|
val formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
||||||
|
"Original attestation data: $formattedString"
|
||||||
|
}
|
||||||
|
|
||||||
// Build the new Root of Trust and add/replace it in the map.
|
// Build the new Root of Trust and add/replace it in the map.
|
||||||
val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust)
|
val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust)
|
||||||
@@ -260,8 +316,10 @@ object AttestationPatcher {
|
|||||||
|
|
||||||
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
|
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
|
||||||
val patchedSequence = DERSequence(allFields)
|
val patchedSequence = DERSequence(allFields)
|
||||||
formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
SystemLogger.verbose {
|
||||||
SystemLogger.verbose("Patched attestation data: ${formattedString}")
|
val formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
||||||
|
"Patched attestation data: $formattedString"
|
||||||
|
}
|
||||||
val patchedOctets = DEROctetString(patchedSequence)
|
val patchedOctets = DEROctetString(patchedSequence)
|
||||||
|
|
||||||
return Extension(ATTESTATION_OID, false, patchedOctets)
|
return Extension(ATTESTATION_OID, false, patchedOctets)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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.os.Build
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
import android.security.keystore.KeyProperties
|
import android.security.keystore.KeyProperties
|
||||||
@@ -62,12 +61,23 @@ object DeviceAttestationService {
|
|||||||
// A unique alias for the key used to perform the TEE functionality check.
|
// A unique alias for the key used to perform the TEE functionality check.
|
||||||
private const val TEE_CHECK_KEY_ALIAS = "TEESimulator_AttestationCheck"
|
private const val TEE_CHECK_KEY_ALIAS = "TEESimulator_AttestationCheck"
|
||||||
|
|
||||||
|
// Alias for the device-ID attestation capability probe.
|
||||||
|
private const val DEVICE_ID_CHECK_KEY_ALIAS = "TEESimulator_DeviceIdCheck"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazily determines if the device's TEE is functional by attempting to generate an
|
* Lazily determines if the device's TEE is functional by attempting to generate an
|
||||||
* attestation-backed key pair. The result is cached.
|
* attestation-backed key pair. The result is cached.
|
||||||
*/
|
*/
|
||||||
val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() }
|
val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily mirrors whether the real TEE can attest device identifiers/properties (the tags added
|
||||||
|
* by `setDevicePropertiesAttestationIncluded`). Hardware that never provisioned device IDs
|
||||||
|
* returns CANNOT_ATTEST_IDS; the synthesizer consults this so it never forges a capability the
|
||||||
|
* real silicon lacks. Cached.
|
||||||
|
*/
|
||||||
|
val canAttestDeviceIds: Boolean by lazy { checkDeviceIdAttestation() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazily fetches and parses attestation data from a genuinely generated certificate. The result
|
* 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.
|
||||||
@@ -83,16 +93,6 @@ object DeviceAttestationService {
|
|||||||
private fun checkTeeFunctionality(): Boolean {
|
private fun checkTeeFunctionality(): Boolean {
|
||||||
SystemLogger.info("Performing TEE functionality check...")
|
SystemLogger.info("Performing TEE functionality check...")
|
||||||
return try {
|
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 keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
||||||
val keyPairGenerator =
|
val keyPairGenerator =
|
||||||
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
|
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
|
||||||
@@ -118,6 +118,37 @@ object DeviceAttestationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Probes whether the real TEE can satisfy device-ID/property attestation, mirroring its actual
|
||||||
|
* capability. Gated behind [isTeeFunctional] so a dead TEE never triggers a second doomed
|
||||||
|
* probe — it simply reports `false` (cannot attest), the faithful result for such hardware.
|
||||||
|
*/
|
||||||
|
private fun checkDeviceIdAttestation(): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return false
|
||||||
|
if (!isTeeFunctional) return false
|
||||||
|
return try {
|
||||||
|
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
||||||
|
val keyPairGenerator =
|
||||||
|
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
|
||||||
|
val challenge = ByteArray(16).apply { SecureRandom().nextBytes(this) }
|
||||||
|
val spec =
|
||||||
|
KeyGenParameterSpec.Builder(DEVICE_ID_CHECK_KEY_ALIAS, KeyProperties.PURPOSE_SIGN)
|
||||||
|
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
|
||||||
|
.setDigests(KeyProperties.DIGEST_SHA256)
|
||||||
|
.setAttestationChallenge(challenge)
|
||||||
|
.setDevicePropertiesAttestationIncluded(true)
|
||||||
|
.build()
|
||||||
|
keyPairGenerator.initialize(spec)
|
||||||
|
keyPairGenerator.generateKeyPair()
|
||||||
|
runCatching { keyStore.deleteEntry(DEVICE_ID_CHECK_KEY_ALIAS) }
|
||||||
|
SystemLogger.info("Device-ID attestation supported by TEE.")
|
||||||
|
true
|
||||||
|
} catch (_: Exception) {
|
||||||
|
SystemLogger.info("Device-ID attestation not supported by TEE; mirroring as cannot-attest.")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the attestation certificate generated during the TEE check. The key entry is
|
* 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.
|
||||||
@@ -160,11 +191,12 @@ object DeviceAttestationService {
|
|||||||
|
|
||||||
// The extension's value is an ASN.1 sequence.
|
// The extension's value is an ASN.1 sequence.
|
||||||
val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets)
|
val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets)
|
||||||
var formattedString =
|
SystemLogger.verbose {
|
||||||
keyDescriptionSeq.joinToString(separator = ", ") {
|
val formattedString = keyDescriptionSeq.joinToString(separator = ", ") {
|
||||||
AttestationPatcher.formatAsn1Primitive(it)
|
AttestationPatcher.formatAsn1Primitive(it)
|
||||||
}
|
}
|
||||||
SystemLogger.verbose("Cached attestation data: ${formattedString}")
|
"Cached attestation data: $formattedString"
|
||||||
|
}
|
||||||
val fields = keyDescriptionSeq.toArray()
|
val fields = keyDescriptionSeq.toArray()
|
||||||
|
|
||||||
val attestVersion =
|
val attestVersion =
|
||||||
@@ -192,13 +224,15 @@ object DeviceAttestationService {
|
|||||||
ASN1Sequence.getInstance(
|
ASN1Sequence.getInstance(
|
||||||
fields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX]
|
fields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX]
|
||||||
)
|
)
|
||||||
if (softwareEnforced.size() >= 3) {
|
moduleHash =
|
||||||
moduleHash =
|
softwareEnforced
|
||||||
ASN1OctetString.getInstance(
|
.toArray()
|
||||||
ASN1TaggedObject.getInstance(softwareEnforced.getObjectAt(2)).baseObject
|
.firstOrNull {
|
||||||
)
|
(it as? ASN1TaggedObject)?.tagNo == AttestationConstants.TAG_MODULE_HASH
|
||||||
.octets
|
}
|
||||||
}
|
?.let {
|
||||||
|
ASN1OctetString.getInstance((it as ASN1TaggedObject).baseObject).octets
|
||||||
|
}
|
||||||
|
|
||||||
val teeEnforced =
|
val teeEnforced =
|
||||||
ASN1Sequence.getInstance(
|
ASN1Sequence.getInstance(
|
||||||
@@ -259,6 +293,10 @@ object DeviceAttestationService {
|
|||||||
verifiedBootKey = null
|
verifiedBootKey = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (verifiedBootHash?.all { it == 0.toByte() } == true) {
|
||||||
|
verifiedBootHash = null
|
||||||
|
}
|
||||||
|
|
||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, osPatch=$osPatchLevel, vendorPatch=$vendorPatchLevel, bootPatch=$bootPatchLevel, moduleHash=${moduleHash?.toHex()}, bootKey=${verifiedBootKey?.toHex()}, bootHash=${verifiedBootHash?.toHex()}"
|
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, osPatch=$osPatchLevel, vendorPatch=$vendorPatchLevel, bootPatch=$bootPatchLevel, moduleHash=${moduleHash?.toHex()}, bootKey=${verifiedBootKey?.toHex()}, bootHash=${verifiedBootHash?.toHex()}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
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.KeyOrigin
|
||||||
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
|
||||||
@@ -20,8 +19,11 @@ import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
|||||||
data class KeyMintAttestation(
|
data class KeyMintAttestation(
|
||||||
val keySize: Int,
|
val keySize: Int,
|
||||||
val algorithm: Int,
|
val algorithm: Int,
|
||||||
val ecCurve: Int,
|
val ecCurve: Int?,
|
||||||
val ecCurveName: String,
|
val ecCurveName: String,
|
||||||
|
val origin: Int?,
|
||||||
|
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?,
|
||||||
@@ -39,21 +41,47 @@ data class KeyMintAttestation(
|
|||||||
val manufacturer: ByteArray?,
|
val manufacturer: ByteArray?,
|
||||||
val model: ByteArray?,
|
val model: ByteArray?,
|
||||||
val secondImei: ByteArray?,
|
val secondImei: ByteArray?,
|
||||||
|
val activeDateTime: Date?,
|
||||||
|
val originationExpireDateTime: Date?,
|
||||||
|
val usageExpireDateTime: Date?,
|
||||||
|
val usageCountLimit: Int?,
|
||||||
|
val callerNonce: Boolean?,
|
||||||
|
val nonce: ByteArray?,
|
||||||
|
val unlockedDeviceRequired: Boolean?,
|
||||||
|
val includeUniqueId: Boolean?,
|
||||||
|
val rollbackResistance: Boolean?,
|
||||||
|
val earlyBootOnly: Boolean?,
|
||||||
|
val allowWhileOnBody: Boolean?,
|
||||||
|
val trustedUserPresenceRequired: Boolean?,
|
||||||
|
val trustedConfirmationRequired: Boolean?,
|
||||||
|
val noAuthRequired: Boolean?,
|
||||||
|
val maxUsesPerBoot: Int?,
|
||||||
|
val maxBootLevel: Int?,
|
||||||
|
val minMacLength: Int?,
|
||||||
|
val rsaOaepMgfDigest: List<Int>,
|
||||||
) {
|
) {
|
||||||
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */
|
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */
|
||||||
constructor(
|
constructor(
|
||||||
params: Array<KeyParameter>
|
params: Array<KeyParameter>
|
||||||
) : this(
|
) : this(
|
||||||
// AOSP: [key_param(tag = KEY_SIZE, field = Integer)]
|
keySize = params.findInteger(Tag.KEY_SIZE) ?: params.deriveKeySizeFromCurve(),
|
||||||
keySize = params.findInteger(Tag.KEY_SIZE) ?: 0,
|
|
||||||
|
|
||||||
// AOSP: [key_param(tag = ALGORITHM, field = Algorithm)]
|
// AOSP: [key_param(tag = ALGORITHM, field = Algorithm)]
|
||||||
algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0,
|
algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0,
|
||||||
|
|
||||||
// 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 = 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),
|
||||||
|
|
||||||
@@ -89,10 +117,32 @@ data class KeyMintAttestation(
|
|||||||
manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER),
|
manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER),
|
||||||
model = params.findBlob(Tag.ATTESTATION_ID_MODEL),
|
model = params.findBlob(Tag.ATTESTATION_ID_MODEL),
|
||||||
secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI),
|
secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI),
|
||||||
|
activeDateTime = params.findDate(Tag.ACTIVE_DATETIME),
|
||||||
|
originationExpireDateTime = params.findDate(Tag.ORIGINATION_EXPIRE_DATETIME),
|
||||||
|
usageExpireDateTime = params.findDate(Tag.USAGE_EXPIRE_DATETIME),
|
||||||
|
usageCountLimit = params.findInteger(Tag.USAGE_COUNT_LIMIT),
|
||||||
|
callerNonce = params.findBoolean(Tag.CALLER_NONCE),
|
||||||
|
nonce = params.findBlob(Tag.NONCE),
|
||||||
|
unlockedDeviceRequired = params.findBoolean(Tag.UNLOCKED_DEVICE_REQUIRED),
|
||||||
|
includeUniqueId = params.findBoolean(Tag.INCLUDE_UNIQUE_ID),
|
||||||
|
rollbackResistance = params.findBoolean(Tag.ROLLBACK_RESISTANCE),
|
||||||
|
earlyBootOnly = params.findBoolean(Tag.EARLY_BOOT_ONLY),
|
||||||
|
allowWhileOnBody = params.findBoolean(Tag.ALLOW_WHILE_ON_BODY),
|
||||||
|
trustedUserPresenceRequired = params.findBoolean(Tag.TRUSTED_USER_PRESENCE_REQUIRED),
|
||||||
|
trustedConfirmationRequired = params.findBoolean(Tag.TRUSTED_CONFIRMATION_REQUIRED),
|
||||||
|
noAuthRequired = params.findBoolean(Tag.NO_AUTH_REQUIRED),
|
||||||
|
maxUsesPerBoot = params.findInteger(Tag.MAX_USES_PER_BOOT),
|
||||||
|
maxBootLevel = params.findInteger(Tag.MAX_BOOT_LEVEL),
|
||||||
|
minMacLength = params.findInteger(Tag.MIN_MAC_LENGTH),
|
||||||
|
rsaOaepMgfDigest = params.findAllDigests(Tag.RSA_OAEP_MGF_DIGEST),
|
||||||
) {
|
) {
|
||||||
// Log all parsed parameters for debugging purposes.
|
// Log all parsed parameters for debugging purposes.
|
||||||
params.forEach { KeyMintParameterLogger.logParameter(it) }
|
params.forEach { KeyMintParameterLogger.logParameter(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isAttestKey(): Boolean = purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY)
|
||||||
|
|
||||||
|
fun isImportKey(): Boolean = origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Private helper extension functions for parsing KeyParameter arrays ---
|
// --- Private helper extension functions for parsing KeyParameter arrays ---
|
||||||
@@ -109,6 +159,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()
|
||||||
@@ -121,6 +175,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 = BlockMode (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 }
|
||||||
@@ -129,6 +191,21 @@ private fun Array<KeyParameter>.findAllKeyPurpose(tag: Int): List<Int> =
|
|||||||
private fun Array<KeyParameter>.findAllDigests(tag: Int): List<Int> =
|
private fun Array<KeyParameter>.findAllDigests(tag: Int): List<Int> =
|
||||||
this.filter { it.tag == tag }.map { it.value.digest }
|
this.filter { it.tag == tag }.map { it.value.digest }
|
||||||
|
|
||||||
|
private fun Array<KeyParameter>.findBoolean(tag: Int): Boolean? =
|
||||||
|
if (this.any { it.tag == tag }) true else null
|
||||||
|
|
||||||
|
private fun Array<KeyParameter>.deriveKeySizeFromCurve(): Int {
|
||||||
|
val curveId = this.find { it.tag == Tag.EC_CURVE }?.value?.ecCurve ?: return 0
|
||||||
|
return when (curveId) {
|
||||||
|
EcCurve.P_224 -> 224
|
||||||
|
EcCurve.P_256 -> 256
|
||||||
|
EcCurve.P_384 -> 384
|
||||||
|
EcCurve.P_521 -> 521
|
||||||
|
EcCurve.CURVE_25519 -> 256
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derives the EC Curve name. Logic: Checks specific EC_CURVE tag first (field=EcCurve), falls back
|
* Derives the EC Curve name. Logic: Checks specific EC_CURVE tag first (field=EcCurve), falls back
|
||||||
* to KEY_SIZE (field=Integer).
|
* to KEY_SIZE (field=Integer).
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package org.matrix.TEESimulator.config
|
||||||
|
|
||||||
|
import android.os.SystemProperties
|
||||||
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||||
|
|
||||||
|
object BootStateManager {
|
||||||
|
private val targets =
|
||||||
|
linkedMapOf(
|
||||||
|
"ro.boot.verifiedbootstate" to "green",
|
||||||
|
"ro.boot.flash.locked" to "1",
|
||||||
|
"ro.boot.veritymode" to "enforcing",
|
||||||
|
"ro.boot.vbmeta.device_state" to "locked",
|
||||||
|
)
|
||||||
|
|
||||||
|
private val fillIfAbsent =
|
||||||
|
linkedMapOf(
|
||||||
|
"ro.boot.vbmeta.invalidate_on_error" to "yes",
|
||||||
|
"ro.boot.vbmeta.avb_version" to "1.2",
|
||||||
|
"ro.boot.vbmeta.hash_alg" to "sha256",
|
||||||
|
"ro.boot.vbmeta.size" to "11904",
|
||||||
|
)
|
||||||
|
|
||||||
|
fun apply() {
|
||||||
|
for ((name, target) in targets) {
|
||||||
|
val current = SystemProperties.get(name, "")
|
||||||
|
if (current.isEmpty()) {
|
||||||
|
SystemLogger.debug("BootStateManager: $name absent on this device, skip")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (current == target) {
|
||||||
|
SystemLogger.debug("BootStateManager: $name already $target, skip")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
SystemLogger.info("BootStateManager: setting $name=$target (was: '$current')")
|
||||||
|
AndroidDeviceUtils.setProperty(name, target)
|
||||||
|
}
|
||||||
|
for ((name, value) in fillIfAbsent) {
|
||||||
|
val current = SystemProperties.get(name, "")
|
||||||
|
if (current.isNotEmpty()) {
|
||||||
|
SystemLogger.debug("BootStateManager: $name already '$current', skip")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
SystemLogger.info("BootStateManager: filling absent $name=$value")
|
||||||
|
AndroidDeviceUtils.setProperty(name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,7 +31,6 @@ object ConfigurationManager {
|
|||||||
// --- Configuration Paths ---
|
// --- Configuration Paths ---
|
||||||
const val CONFIG_PATH = "/data/adb/tricky_store"
|
const val CONFIG_PATH = "/data/adb/tricky_store"
|
||||||
private const val TARGET_PACKAGES_FILE = "target.txt"
|
private const val TARGET_PACKAGES_FILE = "target.txt"
|
||||||
private const val TEE_STATUS_FILE = "tee_status.txt"
|
|
||||||
private const val PATCH_LEVEL_FILE = "security_patch.txt"
|
private const val PATCH_LEVEL_FILE = "security_patch.txt"
|
||||||
private const val DEFAULT_KEYBOX_FILE = "keybox.xml"
|
private const val DEFAULT_KEYBOX_FILE = "keybox.xml"
|
||||||
private val configRoot = File(CONFIG_PATH)
|
private val configRoot = File(CONFIG_PATH)
|
||||||
@@ -39,7 +38,6 @@ object ConfigurationManager {
|
|||||||
// --- In-Memory Configuration State ---
|
// --- In-Memory Configuration State ---
|
||||||
@Volatile private var packageModes = mapOf<String, Mode>()
|
@Volatile private var packageModes = mapOf<String, Mode>()
|
||||||
@Volatile private var packageKeyboxes = mapOf<String, String>()
|
@Volatile private var packageKeyboxes = mapOf<String, String>()
|
||||||
@Volatile private var isTeeBroken: Boolean? = null
|
|
||||||
@Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null
|
@Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null
|
||||||
@Volatile private var packagePatchLevels = mapOf<String, CustomPatchLevel>()
|
@Volatile private var packagePatchLevels = mapOf<String, CustomPatchLevel>()
|
||||||
|
|
||||||
@@ -54,11 +52,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()
|
||||||
SystemLogger.info("Configuration initialized and file observer started.")
|
SystemLogger.info("Configuration initialized and file observer started.")
|
||||||
@@ -76,33 +83,40 @@ object ConfigurationManager {
|
|||||||
return packages.firstNotNullOfOrNull { pkg -> packageKeyboxes[pkg] } ?: DEFAULT_KEYBOX_FILE
|
return packages.firstNotNullOfOrNull { pkg -> packageKeyboxes[pkg] } ?: DEFAULT_KEYBOX_FILE
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Determines if the certificate for a given UID needs to be patched. */
|
fun shouldPatch(uid: Int): Boolean {
|
||||||
fun shouldPatch(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.PATCH
|
val mode = getPackageModeForUid(uid)
|
||||||
|
return mode == Mode.PATCH || mode == Mode.AUTO
|
||||||
|
}
|
||||||
|
|
||||||
/** Determines if a new certificate needs to be generated for a given UID. */
|
/** Determines if a new certificate needs to be generated for a given UID. */
|
||||||
fun shouldGenerate(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.GENERATE
|
fun shouldGenerate(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.GENERATE
|
||||||
|
|
||||||
/** Determines if no operation is needed for a given UID. */
|
|
||||||
fun shouldSkipUid(uid: Int): Boolean = getPackageModeForUid(uid) == null
|
fun shouldSkipUid(uid: Int): Boolean = getPackageModeForUid(uid) == null
|
||||||
|
|
||||||
/** Resolves the operating mode for a given UID based on its packages and the TEE status. */
|
fun isAutoMode(uid: Int): Boolean {
|
||||||
|
for (pkg in getPackagesForUid(uid)) {
|
||||||
|
when (packageModes[pkg]) {
|
||||||
|
Mode.GENERATE, Mode.PATCH -> return false
|
||||||
|
Mode.AUTO -> return true
|
||||||
|
null -> continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun getPackageModeForUid(uid: Int): Mode? {
|
private fun getPackageModeForUid(uid: Int): Mode? {
|
||||||
val packages = getPackagesForUid(uid)
|
val packages = getPackagesForUid(uid)
|
||||||
if (packages.isEmpty()) return null
|
if (packages.isEmpty()) return null
|
||||||
|
|
||||||
// Lazily load TEE status if it hasn't been checked yet.
|
|
||||||
if (isTeeBroken == null) loadTeeStatus()
|
|
||||||
|
|
||||||
// Find the first configured mode for any of the UID's packages.
|
|
||||||
for (pkg in packages) {
|
for (pkg in packages) {
|
||||||
when (packageModes[pkg]) {
|
when (packageModes[pkg]) {
|
||||||
Mode.GENERATE -> return Mode.GENERATE
|
Mode.GENERATE -> return Mode.GENERATE
|
||||||
Mode.PATCH -> return Mode.PATCH
|
Mode.PATCH -> return Mode.PATCH
|
||||||
Mode.AUTO -> return if (isTeeBroken == true) Mode.GENERATE else Mode.PATCH
|
Mode.AUTO -> return if (DeviceAttestationService.isTeeFunctional) Mode.PATCH else Mode.GENERATE
|
||||||
null -> continue // No config for this package, check the next one.
|
null -> continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null // No configuration found for this UID.
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,7 +174,6 @@ object ConfigurationManager {
|
|||||||
newModes[pkg] = Mode.PATCH
|
newModes[pkg] = Mode.PATCH
|
||||||
newKeyboxes[pkg] = currentKeybox
|
newKeyboxes[pkg] = currentKeybox
|
||||||
}
|
}
|
||||||
// No suffix means AUTO mode.
|
|
||||||
else -> {
|
else -> {
|
||||||
newModes[trimmedLine] = Mode.AUTO
|
newModes[trimmedLine] = Mode.AUTO
|
||||||
newKeyboxes[trimmedLine] = currentKeybox
|
newKeyboxes[trimmedLine] = currentKeybox
|
||||||
@@ -242,7 +255,14 @@ object ConfigurationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse global and per-package configurations.
|
// Parse global and per-package configurations.
|
||||||
val newGlobalLevel = parseLines(contextLines[""])
|
var newGlobalLevel = parseLines(contextLines[""])
|
||||||
|
// TrickyAddon writes Pixel bulletin dates for boot/vendor but system=prop
|
||||||
|
// resolves to the real device prop — force boot/vendor through the same path
|
||||||
|
// to prevent cross-component date mismatches on non-Pixel devices.
|
||||||
|
if (newGlobalLevel?.system.equals("prop", ignoreCase = true)) {
|
||||||
|
SystemLogger.info("system=prop: forcing boot/vendor to derive from device props (were: boot=${newGlobalLevel?.boot}, vendor=${newGlobalLevel?.vendor})")
|
||||||
|
newGlobalLevel = newGlobalLevel?.copy(boot = "prop", vendor = "prop")
|
||||||
|
}
|
||||||
contextLines.remove("") // Remove global context to iterate over packages next
|
contextLines.remove("") // Remove global context to iterate over packages next
|
||||||
|
|
||||||
for ((pkg, lines) in contextLines) {
|
for ((pkg, lines) in contextLines) {
|
||||||
@@ -262,29 +282,6 @@ object ConfigurationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Checks the device's TEE status and writes the result to a file for persistence. */
|
|
||||||
private fun storeTeeStatus() {
|
|
||||||
val statusFile = File(configRoot, TEE_STATUS_FILE)
|
|
||||||
isTeeBroken = !DeviceAttestationService.isTeeFunctional
|
|
||||||
try {
|
|
||||||
statusFile.writeText("tee_broken=$isTeeBroken")
|
|
||||||
SystemLogger.info("TEE status stored: isTeeBroken=$isTeeBroken")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SystemLogger.error("Failed to write TEE status to file.", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Loads the TEE status from the file. */
|
|
||||||
private fun loadTeeStatus() {
|
|
||||||
val statusFile = File(configRoot, TEE_STATUS_FILE)
|
|
||||||
isTeeBroken =
|
|
||||||
if (statusFile.exists()) {
|
|
||||||
statusFile.readText().trim() == "tee_broken=true"
|
|
||||||
} else {
|
|
||||||
null // Status is unknown.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A FileObserver that monitors the configuration directory for changes and triggers reloads of
|
* A FileObserver that monitors the configuration directory for changes and triggers reloads of
|
||||||
* the relevant settings.
|
* the relevant settings.
|
||||||
@@ -296,8 +293,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 ->
|
||||||
@@ -307,10 +306,15 @@ object ConfigurationManager {
|
|||||||
)
|
)
|
||||||
KeyBoxManager.invalidateCache(path)
|
KeyBoxManager.invalidateCache(path)
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
|
||||||
// Clear cached keys possibly containing old certificates
|
// Drop only the patched cert chains so the next
|
||||||
|
// attestation request re-signs with the new keybox.
|
||||||
|
// Do NOT drop generatedKeys — that would destroy
|
||||||
|
// every alias/private key in memory and on disk,
|
||||||
|
// logging users out of any app that pinned a
|
||||||
|
// persisted keystore alias.
|
||||||
org.matrix.TEESimulator.interception.keystore.shim
|
org.matrix.TEESimulator.interception.keystore.shim
|
||||||
.KeyMintSecurityLevelInterceptor
|
.KeyMintSecurityLevelInterceptor
|
||||||
.clearAllGeneratedKeys("updating $file")
|
.invalidatePatchedChains("updating $file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -340,7 +344,29 @@ object ConfigurationManager {
|
|||||||
return iPackageManager
|
return iPackageManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Retrieves the package names associated with a UID. */
|
fun checkSELinuxPermission(callingPid: Int, tclass: String, perm: String): Boolean {
|
||||||
|
return try {
|
||||||
|
val callerCtx =
|
||||||
|
java.io.File("/proc/$callingPid/attr/current").readText().trim('\u0000', ' ', '\n')
|
||||||
|
val selfCtx =
|
||||||
|
java.io.File("/proc/self/attr/current").readText().trim('\u0000', ' ', '\n')
|
||||||
|
android.os.SELinux.checkSELinuxAccess(callerCtx, selfCtx, tclass, perm)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasPermissionForUid(uid: Int, permission: String): Boolean {
|
||||||
|
val userId = uid / 100000
|
||||||
|
return getPackagesForUid(uid).any { pkg ->
|
||||||
|
try {
|
||||||
|
getPackageManager()?.checkPermission(permission, pkg, userId) == 0
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getPackagesForUid(uid: Int): Array<String> {
|
fun getPackagesForUid(uid: Int): Array<String> {
|
||||||
return uidToPackagesCache.getOrPut(uid) {
|
return uidToPackagesCache.getOrPut(uid) {
|
||||||
try {
|
try {
|
||||||
@@ -354,7 +380,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
|
||||||
}
|
}
|
||||||
@@ -248,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.
|
||||||
@@ -291,15 +293,21 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Uses the backdoor binder to register an interceptor for a specific target service. */
|
fun register(
|
||||||
fun register(backdoor: IBinder, target: IBinder, interceptor: BinderInterceptor) {
|
backdoor: IBinder,
|
||||||
|
target: IBinder,
|
||||||
|
interceptor: BinderInterceptor,
|
||||||
|
filteredCodes: IntArray = intArrayOf(),
|
||||||
|
) {
|
||||||
val data = Parcel.obtain()
|
val data = Parcel.obtain()
|
||||||
val reply = Parcel.obtain()
|
val reply = Parcel.obtain()
|
||||||
try {
|
try {
|
||||||
data.writeStrongBinder(target)
|
data.writeStrongBinder(target)
|
||||||
data.writeStrongBinder(interceptor)
|
data.writeStrongBinder(interceptor)
|
||||||
|
data.writeInt(filteredCodes.size)
|
||||||
|
for (code in filteredCodes) data.writeInt(code)
|
||||||
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
|
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
|
||||||
SystemLogger.info("Registered interceptor for target: $target")
|
SystemLogger.info("Registered interceptor for target: $target (${filteredCodes.size} filtered codes)")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("Failed to register binder interceptor.", e)
|
SystemLogger.error("Failed to register binder interceptor.", e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -307,5 +315,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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -68,11 +68,12 @@ abstract class AbstractKeystoreInterceptor : BinderInterceptor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Registers this interceptor with the native hook layer and sets up a death recipient. */
|
protected open val interceptedCodes: IntArray = intArrayOf()
|
||||||
|
|
||||||
private fun setupInterceptor(service: IBinder, backdoor: IBinder) {
|
private fun setupInterceptor(service: IBinder, backdoor: IBinder) {
|
||||||
keystoreService = service
|
keystoreService = service
|
||||||
SystemLogger.info("Registering interceptor for service: $serviceName")
|
SystemLogger.info("Registering interceptor for service: $serviceName")
|
||||||
register(backdoor, service, this)
|
register(backdoor, service, this, interceptedCodes)
|
||||||
service.linkToDeath(createDeathRecipient(), 0)
|
service.linkToDeath(createDeathRecipient(), 0)
|
||||||
onInterceptorReady(service, backdoor)
|
onInterceptorReady(service, backdoor)
|
||||||
}
|
}
|
||||||
|
|||||||
+130
-9
@@ -1,17 +1,51 @@
|
|||||||
package org.matrix.TEESimulator.interception.keystore
|
package org.matrix.TEESimulator.interception.keystore
|
||||||
|
|
||||||
|
import android.hardware.security.keymint.KeyParameter
|
||||||
|
import android.hardware.security.keymint.KeyParameterValue
|
||||||
|
import android.hardware.security.keymint.Tag
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.security.KeyStore
|
import android.security.KeyStore
|
||||||
import android.security.keystore.KeystoreResponse
|
import android.security.keystore.KeystoreResponse
|
||||||
|
import android.system.keystore2.Authorization
|
||||||
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||||
|
|
||||||
data class KeyIdentifier(val uid: Int, val alias: String)
|
data class KeyIdentifier(val uid: Int, val alias: String)
|
||||||
|
|
||||||
/** 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
|
||||||
|
|
||||||
|
private fun synthesizeSseMessage(errorCode: Int): String =
|
||||||
|
when (errorCode) {
|
||||||
|
2 -> "Error::Rc(SYSTEM_ERROR)"
|
||||||
|
4 -> "Error::Rc(PERMISSION_DENIED)"
|
||||||
|
6 -> "Error::Rc(VALUE_CORRUPTED)"
|
||||||
|
7 -> "Error::Rc(KEY_NOT_FOUND)"
|
||||||
|
10 -> "Error::Rc(BACKEND_BUSY)"
|
||||||
|
-3 -> "Error::Km(UNSUPPORTED_KEY_SIZE)"
|
||||||
|
-6 -> "Error::Km(INCOMPATIBLE_PURPOSE)"
|
||||||
|
-7 -> "Error::Km(INCOMPATIBLE_ALGORITHM)"
|
||||||
|
-29 -> "Error::Km(TOO_MANY_OPERATIONS)"
|
||||||
|
-49 -> "Error::Km(UNSUPPORTED_TAG)"
|
||||||
|
-75 -> "Error::Km(INVALID_INPUT_LENGTH)"
|
||||||
|
-76 -> "Error::Km(INVALID_TAG)"
|
||||||
|
else -> if (errorCode > 0) "Error::Rc($errorCode)" else "Error::Km($errorCode)"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply {
|
||||||
|
val parcel = Parcel.obtain().apply {
|
||||||
|
writeInt(EX_SERVICE_SPECIFIC)
|
||||||
|
writeString(synthesizeSseMessage(errorCode))
|
||||||
|
writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196)
|
||||||
|
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.
|
||||||
@@ -52,7 +86,7 @@ object InterceptorUtils {
|
|||||||
writeInt(KeyStore.NO_ERROR)
|
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. */
|
||||||
@@ -62,38 +96,125 @@ 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. */
|
||||||
fun <T : Parcelable?> createTypedObjectReply(
|
fun <T : Parcelable?> createTypedObjectReply(
|
||||||
obj: T,
|
obj: T,
|
||||||
flags: Int = 0,
|
flags: Int = 0,
|
||||||
|
diagnosticTag: String? = null,
|
||||||
): BinderInterceptor.TransactionResult.OverrideReply {
|
): BinderInterceptor.TransactionResult.OverrideReply {
|
||||||
val parcel =
|
val parcel =
|
||||||
Parcel.obtain().apply {
|
Parcel.obtain().apply {
|
||||||
writeNoException()
|
writeNoException()
|
||||||
writeTypedObject(obj, flags)
|
writeTypedObject(obj, flags)
|
||||||
}
|
}
|
||||||
return BinderInterceptor.TransactionResult.OverrideReply(0, parcel)
|
if (diagnosticTag != null && SystemLogger.isDebugBuild) {
|
||||||
|
val savedPos = parcel.dataPosition()
|
||||||
|
val wire = parcel.marshall()
|
||||||
|
parcel.setDataPosition(savedPos)
|
||||||
|
val path = "/data/local/tmp/teesim-$diagnosticTag.bin"
|
||||||
|
runCatching { java.io.File(path).writeBytes(wire) }
|
||||||
|
SystemLogger.debug("[$diagnosticTag] reply len=${wire.size} path=$path")
|
||||||
|
}
|
||||||
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createServiceSpecificErrorReply(
|
||||||
|
errorCode: Int
|
||||||
|
): BinderInterceptor.TransactionResult.OverrideReply = createErrorReply(errorCode)
|
||||||
|
|
||||||
|
fun normalizeServiceSpecificReply(reply: Parcel): Parcel? {
|
||||||
|
reply.setDataPosition(0)
|
||||||
|
if (reply.readInt() != EX_SERVICE_SPECIFIC) {
|
||||||
|
reply.setDataPosition(0)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// Advance position past message and stack header to reach errorCode.
|
||||||
|
reply.readString()
|
||||||
|
reply.readInt()
|
||||||
|
val errorCode = reply.readInt()
|
||||||
|
reply.setDataPosition(0)
|
||||||
|
return Parcel.obtain().apply {
|
||||||
|
writeInt(EX_SERVICE_SPECIFIC)
|
||||||
|
writeString(synthesizeSseMessage(errorCode))
|
||||||
|
writeInt(0)
|
||||||
|
writeInt(errorCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun patchAuthorizations(
|
||||||
|
authorizations: Array<Authorization>?,
|
||||||
|
callingUid: Int,
|
||||||
|
): Array<Authorization>? {
|
||||||
|
if (authorizations == null) return null
|
||||||
|
|
||||||
|
val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid)
|
||||||
|
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
|
||||||
|
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
|
||||||
|
|
||||||
|
return authorizations
|
||||||
|
.map { auth ->
|
||||||
|
val replacement =
|
||||||
|
when (auth.keyParameter.tag) {
|
||||||
|
Tag.OS_PATCHLEVEL ->
|
||||||
|
if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) osPatch else null
|
||||||
|
Tag.VENDOR_PATCHLEVEL ->
|
||||||
|
if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) vendorPatch
|
||||||
|
else null
|
||||||
|
Tag.BOOT_PATCHLEVEL ->
|
||||||
|
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) bootPatch else null
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (replacement != null) {
|
||||||
|
Authorization().apply {
|
||||||
|
keyParameter =
|
||||||
|
KeyParameter().apply {
|
||||||
|
tag = auth.keyParameter.tag
|
||||||
|
value = KeyParameterValue.integer(replacement)
|
||||||
|
}
|
||||||
|
securityLevel = auth.securityLevel
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
auth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toTypedArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+546
-74
@@ -1,20 +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.os.ServiceManager
|
||||||
|
import android.system.keystore2.Domain
|
||||||
import android.system.keystore2.IKeystoreService
|
import android.system.keystore2.IKeystoreService
|
||||||
import android.system.keystore2.KeyDescriptor
|
import android.system.keystore2.KeyDescriptor
|
||||||
import android.system.keystore2.KeyEntryResponse
|
import android.system.keystore2.KeyEntryResponse
|
||||||
|
import java.security.SecureRandom
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
|
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.GeneratedKeyPersistence
|
||||||
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
|
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
|
||||||
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
import org.matrix.TEESimulator.pki.CertificateGenerator
|
||||||
import org.matrix.TEESimulator.pki.CertificateHelper
|
import org.matrix.TEESimulator.pki.CertificateHelper
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,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 GRANT_TRANSACTION = InterceptorUtils.getTransactCode(stubBinderClass, "grant")
|
||||||
|
private val UNGRANT_TRANSACTION = InterceptorUtils.getTransactCode(stubBinderClass, "ungrant")
|
||||||
|
|
||||||
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_")
|
||||||
@@ -43,10 +61,40 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
|
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val RESPONSE_KEY_NOT_FOUND = 7
|
||||||
|
private const val RESPONSE_PERMISSION_DENIED = 6
|
||||||
|
|
||||||
|
// KeyStoreManager.grantKeyAccess() became a public app API in Android 16 (API 36). Before that,
|
||||||
|
// grant was a hidden API and SELinux denied untrusted_app, so a synthetic-key grant must answer
|
||||||
|
// PERMISSION_DENIED pre-36 and a coherent virtualized grant on 36+.
|
||||||
|
private const val GRANT_PUBLIC_API_SDK = 36
|
||||||
|
private val deletedSoftwareKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
||||||
|
private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
|
||||||
|
|
||||||
|
fun forgetDeletedKey(keyId: KeyIdentifier) {
|
||||||
|
if (deletedSoftwareKeys.remove(keyId)) {
|
||||||
|
SystemLogger.debug("Cleared deletion marker for ${keyId.alias}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override val serviceName = "android.system.keystore2.IKeystoreService/default"
|
override val 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,
|
||||||
|
GRANT_TRANSACTION,
|
||||||
|
UNGRANT_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).
|
||||||
@@ -54,6 +102,27 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
override fun onInterceptorReady(service: IBinder, backdoor: IBinder) {
|
override fun onInterceptorReady(service: IBinder, backdoor: IBinder) {
|
||||||
val keystoreInterface = IKeystoreService.Stub.asInterface(service)
|
val keystoreInterface = IKeystoreService.Stub.asInterface(service)
|
||||||
setupSecurityLevelInterceptors(keystoreInterface, backdoor)
|
setupSecurityLevelInterceptors(keystoreInterface, backdoor)
|
||||||
|
setupMaintenanceInterceptor(backdoor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks the keystore2 daemon's `android.security.maintenance` binder, which is hosted by the
|
||||||
|
* same process, so synthetic key state follows real key-lifecycle events. Best-effort: if the
|
||||||
|
* service is absent the synthetic plane simply forgoes lifecycle parity.
|
||||||
|
*/
|
||||||
|
private fun setupMaintenanceInterceptor(backdoor: IBinder) {
|
||||||
|
runCatching {
|
||||||
|
ServiceManager.getService("android.security.maintenance")?.let { maintenance ->
|
||||||
|
SystemLogger.info("Found maintenance binder. Registering interceptor...")
|
||||||
|
register(
|
||||||
|
backdoor,
|
||||||
|
maintenance,
|
||||||
|
Keystore2MaintenanceInterceptor,
|
||||||
|
Keystore2MaintenanceInterceptor.interceptedCodes,
|
||||||
|
)
|
||||||
|
} ?: SystemLogger.warning("Maintenance binder not found; skipping lifecycle parity.")
|
||||||
|
}
|
||||||
|
.onFailure { SystemLogger.error("Failed to intercept maintenance binder.", it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupSecurityLevelInterceptors(service: IKeystoreService, backdoor: IBinder) {
|
private fun setupSecurityLevelInterceptors(service: IKeystoreService, backdoor: IBinder) {
|
||||||
@@ -63,7 +132,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)
|
register(
|
||||||
|
backdoor,
|
||||||
|
tee.asBinder(),
|
||||||
|
interceptor,
|
||||||
|
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
|
||||||
|
)
|
||||||
|
interceptor.loadPersistedKeys()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) }
|
.onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) }
|
||||||
@@ -74,7 +149,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
|
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
|
||||||
val interceptor =
|
val interceptor =
|
||||||
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
|
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
|
||||||
register(backdoor, strongbox.asBinder(), interceptor)
|
register(
|
||||||
|
backdoor,
|
||||||
|
strongbox.asBinder(),
|
||||||
|
interceptor,
|
||||||
|
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
|
||||||
|
)
|
||||||
|
interceptor.loadPersistedKeys()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) }
|
.onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) }
|
||||||
@@ -89,34 +170,158 @@ 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, true)
|
||||||
|
return if (ConfigurationManager.shouldSkipUid(callingUid))
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
else TransactionResult.Continue
|
||||||
|
} else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||||
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
|
||||||
|
|
||||||
|
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
|
||||||
|
val isGMS = packages.contains("com.google.android.gms")
|
||||||
|
|
||||||
|
if (isGMS || ConfigurationManager.shouldSkipUid(callingUid)) {
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
|
||||||
|
return runCatching {
|
||||||
|
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
|
||||||
|
if (ListEntriesHandler.cacheParameters(txId, data, isBatchMode)) {
|
||||||
|
TransactionResult.Continue
|
||||||
|
} else {
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.error(
|
||||||
|
"[TX_ID: $txId] Failed to parse parameters for ${transactionNames[code]!!}",
|
||||||
|
it,
|
||||||
|
)
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
code == GET_KEY_ENTRY_TRANSACTION ||
|
||||||
|
code == DELETE_KEY_TRANSACTION ||
|
||||||
|
code == UPDATE_SUBCOMPONENT_TRANSACTION
|
||||||
|
) {
|
||||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||||
|
|
||||||
|
if (code == UPDATE_SUBCOMPONENT_TRANSACTION) {
|
||||||
|
if (ConfigurationManager.shouldSkipUid(callingUid))
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
return handleUpdateSubcomponent(callingUid, data)
|
||||||
|
}
|
||||||
|
|
||||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
val descriptor =
|
val descriptor =
|
||||||
data.readTypedObject(KeyDescriptor.CREATOR)
|
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
?: return TransactionResult.SkipTransaction
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
|
|
||||||
|
// Domain.GRANT read (Android 16+ KeyStoreManager grant). Served for ANY grantee uid —
|
||||||
|
// including isolated services (bindIsolatedService) with no package mapping — so resolve
|
||||||
|
// it before the package-scoped skip; caller-binding in resolveGrant() is the real access
|
||||||
|
// gate. On Android <= 15 no grants are ever issued (grant() denies), so softwareGrants is
|
||||||
|
// empty and this falls through to the real keystore2.
|
||||||
|
if (code == GET_KEY_ENTRY_TRANSACTION && descriptor.domain == Domain.GRANT) {
|
||||||
|
val grant =
|
||||||
|
KeyMintSecurityLevelInterceptor.resolveGrant(descriptor.nspace, callingUid)
|
||||||
|
if (grant == null) {
|
||||||
|
// Ours but wrong caller -> KEY_NOT_FOUND (caller-binding); not ours -> real keystore2.
|
||||||
|
return if (
|
||||||
|
KeyMintSecurityLevelInterceptor.softwareGrants.containsKey(descriptor.nspace)
|
||||||
|
)
|
||||||
|
InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
|
||||||
|
else TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
if ((grant.accessVector and 0x4) == 0) { // GET_INFO = 0x4 (access-vector gate)
|
||||||
|
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
|
||||||
|
}
|
||||||
|
val response =
|
||||||
|
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(grant.ownerKeyId)
|
||||||
|
?: return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
|
||||||
|
// Same object the owner read returns -> coherent chain across planes.
|
||||||
|
return InterceptorUtils.createTypedObjectReply(response)
|
||||||
|
}
|
||||||
|
|
||||||
if (ConfigurationManager.shouldSkipUid(callingUid))
|
if (ConfigurationManager.shouldSkipUid(callingUid))
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
|
||||||
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
|
|
||||||
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
|
||||||
|
|
||||||
if (code == DELETE_KEY_TRANSACTION) {
|
if (code == DELETE_KEY_TRANSACTION) {
|
||||||
if (KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null) {
|
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)
|
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
||||||
SystemLogger.info(
|
if (isSoftwareKey) {
|
||||||
"[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response."
|
deletedSoftwareKeys.add(keyId)
|
||||||
)
|
SystemLogger.info(
|
||||||
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
"[TX_ID: $txId] Deleted cached keypair ${keyId.alias}, replying with empty response."
|
||||||
|
)
|
||||||
|
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
|
|
||||||
val response =
|
if (descriptor.alias == null) {
|
||||||
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
if (descriptor.domain == Domain.KEY_ID) {
|
||||||
?: return TransactionResult.Continue
|
// The probe pipeline (and some AOSP callers) switch follow-up
|
||||||
|
// operations to KEY_ID semantics after generateKey returns a
|
||||||
|
// KEY_ID descriptor. Without this branch, our software keys
|
||||||
|
// are invisible to KEY_ID-based getKeyEntry calls and the
|
||||||
|
// request falls through to the real keystore2 daemon, which
|
||||||
|
// legitimately responds with KEY_NOT_FOUND. Duck Detector's
|
||||||
|
// TimingSideChannelProbe captures that exception during its
|
||||||
|
// warmup phase and surfaces it as
|
||||||
|
// "Captured private binder exception during timing skip".
|
||||||
|
// Resolving by KEY_ID and returning the cached response keeps
|
||||||
|
// the call on the happy path, eliminating the warmup signal.
|
||||||
|
val info = KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||||
|
callingUid, descriptor.nspace
|
||||||
|
)
|
||||||
|
if (info?.response != null) {
|
||||||
|
SystemLogger.info(
|
||||||
|
"[TX_ID: $txId] Found generated response via KEY_ID nspace=${descriptor.nspace}"
|
||||||
|
)
|
||||||
|
return InterceptorUtils.createTypedObjectReply(info.response)
|
||||||
|
}
|
||||||
|
val teeResp = KeyMintSecurityLevelInterceptor.findTeeResponseByKeyId(
|
||||||
|
callingUid, descriptor.nspace
|
||||||
|
)
|
||||||
|
if (teeResp != null) {
|
||||||
|
SystemLogger.info(
|
||||||
|
"[TX_ID: $txId] Found TEE response via KEY_ID nspace=${descriptor.nspace}"
|
||||||
|
)
|
||||||
|
return InterceptorUtils.createTypedObjectReply(teeResp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Domain.GRANT is handled earlier (before the package-scoped skip); an alias-less
|
||||||
|
// read reaching here is KEY_ID or unknown, so it falls through to the real keystore2.
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
||||||
|
|
||||||
|
val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
||||||
|
if (response == null) {
|
||||||
|
if (deletedSoftwareKeys.remove(keyId)) {
|
||||||
|
SystemLogger.info("[TX_ID: $txId] Returning KEY_NOT_FOUND for deleted key ${descriptor.alias}")
|
||||||
|
return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
|
||||||
|
}
|
||||||
|
return TransactionResult.Continue
|
||||||
|
}
|
||||||
|
|
||||||
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
|
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
|
||||||
SystemLogger.info("${descriptor.alias} was an attestation key")
|
SystemLogger.info("${descriptor.alias} was an attestation key")
|
||||||
@@ -126,6 +331,57 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
KeyMintParameterLogger.logParameter(it.keyParameter)
|
KeyMintParameterLogger.logParameter(it.keyParameter)
|
||||||
}
|
}
|
||||||
return InterceptorUtils.createTypedObjectReply(response)
|
return InterceptorUtils.createTypedObjectReply(response)
|
||||||
|
} else if (code == GRANT_TRANSACTION) {
|
||||||
|
logTransaction(txId, transactionNames[code] ?: "grant", callingUid, callingPid)
|
||||||
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
val key =
|
||||||
|
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
|
val granteeUid = data.readInt()
|
||||||
|
val accessVector = data.readInt()
|
||||||
|
// Synthetic (generatedKeys) AND patch-mode (teeResponses) keys are ours; both must grant
|
||||||
|
// coherently so the Domain.GRANT readback returns the same chain the owner read returns.
|
||||||
|
// Real hardware keys fall through to the real keystore2, which applies the same SELinux
|
||||||
|
// gate the platform would.
|
||||||
|
val ownerKeyId =
|
||||||
|
resolveOwnerKeyId(key, callingUid)
|
||||||
|
?.takeIf { KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) }
|
||||||
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
|
// Version-gated to mirror the real TEE 1:1. Pre-Android-16, grant was a hidden API and
|
||||||
|
// SELinux denied untrusted_app, so keystore2 returns PERMISSION_DENIED. Android 16
|
||||||
|
// (API 36) exposes KeyStoreManager.grantKeyAccess(), so an app grants its own key:
|
||||||
|
// issue a coherent, caller-bound, access-vector-carrying grant whose Domain.GRANT read
|
||||||
|
// returns the owner's chain.
|
||||||
|
if (Build.VERSION.SDK_INT < GRANT_PUBLIC_API_SDK) {
|
||||||
|
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
|
||||||
|
}
|
||||||
|
val grantId =
|
||||||
|
KeyMintSecurityLevelInterceptor.issueGrant(ownerKeyId, granteeUid, accessVector)
|
||||||
|
val reply =
|
||||||
|
KeyDescriptor().apply {
|
||||||
|
domain = Domain.GRANT
|
||||||
|
nspace = grantId
|
||||||
|
alias = null
|
||||||
|
blob = null
|
||||||
|
}
|
||||||
|
return InterceptorUtils.createTypedObjectReply(reply)
|
||||||
|
} else if (code == UNGRANT_TRANSACTION) {
|
||||||
|
logTransaction(txId, transactionNames[code] ?: "ungrant", callingUid, callingPid)
|
||||||
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
val key =
|
||||||
|
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
|
val granteeUid = data.readInt()
|
||||||
|
val ownerKeyId =
|
||||||
|
resolveOwnerKeyId(key, callingUid)
|
||||||
|
?.takeIf { KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) }
|
||||||
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
|
// Same version gate as grant(): denied pre-36, revoke the virtualized grant on 36+.
|
||||||
|
if (Build.VERSION.SDK_INT < GRANT_PUBLIC_API_SDK) {
|
||||||
|
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
|
||||||
|
}
|
||||||
|
KeyMintSecurityLevelInterceptor.revokeGrant(ownerKeyId, granteeUid)
|
||||||
|
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||||
} else {
|
} else {
|
||||||
logTransaction(
|
logTransaction(
|
||||||
txId,
|
txId,
|
||||||
@@ -151,76 +407,292 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
reply: Parcel?,
|
reply: Parcel?,
|
||||||
resultCode: Int,
|
resultCode: Int,
|
||||||
): TransactionResult {
|
): TransactionResult {
|
||||||
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
|
if (target != keystoreService || reply == null) return TransactionResult.SkipTransaction
|
||||||
return TransactionResult.SkipTransaction
|
if (InterceptorUtils.hasException(reply)) {
|
||||||
|
val normalized = InterceptorUtils.normalizeServiceSpecificReply(reply)
|
||||||
|
return if (normalized != null) TransactionResult.OverrideReply(normalized)
|
||||||
|
else TransactionResult.SkipTransaction
|
||||||
|
}
|
||||||
|
|
||||||
if (code == GET_KEY_ENTRY_TRANSACTION) {
|
if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) {
|
||||||
|
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||||
|
return runCatching {
|
||||||
|
val hardwareCount = reply.readInt()
|
||||||
|
val softwareCount =
|
||||||
|
KeyMintSecurityLevelInterceptor.generatedKeys.keys.count {
|
||||||
|
it.uid == callingUid
|
||||||
|
}
|
||||||
|
val totalCount = hardwareCount + softwareCount
|
||||||
|
val parcel = Parcel.obtain().apply {
|
||||||
|
writeNoException()
|
||||||
|
writeInt(totalCount)
|
||||||
|
}
|
||||||
|
TransactionResult.OverrideReply(parcel)
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.error("[TX_ID: $txId] Failed to modify getNumberOfEntries.", it)
|
||||||
|
TransactionResult.SkipTransaction
|
||||||
|
}
|
||||||
|
} else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||||
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||||
|
|
||||||
|
return runCatching {
|
||||||
|
val updatedKeyDescriptors =
|
||||||
|
ListEntriesHandler.injectGeneratedKeys(txId, callingUid, 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)
|
if (userUpdatedKeys.remove(keyId)) {
|
||||||
val authorizations = response.metadata?.authorizations
|
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: userUpdated=true, skipping patch" }
|
||||||
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()
|
||||||
|
)
|
||||||
|
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: isImport=${parsedParameters.isImportKey()} origin=${parsedParameters.origin} inImportedKeys=${KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)} hasPatchedChain=${KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) != null} isAttestKey=${parsedParameters.isAttestKey()}" }
|
||||||
|
|
||||||
|
if (parsedParameters.isImportKey()) {
|
||||||
|
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
||||||
|
if (retainedChain == null) {
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, no retained chain, skip" }
|
||||||
|
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
|
||||||
|
return TransactionResult.SkipTransaction
|
||||||
|
}
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, SERVING RETAINED CHAIN (detection vector!)" }
|
||||||
|
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
|
||||||
|
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
|
||||||
|
response.metadata.authorizations =
|
||||||
|
InterceptorUtils.patchAuthorizations(
|
||||||
|
response.metadata.authorizations,
|
||||||
|
callingUid,
|
||||||
|
)
|
||||||
|
return InterceptorUtils.createTypedObjectReply(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)) {
|
||||||
|
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: in importedKeys set, skip" }
|
||||||
|
SystemLogger.debug("[TX_ID: $txId] Skipping attest-key override for imported key $keyId")
|
||||||
|
return TransactionResult.SkipTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedParameters.isAttestKey()) {
|
||||||
|
SystemLogger.warning(
|
||||||
|
"[TX_ID: $txId] Found hardware attest key ${keyId.alias} in the reply."
|
||||||
|
)
|
||||||
|
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 newNspace = SecureRandom().nextLong()
|
||||||
|
response.metadata.key?.let { it.nspace = newNspace }
|
||||||
|
KeyMintSecurityLevelInterceptor.generatedKeys[keyId] =
|
||||||
|
KeyMintSecurityLevelInterceptor.GeneratedKeyInfo(
|
||||||
|
keyData.first,
|
||||||
|
null,
|
||||||
|
newNspace,
|
||||||
|
response,
|
||||||
|
parsedParameters,
|
||||||
|
)
|
||||||
|
KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId)
|
||||||
|
|
||||||
|
// Snapshot metadata bytes for the same reason as the
|
||||||
|
// primary doSoftwareKeyGen path — loss-less restore
|
||||||
|
// after reboot.
|
||||||
|
val metadataBytesForPersist = response.metadata?.let { md ->
|
||||||
|
runCatching {
|
||||||
|
val parcel = android.os.Parcel.obtain()
|
||||||
|
try {
|
||||||
|
md.writeToParcel(parcel, 0)
|
||||||
|
parcel.marshall()
|
||||||
|
} finally {
|
||||||
|
parcel.recycle()
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
GeneratedKeyPersistence.save(
|
||||||
|
keyId = keyId,
|
||||||
|
keyPair = keyData.first,
|
||||||
|
secretKey = null,
|
||||||
|
nspace = newNspace,
|
||||||
|
securityLevel = response.metadata.keySecurityLevel,
|
||||||
|
certChain = keyData.second,
|
||||||
|
algorithm = parsedParameters.algorithm,
|
||||||
|
keySize = parsedParameters.keySize,
|
||||||
|
ecCurve = parsedParameters.ecCurve ?: 0,
|
||||||
|
purposes = parsedParameters.purpose,
|
||||||
|
digests = parsedParameters.digest,
|
||||||
|
isAttestationKey = true,
|
||||||
|
metadataBytes = metadataBytesForPersist,
|
||||||
|
)
|
||||||
|
|
||||||
|
return InterceptorUtils.createTypedObjectReply(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
val originalChain = CertificateHelper.getCertificateChain(response)
|
||||||
|
|
||||||
|
if (originalChain == null || originalChain.size < 2) {
|
||||||
|
SystemLogger.info(
|
||||||
|
"[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}."
|
||||||
|
)
|
||||||
|
return TransactionResult.SkipTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
CertificateHelper.updateCertificateChain(response.metadata, finalChain).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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the owner [KeyIdentifier] a grant/ungrant call targets. APP/alias keys map
|
||||||
|
* directly; KEY_ID keys are looked up by nspace (mirrors the deleteKey resolver). Returns
|
||||||
|
* null for anything not addressable, so callers fall through to the real keystore2.
|
||||||
|
*/
|
||||||
|
private fun resolveOwnerKeyId(descriptor: KeyDescriptor, callingUid: Int): KeyIdentifier? =
|
||||||
|
when {
|
||||||
|
descriptor.alias != null -> KeyIdentifier(callingUid, descriptor.alias)
|
||||||
|
descriptor.domain == Domain.KEY_ID ->
|
||||||
|
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(callingUid, descriptor.nspace)
|
||||||
|
?.let { info ->
|
||||||
|
KeyMintSecurityLevelInterceptor.generatedKeys.entries
|
||||||
|
.firstOrNull { it.value.nspace == info.nspace && it.key.uid == callingUid }
|
||||||
|
?.key
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult {
|
||||||
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
|
|
||||||
|
val generatedKeyInfo =
|
||||||
|
when (descriptor.domain) {
|
||||||
|
Domain.KEY_ID ->
|
||||||
|
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||||
|
callingUid, descriptor.nspace
|
||||||
|
)
|
||||||
|
Domain.APP ->
|
||||||
|
descriptor.alias?.let {
|
||||||
|
KeyMintSecurityLevelInterceptor.generatedKeys[KeyIdentifier(callingUid, it)]
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generatedKeyInfo == null) {
|
||||||
|
// Patch-mode key (cached in teeResponses, not generatedKeys): the real keystore2 applies
|
||||||
|
// the update, so drop our stale cached chain. Otherwise getKeyEntry replays the
|
||||||
|
// pre-update generated attestation (duck STALE_TEE_RESPONSE_AFTER_KEY_ID_UPDATE).
|
||||||
|
when (descriptor.domain) {
|
||||||
|
Domain.KEY_ID ->
|
||||||
|
KeyMintSecurityLevelInterceptor.evictTeeResponseByKeyId(callingUid, descriptor.nspace)
|
||||||
|
Domain.APP ->
|
||||||
|
descriptor.alias?.let {
|
||||||
|
KeyMintSecurityLevelInterceptor.evictTeeResponse(KeyIdentifier(callingUid, it))
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
descriptor.alias?.let {
|
||||||
|
val kid = KeyIdentifier(callingUid, it)
|
||||||
|
userUpdatedKeys.add(kid)
|
||||||
|
SystemLogger.trace { "[TRACE] updateSubcomponent $kid: not generated key, added to userUpdatedKeys" }
|
||||||
|
}
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemLogger.info("Updating sub-component with key[${generatedKeyInfo.nspace}]")
|
||||||
|
val metadata = generatedKeyInfo.response.metadata
|
||||||
|
val publicCert = data.createByteArray()
|
||||||
|
val certificateChain = data.createByteArray()
|
||||||
|
|
||||||
|
metadata.certificate = publicCert
|
||||||
|
metadata.certificateChain = certificateChain
|
||||||
|
|
||||||
|
GeneratedKeyPersistence.rePersistIfNeeded(callingUid, generatedKeyInfo)
|
||||||
|
|
||||||
|
SystemLogger.verbose(
|
||||||
|
"Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]"
|
||||||
|
)
|
||||||
|
|
||||||
|
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
package org.matrix.TEESimulator.interception.keystore
|
||||||
|
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.security.maintenance.IKeystoreMaintenance
|
||||||
|
import android.system.keystore2.Domain
|
||||||
|
import android.system.keystore2.KeyDescriptor
|
||||||
|
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||||
|
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
|
||||||
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intercepts the keystore2 daemon's `android.security.maintenance` binder so our synthetic key
|
||||||
|
* state follows the same lifecycle events the platform applies to real keys.
|
||||||
|
*
|
||||||
|
* This is a pure side-effect hook: every handled transaction mutates only our own synthetic state
|
||||||
|
* and then returns [TransactionResult.ContinueAndSkipPost], so the real keystore2 still performs the
|
||||||
|
* real operation. We never fabricate a maintenance reply, so real key lifecycle is never disturbed.
|
||||||
|
*
|
||||||
|
* Mounted via `register()` from [Keystore2Interceptor.onInterceptorReady]; the maintenance binder is
|
||||||
|
* hosted by the same keystore2 process, so the already-injected native hook reaches it too.
|
||||||
|
*/
|
||||||
|
object Keystore2MaintenanceInterceptor : BinderInterceptor() {
|
||||||
|
private val stubClass = IKeystoreMaintenance.Stub::class.java
|
||||||
|
|
||||||
|
private val CLEAR_NAMESPACE_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(stubClass, "clearNamespace")
|
||||||
|
private val DELETE_ALL_KEYS_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(stubClass, "deleteAllKeys")
|
||||||
|
private val MIGRATE_KEY_NAMESPACE_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(stubClass, "migrateKeyNamespace")
|
||||||
|
|
||||||
|
/** Only the lifecycle transactions we mirror; unresolved codes (-1) are dropped. */
|
||||||
|
val interceptedCodes: IntArray by lazy {
|
||||||
|
listOf(
|
||||||
|
CLEAR_NAMESPACE_TRANSACTION,
|
||||||
|
DELETE_ALL_KEYS_TRANSACTION,
|
||||||
|
MIGRATE_KEY_NAMESPACE_TRANSACTION,
|
||||||
|
)
|
||||||
|
.filter { it != -1 }
|
||||||
|
.toIntArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPreTransact(
|
||||||
|
txId: Long,
|
||||||
|
target: IBinder,
|
||||||
|
code: Int,
|
||||||
|
flags: Int,
|
||||||
|
callingUid: Int,
|
||||||
|
callingPid: Int,
|
||||||
|
data: Parcel,
|
||||||
|
): TransactionResult {
|
||||||
|
when (code) {
|
||||||
|
CLEAR_NAMESPACE_TRANSACTION -> handleClearNamespace(data)
|
||||||
|
DELETE_ALL_KEYS_TRANSACTION ->
|
||||||
|
KeyMintSecurityLevelInterceptor.clearAllGeneratedKeys("maintenance.deleteAllKeys")
|
||||||
|
MIGRATE_KEY_NAMESPACE_TRANSACTION -> handleMigrateKeyNamespace(data, callingUid)
|
||||||
|
}
|
||||||
|
// Always let the real keystore2 perform the real lifecycle operation.
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleClearNamespace(data: Parcel) {
|
||||||
|
data.enforceInterface(IKeystoreMaintenance.DESCRIPTOR)
|
||||||
|
val domain = data.readInt()
|
||||||
|
val nspace = data.readLong()
|
||||||
|
// Only Domain.APP namespaces map to our per-uid synthetic keys; nspace is the app uid.
|
||||||
|
if (domain == Domain.APP) {
|
||||||
|
KeyMintSecurityLevelInterceptor.clearNamespaceKeys(nspace.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleMigrateKeyNamespace(data: Parcel, callingUid: Int) {
|
||||||
|
data.enforceInterface(IKeystoreMaintenance.DESCRIPTOR)
|
||||||
|
val source = data.readTypedObject(KeyDescriptor.CREATOR) ?: return
|
||||||
|
val destination = data.readTypedObject(KeyDescriptor.CREATOR) ?: return
|
||||||
|
val srcId = resolveSyntheticKeyId(source, callingUid) ?: return
|
||||||
|
if (!KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(srcId)) return // not ours
|
||||||
|
|
||||||
|
val dstId = resolveDestinationKeyId(destination, callingUid)
|
||||||
|
if (dstId == null) {
|
||||||
|
// Migrated out of our trackable (Domain.APP/alias) space -> drop our shadow so reads
|
||||||
|
// fall through to the real keystore2, which now owns it at the new namespace.
|
||||||
|
KeyMintSecurityLevelInterceptor.cleanupKeyData(srcId)
|
||||||
|
} else {
|
||||||
|
KeyMintSecurityLevelInterceptor.migrateGeneratedKey(srcId, dstId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolves a synthetic owner key from a source descriptor (Domain.APP alias or KEY_ID). */
|
||||||
|
private fun resolveSyntheticKeyId(descriptor: KeyDescriptor, callingUid: Int): KeyIdentifier? =
|
||||||
|
when {
|
||||||
|
descriptor.alias != null -> KeyIdentifier(callingUid, descriptor.alias)
|
||||||
|
descriptor.domain == Domain.KEY_ID ->
|
||||||
|
KeyMintSecurityLevelInterceptor.generatedKeys.entries
|
||||||
|
.firstOrNull { it.key.uid == callingUid && it.value.nspace == descriptor.nspace }
|
||||||
|
?.key
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Destination must be an addressable Domain.APP alias for us to keep tracking the key. */
|
||||||
|
private fun resolveDestinationKeyId(descriptor: KeyDescriptor, callingUid: Int): KeyIdentifier? {
|
||||||
|
val alias = descriptor.alias ?: return null
|
||||||
|
if (descriptor.domain != Domain.APP) return null
|
||||||
|
val uid = if (descriptor.nspace > 0) descriptor.nspace.toInt() else callingUid
|
||||||
|
return KeyIdentifier(uid, alias)
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
-16
@@ -55,6 +55,28 @@ 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"
|
||||||
@@ -76,26 +98,33 @@ 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)) {
|
||||||
return when (code) {
|
generateKeyHandlers[code]?.let { handler ->
|
||||||
GENERATE_KEY_TRANSACTION -> handleGenerateKey(txId, callingUid, callingPid, data)
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||||
GET_KEY_CHARACTERISTICS_TRANSACTION ->
|
return handler(txId, callingUid, callingPid, data)
|
||||||
handleGetKeyCharacteristics(txId, callingUid, callingPid, data)
|
|
||||||
EXPORT_KEY_TRANSACTION -> handleExportKey(txId, callingUid, callingPid, data)
|
|
||||||
ATTEST_KEY_TRANSACTION -> handleAttestKey(txId, callingUid, callingPid, data)
|
|
||||||
else -> TransactionResult.ContinueAndSkipPost
|
|
||||||
}
|
}
|
||||||
} else if (ConfigurationManager.shouldPatch(callingUid)) {
|
|
||||||
// In patch mode, we only care about the 'get' transaction in onPostTransact.
|
|
||||||
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 {
|
private fun handleGenerateKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
logTransaction(txId, "generateKey", uid, pid)
|
|
||||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
val callback =
|
val callback =
|
||||||
IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
|
IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
|
||||||
@@ -133,7 +162,6 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
|
|||||||
data: Parcel,
|
data: Parcel,
|
||||||
): TransactionResult {
|
): TransactionResult {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
logTransaction(txId, "getKeyCharacteristics", uid, pid)
|
|
||||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
val callback =
|
val callback =
|
||||||
IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
|
IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
|
||||||
@@ -168,7 +196,6 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
|
|||||||
|
|
||||||
private fun handleExportKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
|
private fun handleExportKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
logTransaction(txId, "exportKey", uid, pid)
|
|
||||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
val callback = IKeystoreExportKeyCallback.Stub.asInterface(data.readStrongBinder())
|
val callback = IKeystoreExportKeyCallback.Stub.asInterface(data.readStrongBinder())
|
||||||
val alias = InterceptorUtils.extractAlias(data.readString()!!)
|
val alias = InterceptorUtils.extractAlias(data.readString()!!)
|
||||||
@@ -206,7 +233,6 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
|
|||||||
|
|
||||||
private fun handleAttestKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
|
private fun handleAttestKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
logTransaction(txId, "attestKey", uid, pid)
|
|
||||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
val callback =
|
val callback =
|
||||||
IKeystoreCertificateChainCallback.Stub.asInterface(data.readStrongBinder())
|
IKeystoreCertificateChainCallback.Stub.asInterface(data.readStrongBinder())
|
||||||
@@ -230,7 +256,6 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
|
|||||||
ByteArray(0),
|
ByteArray(0),
|
||||||
)
|
)
|
||||||
params.attestationChallenge = challenge
|
params.attestationChallenge = challenge
|
||||||
params.attestationChallenge = challenge
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val certificateChain =
|
val certificateChain =
|
||||||
@@ -271,6 +296,9 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,6 +309,9 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
|
|||||||
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.
|
||||||
@@ -376,8 +407,11 @@ private data class LegacyKeygenParameters(
|
|||||||
return KeyMintAttestation(
|
return KeyMintAttestation(
|
||||||
keySize = this.keySize,
|
keySize = this.keySize,
|
||||||
algorithm = this.algorithm,
|
algorithm = this.algorithm,
|
||||||
ecCurve = 0, // Not explicitly available in legacy args, but not critical
|
ecCurve = 0,
|
||||||
ecCurveName = this.ecCurveName ?: "",
|
ecCurveName = this.ecCurveName ?: "",
|
||||||
|
origin = null,
|
||||||
|
blockMode = listOf<Int>(),
|
||||||
|
padding = listOf<Int>(),
|
||||||
purpose = this.purpose,
|
purpose = this.purpose,
|
||||||
digest = this.digest,
|
digest = this.digest,
|
||||||
rsaPublicExponent = this.rsaPublicExponent,
|
rsaPublicExponent = this.rsaPublicExponent,
|
||||||
@@ -397,6 +431,24 @@ private data class LegacyKeygenParameters(
|
|||||||
manufacturer = null,
|
manufacturer = null,
|
||||||
model = null,
|
model = null,
|
||||||
secondImei = null,
|
secondImei = null,
|
||||||
|
activeDateTime = null,
|
||||||
|
originationExpireDateTime = null,
|
||||||
|
usageExpireDateTime = null,
|
||||||
|
usageCountLimit = null,
|
||||||
|
callerNonce = null,
|
||||||
|
nonce = null,
|
||||||
|
unlockedDeviceRequired = null,
|
||||||
|
includeUniqueId = null,
|
||||||
|
rollbackResistance = null,
|
||||||
|
earlyBootOnly = null,
|
||||||
|
allowWhileOnBody = null,
|
||||||
|
trustedUserPresenceRequired = null,
|
||||||
|
trustedConfirmationRequired = null,
|
||||||
|
noAuthRequired = null,
|
||||||
|
maxUsesPerBoot = null,
|
||||||
|
maxBootLevel = null,
|
||||||
|
minMacLength = null,
|
||||||
|
rsaOaepMgfDigest = emptyList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+142
@@ -0,0 +1,142 @@
|
|||||||
|
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 java.util.concurrent.ConcurrentHashMap
|
||||||
|
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
|
||||||
|
|
||||||
|
// Parameters of AOSP function `list_key_entries` in utils.rs.
|
||||||
|
private data class ListEntriesParams(
|
||||||
|
val domain: Int,
|
||||||
|
val namespace: Long,
|
||||||
|
val startPastAlias: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val pendingParams = ConcurrentHashMap<Long, ListEntriesParams>()
|
||||||
|
|
||||||
|
// Based on AOSP function `estimate_safe_amount_to_return` in utils.rs.
|
||||||
|
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): Boolean {
|
||||||
|
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) {
|
||||||
|
pendingParams[txId] = ListEntriesParams(domain, namespace, startPastAlias)
|
||||||
|
SystemLogger.debug("[TX_ID: $txId] Cached ${pendingParams[txId]}.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge software-backed keys with hardware-backed keys in the reply parcel.
|
||||||
|
fun injectGeneratedKeys(txId: Long, callingUid: Int, reply: Parcel): Array<KeyDescriptor> {
|
||||||
|
val params =
|
||||||
|
pendingParams.remove(txId)
|
||||||
|
?: throw IllegalStateException("No params found for listing entries")
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
package org.matrix.TEESimulator.interception.keystore.shim
|
||||||
|
|
||||||
|
import android.hardware.security.keymint.Algorithm
|
||||||
|
import android.hardware.security.keymint.KeyPurpose
|
||||||
|
import android.hardware.security.keymint.KeyParameter
|
||||||
|
import android.hardware.security.keymint.Tag
|
||||||
|
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||||
|
|
||||||
|
object AuthorizeCreate {
|
||||||
|
|
||||||
|
fun check(
|
||||||
|
keyParams: KeyMintAttestation?,
|
||||||
|
opParams: KeyMintAttestation,
|
||||||
|
rawOpParams: Array<KeyParameter>? = null,
|
||||||
|
): Int? {
|
||||||
|
if (keyParams == null) return null
|
||||||
|
val purpose = opParams.purpose.firstOrNull() ?: return null
|
||||||
|
// Algorithm-level rejection runs before purpose-list check (AOSP HAL behavior)
|
||||||
|
return checkAlgorithmPurpose(keyParams, purpose)
|
||||||
|
?: checkPurpose(keyParams, purpose)
|
||||||
|
?: checkTemporalValidity(keyParams, purpose)
|
||||||
|
?: checkCallerNonce(keyParams, purpose, rawOpParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
|
||||||
|
val algo = keyParams.algorithm
|
||||||
|
if ((algo == Algorithm.EC || algo == Algorithm.RSA) &&
|
||||||
|
(purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.ENCRYPT)
|
||||||
|
) {
|
||||||
|
return KeystoreErrorCodes.unsupportedPurpose
|
||||||
|
}
|
||||||
|
if (algo == Algorithm.RSA && purpose == KeyPurpose.AGREE_KEY)
|
||||||
|
return KeystoreErrorCodes.unsupportedPurpose
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
|
||||||
|
if (purpose == KeyPurpose.WRAP_KEY)
|
||||||
|
return KeystoreErrorCodes.incompatiblePurpose
|
||||||
|
if (purpose !in keyParams.purpose)
|
||||||
|
return KeystoreErrorCodes.incompatiblePurpose
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkTemporalValidity(keyParams: KeyMintAttestation, purpose: Int): Int? {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
|
||||||
|
keyParams.activeDateTime?.let { activeDate ->
|
||||||
|
if (now < activeDate.time) return KeystoreErrorCodes.keyNotYetValid
|
||||||
|
}
|
||||||
|
|
||||||
|
keyParams.originationExpireDateTime?.let { expireDate ->
|
||||||
|
if (purpose == KeyPurpose.SIGN || purpose == KeyPurpose.ENCRYPT) {
|
||||||
|
if (now > expireDate.time) return KeystoreErrorCodes.keyExpired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyParams.usageExpireDateTime?.let { expireDate ->
|
||||||
|
if (purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.DECRYPT) {
|
||||||
|
if (now > expireDate.time) return KeystoreErrorCodes.keyExpired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkCallerNonce(keyParams: KeyMintAttestation, purpose: Int, rawOpParams: Array<KeyParameter>?): Int? {
|
||||||
|
if (purpose != KeyPurpose.SIGN && purpose != KeyPurpose.ENCRYPT) return null
|
||||||
|
if (keyParams.callerNonce == true) return null
|
||||||
|
if (rawOpParams?.any { it.tag == Tag.NONCE } == true)
|
||||||
|
return KeystoreErrorCodes.callerNonceProhibited
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
+494
@@ -0,0 +1,494 @@
|
|||||||
|
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
|
||||||
|
import org.matrix.TEESimulator.pki.CertificateHelper
|
||||||
|
|
||||||
|
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>,
|
||||||
|
/** PKCS#8-encoded private key for asymmetric records, empty for symmetric. */
|
||||||
|
val privateKeyBytes: ByteArray,
|
||||||
|
val certChainBytes: List<ByteArray>,
|
||||||
|
/**
|
||||||
|
* Byte-identical KeyMetadata parcel snapshot. Restoring authorizations
|
||||||
|
* directly from these bytes preserves tag count, order, and exact
|
||||||
|
* security-level annotations across reboots — the kind of structural
|
||||||
|
* details apps fingerprint to decide whether the alias is still
|
||||||
|
* "the same key".
|
||||||
|
*/
|
||||||
|
val metadataBytes: ByteArray,
|
||||||
|
/**
|
||||||
|
* Raw secret material for symmetric records (AES, HMAC, 3DES). Empty
|
||||||
|
* for asymmetric. Critical for AndroidX security crypto MasterKey
|
||||||
|
* (AES-GCM-256) — without this every reboot regenerates a fresh AES
|
||||||
|
* key and EncryptedSharedPreferences becomes undecryptable, which is
|
||||||
|
* what banking apps interpret as session expiry and force a relogin.
|
||||||
|
*/
|
||||||
|
val symmetricKeyBytes: ByteArray,
|
||||||
|
val symmetricAlgorithm: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
object GeneratedKeyPersistence {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single source of truth for the on-disk format. Bump this every time
|
||||||
|
* the layout changes; older numbers are silently skipped on read so
|
||||||
|
* stale dev artifacts and pre-fix upstream files can't be partially
|
||||||
|
* rehydrated into broken in-memory state.
|
||||||
|
*
|
||||||
|
* History:
|
||||||
|
* 1 — original upstream layout (no metadata snapshot, no symmetric
|
||||||
|
* block; restored keys lose authorization tags and AES master
|
||||||
|
* keys altogether — apps relying on persisted keystore state
|
||||||
|
* across reboots get logged out)
|
||||||
|
* 2 — transitional dev-only format that added metadata but still
|
||||||
|
* missed the symmetric block; never shipped
|
||||||
|
* 3 — current: byte-identical KeyMetadata snapshot + raw symmetric
|
||||||
|
* key material so AES/HMAC keys survive reboots
|
||||||
|
*/
|
||||||
|
private const val FORMAT_VERSION = 3
|
||||||
|
private val PERSISTENCE_DIR = File(CONFIG_PATH, "persistent_keys")
|
||||||
|
|
||||||
|
// Per-filename locks to prevent concurrent writes to the same key file
|
||||||
|
private val fileLocks = ConcurrentHashMap<String, ReentrantLock>()
|
||||||
|
|
||||||
|
private fun getLockForKey(filename: String): ReentrantLock {
|
||||||
|
return fileLocks.computeIfAbsent(filename) { ReentrantLock() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(
|
||||||
|
keyId: KeyIdentifier,
|
||||||
|
keyPair: KeyPair?,
|
||||||
|
secretKey: javax.crypto.SecretKey?,
|
||||||
|
nspace: Long,
|
||||||
|
securityLevel: Int,
|
||||||
|
certChain: List<Certificate>,
|
||||||
|
algorithm: Int,
|
||||||
|
keySize: Int,
|
||||||
|
ecCurve: Int,
|
||||||
|
purposes: List<Int>,
|
||||||
|
digests: List<Int>,
|
||||||
|
isAttestationKey: Boolean,
|
||||||
|
metadataBytes: ByteArray? = null,
|
||||||
|
) {
|
||||||
|
require(keyPair != null || secretKey != null) {
|
||||||
|
"Either keyPair or secretKey must be provided"
|
||||||
|
}
|
||||||
|
val filename = keyFileName(keyId.uid, keyId.alias)
|
||||||
|
val lock = getLockForKey(filename)
|
||||||
|
SystemLogger.debug("[Persistence] Acquiring lock for $filename")
|
||||||
|
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) }
|
||||||
|
|
||||||
|
// Asymmetric key block (empty for symmetric-only).
|
||||||
|
val pkBytes = keyPair?.private?.encoded ?: ByteArray(0)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata snapshot (always present, may be empty
|
||||||
|
// if the live KeyMetadata could not be marshalled).
|
||||||
|
val mdBytes = metadataBytes ?: ByteArray(0)
|
||||||
|
out.writeInt(mdBytes.size)
|
||||||
|
if (mdBytes.isNotEmpty()) out.write(mdBytes)
|
||||||
|
|
||||||
|
// Symmetric key block (empty for asymmetric keys).
|
||||||
|
if (secretKey != null) {
|
||||||
|
val skBytes = secretKey.encoded
|
||||||
|
out.writeUTF(secretKey.algorithm)
|
||||||
|
out.writeInt(skBytes.size)
|
||||||
|
out.write(skBytes)
|
||||||
|
} else {
|
||||||
|
out.writeUTF("")
|
||||||
|
out.writeInt(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
tmpFile.delete()
|
||||||
|
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) {
|
||||||
|
// Old upstream files (v1) and dev-only intermediate
|
||||||
|
// files (v2) are missing the metadata snapshot
|
||||||
|
// and/or symmetric key block — restoring them
|
||||||
|
// would put broken state in memory (apps relying
|
||||||
|
// on those records get logged out). Skip and let
|
||||||
|
// the next generateKey re-create cleanly with the
|
||||||
|
// new format. Affected apps re-login once after
|
||||||
|
// upgrade, then never again.
|
||||||
|
SystemLogger.info(
|
||||||
|
"Skipping ${file.name}: legacy format version $version. " +
|
||||||
|
"It will be replaced on next generateKey for this alias."
|
||||||
|
)
|
||||||
|
return@runCatching
|
||||||
|
}
|
||||||
|
|
||||||
|
val storedSecLevel = input.readInt()
|
||||||
|
val uid = input.readInt()
|
||||||
|
val alias = input.readUTF()
|
||||||
|
val nspace = input.readLong()
|
||||||
|
val isAttestKey = input.readBoolean()
|
||||||
|
val algo = input.readInt()
|
||||||
|
val kSize = input.readInt()
|
||||||
|
val curve = input.readInt()
|
||||||
|
|
||||||
|
val purposeCount = requireBounds(input.readInt(), 64, "purposeCount")
|
||||||
|
val purposes = (0 until purposeCount).map { input.readInt() }
|
||||||
|
|
||||||
|
val digestCount = requireBounds(input.readInt(), 64, "digestCount")
|
||||||
|
val digests = (0 until digestCount).map { input.readInt() }
|
||||||
|
|
||||||
|
val pkLen = requireBounds(input.readInt(), 8192, "pkLen")
|
||||||
|
val pkBytes = ByteArray(pkLen)
|
||||||
|
if (pkLen > 0) input.readFully(pkBytes)
|
||||||
|
|
||||||
|
val certCount = requireBounds(input.readInt(), 10, "certCount")
|
||||||
|
val certChainBytes = (0 until certCount).map {
|
||||||
|
val certLen = requireBounds(input.readInt(), 65536, "certLen")
|
||||||
|
val certBytes = ByteArray(certLen)
|
||||||
|
input.readFully(certBytes)
|
||||||
|
certBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
val metaLen = requireBounds(input.readInt(), 256 * 1024, "metaLen")
|
||||||
|
val metadataBytes = ByteArray(metaLen).also {
|
||||||
|
if (metaLen > 0) input.readFully(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val skAlgo = input.readUTF()
|
||||||
|
val skLen = requireBounds(input.readInt(), 8192, "skLen")
|
||||||
|
val skBytes = ByteArray(skLen).also {
|
||||||
|
if (skLen > 0) input.readFully(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storedSecLevel == securityLevel) {
|
||||||
|
result.add(
|
||||||
|
PersistedKeyData(
|
||||||
|
uid = uid,
|
||||||
|
alias = alias,
|
||||||
|
nspace = nspace,
|
||||||
|
securityLevel = storedSecLevel,
|
||||||
|
isAttestationKey = isAttestKey,
|
||||||
|
algorithm = algo,
|
||||||
|
keySize = kSize,
|
||||||
|
ecCurve = curve,
|
||||||
|
purposes = purposes,
|
||||||
|
digests = digests,
|
||||||
|
privateKeyBytes = pkBytes,
|
||||||
|
certChainBytes = certChainBytes,
|
||||||
|
metadataBytes = metadataBytes,
|
||||||
|
symmetricKeyBytes = skBytes,
|
||||||
|
symmetricAlgorithm = skAlgo,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onFailure { e ->
|
||||||
|
SystemLogger.warning("Skipping corrupted persisted key file: ${file.name}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemLogger.info("Loaded ${result.size} persisted keys for security level $securityLevel")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-persist updates the cert chain for an already-persisted key without
|
||||||
|
// reconstructing authorization parameters from the response. This avoids
|
||||||
|
// pulling keymint Tag dependencies into this file and is correct because
|
||||||
|
// the only field that changes post-generation is the patched cert chain.
|
||||||
|
fun rePersistIfNeeded(
|
||||||
|
callingUid: Int,
|
||||||
|
generatedKeyInfo: KeyMintSecurityLevelInterceptor.GeneratedKeyInfo,
|
||||||
|
) {
|
||||||
|
val metadata = generatedKeyInfo.response.metadata
|
||||||
|
if (metadata == null) {
|
||||||
|
SystemLogger.debug("rePersist: no metadata, skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val secLevel = metadata.keySecurityLevel
|
||||||
|
|
||||||
|
val entry = KeyMintSecurityLevelInterceptor.generatedKeys.entries.find { (id, info) ->
|
||||||
|
id.uid == callingUid && info.nspace == generatedKeyInfo.nspace
|
||||||
|
}
|
||||||
|
if (entry == null) {
|
||||||
|
SystemLogger.debug("rePersist: key not found in map for uid=$callingUid nspace=${generatedKeyInfo.nspace}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyId = entry.key
|
||||||
|
val filename = keyFileName(keyId.uid, keyId.alias)
|
||||||
|
val existing = File(PERSISTENCE_DIR, filename)
|
||||||
|
|
||||||
|
if (!existing.exists()) {
|
||||||
|
SystemLogger.debug("rePersist: no existing file for $keyId, skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val newChain = CertificateHelper.getCertificateChain(metadata)
|
||||||
|
if (newChain == null) {
|
||||||
|
SystemLogger.warning("rePersist: could not extract cert chain for $keyId")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val persisted = runCatching {
|
||||||
|
DataInputStream(BufferedInputStream(FileInputStream(existing))).use { input ->
|
||||||
|
val version = input.readInt()
|
||||||
|
if (version != FORMAT_VERSION) {
|
||||||
|
SystemLogger.warning("rePersist: legacy format version $version for $keyId, will not re-persist (next generateKey replaces it)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
readPersistedKeyData(input)
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
if (persisted == null) {
|
||||||
|
SystemLogger.warning("rePersist: failed to read existing data for $keyId")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyPair = generatedKeyInfo.keyPair
|
||||||
|
val secretKey = generatedKeyInfo.secretKey
|
||||||
|
if (keyPair == null && secretKey == null) {
|
||||||
|
SystemLogger.warning("rePersist: no key material for $keyId")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Serialize the live KeyMetadata (now contains the user-installed cert
|
||||||
|
// chain via updateSubcomponent) so the next boot restores byte-identical
|
||||||
|
// metadata. KeyMetadata is binder-free, so marshall() is safe here.
|
||||||
|
val metadataBytes = runCatching {
|
||||||
|
android.os.Parcel.obtain().let { parcel ->
|
||||||
|
try {
|
||||||
|
metadata.writeToParcel(parcel, 0)
|
||||||
|
parcel.marshall()
|
||||||
|
} finally {
|
||||||
|
parcel.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
save(
|
||||||
|
keyId = keyId,
|
||||||
|
keyPair = keyPair,
|
||||||
|
secretKey = secretKey,
|
||||||
|
nspace = generatedKeyInfo.nspace,
|
||||||
|
securityLevel = secLevel,
|
||||||
|
certChain = newChain.toList(),
|
||||||
|
algorithm = persisted.algorithm,
|
||||||
|
keySize = persisted.keySize,
|
||||||
|
ecCurve = persisted.ecCurve,
|
||||||
|
purposes = persisted.purposes,
|
||||||
|
digests = persisted.digests,
|
||||||
|
isAttestationKey = persisted.isAttestationKey,
|
||||||
|
metadataBytes = metadataBytes,
|
||||||
|
)
|
||||||
|
SystemLogger.debug("Re-persisted key $keyId with updated cert chain")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corrupted binary files can have arbitrary length fields — cap allocations
|
||||||
|
private fun requireBounds(value: Int, max: Int, name: String): Int {
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads all fields after the version int has already been consumed
|
||||||
|
// and validated by the caller.
|
||||||
|
private fun readPersistedKeyData(input: DataInputStream): PersistedKeyData {
|
||||||
|
val secLevel = input.readInt()
|
||||||
|
val uid = input.readInt()
|
||||||
|
val alias = input.readUTF()
|
||||||
|
val nspace = input.readLong()
|
||||||
|
val isAttestKey = input.readBoolean()
|
||||||
|
val algo = input.readInt()
|
||||||
|
val kSize = input.readInt()
|
||||||
|
val curve = input.readInt()
|
||||||
|
|
||||||
|
val purposeCount = requireBounds(input.readInt(), 64, "purposeCount")
|
||||||
|
val purposes = (0 until purposeCount).map { input.readInt() }
|
||||||
|
|
||||||
|
val digestCount = requireBounds(input.readInt(), 64, "digestCount")
|
||||||
|
val digests = (0 until digestCount).map { input.readInt() }
|
||||||
|
|
||||||
|
val pkLen = requireBounds(input.readInt(), 8192, "pkLen")
|
||||||
|
val pkBytes = ByteArray(pkLen)
|
||||||
|
if (pkLen > 0) input.readFully(pkBytes)
|
||||||
|
|
||||||
|
val certCount = requireBounds(input.readInt(), 10, "certCount")
|
||||||
|
val certChainBytes = (0 until certCount).map {
|
||||||
|
val certLen = requireBounds(input.readInt(), 65536, "certLen")
|
||||||
|
val certBytes = ByteArray(certLen)
|
||||||
|
input.readFully(certBytes)
|
||||||
|
certBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
val metaLen = requireBounds(input.readInt(), 256 * 1024, "metaLen")
|
||||||
|
val metadataBytes = ByteArray(metaLen).also {
|
||||||
|
if (metaLen > 0) input.readFully(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val skAlgo = input.readUTF()
|
||||||
|
val skLen = requireBounds(input.readInt(), 8192, "skLen")
|
||||||
|
val skBytes = ByteArray(skLen).also {
|
||||||
|
if (skLen > 0) input.readFully(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return PersistedKeyData(
|
||||||
|
uid = uid,
|
||||||
|
alias = alias,
|
||||||
|
nspace = nspace,
|
||||||
|
securityLevel = secLevel,
|
||||||
|
isAttestationKey = isAttestKey,
|
||||||
|
algorithm = algo,
|
||||||
|
keySize = kSize,
|
||||||
|
ecCurve = curve,
|
||||||
|
purposes = purposes,
|
||||||
|
digests = digests,
|
||||||
|
privateKeyBytes = pkBytes,
|
||||||
|
certChainBytes = certChainBytes,
|
||||||
|
metadataBytes = metadataBytes,
|
||||||
|
symmetricKeyBytes = skBytes,
|
||||||
|
symmetricAlgorithm = skAlgo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+1331
-130
File diff suppressed because it is too large
Load Diff
+66
@@ -0,0 +1,66 @@
|
|||||||
|
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,
|
||||||
|
private val isAead: Boolean,
|
||||||
|
) : 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 == UPDATE_AAD_TRANSACTION && !isAead) {
|
||||||
|
return InterceptorUtils.createServiceSpecificErrorReply(KeystoreErrorCodes.invalidTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
val INTERCEPTED_CODES =
|
||||||
|
intArrayOf(UPDATE_AAD_TRANSACTION, 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] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+470
@@ -0,0 +1,470 @@
|
|||||||
|
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.ServiceSpecificException
|
||||||
|
import java.util.concurrent.locks.LockSupport
|
||||||
|
import android.system.keystore2.IKeystoreOperation
|
||||||
|
import android.system.keystore2.KeyParameters
|
||||||
|
import java.security.KeyPair
|
||||||
|
import java.security.Signature
|
||||||
|
import java.security.SignatureException
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||||
|
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||||
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
|
||||||
|
private sealed interface CryptoPrimitive {
|
||||||
|
fun updateAad(aadInput: ByteArray?) {
|
||||||
|
throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
|
||||||
|
}
|
||||||
|
fun update(data: ByteArray?): ByteArray?
|
||||||
|
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
|
||||||
|
fun abort()
|
||||||
|
fun getBeginParameters(): Array<KeyParameter>? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
return when (params.algorithm) {
|
||||||
|
Algorithm.EC -> "${digest}withECDSA"
|
||||||
|
Algorithm.RSA -> {
|
||||||
|
val isPss = params.padding.firstOrNull() == PaddingMode.RSA_PSS
|
||||||
|
if (isPss) "${digest}withRSA/PSS" else "${digest}withRSA"
|
||||||
|
}
|
||||||
|
else ->
|
||||||
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.incompatibleAlgorithm,
|
||||||
|
"Unsupported signature algorithm: ${params.algorithm}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
|
||||||
|
val keyAlgo =
|
||||||
|
when (params.algorithm) {
|
||||||
|
Algorithm.RSA -> "RSA"
|
||||||
|
Algorithm.AES -> "AES"
|
||||||
|
else ->
|
||||||
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.incompatibleAlgorithm,
|
||||||
|
"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_PKCS1_1_5_SIGN -> "PKCS1Padding"
|
||||||
|
PaddingMode.RSA_OAEP -> "OAEPPadding"
|
||||||
|
else -> "NoPadding"
|
||||||
|
}
|
||||||
|
return "$keyAlgo/$blockMode/$padding"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
|
||||||
|
private val signature: Signature =
|
||||||
|
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
|
||||||
|
initSign(keyPair.private)
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
|
||||||
|
private val signature: Signature =
|
||||||
|
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
|
||||||
|
initVerify(keyPair.public)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(KeystoreErrorCodes.verificationFailed, "Signature to verify is null")
|
||||||
|
}
|
||||||
|
if (!this.signature.verify(signature)) {
|
||||||
|
throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature verification failed")
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun abort() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CipherPrimitive(
|
||||||
|
cryptoKey: java.security.Key,
|
||||||
|
params: KeyMintAttestation,
|
||||||
|
private val opMode: Int,
|
||||||
|
) : CryptoPrimitive {
|
||||||
|
private val isAead = params.blockMode.firstOrNull() == BlockMode.GCM
|
||||||
|
private val cipher: Cipher =
|
||||||
|
Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply {
|
||||||
|
val nonce = params.nonce
|
||||||
|
if (nonce != null && isAead) {
|
||||||
|
init(opMode, cryptoKey, javax.crypto.spec.GCMParameterSpec(128, nonce))
|
||||||
|
} else if (nonce != null) {
|
||||||
|
init(opMode, cryptoKey, javax.crypto.spec.IvParameterSpec(nonce))
|
||||||
|
} else {
|
||||||
|
init(opMode, cryptoKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateAad(aadInput: ByteArray?) {
|
||||||
|
if (!isAead) throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
|
||||||
|
if (aadInput != null) cipher.updateAAD(aadInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(data: ByteArray?): ByteArray? =
|
||||||
|
if (data != null) cipher.update(data) else null
|
||||||
|
|
||||||
|
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? =
|
||||||
|
if (data != null) cipher.doFinal(data) else cipher.doFinal()
|
||||||
|
|
||||||
|
override fun getBeginParameters(): Array<KeyParameter>? {
|
||||||
|
val iv = cipher.iv ?: return null
|
||||||
|
return arrayOf(
|
||||||
|
KeyParameter().apply {
|
||||||
|
tag = Tag.NONCE
|
||||||
|
value = KeyParameterValue.blob(iv)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun abort() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive {
|
||||||
|
private val agreement: javax.crypto.KeyAgreement =
|
||||||
|
javax.crypto.KeyAgreement.getInstance("ECDH").apply { init(keyPair.private) }
|
||||||
|
|
||||||
|
override fun update(data: ByteArray?): ByteArray? = null
|
||||||
|
|
||||||
|
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
|
if (data == null)
|
||||||
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.invalidArgument,
|
||||||
|
"Peer public key required for key agreement",
|
||||||
|
)
|
||||||
|
val peerKey =
|
||||||
|
java.security.KeyFactory.getInstance("EC")
|
||||||
|
.generatePublic(java.security.spec.X509EncodedKeySpec(data))
|
||||||
|
agreement.doPhase(peerKey, true)
|
||||||
|
return agreement.generateSecret()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun abort() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SoftwareOperation(
|
||||||
|
private val txId: Long,
|
||||||
|
keyPair: KeyPair?,
|
||||||
|
secretKey: javax.crypto.SecretKey?,
|
||||||
|
params: KeyMintAttestation,
|
||||||
|
private val latencyFloorMs: Long = 0L,
|
||||||
|
) {
|
||||||
|
private val primitive: CryptoPrimitive
|
||||||
|
@Volatile var finalized = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
var onFinishCallback: (() -> Unit)? = null
|
||||||
|
|
||||||
|
val beginParameters: KeyParameters?
|
||||||
|
get() {
|
||||||
|
val params = primitive.getBeginParameters() ?: return null
|
||||||
|
if (params.isEmpty()) return null
|
||||||
|
return KeyParameters().apply { keyParameter = params }
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val purpose = params.purpose.firstOrNull()
|
||||||
|
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
|
||||||
|
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
|
||||||
|
|
||||||
|
if (purpose == null) {
|
||||||
|
// Defensive: if params somehow restored without a PURPOSE tag
|
||||||
|
// (corrupt v2 metadata, mismatched authorizations array on load,
|
||||||
|
// or future format drift) the original code crashed with NPE
|
||||||
|
// because Signer/Verifier/Cipher all dereference keyPair!!
|
||||||
|
// before checking purpose. Surface a clean keystore error
|
||||||
|
// instead so callers see a normal-looking operation failure
|
||||||
|
// they can recover from rather than the process appearing to
|
||||||
|
// silently corrupt their session.
|
||||||
|
SystemLogger.warning(
|
||||||
|
"[SoftwareOp TX_ID: $txId] Purpose missing on restored key " +
|
||||||
|
"(authorizations=${params.purpose}, keyPair=${if (keyPair != null) "present" else "null"}, " +
|
||||||
|
"secretKey=${if (secretKey != null) "present" else "null"}). " +
|
||||||
|
"Returning unsupportedPurpose."
|
||||||
|
)
|
||||||
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.unsupportedPurpose,
|
||||||
|
"Restored key has no PURPOSE authorization",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
primitive =
|
||||||
|
when (purpose) {
|
||||||
|
KeyPurpose.SIGN -> {
|
||||||
|
val kp = keyPair ?: throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.invalidArgument,
|
||||||
|
"[SoftwareOp TX_ID: $txId] SIGN requested but keyPair is null",
|
||||||
|
)
|
||||||
|
Signer(kp, params)
|
||||||
|
}
|
||||||
|
KeyPurpose.VERIFY -> {
|
||||||
|
val kp = keyPair ?: throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.invalidArgument,
|
||||||
|
"[SoftwareOp TX_ID: $txId] VERIFY requested but keyPair is null",
|
||||||
|
)
|
||||||
|
Verifier(kp, params)
|
||||||
|
}
|
||||||
|
KeyPurpose.ENCRYPT -> {
|
||||||
|
val key: java.security.Key = secretKey ?: keyPair?.public
|
||||||
|
?: throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.unsupportedPurpose,
|
||||||
|
"[SoftwareOp TX_ID: $txId] ENCRYPT requires either secretKey or keyPair.public",
|
||||||
|
)
|
||||||
|
CipherPrimitive(key, params, Cipher.ENCRYPT_MODE)
|
||||||
|
}
|
||||||
|
KeyPurpose.DECRYPT -> {
|
||||||
|
val key: java.security.Key = secretKey ?: keyPair?.private
|
||||||
|
?: throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.unsupportedPurpose,
|
||||||
|
"[SoftwareOp TX_ID: $txId] DECRYPT requires either secretKey or keyPair.private",
|
||||||
|
)
|
||||||
|
CipherPrimitive(key, params, Cipher.DECRYPT_MODE)
|
||||||
|
}
|
||||||
|
KeyPurpose.AGREE_KEY -> {
|
||||||
|
val kp = keyPair ?: throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.invalidArgument,
|
||||||
|
"[SoftwareOp TX_ID: $txId] AGREE_KEY requested but keyPair is null",
|
||||||
|
)
|
||||||
|
KeyAgreementPrimitive(kp)
|
||||||
|
}
|
||||||
|
else ->
|
||||||
|
throw ServiceSpecificException(
|
||||||
|
KeystoreErrorCodes.unsupportedPurpose,
|
||||||
|
"Unsupported operation purpose: $purpose",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkActive() {
|
||||||
|
if (finalized) {
|
||||||
|
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)")
|
||||||
|
throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkInputLength(data: ByteArray?) {
|
||||||
|
if (data != null && data.size > MAX_RECEIVE_DATA) {
|
||||||
|
SystemLogger.info("[SoftwareOp TX_ID: $txId] Input too large: ${data.size} > $MAX_RECEIVE_DATA, throwing TOO_MUCH_DATA(${KeystoreErrorCodes.tooMuchData})")
|
||||||
|
throw ServiceSpecificException(KeystoreErrorCodes.tooMuchData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateAad(aadInput: ByteArray?) {
|
||||||
|
SystemLogger.info("[SoftwareOp TX_ID: $txId] updateAad() ENTRY inputSize=${aadInput?.size ?: 0} primitive=${primitive::class.simpleName}")
|
||||||
|
checkActive()
|
||||||
|
checkInputLength(aadInput)
|
||||||
|
try {
|
||||||
|
primitive.updateAad(aadInput)
|
||||||
|
SystemLogger.info("[SoftwareOp TX_ID: $txId] updateAad() RETURNED_NORMALLY (unexpected for non-AEAD)")
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
val top = throwable.stackTrace.firstOrNull()?.toString() ?: "<no-frame>"
|
||||||
|
val code = (throwable as? ServiceSpecificException)?.errorCode
|
||||||
|
SystemLogger.info("[SoftwareOp TX_ID: $txId] updateAad() THREW class=${throwable::class.java.name} code=$code msg=${throwable.message} top=$top")
|
||||||
|
throw throwable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(data: ByteArray?): ByteArray? {
|
||||||
|
SystemLogger.debug("[SoftwareOp TX_ID: $txId] update() inputSize=${data?.size ?: 0}")
|
||||||
|
checkActive()
|
||||||
|
checkInputLength(data)
|
||||||
|
try {
|
||||||
|
return primitive.update(data)
|
||||||
|
} catch (e: ServiceSpecificException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
|
||||||
|
throw mapToServiceSpecificException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
|
checkActive()
|
||||||
|
checkInputLength(data)
|
||||||
|
try {
|
||||||
|
val startNs = if (latencyFloorMs > 0) System.nanoTime() else 0L
|
||||||
|
val result = primitive.finish(data, signature)
|
||||||
|
if (latencyFloorMs > 0) {
|
||||||
|
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
|
||||||
|
val delayMs = latencyFloorMs - elapsedMs
|
||||||
|
if (delayMs > 0) LockSupport.parkNanos(delayMs * 1_000_000)
|
||||||
|
}
|
||||||
|
finalized = true
|
||||||
|
onFinishCallback?.invoke()
|
||||||
|
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
|
||||||
|
return result
|
||||||
|
} catch (e: ServiceSpecificException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
|
||||||
|
throw mapToServiceSpecificException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun abort() {
|
||||||
|
finalized = true
|
||||||
|
primitive.abort()
|
||||||
|
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapToServiceSpecificException(e: Exception): ServiceSpecificException = when (e) {
|
||||||
|
is SignatureException -> ServiceSpecificException(KeystoreErrorCodes.verificationFailed, e.message)
|
||||||
|
is javax.crypto.BadPaddingException -> ServiceSpecificException(KeystoreErrorCodes.invalidArgument, e.message)
|
||||||
|
is javax.crypto.IllegalBlockSizeException -> ServiceSpecificException(KeystoreErrorCodes.invalidInputLength, e.message)
|
||||||
|
is java.security.InvalidKeyException -> ServiceSpecificException(KeystoreErrorCodes.incompatibleKey, e.message)
|
||||||
|
else -> ServiceSpecificException(KeystoreErrorCodes.unknownError, e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAX_RECEIVE_DATA = 0x8000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object KeystoreErrorCodes {
|
||||||
|
val tooMuchData: Int by lazy {
|
||||||
|
resolveField("android.system.keystore2.ResponseCode", "TOO_MUCH_DATA", 21)
|
||||||
|
}
|
||||||
|
|
||||||
|
val invalidOperationHandle: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_OPERATION_HANDLE", -28)
|
||||||
|
}
|
||||||
|
|
||||||
|
val invalidTag: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_TAG", -76)
|
||||||
|
}
|
||||||
|
|
||||||
|
val verificationFailed: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "VERIFICATION_FAILED", -30)
|
||||||
|
}
|
||||||
|
|
||||||
|
val invalidArgument: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_ARGUMENT", -38)
|
||||||
|
}
|
||||||
|
|
||||||
|
val invalidInputLength: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_INPUT_LENGTH", -21)
|
||||||
|
}
|
||||||
|
|
||||||
|
val incompatibleKey: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_KEY", -31)
|
||||||
|
}
|
||||||
|
|
||||||
|
val incompatiblePurpose: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_PURPOSE", -13)
|
||||||
|
}
|
||||||
|
|
||||||
|
val unsupportedPurpose: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "UNSUPPORTED_PURPOSE", -14)
|
||||||
|
}
|
||||||
|
|
||||||
|
val incompatibleAlgorithm: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_ALGORITHM", -18)
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyNotYetValid: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "KEY_NOT_YET_VALID", -39)
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyExpired: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "KEY_EXPIRED", -40)
|
||||||
|
}
|
||||||
|
|
||||||
|
val callerNonceProhibited: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "CALLER_NONCE_PROHIBITED", -55)
|
||||||
|
}
|
||||||
|
|
||||||
|
val unknownError: Int by lazy {
|
||||||
|
resolveField("android.hardware.security.keymint.ErrorCode", "UNKNOWN_ERROR", -1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resolveField(className: String, fieldName: String, fallback: Int): Int =
|
||||||
|
runCatching {
|
||||||
|
Class.forName(className).getField(fieldName).getInt(null)
|
||||||
|
}.getOrElse {
|
||||||
|
SystemLogger.debug("Resolved $className.$fieldName via fallback: $fallback")
|
||||||
|
fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
||||||
|
IKeystoreOperation.Stub() {
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun updateAad(aadInput: ByteArray?) {
|
||||||
|
SystemLogger.info("[SoftwareOpBinder] updateAad() ENTRY callingUid=${android.os.Binder.getCallingUid()} size=${aadInput?.size ?: 0}")
|
||||||
|
try {
|
||||||
|
operation.updateAad(aadInput)
|
||||||
|
SystemLogger.info("[SoftwareOpBinder] updateAad() RETURNED_NORMALLY")
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
val code = (throwable as? ServiceSpecificException)?.errorCode
|
||||||
|
SystemLogger.info("[SoftwareOpBinder] updateAad() PROPAGATING class=${throwable::class.java.name} code=$code msg=${throwable.message}")
|
||||||
|
throw throwable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun update(input: ByteArray?): ByteArray? {
|
||||||
|
return operation.update(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
|
return operation.finish(input, signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun abort() {
|
||||||
|
operation.abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,23 @@ 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 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,7 +80,9 @@ 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.BLOCK_MODE -> blockModeNames[value.blockMode]
|
||||||
Tag.EC_CURVE -> ecCurveNames[value.ecCurve]
|
Tag.EC_CURVE -> ecCurveNames[value.ecCurve]
|
||||||
|
Tag.PADDING -> paddingNames[value.paddingMode]
|
||||||
Tag.PURPOSE -> purposeNames[value.keyPurpose]
|
Tag.PURPOSE -> purposeNames[value.keyPurpose]
|
||||||
Tag.DIGEST -> digestNames[value.digest]
|
Tag.DIGEST -> digestNames[value.digest]
|
||||||
Tag.AUTH_TIMEOUT,
|
Tag.AUTH_TIMEOUT,
|
||||||
|
|||||||
@@ -1,41 +1,86 @@
|
|||||||
package org.matrix.TEESimulator.logging
|
package org.matrix.TEESimulator.logging
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
import org.matrix.TEESimulator.BuildConfig
|
import org.matrix.TEESimulator.BuildConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A centralized logging utility for the TEESimulator application. This object provides a consistent
|
* A centralized logging utility for the TEESimulator application. This object provides a consistent
|
||||||
* logging tag and format for all application logs, making it easier to filter and debug in Logcat.
|
* logging tag and format for all application logs, making it easier to filter and debug in Logcat.
|
||||||
|
*
|
||||||
|
* Includes a rate limiter that caps logd syscalls during binder stress to prevent thread pool
|
||||||
|
* contention. The first [RATE_LIMIT_BURST] messages per [RATE_LIMIT_WINDOW_MS] window are logged
|
||||||
|
* normally; subsequent messages are suppressed and a summary is emitted when the window resets.
|
||||||
*/
|
*/
|
||||||
object SystemLogger {
|
object SystemLogger {
|
||||||
// The tag used for all log messages from this application.
|
@PublishedApi internal const val TAG = "TEESimulator"
|
||||||
private const val TAG = "TEESimulator"
|
|
||||||
|
|
||||||
private val isDebugBuild = BuildConfig.DEBUG
|
@PublishedApi internal val isDebugBuild = BuildConfig.DEBUG
|
||||||
|
|
||||||
|
// Rate limiter: allow BURST messages per WINDOW, then suppress until window resets.
|
||||||
|
private const val RATE_LIMIT_BURST = 15
|
||||||
|
private const val RATE_LIMIT_WINDOW_MS = 1000L
|
||||||
|
private val windowStart = AtomicLong(System.currentTimeMillis())
|
||||||
|
private val windowCount = AtomicInteger(0)
|
||||||
|
private val suppressedCount = AtomicInteger(0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this message should be emitted. Resets the window if expired
|
||||||
|
* and emits a suppression summary for the previous window.
|
||||||
|
*/
|
||||||
|
@PublishedApi internal fun acquireLogPermit(): Boolean {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val start = windowStart.get()
|
||||||
|
if (now - start > RATE_LIMIT_WINDOW_MS) {
|
||||||
|
// Window expired: reset and emit suppression summary if needed.
|
||||||
|
if (windowStart.compareAndSet(start, now)) {
|
||||||
|
val suppressed = suppressedCount.getAndSet(0)
|
||||||
|
windowCount.set(1) // this call counts as #1 in the new window
|
||||||
|
if (suppressed > 0) {
|
||||||
|
Log.i(TAG, "[rate-limit] suppressed $suppressed log messages in previous window")
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val count = windowCount.incrementAndGet()
|
||||||
|
if (count <= RATE_LIMIT_BURST) return true
|
||||||
|
suppressedCount.incrementAndGet()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs a debug message. Use this for fine-grained information that is useful for debugging.
|
* Logs a debug message. Use this for fine-grained information that is useful for debugging.
|
||||||
*
|
|
||||||
* @param message The message to log.
|
|
||||||
*/
|
*/
|
||||||
fun debug(message: String) {
|
fun debug(message: String) {
|
||||||
|
if (!isDebugBuild) return
|
||||||
|
if (!acquireLogPermit()) return
|
||||||
Log.d(TAG, message)
|
Log.d(TAG, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Lazy debug: lambda only evaluates if message will be logged. */
|
||||||
|
inline fun debug(message: () -> String) {
|
||||||
|
if (!isDebugBuild) return
|
||||||
|
if (!acquireLogPermit()) return
|
||||||
|
Log.d(TAG, message())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs an informational message. Use this to report major application lifecycle events.
|
* Logs an informational message. Use this to report major application lifecycle events.
|
||||||
*
|
|
||||||
* @param message The message to log.
|
|
||||||
*/
|
*/
|
||||||
fun info(message: String) {
|
fun info(message: String) {
|
||||||
|
if (!acquireLogPermit()) return
|
||||||
Log.i(TAG, message)
|
Log.i(TAG, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Lazy info: lambda only evaluates if message will be logged. */
|
||||||
|
inline fun info(message: () -> String) {
|
||||||
|
if (!acquireLogPermit()) return
|
||||||
|
Log.i(TAG, message())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs a warning message. Use this to report unexpected but non-fatal issues.
|
* Logs a warning message. Warnings are never rate-limited.
|
||||||
*
|
|
||||||
* @param message The message to log.
|
|
||||||
* @param throwable An optional exception to log with the message.
|
|
||||||
*/
|
*/
|
||||||
fun warning(message: String, throwable: Throwable? = null) {
|
fun warning(message: String, throwable: Throwable? = null) {
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
@@ -46,11 +91,7 @@ object SystemLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs an error message. Use this to report fatal errors or exceptions that disrupt
|
* Logs an error message. Errors are never rate-limited.
|
||||||
* functionality.
|
|
||||||
*
|
|
||||||
* @param message The message to log.
|
|
||||||
* @param throwable An optional exception to log with the message.
|
|
||||||
*/
|
*/
|
||||||
fun error(message: String, throwable: Throwable? = null) {
|
fun error(message: String, throwable: Throwable? = null) {
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
@@ -63,11 +104,22 @@ object SystemLogger {
|
|||||||
/**
|
/**
|
||||||
* Logs a verbose message. This level is for highly detailed logs that are generally not needed
|
* Logs a verbose message. This level is for highly detailed logs that are generally not needed
|
||||||
* unless tracking a very specific issue.
|
* unless tracking a very specific issue.
|
||||||
*
|
|
||||||
* @param message The message to log.
|
|
||||||
*/
|
*/
|
||||||
fun verbose(message: String) {
|
fun verbose(message: String) {
|
||||||
if (!isDebugBuild) return
|
if (!isDebugBuild) return
|
||||||
|
if (!acquireLogPermit()) return
|
||||||
Log.v(TAG, message)
|
Log.v(TAG, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Lazy verbose: lambda only evaluates if message will be logged. */
|
||||||
|
inline fun verbose(message: () -> String) {
|
||||||
|
if (!isDebugBuild) return
|
||||||
|
if (!acquireLogPermit()) return
|
||||||
|
Log.v(TAG, message())
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun trace(message: () -> String) {
|
||||||
|
if (!isDebugBuild) return
|
||||||
|
Log.w(TAG, message())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +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.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
|
||||||
@@ -20,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
|
||||||
@@ -34,6 +35,8 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
|||||||
*/
|
*/
|
||||||
object CertificateGenerator {
|
object CertificateGenerator {
|
||||||
|
|
||||||
|
private const val UNDEFINED_NOT_AFTER = 253402300799000L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a software-based cryptographic key pair.
|
* Generates a software-based cryptographic key pair.
|
||||||
*
|
*
|
||||||
@@ -47,7 +50,10 @@ object CertificateGenerator {
|
|||||||
Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName)
|
Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName)
|
||||||
Algorithm.RSA ->
|
Algorithm.RSA ->
|
||||||
"RSA" to
|
"RSA" to
|
||||||
RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent)
|
RSAKeyGenParameterSpec(
|
||||||
|
params.keySize,
|
||||||
|
params.rsaPublicExponent ?: RSAKeyGenParameterSpec.F4,
|
||||||
|
)
|
||||||
else ->
|
else ->
|
||||||
throw IllegalArgumentException(
|
throw IllegalArgumentException(
|
||||||
"Unsupported algorithm: ${params.algorithm}"
|
"Unsupported algorithm: ${params.algorithm}"
|
||||||
@@ -80,33 +86,44 @@ object CertificateGenerator {
|
|||||||
params: KeyMintAttestation,
|
params: KeyMintAttestation,
|
||||||
securityLevel: Int,
|
securityLevel: Int,
|
||||||
): List<Certificate>? {
|
): List<Certificate>? {
|
||||||
return runCatching {
|
val challenge = params.attestationChallenge
|
||||||
|
if (challenge != null && challenge.size > AttestationConstants.CHALLENGE_LENGTH_LIMIT)
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})"
|
||||||
|
)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
// AOSP ta/src/keys.rs:451-478: no challenge + no attestKey = self-signed, depth 1
|
||||||
|
if (challenge == null && attestKeyAlias == null) {
|
||||||
|
SystemLogger.trace { "[certgen] no-challenge key: self-signed, depth=1, purposes=${params.purpose}" }
|
||||||
|
return listOf(buildSelfSignedCertificate(subjectKeyPair, params))
|
||||||
|
}
|
||||||
|
|
||||||
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
|
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
|
||||||
|
|
||||||
// Determine the signing key and issuer. If an attestKey is provided, use it.
|
val attestKeyInfo =
|
||||||
// Otherwise, fall back to the root key from the keybox.
|
|
||||||
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)
|
||||||
?: (keybox.keyPair to getIssuerFromKeybox(keybox))
|
} else null
|
||||||
} else {
|
|
||||||
keybox.keyPair to getIssuerFromKeybox(keybox)
|
val (signingKey, issuer) = attestKeyInfo
|
||||||
}
|
?.let { it.first to it.second }
|
||||||
|
?: (keybox.keyPair to getIssuerFromKeybox(keybox))
|
||||||
|
|
||||||
// Build the new leaf certificate with the simulated attestation.
|
|
||||||
val leafCert =
|
val leafCert =
|
||||||
buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel)
|
buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel)
|
||||||
|
|
||||||
// If not self-attesting, the chain is just the leaf. Otherwise, append the keybox
|
if (attestKeyInfo != null) {
|
||||||
// chain.
|
|
||||||
if (attestKeyAlias != null) {
|
|
||||||
listOf(leafCert)
|
listOf(leafCert)
|
||||||
} else {
|
} else {
|
||||||
listOf(leafCert) + keybox.certificates
|
listOf(leafCert) + keybox.certificates
|
||||||
}
|
}
|
||||||
|
} catch (e: android.os.ServiceSpecificException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("Failed to generate certificate chain.", e)
|
||||||
|
null
|
||||||
}
|
}
|
||||||
.onFailure { SystemLogger.error("Failed to generate certificate chain.", it) }
|
|
||||||
.getOrNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -120,7 +137,7 @@ object CertificateGenerator {
|
|||||||
params: KeyMintAttestation,
|
params: KeyMintAttestation,
|
||||||
securityLevel: Int,
|
securityLevel: Int,
|
||||||
): Pair<KeyPair, List<Certificate>>? {
|
): Pair<KeyPair, List<Certificate>>? {
|
||||||
return runCatching {
|
return try {
|
||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"Generating new attested key pair for alias: '$alias' (UID: $uid)"
|
"Generating new attested key pair for alias: '$alias' (UID: $uid)"
|
||||||
)
|
)
|
||||||
@@ -136,11 +153,12 @@ object CertificateGenerator {
|
|||||||
"Successfully generated new certificate chain for alias: '$alias'."
|
"Successfully generated new certificate chain for alias: '$alias'."
|
||||||
)
|
)
|
||||||
Pair(newKeyPair, chain)
|
Pair(newKeyPair, chain)
|
||||||
|
} catch (e: android.os.ServiceSpecificException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", e)
|
||||||
|
null
|
||||||
}
|
}
|
||||||
.onFailure {
|
|
||||||
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", it)
|
|
||||||
}
|
|
||||||
.getOrNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getIssuerFromKeybox(keybox: KeyBox) =
|
fun getIssuerFromKeybox(keybox: KeyBox) =
|
||||||
@@ -155,7 +173,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. */
|
||||||
@@ -180,6 +201,22 @@ 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,
|
||||||
@@ -189,33 +226,36 @@ object CertificateGenerator {
|
|||||||
uid: Int,
|
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 =
|
val notBefore = params.certificateNotBefore ?: Date(0)
|
||||||
(signingKeyPair.public as? X509Certificate)?.notAfter
|
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
|
||||||
?: Date(System.currentTimeMillis() + 31536000000L)
|
|
||||||
|
|
||||||
val builder =
|
val builder =
|
||||||
JcaX509v3CertificateBuilder(
|
JcaX509v3CertificateBuilder(
|
||||||
issuer,
|
issuer,
|
||||||
params.certificateSerial ?: BigInteger.ONE,
|
params.certificateSerial ?: BigInteger.ONE,
|
||||||
params.certificateNotBefore ?: Date(),
|
notBefore,
|
||||||
params.certificateNotAfter ?: leafNotAfter,
|
notAfter,
|
||||||
subject,
|
subject,
|
||||||
subjectKeyPair.public,
|
subjectKeyPair.public,
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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)
|
||||||
// Add our custom, simulated attestation extension.
|
if (keyUsageBits != 0) {
|
||||||
builder.addExtension(
|
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
|
||||||
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
|
}
|
||||||
)
|
if (params.attestationChallenge != null) {
|
||||||
|
builder.addExtension(
|
||||||
|
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val signerAlgorithm =
|
val signerAlgorithm =
|
||||||
when (params.algorithm) {
|
when (signingKeyPair.private.algorithm) {
|
||||||
Algorithm.EC -> "SHA256withECDSA"
|
"EC", "ECDSA" -> "SHA256withECDSA"
|
||||||
Algorithm.RSA -> "SHA256withRSA"
|
"RSA" -> "SHA256withRSA"
|
||||||
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}")
|
else -> throw IllegalArgumentException("Unsupported signing key: ${signingKeyPair.private.algorithm}")
|
||||||
}
|
}
|
||||||
val contentSigner =
|
val contentSigner =
|
||||||
JcaContentSignerBuilder(signerAlgorithm)
|
JcaContentSignerBuilder(signerAlgorithm)
|
||||||
@@ -224,4 +264,39 @@ object CertificateGenerator {
|
|||||||
|
|
||||||
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AOSP ta/src/keys.rs:452-478, ta/src/cert.rs:111-114
|
||||||
|
private fun buildSelfSignedCertificate(
|
||||||
|
keyPair: KeyPair,
|
||||||
|
params: KeyMintAttestation,
|
||||||
|
): Certificate {
|
||||||
|
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
|
||||||
|
val notBefore = params.certificateNotBefore ?: Date(0)
|
||||||
|
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
|
||||||
|
|
||||||
|
val builder = JcaX509v3CertificateBuilder(
|
||||||
|
subject,
|
||||||
|
params.certificateSerial ?: BigInteger.ONE,
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
subject,
|
||||||
|
keyPair.public,
|
||||||
|
)
|
||||||
|
|
||||||
|
val keyUsageBits = buildKeyUsageFromPurposes(params.purpose)
|
||||||
|
if (keyUsageBits != 0) {
|
||||||
|
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
|
||||||
|
}
|
||||||
|
|
||||||
|
val signerAlgorithm = when (keyPair.private.algorithm) {
|
||||||
|
"EC", "ECDSA" -> "SHA256withECDSA"
|
||||||
|
"RSA" -> "SHA256withRSA"
|
||||||
|
else -> throw IllegalArgumentException("Unsupported key: ${keyPair.private.algorithm}")
|
||||||
|
}
|
||||||
|
val contentSigner = JcaContentSignerBuilder(signerAlgorithm)
|
||||||
|
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
.build(keyPair.private)
|
||||||
|
|
||||||
|
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,9 +1,10 @@
|
|||||||
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.time.LocalDate
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
@@ -11,7 +12,6 @@ import org.bouncycastle.asn1.ASN1EncodableVector
|
|||||||
import org.bouncycastle.asn1.ASN1Integer
|
import org.bouncycastle.asn1.ASN1Integer
|
||||||
import org.bouncycastle.asn1.DEROctetString
|
import org.bouncycastle.asn1.DEROctetString
|
||||||
import org.bouncycastle.asn1.DERSequence
|
import org.bouncycastle.asn1.DERSequence
|
||||||
import org.bouncycastle.asn1.DERSet
|
|
||||||
import org.matrix.TEESimulator.attestation.DeviceAttestationService
|
import org.matrix.TEESimulator.attestation.DeviceAttestationService
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
@@ -90,27 +90,33 @@ object AndroidDeviceUtils {
|
|||||||
attestationValueProvider: () -> ByteArray?,
|
attestationValueProvider: () -> ByteArray?,
|
||||||
expectedSize: Int,
|
expectedSize: Int,
|
||||||
): ByteArray {
|
): ByteArray {
|
||||||
// 1. Attempt to get the value from the system property.
|
|
||||||
getProperty(propertyName, expectedSize)?.let {
|
getProperty(propertyName, expectedSize)?.let {
|
||||||
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
|
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
|
||||||
|
persistToFile(propertyName, it)
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fallback to the value from a cached TEE attestation.
|
|
||||||
try {
|
try {
|
||||||
attestationValueProvider()?.let {
|
attestationValueProvider()?.let {
|
||||||
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
|
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
|
||||||
setProperty(propertyName, it) // Persist for consistency
|
setProperty(propertyName, it)
|
||||||
|
persistToFile(propertyName, it)
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("Failed to get $propertyName from attestation.", e)
|
SystemLogger.error("Failed to get $propertyName from attestation.", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. As a final fallback, generate a random value.
|
readFromFile(propertyName, expectedSize)?.let {
|
||||||
|
SystemLogger.debug("Using $propertyName from persistent file: ${it.toHex()}")
|
||||||
|
setProperty(propertyName, it)
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
return generateRandomBytes(expectedSize).also {
|
return generateRandomBytes(expectedSize).also {
|
||||||
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
|
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
|
||||||
setProperty(propertyName, it)
|
setProperty(propertyName, it)
|
||||||
|
persistToFile(propertyName, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,10 +163,55 @@ object AndroidDeviceUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Generates a cryptographically random byte array of a specified length. */
|
internal fun setProperty(name: String, value: String) {
|
||||||
|
try {
|
||||||
|
SystemLogger.debug("Setting system property '$name' to: $value")
|
||||||
|
val command = arrayOf("resetprop", name, value)
|
||||||
|
val process = Runtime.getRuntime().exec(command)
|
||||||
|
val exitCode = process.waitFor()
|
||||||
|
|
||||||
|
if (exitCode != 0) {
|
||||||
|
val errorOutput = process.errorStream.bufferedReader().readText()
|
||||||
|
SystemLogger.error(
|
||||||
|
"resetprop for '$name' failed with exit code $exitCode: $errorOutput"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("Failed to set '$name' property via resetprop.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun generateRandomBytes(size: Int): ByteArray =
|
private fun generateRandomBytes(size: Int): ByteArray =
|
||||||
ByteArray(size).also { ThreadLocalRandom.current().nextBytes(it) }
|
ByteArray(size).also { ThreadLocalRandom.current().nextBytes(it) }
|
||||||
|
|
||||||
|
private val PERSIST_DIR = File("/data/adb/tricky_store")
|
||||||
|
|
||||||
|
private fun fileForProperty(propertyName: String): File = when (propertyName) {
|
||||||
|
"ro.boot.vbmeta.digest" -> File(PERSIST_DIR, "boot_hash.bin")
|
||||||
|
"ro.boot.vbmeta.public_key_digest" -> File(PERSIST_DIR, "boot_key.bin")
|
||||||
|
else -> File(PERSIST_DIR, "${propertyName.replace('.', '_')}.bin")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun persistToFile(propertyName: String, bytes: ByteArray) {
|
||||||
|
try {
|
||||||
|
fileForProperty(propertyName).writeBytes(bytes)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("Failed to persist $propertyName to file.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readFromFile(propertyName: String, expectedSize: Int): ByteArray? {
|
||||||
|
return try {
|
||||||
|
val file = fileForProperty(propertyName)
|
||||||
|
if (!file.exists()) return null
|
||||||
|
val bytes = file.readBytes()
|
||||||
|
if (bytes.size == expectedSize) bytes else null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("Failed to read $propertyName from file.", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Patch Level Properties ---
|
// --- Patch Level Properties ---
|
||||||
|
|
||||||
fun getPatchLevel(uid: Int): Int {
|
fun getPatchLevel(uid: Int): Int {
|
||||||
@@ -239,11 +290,12 @@ object AndroidDeviceUtils {
|
|||||||
val resolvedValue = resolveDateKeywords(value)
|
val resolvedValue = resolveDateKeywords(value)
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
// "device_default" indicates falling back to the system property.
|
|
||||||
resolvedValue.equals("device_default", ignoreCase = true) -> null
|
resolvedValue.equals("device_default", ignoreCase = true) -> null
|
||||||
// "no" indicates this value should not be reported.
|
// Resolve from live system prop — matches what detectors see via getprop,
|
||||||
|
// even when PIF has spoofed ro.build.version.security_patch via resetprop
|
||||||
|
resolvedValue.equals("prop", ignoreCase = true) ->
|
||||||
|
parsePatchLevelValue(SystemProperties.get("ro.build.version.security_patch", ""), isLong)
|
||||||
resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
|
resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
|
||||||
// Otherwise, parse the resolved date string.
|
|
||||||
else -> parsePatchLevelValue(resolvedValue, isLong)
|
else -> parsePatchLevelValue(resolvedValue, isLong)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,7 +345,9 @@ object AndroidDeviceUtils {
|
|||||||
6 -> { // YYYYMM
|
6 -> { // YYYYMM
|
||||||
val year = normalized.substring(0, 4).toInt()
|
val year = normalized.substring(0, 4).toInt()
|
||||||
val month = normalized.substring(4, 6).toInt()
|
val month = normalized.substring(4, 6).toInt()
|
||||||
if (isLong) year * 10000 + month * 100 + 1 else year * 100 + month
|
// Synthesizing day=01 from YYYY-MM disagrees with real device bulletins;
|
||||||
|
// propagate null so callers fall back to a YYYY-MM-DD source.
|
||||||
|
if (isLong) null else year * 100 + month
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@@ -341,20 +395,26 @@ object AndroidDeviceUtils {
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the attestation version based on security level and OS version. StrongBox (level 2)
|
* Retrieves the attestation version for the given security level. The value follows the device
|
||||||
* requires version 300.
|
* OS: cached attestation data wins, then attestVersionMap[SDK_INT], then 400 as last resort.
|
||||||
|
* A static StrongBox=300 floor would force a major-version mismatch with the TEE chain on
|
||||||
|
* Android 16 devices that report keymaster 400 across both security levels.
|
||||||
*
|
*
|
||||||
* @param securityLevel The security level of the attestation (1 for TEE, 2 for StrongBox).
|
* @param securityLevel The security level of the attestation (1 for TEE, 2 for StrongBox).
|
||||||
* @return The appropriate attestation version number.
|
* @return The appropriate attestation version number.
|
||||||
*/
|
*/
|
||||||
fun getAttestVersion(securityLevel: Int): Int {
|
fun getAttestVersion(securityLevel: Int): Int {
|
||||||
// StrongBox security level requires an attestation version of at least 300.
|
val cached = DeviceAttestationService.CachedAttestationData?.attestVersion
|
||||||
if (securityLevel == SecurityLevel.STRONGBOX) {
|
val version = cached
|
||||||
return 300
|
|
||||||
}
|
|
||||||
return DeviceAttestationService.CachedAttestationData?.attestVersion
|
|
||||||
?: attestVersionMap[Build.VERSION.SDK_INT]
|
?: attestVersionMap[Build.VERSION.SDK_INT]
|
||||||
?: 400 // Default to a recent version
|
?: 400 // Default to a recent version
|
||||||
|
val source = when {
|
||||||
|
cached != null -> "cache"
|
||||||
|
attestVersionMap.containsKey(Build.VERSION.SDK_INT) -> "map"
|
||||||
|
else -> "default"
|
||||||
|
}
|
||||||
|
SystemLogger.debug("attestVersion=$version source=$source securityLevel=$securityLevel")
|
||||||
|
return version
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -363,59 +423,178 @@ object AndroidDeviceUtils {
|
|||||||
* @param securityLevel The security level, used to determine the correct attestation version.
|
* @param securityLevel The security level, used to determine the correct attestation version.
|
||||||
* @return The appropriate Keymaster or KeyMint version number.
|
* @return The appropriate Keymaster or KeyMint version number.
|
||||||
*/
|
*/
|
||||||
fun getKeymasterVersion(securityLevel: Int): Int {
|
fun getKeymasterVersion(securityLevel: Int): Int = getAttestVersion(securityLevel)
|
||||||
val attestVersion = getAttestVersion(securityLevel)
|
|
||||||
return if (attestVersion >= 100) attestVersion else 41 // Keymaster 4.1 for older versions
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- APEX and Module Hash Properties ---
|
// --- APEX and Module Hash Properties ---
|
||||||
|
|
||||||
private val apexInfos: List<Pair<String, Long>> by lazy {
|
// Minimal protobuf parser for apex_manifest.pb (field 1: name, field 2: version)
|
||||||
runCatching {
|
private class MinimalApexManifestParser(private val data: ByteArray) {
|
||||||
val pm = ConfigurationManager.getPackageManager()
|
var pos = 0
|
||||||
val packages =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
fun parse(): Pair<String, Long>? {
|
||||||
pm?.getInstalledPackages(PackageManager.MATCH_APEX.toLong(), 0)
|
var name: String? = null
|
||||||
} else {
|
var version: Long? = null
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
pm?.getInstalledPackages(PackageManager.MATCH_APEX, 0)
|
while (pos < data.size) {
|
||||||
|
val tag = readVarint()
|
||||||
|
val fieldNum = tag ushr 3
|
||||||
|
val wireType = (tag and 0x07).toInt()
|
||||||
|
|
||||||
|
when (fieldNum) {
|
||||||
|
1L -> {
|
||||||
|
val length = readVarint().toInt()
|
||||||
|
if (pos + length > data.size) return null
|
||||||
|
name = String(data, pos, length, Charsets.UTF_8)
|
||||||
|
pos += length
|
||||||
}
|
}
|
||||||
packages?.list.orEmpty().map { it.packageName to it.longVersionCode }
|
2L -> {
|
||||||
|
version = readVarint()
|
||||||
|
}
|
||||||
|
else -> skipField(wireType)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.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()
|
||||||
|
1 -> pos += 8
|
||||||
|
2 -> {
|
||||||
|
val len = readVarint().toInt()
|
||||||
|
pos += len
|
||||||
|
}
|
||||||
|
5 -> pos += 4
|
||||||
|
else -> throw IllegalStateException("Unknown wire type $wireType")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
apexRoot.listFiles()?.forEach { file ->
|
||||||
|
if (!file.isDirectory) return@forEach
|
||||||
|
val name = file.name
|
||||||
|
|
||||||
|
if (name.startsWith(".")) return@forEach
|
||||||
|
if (name.contains("@")) return@forEach
|
||||||
|
if (name == "sharedlibs") return@forEach
|
||||||
|
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.distinctBy { it.first }
|
||||||
}
|
}
|
||||||
|
|
||||||
val moduleHash: ByteArray by lazy {
|
val moduleHash: ByteArray by lazy {
|
||||||
DeviceAttestationService.CachedAttestationData?.moduleHash
|
DeviceAttestationService.CachedAttestationData?.moduleHash
|
||||||
?: runCatching {
|
?: runCatching {
|
||||||
// TODO: figure out the correct calculation
|
data class ModuleEntry(
|
||||||
val moduleSequences = ASN1EncodableVector()
|
val nameEncoded: ByteArray,
|
||||||
|
val fullEncoded: ByteArray,
|
||||||
|
)
|
||||||
|
|
||||||
// 1. Create a DERSequence for each module.
|
val modules =
|
||||||
apexInfos.forEach { (packageName, versionCode) ->
|
apexInfos.map { (packageName, versionCode) ->
|
||||||
val moduleVector = ASN1EncodableVector()
|
val nameOctet = DEROctetString(packageName.toByteArray(Charsets.UTF_8))
|
||||||
// Use explicit UTF-8 encoding for the package name.
|
val versionInt = ASN1Integer(versionCode)
|
||||||
moduleVector.add(DEROctetString(packageName.toByteArray(Charsets.UTF_8)))
|
|
||||||
moduleVector.add(ASN1Integer(versionCode))
|
|
||||||
moduleSequences.add(DERSequence(moduleVector))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Create a DERSet. Bouncy Castle will automatically handle
|
val vec = ASN1EncodableVector()
|
||||||
// the sorting based on the DER-encoded value of each sequence.
|
vec.add(nameOctet)
|
||||||
val modulesSet = DERSet(moduleSequences)
|
vec.add(versionInt)
|
||||||
|
val sequence = DERSequence(vec)
|
||||||
|
|
||||||
// 3. Get the final DER-encoded byte array of the SET.
|
// AOSP sorts by encoded name only, not full sequence
|
||||||
val encodedModules = modulesSet.encoded
|
ModuleEntry(
|
||||||
|
nameEncoded = nameOctet.encoded,
|
||||||
|
fullEncoded = sequence.encoded,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Compute the SHA-256 hash.
|
val sortedModules =
|
||||||
MessageDigest.getInstance("SHA-256").digest(encodedModules)
|
modules.sortedWith { m1, m2 ->
|
||||||
|
compareByteArrays(m1.nameEncoded, m2.nameEncoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
val payloadStream = ByteArrayOutputStream()
|
||||||
|
sortedModules.forEach { payloadStream.write(it.fullEncoded) }
|
||||||
|
val payload = payloadStream.toByteArray()
|
||||||
|
|
||||||
|
// Wrap in DER SET tag manually — DERSet() re-sorts by full encoding
|
||||||
|
val finalDerSet = encodeAsDerSet(payload)
|
||||||
|
|
||||||
|
MessageDigest.getInstance("SHA-256").digest(finalDerSet)
|
||||||
}
|
}
|
||||||
.getOrElse {
|
.getOrElse {
|
||||||
SystemLogger.error("Failed to compute module hash.", it)
|
SystemLogger.error("Failed to compute module hash.", it)
|
||||||
ByteArray(32) // Return empty hash on failure
|
ByteArray(32)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a.size - b.size
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun encodeAsDerSet(payload: ByteArray): ByteArray {
|
||||||
|
val out = ByteArrayOutputStream()
|
||||||
|
out.write(0x31)
|
||||||
|
writeDerLength(out, payload.size)
|
||||||
|
out.write(payload)
|
||||||
|
return out.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeDerLength(out: ByteArrayOutputStream, length: Int) {
|
||||||
|
if (length < 128) {
|
||||||
|
out.write(length)
|
||||||
|
} else {
|
||||||
|
var size = length
|
||||||
|
val bytes = ArrayList<Byte>()
|
||||||
|
while (size > 0) {
|
||||||
|
bytes.add((size and 0xFF).toByte())
|
||||||
|
size = size ushr 8
|
||||||
|
}
|
||||||
|
out.write(0x80 or bytes.size)
|
||||||
|
for (i in bytes.indices.reversed()) {
|
||||||
|
out.write(bytes[i].toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package org.matrix.TEESimulator.util
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
|
||||||
|
object AndroidPermissionUtils {
|
||||||
|
|
||||||
|
@SuppressLint("PrivateApi", "DiscouragedPrivateApi")
|
||||||
|
private fun getGlobalContext(): Context? {
|
||||||
|
return try {
|
||||||
|
// 1. Get the hidden ActivityThread class via reflection
|
||||||
|
val activityThreadClass = Class.forName("android.app.ActivityThread")
|
||||||
|
|
||||||
|
// 2. Invoke the static currentActivityThread() method
|
||||||
|
val currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread")
|
||||||
|
currentActivityThreadMethod.isAccessible = true
|
||||||
|
val activityThread = currentActivityThreadMethod.invoke(null)
|
||||||
|
|
||||||
|
if (activityThread == null) {
|
||||||
|
SystemLogger.warning("Reflection: ActivityThread.currentActivityThread() returned null")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Try to get the application context
|
||||||
|
val getApplicationMethod = activityThreadClass.getDeclaredMethod("getApplication")
|
||||||
|
getApplicationMethod.isAccessible = true
|
||||||
|
val application = getApplicationMethod.invoke(activityThread) as? Context
|
||||||
|
|
||||||
|
if (application != null) return application
|
||||||
|
|
||||||
|
// 4. Fallback to getSystemContext() if application is null (often happens in system_server)
|
||||||
|
val getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext")
|
||||||
|
getSystemContextMethod.isAccessible = true
|
||||||
|
getSystemContextMethod.invoke(activityThread) as? Context
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("Reflection failed to get global context for permission check", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core permission check.
|
||||||
|
*/
|
||||||
|
fun hasPermission(uid: Int, permission: String): Boolean {
|
||||||
|
val context = getGlobalContext() ?: run {
|
||||||
|
SystemLogger.warning("AndroidPermissionUtils: Context is null, failing permission check safely.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = context.checkPermission(permission, -1, uid)
|
||||||
|
return result == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasDeviceAttestationPermission(uid: Int): Boolean {
|
||||||
|
return hasPermission(uid, "android.permission.READ_PRIVILEGED_PHONE_STATE")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasUniqueIdAttestationPermission(uid: Int): Boolean {
|
||||||
|
return hasPermission(uid, "android.permission.REQUEST_UNIQUE_ID_ATTESTATION")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasManageUsersPermission(uid: Int): Boolean {
|
||||||
|
return hasPermission(uid, "android.permission.MANAGE_USERS")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasDumpPermission(uid: Int): Boolean {
|
||||||
|
return hasPermission(uid, "android.permission.DUMP")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,64 @@
|
|||||||
|
package org.matrix.TEESimulator.util
|
||||||
|
|
||||||
|
import android.hardware.security.keymint.Algorithm
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.util.concurrent.locks.LockSupport
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.exp
|
||||||
|
import kotlin.math.ln
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
object TeeLatencySimulator {
|
||||||
|
|
||||||
|
private val rng = SecureRandom()
|
||||||
|
|
||||||
|
private val sessionBiasMs: Double by lazy { rng.nextGaussian() * 5.0 }
|
||||||
|
private val coldPenaltyMs: Double by lazy { abs(rng.nextGaussian() * 12.0) }
|
||||||
|
|
||||||
|
@Volatile private var firstCall = true
|
||||||
|
|
||||||
|
fun simulateGenerateKeyDelay(algorithm: Int, elapsedNanos: Long) {
|
||||||
|
val elapsedMs = elapsedNanos / 1_000_000.0
|
||||||
|
val targetMs = sampleTotalDelay(algorithm)
|
||||||
|
val remainingMs = targetMs - elapsedMs
|
||||||
|
|
||||||
|
if (remainingMs > 1.0) {
|
||||||
|
LockSupport.parkNanos((remainingMs * 1_000_000).toLong())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sampleTotalDelay(algorithm: Int): Double {
|
||||||
|
val base = sampleBaseCryptoDelay(algorithm)
|
||||||
|
val transit = sampleExponential(2.5)
|
||||||
|
val jitter = (rng.nextGaussian() * 2.5).coerceIn(-8.0, 12.0)
|
||||||
|
|
||||||
|
var cold = 0.0
|
||||||
|
if (firstCall) {
|
||||||
|
firstCall = false
|
||||||
|
cold = coldPenaltyMs
|
||||||
|
}
|
||||||
|
|
||||||
|
return max(20.0, base + transit + jitter + sessionBiasMs + cold)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sampleBaseCryptoDelay(algorithm: Int): Double {
|
||||||
|
val (mu, sigma) =
|
||||||
|
when (algorithm) {
|
||||||
|
Algorithm.EC -> ln(60.0) to 0.08
|
||||||
|
Algorithm.RSA -> ln(70.0) to 0.08
|
||||||
|
Algorithm.AES -> ln(35.0) to 0.10
|
||||||
|
else -> ln(40.0) to 0.10
|
||||||
|
}
|
||||||
|
return sampleLogNormal(mu, sigma)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sampleLogNormal(mu: Double, sigma: Double): Double {
|
||||||
|
return exp(mu + sigma * rng.nextGaussian())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sampleExponential(mean: Double): Double {
|
||||||
|
var u = rng.nextDouble()
|
||||||
|
while (u == 0.0) u = rng.nextDouble()
|
||||||
|
return -mean * ln(u)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,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,46 @@
|
|||||||
|
#!/system/bin/sh
|
||||||
|
MODDIR=${0%/*}
|
||||||
|
CONFIG_DIR=/data/adb/tricky_store
|
||||||
|
|
||||||
|
. "$MODDIR/action_i18n.sh"
|
||||||
|
|
||||||
|
echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo " ⚠️ $(_msg confirm_header)"
|
||||||
|
echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo " "
|
||||||
|
echo " $(_msg confirm_warning_1)"
|
||||||
|
echo " $(_msg confirm_warning_2)"
|
||||||
|
echo " "
|
||||||
|
echo " 🔊 $(_msg confirm_vol_up)"
|
||||||
|
echo " 🔉 $(_msg confirm_vol_down)"
|
||||||
|
echo " "
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
# Sample getevent in 1s bursts; a piped stream block-buffers and misses
|
||||||
|
# a single key-press before the timeout.
|
||||||
|
deadline=$(( $(date +%s) + 10 ))
|
||||||
|
while [ "$(date +%s)" -lt "$deadline" ]; do
|
||||||
|
events=$(/system/bin/timeout 1 /system/bin/getevent -l 2>/dev/null)
|
||||||
|
case "$events" in
|
||||||
|
*KEY_VOLUMEUP*) return 0 ;;
|
||||||
|
*KEY_VOLUMEDOWN*) return 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! confirm; then
|
||||||
|
echo " "
|
||||||
|
echo " ❌ $(_msg confirm_cancelled)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "$CONFIG_DIR/persistent_keys" ]; then
|
||||||
|
rm -rf "$CONFIG_DIR/persistent_keys"
|
||||||
|
mkdir -p "$CONFIG_DIR/persistent_keys"
|
||||||
|
echo " "
|
||||||
|
echo " ✅ $(_msg confirm_cleared)"
|
||||||
|
else
|
||||||
|
echo " "
|
||||||
|
echo " ℹ️ $(_msg confirm_not_found)"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
ACTION_LANG="en"
|
||||||
|
_detect_lang() {
|
||||||
|
local raw
|
||||||
|
raw=$(getprop persist.sys.locale 2>/dev/null)
|
||||||
|
[ -z "$raw" ] && raw=$(getprop ro.product.locale 2>/dev/null)
|
||||||
|
[ -z "$raw" ] && raw=$(getprop ro.system.locale 2>/dev/null)
|
||||||
|
local code=$(printf '%s' "$raw" | sed 's/_/-/g')
|
||||||
|
case "$code" in
|
||||||
|
zh-Hans*|zh-CN*) code="zh-CN" ;;
|
||||||
|
zh-Hant*|zh-TW*|zh-HK*) code="zh-TW" ;;
|
||||||
|
pt-BR*) code="pt-BR" ;;
|
||||||
|
pt*) code="pt-BR" ;;
|
||||||
|
es-ES*|es*) code="es-ES" ;;
|
||||||
|
*-*) code="${code%%-*}" ;;
|
||||||
|
esac
|
||||||
|
case "$code" in
|
||||||
|
ar|az|bn|de|el|es-ES|fa|fr|id|it|ja|ko|pl|pt-BR|ru|th|tl|tr|uk|vi|zh-CN|zh-TW) ACTION_LANG="$code" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
_detect_lang
|
||||||
|
|
||||||
|
_msg() {
|
||||||
|
case "$ACTION_LANG" in
|
||||||
|
zh-CN) case "$1" in
|
||||||
|
confirm_header) echo "清除持久化密钥存储" ;;
|
||||||
|
confirm_warning_1) echo "这将删除所有缓存的证明密钥。" ;;
|
||||||
|
confirm_warning_2) echo "使用证明的应用将在下次使用时重新注册。" ;;
|
||||||
|
confirm_vol_up) echo "音量+ = 确认清除" ;;
|
||||||
|
confirm_vol_down) echo "音量- = 取消(10秒后默认)" ;;
|
||||||
|
confirm_cancelled) echo "已取消 - 密钥已保留" ;;
|
||||||
|
confirm_cleared) echo "持久化密钥存储已清除" ;;
|
||||||
|
confirm_not_found) echo "未找到持久化密钥存储" ;;
|
||||||
|
esac ;;
|
||||||
|
zh-TW) case "$1" in
|
||||||
|
confirm_header) echo "清除持久化金鑰儲存" ;;
|
||||||
|
confirm_warning_1) echo "這將刪除所有快取的證明金鑰。" ;;
|
||||||
|
confirm_warning_2) echo "使用證明的應用程式將在下次使用時重新註冊。" ;;
|
||||||
|
confirm_vol_up) echo "音量+ = 確認清除" ;;
|
||||||
|
confirm_vol_down) echo "音量- = 取消(10秒後預設)" ;;
|
||||||
|
confirm_cancelled) echo "已取消 - 金鑰已保留" ;;
|
||||||
|
confirm_cleared) echo "持久化金鑰儲存已清除" ;;
|
||||||
|
confirm_not_found) echo "未找到持久化金鑰儲存" ;;
|
||||||
|
esac ;;
|
||||||
|
ja) case "$1" in
|
||||||
|
confirm_header) echo "永続キーストレージを消去" ;;
|
||||||
|
confirm_warning_1) echo "キャッシュされた証明キーをすべて削除します。" ;;
|
||||||
|
confirm_warning_2) echo "証明を使用するアプリは次回使用時に再登録されます。" ;;
|
||||||
|
confirm_vol_up) echo "音量+ = 消去を確認" ;;
|
||||||
|
confirm_vol_down) echo "音量- = キャンセル(10秒後デフォルト)" ;;
|
||||||
|
confirm_cancelled) echo "キャンセルされました - キーは保持されます" ;;
|
||||||
|
confirm_cleared) echo "永続キーストレージを消去しました" ;;
|
||||||
|
confirm_not_found) echo "永続キーストレージが見つかりません" ;;
|
||||||
|
esac ;;
|
||||||
|
ko) case "$1" in
|
||||||
|
confirm_header) echo "영구 키 저장소 지우기" ;;
|
||||||
|
confirm_warning_1) echo "캐시된 모든 증명 키를 삭제합니다." ;;
|
||||||
|
confirm_warning_2) echo "증명을 사용하는 앱은 다음 사용 시 재등록됩니다." ;;
|
||||||
|
confirm_vol_up) echo "볼륨+ = 지우기 확인" ;;
|
||||||
|
confirm_vol_down) echo "볼륨- = 취소 (10초 후 기본값)" ;;
|
||||||
|
confirm_cancelled) echo "취소됨 - 키 유지됨" ;;
|
||||||
|
confirm_cleared) echo "영구 키 저장소가 지워졌습니다" ;;
|
||||||
|
confirm_not_found) echo "영구 키 저장소를 찾을 수 없습니다" ;;
|
||||||
|
esac ;;
|
||||||
|
ru) case "$1" in
|
||||||
|
confirm_header) echo "Очистить постоянное хранилище ключей" ;;
|
||||||
|
confirm_warning_1) echo "Это удалит все кэшированные ключи аттестации." ;;
|
||||||
|
confirm_warning_2) echo "Приложения, использующие аттестацию, перерегистрируются при следующем использовании." ;;
|
||||||
|
confirm_vol_up) echo "Громкость+ = Подтвердить очистку" ;;
|
||||||
|
confirm_vol_down) echo "Громкость- = Отмена (по умолчанию через 10с)" ;;
|
||||||
|
confirm_cancelled) echo "Отменено - ключи сохранены" ;;
|
||||||
|
confirm_cleared) echo "Постоянное хранилище ключей очищено" ;;
|
||||||
|
confirm_not_found) echo "Постоянное хранилище ключей не найдено" ;;
|
||||||
|
esac ;;
|
||||||
|
de) case "$1" in
|
||||||
|
confirm_header) echo "Persistenten Schlüsselspeicher löschen" ;;
|
||||||
|
confirm_warning_1) echo "Dies löscht alle zwischengespeicherten Attestierungsschlüssel." ;;
|
||||||
|
confirm_warning_2) echo "Apps mit Attestierung registrieren sich bei der nächsten Nutzung neu." ;;
|
||||||
|
confirm_vol_up) echo "Laut+ = Löschen bestätigen" ;;
|
||||||
|
confirm_vol_down) echo "Leise- = Abbrechen (Standard nach 10s)" ;;
|
||||||
|
confirm_cancelled) echo "Abgebrochen - Schlüssel beibehalten" ;;
|
||||||
|
confirm_cleared) echo "Persistenter Schlüsselspeicher gelöscht" ;;
|
||||||
|
confirm_not_found) echo "Kein persistenter Schlüsselspeicher gefunden" ;;
|
||||||
|
esac ;;
|
||||||
|
fr) case "$1" in
|
||||||
|
confirm_header) echo "Effacer le stockage de clés persistant" ;;
|
||||||
|
confirm_warning_1) echo "Ceci supprime toutes les clés d'attestation en cache." ;;
|
||||||
|
confirm_warning_2) echo "Les apps utilisant l'attestation se réinscriront à la prochaine utilisation." ;;
|
||||||
|
confirm_vol_up) echo "Vol+ = Confirmer l'effacement" ;;
|
||||||
|
confirm_vol_down) echo "Vol- = Annuler (par défaut après 10s)" ;;
|
||||||
|
confirm_cancelled) echo "Annulé - clés conservées" ;;
|
||||||
|
confirm_cleared) echo "Stockage de clés persistant effacé" ;;
|
||||||
|
confirm_not_found) echo "Aucun stockage de clés persistant trouvé" ;;
|
||||||
|
esac ;;
|
||||||
|
es-ES) case "$1" in
|
||||||
|
confirm_header) echo "Borrar almacenamiento persistente de claves" ;;
|
||||||
|
confirm_warning_1) echo "Esto elimina todas las claves de atestación en caché." ;;
|
||||||
|
confirm_warning_2) echo "Las apps que usan atestación se volverán a registrar en el próximo uso." ;;
|
||||||
|
confirm_vol_up) echo "Vol+ = Confirmar borrado" ;;
|
||||||
|
confirm_vol_down) echo "Vol- = Cancelar (predeterminado tras 10s)" ;;
|
||||||
|
confirm_cancelled) echo "Cancelado - claves conservadas" ;;
|
||||||
|
confirm_cleared) echo "Almacenamiento persistente de claves borrado" ;;
|
||||||
|
confirm_not_found) echo "No se encontró almacenamiento persistente de claves" ;;
|
||||||
|
esac ;;
|
||||||
|
pt-BR) case "$1" in
|
||||||
|
confirm_header) echo "Limpar armazenamento persistente de chaves" ;;
|
||||||
|
confirm_warning_1) echo "Isso exclui todas as chaves de atestação em cache." ;;
|
||||||
|
confirm_warning_2) echo "Apps que usam atestação serão re-registrados no próximo uso." ;;
|
||||||
|
confirm_vol_up) echo "Vol+ = Confirmar limpeza" ;;
|
||||||
|
confirm_vol_down) echo "Vol- = Cancelar (padrão após 10s)" ;;
|
||||||
|
confirm_cancelled) echo "Cancelado - chaves preservadas" ;;
|
||||||
|
confirm_cleared) echo "Armazenamento persistente de chaves limpo" ;;
|
||||||
|
confirm_not_found) echo "Nenhum armazenamento persistente de chaves encontrado" ;;
|
||||||
|
esac ;;
|
||||||
|
it) case "$1" in
|
||||||
|
confirm_header) echo "Cancella archivio chiavi persistente" ;;
|
||||||
|
confirm_warning_1) echo "Questo elimina tutte le chiavi di attestazione in cache." ;;
|
||||||
|
confirm_warning_2) echo "Le app che usano l'attestazione si re-registreranno al prossimo utilizzo." ;;
|
||||||
|
confirm_vol_up) echo "Vol+ = Conferma cancellazione" ;;
|
||||||
|
confirm_vol_down) echo "Vol- = Annulla (predefinito dopo 10s)" ;;
|
||||||
|
confirm_cancelled) echo "Annullato - chiavi conservate" ;;
|
||||||
|
confirm_cleared) echo "Archivio chiavi persistente cancellato" ;;
|
||||||
|
confirm_not_found) echo "Nessun archivio chiavi persistente trovato" ;;
|
||||||
|
esac ;;
|
||||||
|
tr) case "$1" in
|
||||||
|
confirm_header) echo "Kalıcı Anahtar Deposunu Temizle" ;;
|
||||||
|
confirm_warning_1) echo "Bu, önbelleğe alınmış tüm doğrulama anahtarlarını siler." ;;
|
||||||
|
confirm_warning_2) echo "Doğrulama kullanan uygulamalar bir sonraki kullanımda yeniden kaydolacak." ;;
|
||||||
|
confirm_vol_up) echo "Ses+ = Temizlemeyi onayla" ;;
|
||||||
|
confirm_vol_down) echo "Ses- = İptal (10sn sonra varsayılan)" ;;
|
||||||
|
confirm_cancelled) echo "İptal edildi - anahtarlar korundu" ;;
|
||||||
|
confirm_cleared) echo "Kalıcı anahtar deposu temizlendi" ;;
|
||||||
|
confirm_not_found) echo "Kalıcı anahtar deposu bulunamadı" ;;
|
||||||
|
esac ;;
|
||||||
|
id) case "$1" in
|
||||||
|
confirm_header) echo "Hapus Penyimpanan Kunci Persisten" ;;
|
||||||
|
confirm_warning_1) echo "Ini menghapus semua kunci atestasi yang di-cache." ;;
|
||||||
|
confirm_warning_2) echo "Aplikasi yang menggunakan atestasi akan mendaftar ulang saat digunakan." ;;
|
||||||
|
confirm_vol_up) echo "Vol+ = Konfirmasi hapus" ;;
|
||||||
|
confirm_vol_down) echo "Vol- = Batal (default setelah 10 detik)" ;;
|
||||||
|
confirm_cancelled) echo "Dibatalkan - kunci dipertahankan" ;;
|
||||||
|
confirm_cleared) echo "Penyimpanan kunci persisten dihapus" ;;
|
||||||
|
confirm_not_found) echo "Penyimpanan kunci persisten tidak ditemukan" ;;
|
||||||
|
esac ;;
|
||||||
|
vi) case "$1" in
|
||||||
|
confirm_header) echo "Xóa lưu trữ khóa cố định" ;;
|
||||||
|
confirm_warning_1) echo "Thao tác này xóa tất cả khóa chứng thực được lưu cache." ;;
|
||||||
|
confirm_warning_2) echo "Các ứng dụng dùng chứng thực sẽ đăng ký lại khi sử dụng tiếp theo." ;;
|
||||||
|
confirm_vol_up) echo "Vol+ = Xác nhận xóa" ;;
|
||||||
|
confirm_vol_down) echo "Vol- = Hủy (mặc định sau 10s)" ;;
|
||||||
|
confirm_cancelled) echo "Đã hủy - giữ nguyên khóa" ;;
|
||||||
|
confirm_cleared) echo "Đã xóa lưu trữ khóa cố định" ;;
|
||||||
|
confirm_not_found) echo "Không tìm thấy lưu trữ khóa cố định" ;;
|
||||||
|
esac ;;
|
||||||
|
ar) case "$1" in
|
||||||
|
confirm_header) echo "مسح تخزين المفاتيح الدائم" ;;
|
||||||
|
confirm_warning_1) echo "يؤدي هذا إلى حذف جميع مفاتيح التصديق المخزنة مؤقتاً." ;;
|
||||||
|
confirm_warning_2) echo "التطبيقات التي تستخدم التصديق ستعيد التسجيل في الاستخدام التالي." ;;
|
||||||
|
confirm_vol_up) echo "رفع الصوت = تأكيد المسح" ;;
|
||||||
|
confirm_vol_down) echo "خفض الصوت = إلغاء (افتراضي بعد 10 ثوانٍ)" ;;
|
||||||
|
confirm_cancelled) echo "تم الإلغاء - تم الاحتفاظ بالمفاتيح" ;;
|
||||||
|
confirm_cleared) echo "تم مسح تخزين المفاتيح الدائم" ;;
|
||||||
|
confirm_not_found) echo "لم يتم العثور على تخزين مفاتيح دائم" ;;
|
||||||
|
esac ;;
|
||||||
|
th) case "$1" in
|
||||||
|
confirm_header) echo "ล้างที่จัดเก็บคีย์ถาวร" ;;
|
||||||
|
confirm_warning_1) echo "การดำเนินการนี้จะลบคีย์การรับรองที่แคชไว้ทั้งหมด" ;;
|
||||||
|
confirm_warning_2) echo "แอปที่ใช้การรับรองจะลงทะเบียนใหม่ในการใช้งานครั้งถัดไป" ;;
|
||||||
|
confirm_vol_up) echo "เพิ่มเสียง = ยืนยันการล้าง" ;;
|
||||||
|
confirm_vol_down) echo "ลดเสียง = ยกเลิก (ค่าเริ่มต้นหลัง 10 วินาที)" ;;
|
||||||
|
confirm_cancelled) echo "ยกเลิกแล้ว - คีย์ยังคงอยู่" ;;
|
||||||
|
confirm_cleared) echo "ล้างที่จัดเก็บคีย์ถาวรแล้ว" ;;
|
||||||
|
confirm_not_found) echo "ไม่พบที่จัดเก็บคีย์ถาวร" ;;
|
||||||
|
esac ;;
|
||||||
|
uk) case "$1" in
|
||||||
|
confirm_header) echo "Очистити постійне сховище ключів" ;;
|
||||||
|
confirm_warning_1) echo "Це видаляє всі кешовані ключі атестації." ;;
|
||||||
|
confirm_warning_2) echo "Програми, що використовують атестацію, повторно зареєструються при наступному використанні." ;;
|
||||||
|
confirm_vol_up) echo "Гучність+ = Підтвердити очищення" ;;
|
||||||
|
confirm_vol_down) echo "Гучність- = Скасувати (за замовчуванням через 10с)" ;;
|
||||||
|
confirm_cancelled) echo "Скасовано - ключі збережено" ;;
|
||||||
|
confirm_cleared) echo "Постійне сховище ключів очищено" ;;
|
||||||
|
confirm_not_found) echo "Постійне сховище ключів не знайдено" ;;
|
||||||
|
esac ;;
|
||||||
|
pl) case "$1" in
|
||||||
|
confirm_header) echo "Wyczyść trwały magazyn kluczy" ;;
|
||||||
|
confirm_warning_1) echo "To usuwa wszystkie buforowane klucze atestacji." ;;
|
||||||
|
confirm_warning_2) echo "Aplikacje używające atestacji zarejestrują się ponownie przy następnym użyciu." ;;
|
||||||
|
confirm_vol_up) echo "Głośność+ = Potwierdź czyszczenie" ;;
|
||||||
|
confirm_vol_down) echo "Głośność- = Anuluj (domyślnie po 10s)" ;;
|
||||||
|
confirm_cancelled) echo "Anulowano - klucze zachowane" ;;
|
||||||
|
confirm_cleared) echo "Trwały magazyn kluczy wyczyszczony" ;;
|
||||||
|
confirm_not_found) echo "Nie znaleziono trwałego magazynu kluczy" ;;
|
||||||
|
esac ;;
|
||||||
|
az) case "$1" in
|
||||||
|
confirm_header) echo "Davamlı Açar Yaddaşını Təmizlə" ;;
|
||||||
|
confirm_warning_1) echo "Bu, keşlənmiş bütün təsdiqləmə açarlarını silir." ;;
|
||||||
|
confirm_warning_2) echo "Təsdiqləmədən istifadə edən tətbiqlər növbəti istifadədə yenidən qeydiyyatdan keçəcək." ;;
|
||||||
|
confirm_vol_up) echo "Səs+ = Təmizləməni təsdiqlə" ;;
|
||||||
|
confirm_vol_down) echo "Səs- = Ləğv et (10 saniyə sonra defolt)" ;;
|
||||||
|
confirm_cancelled) echo "Ləğv edildi - açarlar saxlanıldı" ;;
|
||||||
|
confirm_cleared) echo "Davamlı açar yaddaşı təmizləndi" ;;
|
||||||
|
confirm_not_found) echo "Davamlı açar yaddaşı tapılmadı" ;;
|
||||||
|
esac ;;
|
||||||
|
bn) case "$1" in
|
||||||
|
confirm_header) echo "স্থায়ী কী সংরক্ষণ পরিষ্কার করুন" ;;
|
||||||
|
confirm_warning_1) echo "এটি সমস্ত ক্যাশড অ্যাটেস্টেশন কী মুছে ফেলে।" ;;
|
||||||
|
confirm_warning_2) echo "অ্যাটেস্টেশন ব্যবহারকারী অ্যাপগুলি পরবর্তী ব্যবহারে পুনরায় নিবন্ধন করবে।" ;;
|
||||||
|
confirm_vol_up) echo "ভলিউম+ = পরিষ্কার নিশ্চিত করুন" ;;
|
||||||
|
confirm_vol_down) echo "ভলিউম- = বাতিল (১০ সেকেন্ডে ডিফল্ট)" ;;
|
||||||
|
confirm_cancelled) echo "বাতিল করা হয়েছে - কী সংরক্ষিত" ;;
|
||||||
|
confirm_cleared) echo "স্থায়ী কী সংরক্ষণ পরিষ্কার করা হয়েছে" ;;
|
||||||
|
confirm_not_found) echo "কোনো স্থায়ী কী সংরক্ষণ পাওয়া যায়নি" ;;
|
||||||
|
esac ;;
|
||||||
|
el) case "$1" in
|
||||||
|
confirm_header) echo "Εκκαθάριση Μόνιμου Αποθηκευτικού Χώρου Κλειδιών" ;;
|
||||||
|
confirm_warning_1) echo "Διαγράφει όλα τα προσωρινά αποθηκευμένα κλειδιά πιστοποίησης." ;;
|
||||||
|
confirm_warning_2) echo "Οι εφαρμογές που χρησιμοποιούν πιστοποίηση θα επανεγγραφούν στην επόμενη χρήση." ;;
|
||||||
|
confirm_vol_up) echo "Ένταση+ = Επιβεβαίωση εκκαθάρισης" ;;
|
||||||
|
confirm_vol_down) echo "Ένταση- = Ακύρωση (προεπιλογή μετά από 10 δευτ)" ;;
|
||||||
|
confirm_cancelled) echo "Ακυρώθηκε - τα κλειδιά διατηρήθηκαν" ;;
|
||||||
|
confirm_cleared) echo "Ο μόνιμος αποθηκευτικός χώρος κλειδιών εκκαθαρίστηκε" ;;
|
||||||
|
confirm_not_found) echo "Δεν βρέθηκε μόνιμος αποθηκευτικός χώρος κλειδιών" ;;
|
||||||
|
esac ;;
|
||||||
|
fa) case "$1" in
|
||||||
|
confirm_header) echo "پاک کردن ذخیرهسازی دائمی کلید" ;;
|
||||||
|
confirm_warning_1) echo "این کار همه کلیدهای تأیید کششده را حذف میکند." ;;
|
||||||
|
confirm_warning_2) echo "برنامههای استفادهکننده از تأیید در استفاده بعدی دوباره ثبتنام میکنند." ;;
|
||||||
|
confirm_vol_up) echo "صدا+ = تأیید پاک کردن" ;;
|
||||||
|
confirm_vol_down) echo "صدا- = لغو (پیشفرض پس از ۱۰ ثانیه)" ;;
|
||||||
|
confirm_cancelled) echo "لغو شد - کلیدها حفظ شدند" ;;
|
||||||
|
confirm_cleared) echo "ذخیرهسازی دائمی کلید پاک شد" ;;
|
||||||
|
confirm_not_found) echo "ذخیرهسازی دائمی کلید یافت نشد" ;;
|
||||||
|
esac ;;
|
||||||
|
tl) case "$1" in
|
||||||
|
confirm_header) echo "Burahin ang Persistent Key Storage" ;;
|
||||||
|
confirm_warning_1) echo "Buburahin nito ang lahat ng naka-cache na attestation keys." ;;
|
||||||
|
confirm_warning_2) echo "Magre-rehistro muli ang mga app na gumagamit ng attestation sa susunod na paggamit." ;;
|
||||||
|
confirm_vol_up) echo "Vol+ = Kumpirmahin ang pagbura" ;;
|
||||||
|
confirm_vol_down) echo "Vol- = Kanselahin (default pagkatapos ng 10s)" ;;
|
||||||
|
confirm_cancelled) echo "Nakansela - napanatili ang mga key" ;;
|
||||||
|
confirm_cleared) echo "Nabura ang persistent key storage" ;;
|
||||||
|
confirm_not_found) echo "Walang nahanap na persistent key storage" ;;
|
||||||
|
esac ;;
|
||||||
|
*) case "$1" in
|
||||||
|
confirm_header) echo "Clear Persistent Key Storage" ;;
|
||||||
|
confirm_warning_1) echo "This deletes all cached attestation keys." ;;
|
||||||
|
confirm_warning_2) echo "Apps using attestation will re-enroll on next use." ;;
|
||||||
|
confirm_vol_up) echo "Vol+ = Confirm clear" ;;
|
||||||
|
confirm_vol_down) echo "Vol- = Cancel (default after 10s)" ;;
|
||||||
|
confirm_cancelled) echo "Cancelled - keys preserved" ;;
|
||||||
|
confirm_cleared) echo "Persistent key storage cleared" ;;
|
||||||
|
confirm_not_found) echo "No persistent key storage found" ;;
|
||||||
|
esac ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
+354
-13
@@ -1,21 +1,362 @@
|
|||||||
TEESimulator 3.0 is a significant update focused on powerful new configuration options, major improvements to stealth, and enhanced stability.
|
## TEESimulator-RS v6.0.1-282
|
||||||
|
|
||||||
#### ✨ **Highlights & New Features**
|
AUTO-mode key attestation now forges plain attestation from the keybox instead of deferring to the real TEE.
|
||||||
|
|
||||||
* **🎯 Per-App Security Patch Configuration**: Gain ultimate control by setting security patch levels on a per-package basis. Define a global default in `security_patch.txt` and override it for specific apps like `[com.google.android.gms]`. Moreover, your configuration is now alive! Use the `today` keyword to always report the current date, or create rolling dates with templates like `YYYY-MM-05`. Be sure to check README for more details.
|
### Detection coverage
|
||||||
* **🕰️ Full Software Emulation on Android 11**: We've implemented a complete, software-based key generation and attestation flow for the legacy `IKeystoreService` API, bringing full emulation capabilities to older devices.
|
- AUTO dispatch probed the device with `checkTeeFunctionality`, which only proves the TEE can mint one EC key. It says nothing about RSA attestation, device-ID attestation, or whether a patched chain survives RSA verify. Plain attestation requests (attest-key OFF, challenge present) were routed to PATCH and deferred to hardware, so devices that can't back that surfaced KeyAttestation reds: `ATTESTATION_KEYS_NOT_PROVISIONED` (-49) and `BLOCK_TYPE_IS_NOT_01`.
|
||||||
|
- AUTO targets carrying an attestation challenge now take the FORGE path, the same one attest-key-ON already used: a synthetic chain built from the keybox and rooted under the Google root key. Requests with no challenge still pass through to real hardware, so KeyDetector's hardware-backed checks are unaffected.
|
||||||
|
|
||||||
#### 🛡️ **Stealth & Evasion Upgrades**
|
### Verified
|
||||||
|
- Offline conformance against real FORGE captures: uid10389 and uid10154 chains are GREEN; the root SPKI byte-matches `GOOGLE_ROOT_PUBLIC_KEY`.
|
||||||
|
|
||||||
* **⛓️ Consistent Certificate Signatures**: Say goodbye to a major detection vector in `icu.nullptr.nativetest`. Patched certificates are now cached, ensuring that every request for a key returns a byte-for-byte identical certificate, just like a real TEE.
|
---
|
||||||
* **🔑 Authentic Device Properties**: To appear more genuine, the simulator now sources and uses your device's real `verifiedBootHash` and `moduleHash`, moving away from placeholder values.
|
|
||||||
* **📜 Structurally Sound Certificates**: The patching logic has been rewritten to be less intrusive. It now modifies the attestation extension in-place, preserving the original order of other extensions and preventing duplicates to avoid suspicion.
|
|
||||||
|
|
||||||
#### 🐛 **Bug Fixes & Reliability**
|
## TEESimulator-RS v6.0.1-280
|
||||||
|
|
||||||
* ✅ **Robust Crypto Engine**: Fixed critical crashes related to cryptographic provider conflicts. The signing logic is now more explicit and the KeyBox parser is more resilient against malformed files.
|
Clears the Duck Detector generate-mode parcel fingerprint that real Android 16 hardware also trips, fixes RSA attestation under an EC-only keybox, and restores device-property attestation for Play Integrity hardware apps such as BHIM and UPI. Generate-mode fix field-confirmed on Android 16.
|
||||||
* ➡️ **Improved Compatibility**: Resolved a native crash on Android 11 devices.
|
|
||||||
|
|
||||||
#### 🚀 **The Road Ahead**
|
### Detection coverage
|
||||||
|
- Generate-mode fingerprint: Duck reads the reply at a flat 12-byte stride and flags the sentinel tuple at positions 12 and 13 that the device's native ALGORITHM-first authorization order lands on. Real A16 silicon trips the same probe, so faithful mirroring stayed flagged. `normalizeAuthorizationLayout` marshals the auth array, runs Duck's exact predicate, and applies a minimal deterministic reorder only when it would match. Count, values, security levels, and the cert chain are untouched, and the reorder keys on the byte condition, never on a package. Applied on both the patch and forge reply paths. (#33)
|
||||||
|
- `updateAad` on a non-AEAD operation now answers per vendor: Samsung and Xiaomi-MTK TEEs return success, others return INVALID_TAG, matching Duck's OperationErrorPathProbe on both the sign/verify and cipher paths.
|
||||||
|
|
||||||
Our work to fix detection vectors and provide full support for TEE-broken devices and Android 10/11 is ongoing. We welcome your feedback! Please **report any issues** or **contribute a pull request** on our GitHub.
|
### Attestation correctness (Android 16, EC and RSA)
|
||||||
|
- RSA leaf under an EC-only keybox: patching used to catch the no-RSA-key throw and return the chain untouched, leaking the device's real unlocked Root of Trust for RSA keys while EC keys patched cleanly. It now falls back to any keybox key (EC preferred) and signs the patched leaf with the keybox key's own algorithm, so the RSA leaf re-roots to the Google keybox under a forged locked RoT.
|
||||||
|
- RSA attest-key forge on an EC-only keybox: the forge path matched the algorithm exactly and threw -75 ATTESTATION_KEYS_NOT_PROVISIONED on a miss, so an RSA ATTEST_KEY request never rooted and verifiers reported an unknown certificate. It now falls back to any attestation key, since an EC key validly ECDSA-signs an RSA-subject leaf. No-op on a dual keybox.
|
||||||
|
- A16 attestVersion: the device's KeyMint reports version 100 and the lazy cache shadowed the BAKLAVA-to-400 map, so the forge presented 100. It now caches the AOSP value per SDK and presents the correct 400.
|
||||||
|
- Algorithm-split key on restore: a persisted record holding an EC private key under an RSA leaf failed every signature as DATA_TOO_LARGE_FOR_MODULUS. Restore now drops the record when the private key and served leaf disagree, so the next generateKey rebuilds a coherent key.
|
||||||
|
- Stale chain on regenerate: reusing an alias in generateKey now evicts the cached chain, matching keystore2, so getKeyEntry serves the current key instead of a stale forge from an earlier generation.
|
||||||
|
|
||||||
|
### App compatibility
|
||||||
|
- Device-property attestation (BRAND, MODEL, and the rest) now forges unconditionally. The old gate probed the live TEE, which is dead on every device the module serves, so it rejected GMS Play Integrity's hardware path and broke BHIM and other UPI and Play-Integrity apps. Device-ID attestation (IMEI, serial) stays governed by the real KeyMint caller-permission rule: privileged callers get it, ordinary apps do not.
|
||||||
|
- getKeyEntry now reaches the owned-key lookup for skipped privileged UIDs, so framework attestKeyAlias resolution no longer returns "Invalid attestKeyAlias" for Key Attestation over Shizuku. Non-owned keys still skip post-processing, so a real app's key is never patched.
|
||||||
|
- Device-ID attestation over Shizuku (a privileged UID absent from target.txt) now takes the forge path instead of hitting the real TEE's CANNOT_ATTEST_IDS (-66). "Use attest key" no longer double-roots: a reused persistent attest key is resolved by KEY_ID as well as alias, and an unresolved designated attest key refuses to emit a leaf rather than silently re-rooting under the keybox.
|
||||||
|
|
||||||
|
### Diagnostics (debug builds only)
|
||||||
|
- Per-UID attestation dossier for targeted UIDs at /data/local/tmp/teesim/, recording the decoded chain on both forge and patch paths, key params, the keybox pick (including EC fail-safe), prop sources, forge failures, the emitted authorization shape, and served-versus-verified chains. Release builds strip this through R8 and stay silent. Keybox certificate serials log on every fetch for revocation triage.
|
||||||
|
|
||||||
|
### Verified
|
||||||
|
- Android 16: generate-mode fingerprint signal gone, confirmed on device 2026-06-19.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v6.0.1-251
|
||||||
|
|
||||||
|
14 commits since v6.0.0-235. Clears the remaining Duck Detector grant-domain rows (incl. the Android 16 OnePlus report), restores Google Wallet and fingerprint compatibility, and removes the in-module patch-level/bulletin resolvers. Test device (SDK 35) TEE tamper score 28 → 8.
|
||||||
|
|
||||||
|
### Detection coverage
|
||||||
|
- Grant plane virtualized: owner read and cross-app `Domain.GRANT` read return one identical chain. 6 RED rows cleared. (28 → 18)
|
||||||
|
- Generate-mode fingerprint: dropped 2 surplus authorizations (both patchlevels), USER_ID moved to SOFTWARE to mirror a captured device. (18 → 8)
|
||||||
|
- Android 16 grant: patch-mode keys now served on the grant plane, so owner and grant reads match — fixes CHAIN_SPLIT.
|
||||||
|
- Grant gated to SDK ≥ 36: Android 15 answers PERMISSION_DENIED, no synthetic over-capability.
|
||||||
|
- Stale-chain eviction: import and updateSubcomponent drop the cached attestation; no pre-mutation chain replays.
|
||||||
|
- Lifecycle coherence: clearNamespace / deleteAllKeys / migrateKeyNamespace mirror synthetic key and grant state — defeats delete-then-read probes.
|
||||||
|
- Device-ID attestation mirrors the real TEE: returns CANNOT_ATTEST_IDS where silicon can't attest, instead of forging it.
|
||||||
|
|
||||||
|
### App compatibility
|
||||||
|
- Google Wallet: INCLUDE_UNIQUE_ID stripped (not rejected) when the caller lacks the permission; card binding works. (PR #27)
|
||||||
|
- Fingerprint / vendor keys: KEY_ID miss skips the post-handler, so real HAL operations are no longer wrapped and broken. (PR #26)
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- PatchLevelManager — auto-resolved the security-patch date from an installed PlayIntegrityFix module (with hot-reload) and applied it to props.
|
||||||
|
- BulletinPoller — scheduled security-bulletin refresh.
|
||||||
|
|
||||||
|
### Other
|
||||||
|
- Release builds purge stale `teesim-*.bin` diagnostics from `/data/local/tmp` at boot.
|
||||||
|
- Vol-key confirmation rewritten to 1s `getevent` bursts (piped stream missed single presses on Magisk).
|
||||||
|
|
||||||
|
### Verified
|
||||||
|
- SDK 35, Xiaomi 23106RN0DA: tamper 28 → 8; generate-mode signal gone; 4 grant rows UNAVAILABLE (correct for Android 15); no regressions.
|
||||||
|
- Android 16 grant fix built but unconfirmed on SDK 36 — needs an affected OnePlus user to confirm the grant rows clear.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v6.0.0-235
|
||||||
|
|
||||||
|
11 commits since v6.0.0-224. Duck Detector generate-mode fingerprint cleared. Shizuku-routed BYO attestation fixed. Vol-key confirmation restored on Magisk.
|
||||||
|
|
||||||
|
### Detection Coverage
|
||||||
|
- Duck Detector "TEE Simulator generate-mode fingerprint" cleared. `toAuthorizations` reordered to AOSP keymint reference order; KEY_SIZE moves from auth#4 to auth#2, breaking the byte-224 anchor the probe relied on. 0/31 matches on fresh self-probes (was 15/36).
|
||||||
|
- `persist.logd.size` variants blanked at boot via `service.sh`. Removes a logd-tuning side-channel.
|
||||||
|
|
||||||
|
### BYO & Shizuku Routing
|
||||||
|
- Shizuku-routed BYO attestation no longer fails with `-49 UNSUPPORTED_TAG`. `shouldSkipUid` moved into `handleGenerateKey`, evaluated after BYO parameters are parsed.
|
||||||
|
- `createOperation` parallel fix: outer UID gate removed; the cache-or-forward lookup is the sole gate. BYO keys created under Shizuku UID can now be used for signing under the same UID.
|
||||||
|
- `forceGenerate` simplified: any attest-key or BYO request routes to software unconditionally.
|
||||||
|
- BYO attest-key miss returns the full keybox chain instead of a malformed depth-1 chain.
|
||||||
|
- AUTO TEE race dispatch removed. Resolution uses `DeviceAttestationService.isTeeFunctional` only.
|
||||||
|
- Symmetric gen rejects `attestationKey != null` early with `INVALID_ARGUMENT`. Unsupported-algorithm branch returns `-38` instead of `-49`.
|
||||||
|
|
||||||
|
### Action Button
|
||||||
|
- Vol+ / Vol- confirmation restored on Magisk. Streaming `getevent -lq` matched inline against `KEY_VOLUMEUP DOWN` / `KEY_VOLUMEDOWN DOWN`, wrapped in `/system/bin/timeout 10`. The prior polled approach timed out on six-events-per-keypress kernels.
|
||||||
|
|
||||||
|
### Verified
|
||||||
|
- Android 15 (SDK 35), daemon PID 1466.
|
||||||
|
- Cross-device confirmation pending on OnePlus PKX110 and Samsung SM-S928B.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v6.0.0-224
|
||||||
|
|
||||||
|
59 commits since v6.0.0-162. Self-sufficient spoofing infrastructure, Duck Detector TamperScore-4 cleared on Xiaomi A16, persistent symmetric key storage (PR #22), 22-language action button hardening.
|
||||||
|
|
||||||
|
### Detection Coverage
|
||||||
|
- Duck Detector TimingSideChannelProbe cleared on Xiaomi A16 (SDK 35). Timing ratio dropped 1.555x to 1.055x, verdict WARNING to CLEAR. Threshold is > 1.1x.
|
||||||
|
- `KEY_ID` resolved from `teeResponses` instead of synthesized, matching real KeyMint binder behavior.
|
||||||
|
- Non-attested key cache mirrors attested path for byte-level metadata parity.
|
||||||
|
- `KEY_SIZE` emitted for EC keys; omitted when `ecCurve` is present, matching AOSP attestation_record.h.
|
||||||
|
- SSE messages synthesized canonically on non-AEAD `updateAad`; passthrough shape normalized.
|
||||||
|
- StrongBox attest version no longer hardcoded; resolved from device context.
|
||||||
|
- TEE op latency floor enforced to defeat micro-timing probes.
|
||||||
|
- Attest key resolution restored to nspace-aware lookup after revert/restore cycle.
|
||||||
|
|
||||||
|
### Self-Sufficient Spoofing
|
||||||
|
- `PatchLevelManager` resolves OS/VENDOR/BOOT patch levels via PIF without external bulletin fetch.
|
||||||
|
- `BulletinPoller` refreshes bulletin data on a schedule, isolated from boot path via umbrella `try/catch`.
|
||||||
|
- Bootloader-lock props pushed via `resetprop` at boot; absent vbmeta complement props filled; `vbmeta.device_state` included.
|
||||||
|
- PIF hot-reload via `FileObserver`; empty source files skipped; future patch dates bounded by `MAX_FUTURE_DAYS`.
|
||||||
|
- Default `security_patch.txt` dropped at install time.
|
||||||
|
- `sepolicy.rule` allows UDP egress for DNS resolution.
|
||||||
|
|
||||||
|
### Key Persistence (PR #22)
|
||||||
|
- Symmetric keys persist across reboots with byte-identical metadata.
|
||||||
|
- Keybox edits no longer wipe stored keys.
|
||||||
|
- Delete marker dropped on key regeneration to prevent stale state.
|
||||||
|
- Defensive symmetric fallback path with clean error codes.
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- `atomicWrite` preserves `[pkg]` sections; errors guarded in `updateTo`.
|
||||||
|
- `applyToProps` serialized against concurrent callers.
|
||||||
|
- `pollOnce` wrapped in umbrella `try/catch`; `BulletinPoller.start` failure isolated from spoofer init.
|
||||||
|
- Spoofer ordering fixed: runs before keystore hook to prevent attest-time prop drift.
|
||||||
|
- `isAutoMode` reads raw package mode; `system=prop` passive default respected.
|
||||||
|
- `mergedContents` propagates read errors instead of swallowing them.
|
||||||
|
- Date regex validation on `currentPatch`; YYYY-MM input skips day synthesis.
|
||||||
|
- Global key-assignment check requires `=` delimiter (no more partial matches).
|
||||||
|
- `validation_rejected` status emitted on invalid spoof input.
|
||||||
|
|
||||||
|
### Action Button UX
|
||||||
|
- Vol+ required to clear `persistent_keys`. Vol- cancels. 10-second timeout defaults to cancel.
|
||||||
|
- Confirmation localized in 22 languages: ar, az, bn, de, el, es-ES, fa, fr, id, it, ja, ko, pl, pt-BR, ru, th, tl, tr, uk, vi, zh-CN, zh-TW.
|
||||||
|
- Every echoed string resolves through `_msg()` against device locale.
|
||||||
|
|
||||||
|
### Build & Ops
|
||||||
|
- Kotlin `jvmTarget` raised to JVM 21.
|
||||||
|
- Gradle auto-rewrites `module/update.json` on packaging.
|
||||||
|
- `scripts/package.sh` locates user-local cargo; rust task receives cargo bin path.
|
||||||
|
- Verified on Xiaomi Android 16 (SDK 35) `v6.0.0-224-Release`. Daemon alive PID 1392. Pending cross-device confirm on OnePlus PKX110 (qcom sun) and Samsung SM-S928B (pineapple).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v6.0.0
|
||||||
|
|
||||||
|
Repository consolidation release. All tee-rebuild work merged as the new main branch.
|
||||||
|
|
||||||
|
### AOSP Self-Signed Cert Compliance
|
||||||
|
- No-challenge keys now generate self-signed certs (subject == issuer, depth 1), matching AOSP `ta/src/keys.rs:451-478`
|
||||||
|
- Both Kotlin (BouncyCastle) and Rust (native-certgen) paths corrected
|
||||||
|
- Eliminates attestation behavioral probes that detect keybox issuer on non-attested keys
|
||||||
|
|
||||||
|
### Stability
|
||||||
|
- Binder stress crash hardening for concurrent generateKey calls
|
||||||
|
- AUTO mode TEE race for consistent attestation on devices with working G10
|
||||||
|
- Oversized transactions routed to software gen instead of crashing
|
||||||
|
- Operation-time params (BLOCK_MODE, PADDING, DIGEST) passed through to CipherPrimitive
|
||||||
|
|
||||||
|
### Banking App Compatibility
|
||||||
|
- Bare `target.txt` entries now default to AUTO mode, resolved at config level to PATCH (working TEE) or GENERATE (broken TEE)
|
||||||
|
- Fixes BHIM and similar banking apps that require TEE-backed attestation keys
|
||||||
|
- Restores v5.0 behavior where AUTO was resolved before the interceptor dispatch, avoiding the non-deterministic `raceTeePatch` path
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
- Version scheme changed to semver (v6.0.0)
|
||||||
|
- Repository moved to TEESimulator-RS as canonical source
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v5.0: AOSP Compliance Overhaul
|
||||||
|
|
||||||
|
Major release integrating 30+ AOSP compliance improvements from upstream PR #157 analysis, layered on top of our StrongBox hardening and native cert gen architecture.
|
||||||
|
|
||||||
|
### Attestation Extension Alignment
|
||||||
|
- 17 enforcement tags added to KeyMintAttestation (ACTIVE_DATETIME, ORIGINATION_EXPIRE, USAGE_EXPIRE, USAGE_COUNT_LIMIT, CALLER_NONCE, UNLOCKED_DEVICE_REQUIRED, INCLUDE_UNIQUE_ID, ROLLBACK_RESISTANCE, EARLY_BOOT_ONLY, ALLOW_WHILE_ON_BODY, TRUSTED_USER_PRESENCE_REQUIRED, TRUSTED_CONFIRMATION_REQUIRED, NO_AUTH_REQUIRED, MAX_USES_PER_BOOT, MAX_BOOT_LEVEL, MIN_MAC_LENGTH, RSA_OAEP_MGF_DIGEST)
|
||||||
|
- BLOCK_MODE encoded as SET OF INTEGER per AOSP attestation_record.h
|
||||||
|
- Version-guarded tags (RSA_OAEP_MGF_DIGEST >=100, ROLLBACK_RESISTANCE >=3, EARLY_BOOT_ONLY >=4)
|
||||||
|
- INCLUDE_UNIQUE_ID computed via HMAC-SHA256 per KeyMint HAL spec using device HBK
|
||||||
|
- AAID gated on attestation challenge presence
|
||||||
|
- Certificate validity defaults aligned with AOSP (epoch notBefore, 9999-12-31 notAfter)
|
||||||
|
|
||||||
|
### Binder Infrastructure
|
||||||
|
- Native transaction code filtering at C++ level, skipping JNI for non-intercepted codes
|
||||||
|
- getNumberOfEntries includes software-generated key count
|
||||||
|
- deleteKey resolves KEY_ID domain via generatedKeys lookup
|
||||||
|
- patchAuthorizations for OS/VENDOR/BOOT patch levels in authorization arrays
|
||||||
|
|
||||||
|
### Software Operation AOSP Conformance
|
||||||
|
- updateAad on non-AEAD operations returns INVALID_TAG (-76), matching AOSP operation.rs
|
||||||
|
- All crypto exceptions wrapped as ServiceSpecificException with correct KeyMint error codes
|
||||||
|
- GCM IV returned in CreateOperationResponse.parameters for encrypt operations
|
||||||
|
- SoftwareOperationBinder methods @Synchronized, matching AOSP Mutex per operation
|
||||||
|
- authorize_create enforcement: PURPOSE validation, algorithm-purpose compatibility, temporal constraints, CALLER_NONCE prohibition, WRAP_KEY rejection
|
||||||
|
|
||||||
|
### Security and Configuration
|
||||||
|
- SELinux permission checks via /proc/pid/attr/current
|
||||||
|
- Per-UID permission verification through IPackageManager.checkPermission
|
||||||
|
- Imported key tracking prevents stale attest-key overrides in getKeyEntry
|
||||||
|
- nspace consistency fix in attest-key override path
|
||||||
|
- TeeLatencySimulator with log-normal distribution matching real hardware profiles
|
||||||
|
- Device-unique HBK seed generated on install (32 bytes from /dev/random)
|
||||||
|
|
||||||
|
### Preserved from v4.8
|
||||||
|
- StrongBox op limits (4 concurrent max, TOO_MANY_OPERATIONS rejection)
|
||||||
|
- LRU operation pruning per security level
|
||||||
|
- Hardware keygen rate limiting (2/30s sliding window, 2 concurrent cap)
|
||||||
|
- Native Rust cert generation with BouncyCastle fallback
|
||||||
|
- Key persistence across reboots
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v4.8.1: StrongBox Op Rejection Fix
|
||||||
|
|
||||||
|
- **StrongBox op limit gate fix** — `trackAndEnforceOpLimit` was only called in the `Domain.KEY_ID` not-found path, so software-generated keys (found via `Domain.APP`) bypassed `STRONGBOX_MAX_CONCURRENT_OPS=4` entirely. DuckDetector's concurrent signing handles test created 24+ operations that all succeeded via LRU pruning instead of being rejected with `TOO_MANY_OPERATIONS (-29)`. Now enforced for all StrongBox createOperation paths.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TEESimulator-RS v4.8: StrongBox Hardening & LRU Pruning
|
||||||
|
|
||||||
|
Tested against DuckDetector on OnePlus (Android 16, KSU). Tamper score dropped from 32 to 8.
|
||||||
|
|
||||||
|
- **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).
|
||||||
|
|||||||
+24
-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 action_i18n.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,21 @@ 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
|
||||||
|
|
||||||
|
if [ ! -f "$CONFIG_DIR/security_patch.txt" ]; then
|
||||||
|
ui_print "- Adding default security patch config (mirror device props)"
|
||||||
|
printf '%s\n' \
|
||||||
|
'# TEESimulator default: mirror live device props.' \
|
||||||
|
'# system=prop reads ro.build.version.security_patch at cert-gen time;' \
|
||||||
|
'# boot and vendor are auto-forced to prop too (ConfigurationManager.kt:253-256).' \
|
||||||
|
'# Override with explicit YYYY-MM-DD dates if you want active spoofing.' \
|
||||||
|
'system=prop' > "$CONFIG_DIR/security_patch.txt"
|
||||||
|
chmod 644 "$CONFIG_DIR/security_patch.txt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$CONFIG_DIR/tee_status.txt"
|
||||||
|
|
||||||
|
if [ ! -f "$CONFIG_DIR/hbk" ]; then
|
||||||
|
ui_print "- Generating device-unique hardware-bound key seed"
|
||||||
|
head -c 32 /dev/random > "$CONFIG_DIR/hbk"
|
||||||
|
fi
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
+15
-3
@@ -1,4 +1,16 @@
|
|||||||
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 *
|
||||||
|
|
||||||
|
allow ksu self:tcp_socket { create connect read write getopt setopt }
|
||||||
|
allow ksu node:tcp_socket node_bind
|
||||||
|
allow ksu port:tcp_socket name_connect
|
||||||
|
allow magisk self:tcp_socket { create connect read write getopt setopt }
|
||||||
|
allow magisk node:tcp_socket node_bind
|
||||||
|
allow magisk port:tcp_socket name_connect
|
||||||
|
|
||||||
|
allow ksu self:udp_socket { create connect read write getopt setopt }
|
||||||
|
allow ksu node:udp_socket node_bind
|
||||||
|
allow ksu port:udp_socket name_connect
|
||||||
|
allow magisk self:udp_socket { create connect read write getopt setopt }
|
||||||
|
allow magisk node:udp_socket node_bind
|
||||||
|
allow magisk port:udp_socket name_connect
|
||||||
|
|||||||
+13
-8
@@ -1,11 +1,16 @@
|
|||||||
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
|
# Clear logd size persist properties once boot completes
|
||||||
done &
|
(
|
||||||
|
until [ "$(getprop sys.boot_completed)" = "1" ]; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
setprop persist.logd.size ""
|
||||||
|
setprop persist.logd.size.crash ""
|
||||||
|
setprop persist.logd.size.system ""
|
||||||
|
setprop persist.logd.size.main ""
|
||||||
|
) &
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#!/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"
|
||||||
|
rm -f "$CONFIG_DIR/security_patch.txt" "$CONFIG_DIR/security_patch.txt.next" "$CONFIG_DIR/last_bulletin_fetch.json"
|
||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "v3.0",
|
"version": "v6.0.1-282",
|
||||||
"versionCode": 38,
|
"versionCode": 282,
|
||||||
"zipUrl": "https://github.com/JingMatrix/TEESimulator/releases/download/v3.0/TEESimulator-v3.0-38-Release.zip",
|
"zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/download/v6.0.1-282/TEESimulator-RS-v6.0.1-282-Release.zip",
|
||||||
"changelog": "https://raw.githubusercontent.com/JingMatrix/TEESimulator/main/module/changelog.md"
|
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/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,582 @@
|
|||||||
|
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];
|
||||||
|
|
||||||
|
// AOSP ta/src/keys.rs:451-478: no challenge = self-signed leaf, chain depth 1
|
||||||
|
pub fn build_self_signed_cert(
|
||||||
|
key_pair: &GeneratedKeyPair,
|
||||||
|
params: &CertGenParams,
|
||||||
|
) -> Result<Vec<Vec<u8>>> {
|
||||||
|
let spki_der = extract_spki_from_pkcs8(&key_pair.private_key_pkcs8)?;
|
||||||
|
let sig_alg_der = signature_algorithm_for_signing_key(&key_pair.private_key_pkcs8, params.algorithm)?;
|
||||||
|
|
||||||
|
let serial_bytes = if let Some(ref serial) = params.cert_serial {
|
||||||
|
serial.clone()
|
||||||
|
} else {
|
||||||
|
vec![1u8]
|
||||||
|
};
|
||||||
|
|
||||||
|
let subject_dn_der = if let Some(ref subject) = params.cert_subject {
|
||||||
|
subject.clone()
|
||||||
|
} else {
|
||||||
|
encode_simple_cn_dn("Android Keystore Key")
|
||||||
|
};
|
||||||
|
|
||||||
|
let not_before = timestamp_to_datetime(params.cert_not_before)?;
|
||||||
|
let not_after = if params.cert_not_after == -1 {
|
||||||
|
// No keybox fallback available; use far-future (year 9999)
|
||||||
|
OffsetDateTime::from_unix_timestamp(253402300799)
|
||||||
|
.unwrap_or_else(|_| OffsetDateTime::now_utc() + time::Duration::days(365 * 30))
|
||||||
|
} else {
|
||||||
|
timestamp_to_datetime(params.cert_not_after)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let extensions_der = build_extensions(None, ¶ms.purposes)?;
|
||||||
|
|
||||||
|
let version_der = encode_der_explicit_tag(0, &encode_der_integer(&[2]));
|
||||||
|
let serial_der = encode_der_integer(&serial_bytes);
|
||||||
|
let validity_der = encode_validity(¬_before, ¬_after);
|
||||||
|
let extensions_tagged = encode_der_explicit_tag(3, &extensions_der);
|
||||||
|
|
||||||
|
// issuer == subject (self-signed, per AOSP ta/src/cert.rs:111-114)
|
||||||
|
let tbs_der = encode_der_sequence(&[
|
||||||
|
&version_der,
|
||||||
|
&serial_der,
|
||||||
|
&sig_alg_der,
|
||||||
|
&subject_dn_der,
|
||||||
|
&validity_der,
|
||||||
|
&subject_dn_der,
|
||||||
|
&spki_der,
|
||||||
|
&extensions_tagged,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let signature_bytes = sign_tbs(&tbs_der, &key_pair.private_key_pkcs8, params.algorithm)?;
|
||||||
|
let signature_bit_string = encode_der_bit_string(&signature_bytes);
|
||||||
|
|
||||||
|
let cert_der = encode_der_sequence(&[
|
||||||
|
&tbs_der,
|
||||||
|
&sig_alg_der,
|
||||||
|
&signature_bit_string,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Ok(vec![cert_der])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_certificate_chain(
|
||||||
|
key_pair: &GeneratedKeyPair,
|
||||||
|
attestation_ext_der: Option<&[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: Option<&[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)?
|
||||||
|
};
|
||||||
|
|
||||||
|
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: Option<&[u8]>, purposes: &[i32]) -> Result<Vec<u8>> {
|
||||||
|
let mut extensions: Vec<Vec<u8>> = Vec::new();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(attest_der) = attestation_ext_der {
|
||||||
|
let attest_ext = build_extension(&encode_der_oid(ATTESTATION_OID), false, attest_der);
|
||||||
|
extensions.push(attest_ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(encode_der_sequence_of(&extensions))
|
||||||
|
}
|
||||||
|
|
||||||
|
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,342 @@
|
|||||||
|
#![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 cert_chain = if params.attestation_challenge.is_some() {
|
||||||
|
let attest_ext = attestation::build_attestation_extension(¶ms)?;
|
||||||
|
certbuilder::build_certificate_chain(&key_pair, Some(&attest_ext), &keybox, ¶ms)?
|
||||||
|
} else {
|
||||||
|
tracing::info!("no attestation challenge, generating self-signed cert (depth 1)");
|
||||||
|
certbuilder::build_self_signed_cert(&key_pair, ¶ms)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain);
|
||||||
|
|
||||||
|
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
+268
@@ -0,0 +1,268 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
# Gradle's buildRustCertgen resolves `cargo` against the daemon's inherited PATH,
|
||||||
|
# not the env we inject via gradle's Exec.environment(). Prepend the per-user
|
||||||
|
# rustup install so non-login shells (CI, IDE-launched terminals, fresh tmux)
|
||||||
|
# still find it without sourcing /etc/profile.d/cargo-path.sh.
|
||||||
|
[ -d "$HOME/.cargo/bin" ] && PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
OUT_DIR="$PROJECT_ROOT/out"
|
||||||
|
|
||||||
|
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,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,8 @@
|
|||||||
|
package android.os;
|
||||||
|
|
||||||
|
public class SELinux {
|
||||||
|
public static boolean checkSELinuxAccess(
|
||||||
|
String scon, String tcon, String tclass, String perm) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,10 @@ public class ServiceManager {
|
|||||||
throw new UnsupportedOperationException("STUB!");
|
throw new UnsupportedOperationException("STUB!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isDeclared(String name) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
public static String[] listServices() {
|
public static String[] listServices() {
|
||||||
throw new UnsupportedOperationException("STUB!");
|
throw new UnsupportedOperationException("STUB!");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package android.os;
|
||||||
|
|
||||||
|
public class ServiceSpecificException extends RuntimeException {
|
||||||
|
public final int errorCode;
|
||||||
|
|
||||||
|
public ServiceSpecificException(int errorCode) {
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceSpecificException(int errorCode, String message) {
|
||||||
|
super(message);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package android.security.maintenance;
|
||||||
|
|
||||||
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile-time stub for the hidden keystore2 maintenance binder
|
||||||
|
* ({@code android.security.maintenance.IKeystoreMaintenance}).
|
||||||
|
*
|
||||||
|
* <p>This module is a {@code compileOnly} dependency, so the real framework class
|
||||||
|
* (which carries the actual {@code TRANSACTION_*} codes) is loaded at runtime. We
|
||||||
|
* only need the {@link #DESCRIPTOR} token to parse the transaction parcel and the
|
||||||
|
* inner {@code Stub} class so {@code getTransactCode} can reflect the real codes.
|
||||||
|
*/
|
||||||
|
public interface IKeystoreMaintenance {
|
||||||
|
String DESCRIPTOR = "android.security.maintenance.IKeystoreMaintenance";
|
||||||
|
|
||||||
|
class Stub {
|
||||||
|
public static IKeystoreMaintenance asInterface(IBinder b) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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