Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
+92
-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,88 @@ 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 }}
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
out
|
out
|
||||||
|
.gradle
|
||||||
|
.kotlin
|
||||||
|
app/build
|
||||||
|
build
|
||||||
|
native-certgen/target
|
||||||
|
app/src/main/jniLibs
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
<p align="center"><b>Full TEE Emulation for Rooted Android</b></p>
|
<p align="center"><b>Full TEE Emulation for Rooted Android</b></p>
|
||||||
<p align="center">Hardware attestation. Software keys. Zero detection.</p>
|
<p align="center">Hardware attestation. Software keys. Zero detection.</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/version-v4.0-blue?style=for-the-badge" alt="v4.0">
|
<a href="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml"><img src="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml/badge.svg" alt="Build"></a>
|
||||||
|
<img src="https://img.shields.io/badge/version-v4.2-blue?style=for-the-badge" alt="v4.2">
|
||||||
<img src="https://img.shields.io/badge/Android-10%2B-green?style=for-the-badge&logo=android" alt="Android 10+">
|
<img src="https://img.shields.io/badge/Android-10%2B-green?style=for-the-badge&logo=android" alt="Android 10+">
|
||||||
<img src="https://img.shields.io/badge/Telegram-community-blue?style=for-the-badge&logo=telegram" alt="Telegram">
|
<img src="https://img.shields.io/badge/Telegram-community-blue?style=for-the-badge&logo=telegram" alt="Telegram">
|
||||||
</p>
|
</p>
|
||||||
@@ -112,6 +113,24 @@ TEESimulator replaces TrickyStore, TrickyStoreOSS, and their forks. Existing con
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🔨 Building from Source
|
||||||
|
|
||||||
|
The CI workflow builds on every push to `main`. You can also build locally or trigger a build from your own fork.
|
||||||
|
|
||||||
|
**Prerequisites:** JDK 21, Android SDK/NDK 27, Rust stable with `aarch64-linux-android` target, `cargo-ndk`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Enginex0/TEESimulator.git
|
||||||
|
cd TEESimulator
|
||||||
|
./gradlew zipRelease zipDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
Output ZIPs land in `out/`. The Gradle build automatically invokes `cargo ndk` to cross-compile `libcertgen.so` before packaging.
|
||||||
|
|
||||||
|
To rebuild from a fork, push to `main` or use **Actions → Build → Run workflow**. The workflow installs all toolchains (Java, Rust, cargo-ndk, ccache) and uploads Release + Debug ZIPs as artifacts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ⚙️ Configuration
|
## ⚙️ Configuration
|
||||||
|
|
||||||
All configuration files live at `/data/adb/tricky_store/` and are monitored by `FileObserver` — changes take effect immediately without rebooting.
|
All configuration files live at `/data/adb/tricky_store/` and are monitored by `FileObserver` — changes take effect immediately without rebooting.
|
||||||
|
|||||||
+17
-17
@@ -29,7 +29,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java)
|
|||||||
|
|
||||||
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
|
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
|
||||||
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
|
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
|
||||||
val verName = "v4.0"
|
val verName = "v4.8"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "org.matrix.TEESimulator"
|
namespace = "org.matrix.TEESimulator"
|
||||||
@@ -73,7 +73,7 @@ dependencies {
|
|||||||
|
|
||||||
// --- Rust native cert gen build task ---
|
// --- Rust native cert gen build task ---
|
||||||
val buildRustCertgen by tasks.registering(Exec::class) {
|
val buildRustCertgen by tasks.registering(Exec::class) {
|
||||||
group = "TEESimulator Native Build"
|
group = "TEESimulator-RS Native Build"
|
||||||
description = "Builds libcertgen.so via cargo-ndk for arm64-v8a."
|
description = "Builds libcertgen.so via cargo-ndk for arm64-v8a."
|
||||||
|
|
||||||
workingDir = rootProject.projectDir.resolve("native-certgen")
|
workingDir = rootProject.projectDir.resolve("native-certgen")
|
||||||
@@ -108,21 +108,21 @@ 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(buildRustCertgen)
|
||||||
|
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
@@ -140,12 +140,13 @@ 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)) {
|
||||||
|
into("lib")
|
||||||
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so", "**/libcertgen.so")
|
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so", "**/libcertgen.so")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,8 +162,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})",
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +173,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)
|
||||||
|
|
||||||
@@ -186,7 +186,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)
|
||||||
@@ -200,7 +200,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(
|
||||||
@@ -213,7 +213,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")
|
||||||
|
|||||||
@@ -358,6 +358,12 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
|||||||
if (txn_data->data_size > kMaxInterceptableDataSize)
|
if (txn_data->data_size > kMaxInterceptableDataSize)
|
||||||
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;
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <sys/prctl.h>
|
#include <sys/prctl.h>
|
||||||
|
#include <sys/resource.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
static volatile sig_atomic_t should_exit = 0;
|
static volatile sig_atomic_t should_exit = 0;
|
||||||
|
|
||||||
@@ -27,7 +29,12 @@ int main(int argc, char *argv[]) {
|
|||||||
const char *daemon_path = argv[1];
|
const char *daemon_path = argv[1];
|
||||||
char **daemon_argv = &argv[1];
|
char **daemon_argv = &argv[1];
|
||||||
|
|
||||||
|
int backoff_ms = 500;
|
||||||
|
|
||||||
while (!should_exit) {
|
while (!should_exit) {
|
||||||
|
struct timespec child_start;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &child_start);
|
||||||
|
|
||||||
pid_t pid = fork();
|
pid_t pid = fork();
|
||||||
|
|
||||||
if (pid < 0) {
|
if (pid < 0) {
|
||||||
@@ -39,6 +46,7 @@ int main(int argc, char *argv[]) {
|
|||||||
if (pid == 0) {
|
if (pid == 0) {
|
||||||
// Child: become the daemon
|
// Child: become the daemon
|
||||||
prctl(PR_SET_PDEATHSIG, SIGKILL); // Die if parent dies
|
prctl(PR_SET_PDEATHSIG, SIGKILL); // Die if parent dies
|
||||||
|
setpriority(PRIO_PROCESS, 0, 10); // lower CPU priority than foreground
|
||||||
execv(daemon_path, daemon_argv);
|
execv(daemon_path, daemon_argv);
|
||||||
perror("execv failed");
|
perror("execv failed");
|
||||||
_exit(127);
|
_exit(127);
|
||||||
@@ -50,7 +58,18 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
if (should_exit) break;
|
if (should_exit) break;
|
||||||
|
|
||||||
// Instant restart - no delay
|
// 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;
|
return 0;
|
||||||
|
|||||||
@@ -23,8 +23,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.
|
||||||
|
|||||||
@@ -182,11 +182,40 @@ 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()),
|
||||||
),
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
list.addAll(
|
||||||
|
listOf(
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE),
|
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE),
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
true,
|
true,
|
||||||
@@ -199,6 +228,7 @@ object AttestationBuilder {
|
|||||||
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)
|
||||||
|
|||||||
@@ -89,5 +89,5 @@ object AttestationConstants {
|
|||||||
|
|
||||||
// --- Other Constants ---
|
// --- Other Constants ---
|
||||||
// https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp
|
// https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp
|
||||||
const val CHALLENGE_LENGTH_LIMIT = 128 // kMaximumAttestationChallengeLength
|
const val CHALLENGE_LENGTH_LIMIT = 128
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ 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 origin: Int?,
|
||||||
val blockMode: List<Int>,
|
val blockMode: List<Int>,
|
||||||
@@ -53,7 +53,7 @@ data class KeyMintAttestation(
|
|||||||
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)]
|
// AOSP: [key_param(tag = ORIGIN, field = Origin)]
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ object InterceptorUtils {
|
|||||||
fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply {
|
fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply {
|
||||||
val parcel = Parcel.obtain().apply {
|
val parcel = Parcel.obtain().apply {
|
||||||
writeInt(EX_SERVICE_SPECIFIC)
|
writeInt(EX_SERVICE_SPECIFIC)
|
||||||
writeInt(errorCode)
|
|
||||||
writeString(null)
|
writeString(null)
|
||||||
|
writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196)
|
||||||
|
writeInt(errorCode)
|
||||||
}
|
}
|
||||||
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
}
|
}
|
||||||
@@ -119,6 +120,8 @@ object InterceptorUtils {
|
|||||||
|
|
||||||
/** 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-6
@@ -10,6 +10,7 @@ import android.system.keystore2.KeyDescriptor
|
|||||||
import android.system.keystore2.KeyEntryResponse
|
import android.system.keystore2.KeyEntryResponse
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
||||||
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
@@ -54,6 +55,9 @@ 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 val deletedSoftwareKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
||||||
|
|
||||||
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"
|
||||||
@@ -156,8 +160,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
||||||
|
|
||||||
if (code == DELETE_KEY_TRANSACTION) {
|
if (code == DELETE_KEY_TRANSACTION) {
|
||||||
if (KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null) {
|
val wasSoftwareKey = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null
|
||||||
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
||||||
|
if (wasSoftwareKey) {
|
||||||
|
deletedSoftwareKeys.add(keyId)
|
||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response."
|
"[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response."
|
||||||
)
|
)
|
||||||
@@ -166,9 +172,14 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
|
|
||||||
val response =
|
val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
||||||
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
if (response == null) {
|
||||||
?: return TransactionResult.Continue
|
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")
|
||||||
@@ -294,7 +305,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
certChain = keyData.second,
|
certChain = keyData.second,
|
||||||
algorithm = parsedParameters.algorithm,
|
algorithm = parsedParameters.algorithm,
|
||||||
keySize = parsedParameters.keySize,
|
keySize = parsedParameters.keySize,
|
||||||
ecCurve = parsedParameters.ecCurve,
|
ecCurve = parsedParameters.ecCurve ?: 0,
|
||||||
purposes = parsedParameters.purpose,
|
purposes = parsedParameters.purpose,
|
||||||
digests = parsedParameters.digest,
|
digests = parsedParameters.digest,
|
||||||
isAttestationKey = true,
|
isAttestationKey = true,
|
||||||
@@ -326,6 +337,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
)
|
)
|
||||||
finalChain =
|
finalChain =
|
||||||
AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
||||||
|
KeyMintSecurityLevelInterceptor.patchedChains[keyId] = finalChain
|
||||||
}
|
}
|
||||||
|
|
||||||
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
|
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
|
||||||
|
|||||||
+1
-1
@@ -129,7 +129,7 @@ object ListEntriesHandler {
|
|||||||
startPastAlias: String?,
|
startPastAlias: String?,
|
||||||
): List<KeyDescriptor> {
|
): List<KeyDescriptor> {
|
||||||
return KeyMintSecurityLevelInterceptor.generatedKeys.keys
|
return KeyMintSecurityLevelInterceptor.generatedKeys.keys
|
||||||
.filter { it.uid == uid && (startPastAlias == null || it.alias < startPastAlias) }
|
.filter { it.uid == uid && (startPastAlias == null || it.alias > startPastAlias) }
|
||||||
.map { keyId ->
|
.map { keyId ->
|
||||||
KeyDescriptor().apply {
|
KeyDescriptor().apply {
|
||||||
this.domain = Domain.APP
|
this.domain = Domain.APP
|
||||||
|
|||||||
+2
@@ -129,6 +129,7 @@ object GeneratedKeyPersistence {
|
|||||||
val file = File(PERSISTENCE_DIR, keyFileName(keyId.uid, keyId.alias))
|
val file = File(PERSISTENCE_DIR, keyFileName(keyId.uid, keyId.alias))
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
if (file.delete()) {
|
if (file.delete()) {
|
||||||
|
fileLocks.remove(keyFileName(keyId.uid, keyId.alias))
|
||||||
SystemLogger.debug("Deleted persisted key: $keyId")
|
SystemLogger.debug("Deleted persisted key: $keyId")
|
||||||
} else {
|
} else {
|
||||||
SystemLogger.warning("Failed to delete persisted key file: ${file.name}")
|
SystemLogger.warning("Failed to delete persisted key file: ${file.name}")
|
||||||
@@ -158,6 +159,7 @@ object GeneratedKeyPersistence {
|
|||||||
if (file.delete()) count++
|
if (file.delete()) count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fileLocks.clear()
|
||||||
SystemLogger.info("Deleted $count persisted key files")
|
SystemLogger.info("Deleted $count persisted key files")
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
SystemLogger.error("Failed to delete all persisted keys", e)
|
SystemLogger.error("Failed to delete all persisted keys", e)
|
||||||
|
|||||||
+187
-28
@@ -1,8 +1,11 @@
|
|||||||
package org.matrix.TEESimulator.interception.keystore.shim
|
package org.matrix.TEESimulator.interception.keystore.shim
|
||||||
|
|
||||||
import android.hardware.security.keymint.Algorithm
|
import android.hardware.security.keymint.Algorithm
|
||||||
|
import android.hardware.security.keymint.EcCurve
|
||||||
import android.hardware.security.keymint.KeyParameter
|
import android.hardware.security.keymint.KeyParameter
|
||||||
import android.hardware.security.keymint.KeyParameterValue
|
import android.hardware.security.keymint.KeyParameterValue
|
||||||
|
import android.hardware.security.keymint.KeyOrigin
|
||||||
|
import android.hardware.security.keymint.SecurityLevel
|
||||||
import android.hardware.security.keymint.Tag
|
import android.hardware.security.keymint.Tag
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
@@ -16,6 +19,7 @@ import java.security.cert.Certificate
|
|||||||
import java.security.cert.CertificateFactory
|
import java.security.cert.CertificateFactory
|
||||||
import java.security.spec.PKCS8EncodedKeySpec
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import org.matrix.TEESimulator.attestation.AttestationBuilder
|
import org.matrix.TEESimulator.attestation.AttestationBuilder
|
||||||
import org.matrix.TEESimulator.attestation.AttestationConstants
|
import org.matrix.TEESimulator.attestation.AttestationConstants
|
||||||
@@ -32,6 +36,7 @@ import org.matrix.TEESimulator.pki.CertificateHelper
|
|||||||
import org.matrix.TEESimulator.pki.KeyBoxManager
|
import org.matrix.TEESimulator.pki.KeyBoxManager
|
||||||
import org.matrix.TEESimulator.pki.NativeCertGen
|
import org.matrix.TEESimulator.pki.NativeCertGen
|
||||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||||
|
import org.matrix.TEESimulator.util.AndroidPermissionUtils
|
||||||
|
|
||||||
class KeyMintSecurityLevelInterceptor(
|
class KeyMintSecurityLevelInterceptor(
|
||||||
private val original: IKeystoreSecurityLevel,
|
private val original: IKeystoreSecurityLevel,
|
||||||
@@ -44,6 +49,9 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
val response: KeyEntryResponse,
|
val response: KeyEntryResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val activeOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<SoftwareOperation>>()
|
||||||
|
private val recentOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<Long>>()
|
||||||
|
|
||||||
override fun onPreTransact(
|
override fun onPreTransact(
|
||||||
txId: Long,
|
txId: Long,
|
||||||
target: IBinder,
|
target: IBinder,
|
||||||
@@ -191,35 +199,92 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun pruneOpsForUid(uid: Int, newOp: SoftwareOperation, maxOps: Int = MAX_CONCURRENT_OPS_PER_UID) {
|
||||||
|
val ops = activeOps.computeIfAbsent(uid) { ConcurrentLinkedDeque() }
|
||||||
|
val before = ops.size
|
||||||
|
ops.removeIf { it.finalized }
|
||||||
|
val afterClean = ops.size
|
||||||
|
while (ops.size >= maxOps) {
|
||||||
|
val oldest = ops.pollFirst() ?: break
|
||||||
|
if (!oldest.finalized) {
|
||||||
|
SystemLogger.info("[LRU] Pruning operation for uid=$uid (active=${ops.size}/$maxOps)")
|
||||||
|
oldest.abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ops.addLast(newOp)
|
||||||
|
SystemLogger.debug("[LRU] uid=$uid ops: before=$before cleaned=${before - afterClean} active=${ops.size}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun trackAndEnforceOpLimit(callingUid: Int, txId: Long): TransactionResult? {
|
||||||
|
if (securityLevel != SecurityLevel.STRONGBOX) return null
|
||||||
|
val timestamps = recentOps.computeIfAbsent(callingUid) { ConcurrentLinkedDeque() }
|
||||||
|
val cutoff = System.nanoTime() - STRONGBOX_OP_WINDOW_NS
|
||||||
|
timestamps.removeIf { it < cutoff }
|
||||||
|
val swOps = activeOps[callingUid]?.count { !it.finalized } ?: 0
|
||||||
|
if (timestamps.size + swOps >= STRONGBOX_MAX_CONCURRENT_OPS) {
|
||||||
|
SystemLogger.info("[TX_ID: $txId] StrongBox op limit reached for uid=$callingUid (hw=${timestamps.size} sw=$swOps max=$STRONGBOX_MAX_CONCURRENT_OPS)")
|
||||||
|
return InterceptorUtils.createErrorReply(KEYMINT_TOO_MANY_OPERATIONS)
|
||||||
|
}
|
||||||
|
timestamps.addLast(System.nanoTime())
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleCreateOperation(
|
private fun handleCreateOperation(
|
||||||
txId: Long,
|
txId: Long,
|
||||||
callingUid: Int,
|
callingUid: Int,
|
||||||
data: Parcel,
|
data: Parcel,
|
||||||
): TransactionResult {
|
): TransactionResult {
|
||||||
|
SystemLogger.debug("[TX_ID: $txId] createOperation parcel: dataSize=${data.dataSize()} dataAvail=${data.dataAvail()} dataPos=${data.dataPosition()}")
|
||||||
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||||
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
|
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
|
||||||
|
|
||||||
// An operation must use the KEY_ID domain.
|
SystemLogger.debug("[TX_ID: $txId] createOperation descriptor: domain=${keyDescriptor.domain} nspace=${keyDescriptor.nspace} alias=${keyDescriptor.alias}")
|
||||||
if (keyDescriptor.domain != Domain.KEY_ID) {
|
|
||||||
return TransactionResult.ContinueAndSkipPost
|
// Android framework calls createOperation with domain=APP+alias;
|
||||||
|
// keystore2 internally resolves to KEY_ID — but software keys never
|
||||||
|
// reach keystore2's database, so we must handle both lookup paths.
|
||||||
|
val generatedKeyInfo = when (keyDescriptor.domain) {
|
||||||
|
Domain.APP -> {
|
||||||
|
val alias = keyDescriptor.alias ?: run {
|
||||||
|
SystemLogger.info("[TX_ID: $txId] createOperation domain=APP with null alias, forwarding to HAL")
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
generatedKeys[KeyIdentifier(callingUid, alias)] ?: run {
|
||||||
|
SystemLogger.info("[TX_ID: $txId] createOperation alias=$alias not in generatedKeys, forwarding to HAL")
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Domain.KEY_ID -> {
|
||||||
|
findGeneratedKeyByKeyId(callingUid, keyDescriptor.nspace) ?: run {
|
||||||
|
trackAndEnforceOpLimit(callingUid, txId)?.let { return it }
|
||||||
|
SystemLogger.info("[TX_ID: $txId] createOperation KeyId(${keyDescriptor.nspace}) NOT FOUND for uid=$callingUid. Forwarding to HAL.")
|
||||||
|
return TransactionResult.Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
SystemLogger.info("[TX_ID: $txId] createOperation domain=${keyDescriptor.domain}, forwarding to HAL")
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val nspace = keyDescriptor.nspace
|
trackAndEnforceOpLimit(callingUid, txId)?.let { return it }
|
||||||
val generatedKeyInfo = findGeneratedKeyByKeyId(callingUid, nspace)
|
|
||||||
|
|
||||||
if (generatedKeyInfo == null) {
|
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for uid=$callingUid.")
|
||||||
SystemLogger.debug(
|
|
||||||
"[TX_ID: $txId] Operation for unknown/hardware KeyId ($nspace). Forwarding."
|
|
||||||
)
|
|
||||||
return TransactionResult.Continue
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for KeyId $nspace.")
|
|
||||||
|
|
||||||
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
||||||
val parsedParams = KeyMintAttestation(params)
|
val parsedParams = KeyMintAttestation(params).let { p ->
|
||||||
|
if (p.algorithm != 0) p
|
||||||
|
else p.copy(algorithm = when (generatedKeyInfo.keyPair.private.algorithm) {
|
||||||
|
"EC", "ECDSA" -> Algorithm.EC
|
||||||
|
"RSA" -> Algorithm.RSA
|
||||||
|
else -> p.algorithm
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams)
|
val opLatency = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_OP_LATENCY_FLOOR_MS else 0L
|
||||||
|
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams, opLatency)
|
||||||
|
val maxOps = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_MAX_CONCURRENT_OPS else MAX_CONCURRENT_OPS_PER_UID
|
||||||
|
pruneOpsForUid(callingUid, softwareOperation, maxOps)
|
||||||
val operationBinder = SoftwareOperationBinder(softwareOperation)
|
val operationBinder = SoftwareOperationBinder(softwareOperation)
|
||||||
|
|
||||||
val response =
|
val response =
|
||||||
@@ -254,6 +319,43 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
return InterceptorUtils.createErrorReply(KEYMINT_INVALID_INPUT_LENGTH)
|
return InterceptorUtils.createErrorReply(KEYMINT_INVALID_INPUT_LENGTH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.any { it.tag == Tag.CREATION_DATETIME }) {
|
||||||
|
SystemLogger.warning("[TX_ID: $txId] Rejecting CREATION_DATETIME in generateKey params")
|
||||||
|
return InterceptorUtils.createErrorReply(RESPONSE_INVALID_ARGUMENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.any { it.tag == Tag.DEVICE_UNIQUE_ATTESTATION } && !AndroidPermissionUtils.hasUniqueIdAttestationPermission(callingUid)) {
|
||||||
|
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_UNIQUE_ATTESTATION for uid=$callingUid")
|
||||||
|
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasDeviceIdAttestation = params.any {
|
||||||
|
it.tag == Tag.ATTESTATION_ID_IMEI ||
|
||||||
|
it.tag == Tag.ATTESTATION_ID_MEID ||
|
||||||
|
it.tag == Tag.ATTESTATION_ID_SERIAL ||
|
||||||
|
it.tag == Tag.DEVICE_UNIQUE_ATTESTATION ||
|
||||||
|
it.tag == Tag.ATTESTATION_ID_SECOND_IMEI
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hasDeviceIdAttestation && !AndroidPermissionUtils.hasDeviceAttestationPermission(callingUid)) {
|
||||||
|
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_ID_ATTESTATION for uid=$callingUid")
|
||||||
|
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isSymmetric = parsedParams.algorithm == Algorithm.AES ||
|
||||||
|
parsedParams.algorithm == Algorithm.HMAC ||
|
||||||
|
parsedParams.algorithm == Algorithm.TRIPLE_DES
|
||||||
|
|
||||||
|
if (isSymmetric) {
|
||||||
|
SystemLogger.debug("[TX_ID: $txId] Symmetric algorithm ${parsedParams.algorithm} → forwarding to HAL")
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
|
||||||
|
if (securityLevel == SecurityLevel.STRONGBOX && !isStrongBoxCapable(parsedParams)) {
|
||||||
|
SystemLogger.info("[TX_ID: $txId] StrongBox-unsupported params (algo=${parsedParams.algorithm} size=${parsedParams.keySize}) → forwarding to HAL for rejection")
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
|
||||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||||
val isAttestKeyRequest = parsedParams.isAttestKey()
|
val isAttestKeyRequest = parsedParams.isAttestKey()
|
||||||
|
|
||||||
@@ -304,6 +406,7 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
keyId: KeyIdentifier,
|
keyId: KeyIdentifier,
|
||||||
isAttestKeyRequest: Boolean,
|
isAttestKeyRequest: Boolean,
|
||||||
): TransactionResult {
|
): TransactionResult {
|
||||||
|
val startNs = System.nanoTime()
|
||||||
keyDescriptor.nspace = secureRandom.nextLong()
|
keyDescriptor.nspace = secureRandom.nextLong()
|
||||||
SystemLogger.info("Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}].")
|
SystemLogger.info("Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}].")
|
||||||
|
|
||||||
@@ -319,7 +422,7 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
} ?: throw Exception("Both native and BouncyCastle cert gen failed.")
|
} ?: throw Exception("Both native and BouncyCastle cert gen failed.")
|
||||||
|
|
||||||
cleanupKeyData(keyId)
|
cleanupKeyData(keyId)
|
||||||
val response = buildKeyEntryResponse(keyData.second, parsedParams, keyDescriptor)
|
val response = buildKeyEntryResponse(callingUid, keyData.second, parsedParams, keyDescriptor)
|
||||||
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response)
|
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response)
|
||||||
if (isAttestKeyRequest) attestationKeys.add(keyId)
|
if (isAttestKeyRequest) attestationKeys.add(keyId)
|
||||||
|
|
||||||
@@ -331,12 +434,17 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
certChain = keyData.second.toList(),
|
certChain = keyData.second.toList(),
|
||||||
algorithm = parsedParams.algorithm,
|
algorithm = parsedParams.algorithm,
|
||||||
keySize = parsedParams.keySize,
|
keySize = parsedParams.keySize,
|
||||||
ecCurve = parsedParams.ecCurve,
|
ecCurve = parsedParams.ecCurve ?: 0,
|
||||||
purposes = parsedParams.purpose,
|
purposes = parsedParams.purpose,
|
||||||
digests = parsedParams.digest,
|
digests = parsedParams.digest,
|
||||||
isAttestationKey = isAttestKeyRequest,
|
isAttestationKey = isAttestKeyRequest,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
|
||||||
|
val floor = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_KEYGEN_LATENCY_FLOOR_MS else TEE_LATENCY_FLOOR_MS
|
||||||
|
val delayMs = floor - elapsedMs
|
||||||
|
if (delayMs > 0) Thread.sleep(delayMs)
|
||||||
|
|
||||||
return InterceptorUtils.createTypedObjectReply(response.metadata)
|
return InterceptorUtils.createTypedObjectReply(response.metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +473,7 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
val config = CertGenConfig(
|
val config = CertGenConfig(
|
||||||
algorithm = params.algorithm,
|
algorithm = params.algorithm,
|
||||||
keySize = params.keySize,
|
keySize = params.keySize,
|
||||||
ecCurve = params.ecCurve,
|
ecCurve = params.ecCurve ?: 0,
|
||||||
rsaPublicExponent = params.rsaPublicExponent?.toLong() ?: 65537L,
|
rsaPublicExponent = params.rsaPublicExponent?.toLong() ?: 65537L,
|
||||||
attestationChallenge = params.attestationChallenge,
|
attestationChallenge = params.attestationChallenge,
|
||||||
purposes = params.purpose.toIntArray(),
|
purposes = params.purpose.toIntArray(),
|
||||||
@@ -409,16 +517,25 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildKeyEntryResponse(
|
private fun buildKeyEntryResponse(
|
||||||
|
callingUid: Int,
|
||||||
chain: List<Certificate>,
|
chain: List<Certificate>,
|
||||||
params: KeyMintAttestation,
|
params: KeyMintAttestation,
|
||||||
descriptor: KeyDescriptor,
|
descriptor: KeyDescriptor,
|
||||||
): KeyEntryResponse {
|
): KeyEntryResponse {
|
||||||
|
val normalizedKeyDescriptor =
|
||||||
|
KeyDescriptor().apply {
|
||||||
|
domain = Domain.KEY_ID
|
||||||
|
nspace = descriptor.nspace
|
||||||
|
alias = null
|
||||||
|
blob = null
|
||||||
|
}
|
||||||
val metadata =
|
val metadata =
|
||||||
KeyMetadata().apply {
|
KeyMetadata().apply {
|
||||||
keySecurityLevel = securityLevel
|
keySecurityLevel = securityLevel
|
||||||
key = descriptor
|
key = normalizedKeyDescriptor
|
||||||
CertificateHelper.updateCertificateChain(this, chain.toTypedArray()).getOrThrow()
|
CertificateHelper.updateCertificateChain(this, chain.toTypedArray()).getOrThrow()
|
||||||
authorizations = params.toAuthorizations(securityLevel)
|
authorizations = params.toAuthorizations(callingUid, securityLevel)
|
||||||
|
modificationTimeMs = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
return KeyEntryResponse().apply {
|
return KeyEntryResponse().apply {
|
||||||
this.metadata = metadata
|
this.metadata = metadata
|
||||||
@@ -495,7 +612,7 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
secondImei = null,
|
secondImei = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
val response = buildKeyEntryResponse(certChain, attestation, descriptor)
|
val response = buildKeyEntryResponse(record.uid, certChain, attestation, descriptor)
|
||||||
generatedKeys[keyId] = GeneratedKeyInfo(keyPair, record.nspace, response)
|
generatedKeys[keyId] = GeneratedKeyInfo(keyPair, record.nspace, response)
|
||||||
if (record.isAttestationKey) attestationKeys.add(keyId)
|
if (record.isAttestationKey) attestationKeys.add(keyId)
|
||||||
|
|
||||||
@@ -515,15 +632,29 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
// Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead
|
// Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead
|
||||||
private const val MAX_ALIAS_LENGTH = 256 * 1024
|
private const val MAX_ALIAS_LENGTH = 256 * 1024
|
||||||
private const val KEYMINT_INVALID_INPUT_LENGTH = -21
|
private const val KEYMINT_INVALID_INPUT_LENGTH = -21
|
||||||
|
private const val RESPONSE_INVALID_ARGUMENT = 20
|
||||||
|
private const val TEE_LATENCY_FLOOR_MS = 15L
|
||||||
|
private const val STRONGBOX_KEYGEN_LATENCY_FLOOR_MS = 250L
|
||||||
|
private const val STRONGBOX_OP_LATENCY_FLOOR_MS = 80L
|
||||||
|
private const val KEYMINT_TOO_MANY_OPERATIONS = -29
|
||||||
|
private const val KEYMINT_CANNOT_ATTEST_IDS = -66
|
||||||
|
private const val MAX_CONCURRENT_OPS_PER_UID = 15
|
||||||
|
private const val STRONGBOX_MAX_CONCURRENT_OPS = 4
|
||||||
|
private const val STRONGBOX_OP_WINDOW_NS = 10_000_000_000L // 10s
|
||||||
private const val MAX_CONCURRENT_HW_KEYGEN_PER_UID = 2
|
private const val MAX_CONCURRENT_HW_KEYGEN_PER_UID = 2
|
||||||
// Sliding window: max hardware keygen permits per UID within the burst window
|
// Sliding window: max hardware keygen permits per UID within the burst window
|
||||||
private const val MAX_HW_KEYGEN_PER_WINDOW = 2
|
private const val MAX_HW_KEYGEN_PER_WINDOW = 2
|
||||||
private const val BURST_WINDOW_MS = 30_000L
|
private const val BURST_WINDOW_MS = 30_000L
|
||||||
|
|
||||||
private val uidHardwareKeygenCount = ConcurrentHashMap<Int, AtomicInteger>()
|
private val uidHardwareKeygenCount = ConcurrentHashMap<Int, AtomicInteger>()
|
||||||
private val hardwareKeygenTxIds = ConcurrentHashMap.newKeySet<Long>()
|
private val hardwareKeygenTxIds = ConcurrentHashMap.newKeySet<Long>()
|
||||||
private val uidKeygenTimestamps = ConcurrentHashMap<Int, MutableList<Long>>()
|
private val uidKeygenTimestamps = ConcurrentHashMap<Int, MutableList<Long>>()
|
||||||
|
|
||||||
|
private fun isStrongBoxCapable(params: KeyMintAttestation): Boolean = when (params.algorithm) {
|
||||||
|
Algorithm.RSA -> params.keySize <= 2048
|
||||||
|
Algorithm.EC -> params.ecCurve == null || params.ecCurve == EcCurve.P_256
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
private fun hardwareKeygenCount(uid: Int): AtomicInteger =
|
private fun hardwareKeygenCount(uid: Int): AtomicInteger =
|
||||||
uidHardwareKeygenCount.computeIfAbsent(uid) { AtomicInteger(0) }
|
uidHardwareKeygenCount.computeIfAbsent(uid) { AtomicInteger(0) }
|
||||||
|
|
||||||
@@ -532,6 +663,10 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() }
|
val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() }
|
||||||
synchronized(timestamps) {
|
synchronized(timestamps) {
|
||||||
timestamps.removeAll { now - it > BURST_WINDOW_MS }
|
timestamps.removeAll { now - it > BURST_WINDOW_MS }
|
||||||
|
if (timestamps.isEmpty()) {
|
||||||
|
uidKeygenTimestamps.remove(uid, timestamps)
|
||||||
|
uidHardwareKeygenCount.remove(uid)
|
||||||
|
}
|
||||||
return timestamps.size
|
return timestamps.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -565,8 +700,7 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>()
|
val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>()
|
||||||
// Caches patched chains to prevent re-generation and signature inconsistencies
|
val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
|
||||||
private val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
|
|
||||||
val attestationKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
val attestationKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
||||||
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
|
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
|
||||||
|
|
||||||
@@ -626,7 +760,10 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Authorization> {
|
private fun KeyMintAttestation.toAuthorizations(
|
||||||
|
callingUid: Int,
|
||||||
|
securityLevel: Int,
|
||||||
|
): Array<Authorization> {
|
||||||
val authList = mutableListOf<Authorization>()
|
val authList = mutableListOf<Authorization>()
|
||||||
|
|
||||||
fun createAuth(tag: Int, value: KeyParameterValue): Authorization {
|
fun createAuth(tag: Int, value: KeyParameterValue): Authorization {
|
||||||
@@ -641,13 +778,35 @@ private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Autho
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authList.add(createAuth(Tag.ALGORITHM, KeyParameterValue.algorithm(this.algorithm)))
|
||||||
|
if (this.ecCurve != null) {
|
||||||
|
authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve)))
|
||||||
|
}
|
||||||
this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) }
|
this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) }
|
||||||
this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) }
|
this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) }
|
||||||
|
this.padding.forEach { authList.add(createAuth(Tag.PADDING, KeyParameterValue.paddingMode(it))) }
|
||||||
authList.add(createAuth(Tag.ALGORITHM, KeyParameterValue.algorithm(this.algorithm)))
|
|
||||||
authList.add(createAuth(Tag.KEY_SIZE, KeyParameterValue.integer(this.keySize)))
|
authList.add(createAuth(Tag.KEY_SIZE, KeyParameterValue.integer(this.keySize)))
|
||||||
authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve)))
|
if (this.rsaPublicExponent != null) {
|
||||||
|
authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong())))
|
||||||
|
}
|
||||||
authList.add(createAuth(Tag.NO_AUTH_REQUIRED, KeyParameterValue.boolValue(true)))
|
authList.add(createAuth(Tag.NO_AUTH_REQUIRED, KeyParameterValue.boolValue(true)))
|
||||||
|
authList.add(createAuth(Tag.ORIGIN, KeyParameterValue.origin(this.origin ?: KeyOrigin.GENERATED)))
|
||||||
|
authList.add(createAuth(Tag.OS_VERSION, KeyParameterValue.integer(AndroidDeviceUtils.osVersion)))
|
||||||
|
|
||||||
|
val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid)
|
||||||
|
if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
|
||||||
|
authList.add(createAuth(Tag.OS_PATCHLEVEL, KeyParameterValue.integer(osPatch)))
|
||||||
|
}
|
||||||
|
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
|
||||||
|
if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
|
||||||
|
authList.add(createAuth(Tag.VENDOR_PATCHLEVEL, KeyParameterValue.integer(vendorPatch)))
|
||||||
|
}
|
||||||
|
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
|
||||||
|
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
|
||||||
|
authList.add(createAuth(Tag.BOOT_PATCHLEVEL, KeyParameterValue.integer(bootPatch)))
|
||||||
|
}
|
||||||
|
authList.add(createAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis())))
|
||||||
|
authList.add(createAuth(Tag.USER_ID, KeyParameterValue.integer(callingUid / 100000)))
|
||||||
|
|
||||||
return authList.toTypedArray()
|
return authList.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|||||||
+89
-26
@@ -6,6 +6,7 @@ import android.hardware.security.keymint.Digest
|
|||||||
import android.hardware.security.keymint.KeyPurpose
|
import android.hardware.security.keymint.KeyPurpose
|
||||||
import android.hardware.security.keymint.PaddingMode
|
import android.hardware.security.keymint.PaddingMode
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
|
import android.os.ServiceSpecificException
|
||||||
import android.system.keystore2.IKeystoreOperation
|
import android.system.keystore2.IKeystoreOperation
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.Signature
|
import java.security.Signature
|
||||||
@@ -17,10 +18,9 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
|||||||
|
|
||||||
// A sealed interface to represent the different cryptographic operations we can perform.
|
// A sealed interface to represent the different cryptographic operations we can perform.
|
||||||
private sealed interface CryptoPrimitive {
|
private sealed interface CryptoPrimitive {
|
||||||
|
fun updateAad(aadInput: ByteArray?) {}
|
||||||
fun update(data: ByteArray?): ByteArray?
|
fun update(data: ByteArray?): ByteArray?
|
||||||
|
|
||||||
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
|
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
|
||||||
|
|
||||||
fun abort()
|
fun abort()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,16 +34,17 @@ private object JcaAlgorithmMapper {
|
|||||||
Digest.SHA_2_512 -> "SHA512"
|
Digest.SHA_2_512 -> "SHA512"
|
||||||
else -> "NONE"
|
else -> "NONE"
|
||||||
}
|
}
|
||||||
val keyAlgo =
|
return when (params.algorithm) {
|
||||||
when (params.algorithm) {
|
Algorithm.EC -> "${digest}withECDSA"
|
||||||
Algorithm.EC -> "ECDSA"
|
Algorithm.RSA -> {
|
||||||
Algorithm.RSA -> "RSA"
|
val isPss = params.padding.firstOrNull() == PaddingMode.RSA_PSS
|
||||||
else ->
|
if (isPss) "${digest}withRSA/PSS" else "${digest}withRSA"
|
||||||
throw IllegalArgumentException(
|
|
||||||
"Unsupported signature algorithm: ${params.algorithm}"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return "${digest}with${keyAlgo}"
|
else ->
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Unsupported signature algorithm: ${params.algorithm}"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
|
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
|
||||||
@@ -60,16 +61,18 @@ private object JcaAlgorithmMapper {
|
|||||||
when (params.blockMode.firstOrNull()) {
|
when (params.blockMode.firstOrNull()) {
|
||||||
BlockMode.ECB -> "ECB"
|
BlockMode.ECB -> "ECB"
|
||||||
BlockMode.CBC -> "CBC"
|
BlockMode.CBC -> "CBC"
|
||||||
|
BlockMode.CTR -> "CTR"
|
||||||
BlockMode.GCM -> "GCM"
|
BlockMode.GCM -> "GCM"
|
||||||
else -> "ECB" // Default for RSA
|
else -> "ECB"
|
||||||
}
|
}
|
||||||
val padding =
|
val padding =
|
||||||
when (params.padding.firstOrNull()) {
|
when (params.padding.firstOrNull()) {
|
||||||
PaddingMode.NONE -> "NoPadding"
|
PaddingMode.NONE -> "NoPadding"
|
||||||
PaddingMode.PKCS7 -> "PKCS7Padding"
|
PaddingMode.PKCS7 -> "PKCS7Padding"
|
||||||
PaddingMode.RSA_PKCS1_1_5_ENCRYPT -> "PKCS1Padding"
|
PaddingMode.RSA_PKCS1_1_5_ENCRYPT -> "PKCS1Padding"
|
||||||
|
PaddingMode.RSA_PKCS1_1_5_SIGN -> "PKCS1Padding"
|
||||||
PaddingMode.RSA_OAEP -> "OAEPPadding"
|
PaddingMode.RSA_OAEP -> "OAEPPadding"
|
||||||
else -> "NoPadding" // Default for GCM
|
else -> "NoPadding"
|
||||||
}
|
}
|
||||||
return "$keyAlgo/$blockMode/$padding"
|
return "$keyAlgo/$blockMode/$padding"
|
||||||
}
|
}
|
||||||
@@ -142,17 +145,17 @@ private class CipherPrimitive(
|
|||||||
override fun abort() {}
|
override fun abort() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
class SoftwareOperation(
|
||||||
* A software-only implementation of a cryptographic operation. This class acts as a controller,
|
private val txId: Long,
|
||||||
* delegating to a specific cryptographic primitive based on the operation's purpose.
|
keyPair: KeyPair,
|
||||||
*/
|
params: KeyMintAttestation,
|
||||||
class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMintAttestation) {
|
private val latencyFloorMs: Long = 0L,
|
||||||
// This now holds the specific strategy object (Signer, Verifier, etc.)
|
) {
|
||||||
private val primitive: CryptoPrimitive
|
private val primitive: CryptoPrimitive
|
||||||
|
@Volatile var finalized = false
|
||||||
|
private set
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// The "Strategy" pattern: choose the implementation based on the purpose.
|
|
||||||
// For simplicity, we only consider the first purpose listed.
|
|
||||||
val purpose = params.purpose.firstOrNull()
|
val purpose = params.purpose.firstOrNull()
|
||||||
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
|
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
|
||||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
|
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
|
||||||
@@ -168,9 +171,35 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.debug("[SoftwareOp TX_ID: $txId] updateAad() inputSize=${aadInput?.size ?: 0}")
|
||||||
|
checkActive()
|
||||||
|
checkInputLength(aadInput)
|
||||||
|
primitive.updateAad(aadInput)
|
||||||
|
}
|
||||||
|
|
||||||
fun update(data: ByteArray?): ByteArray? {
|
fun update(data: ByteArray?): ByteArray? {
|
||||||
|
SystemLogger.debug("[SoftwareOp TX_ID: $txId] update() inputSize=${data?.size ?: 0}")
|
||||||
|
checkActive()
|
||||||
|
checkInputLength(data)
|
||||||
try {
|
try {
|
||||||
return primitive.update(data)
|
return primitive.update(data)
|
||||||
|
} catch (e: ServiceSpecificException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
|
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
|
||||||
throw e
|
throw e
|
||||||
@@ -178,38 +207,72 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
|
checkActive()
|
||||||
|
checkInputLength(data)
|
||||||
try {
|
try {
|
||||||
|
val startNs = if (latencyFloorMs > 0) System.nanoTime() else 0L
|
||||||
val result = primitive.finish(data, signature)
|
val result = primitive.finish(data, signature)
|
||||||
|
if (latencyFloorMs > 0) {
|
||||||
|
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
|
||||||
|
val delayMs = latencyFloorMs - elapsedMs
|
||||||
|
if (delayMs > 0) Thread.sleep(delayMs)
|
||||||
|
}
|
||||||
|
finalized = true
|
||||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
|
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
|
||||||
return result
|
return result
|
||||||
|
} catch (e: ServiceSpecificException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
|
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
|
||||||
// Re-throw the exception so the binder can report it to the client.
|
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun abort() {
|
fun abort() {
|
||||||
|
finalized = true
|
||||||
primitive.abort()
|
primitive.abort()
|
||||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
|
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// AOSP keystore2 operation.rs: const MAX_RECEIVE_DATA: usize = 0x8000
|
||||||
|
private const val MAX_RECEIVE_DATA = 0x8000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The Binder interface for our [SoftwareOperation]. */
|
|
||||||
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
||||||
IKeystoreOperation.Stub() {
|
IKeystoreOperation.Stub() {
|
||||||
|
|
||||||
@Throws(RemoteException::class)
|
override fun updateAad(aadInput: ByteArray?) {
|
||||||
|
operation.updateAad(aadInput)
|
||||||
|
}
|
||||||
|
|
||||||
override fun update(input: ByteArray?): ByteArray? {
|
override fun update(input: ByteArray?): ByteArray? {
|
||||||
return operation.update(input)
|
return operation.update(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(RemoteException::class)
|
|
||||||
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
|
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
return operation.finish(input, signature)
|
return operation.finish(input, signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(RemoteException::class)
|
|
||||||
override fun abort() {
|
override fun abort() {
|
||||||
operation.abort()
|
operation.abort()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ object SystemLogger {
|
|||||||
* @param message The message to log.
|
* @param message The message to log.
|
||||||
*/
|
*/
|
||||||
fun debug(message: String) {
|
fun debug(message: String) {
|
||||||
|
if (!isDebugBuild) return
|
||||||
Log.d(TAG, message)
|
Log.d(TAG, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ 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 leafNotAfter =
|
||||||
(signingKeyPair.public as? X509Certificate)?.notAfter
|
(signingKeyPair.public as? X509Certificate)?.notAfter
|
||||||
?: Date(System.currentTimeMillis() + 31536000000L)
|
?: Date(System.currentTimeMillis() + 31536000000L)
|
||||||
@@ -239,10 +239,10 @@ object CertificateGenerator {
|
|||||||
)
|
)
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ object NativeCertGen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val algorithmName = when (certs[0].publicKey.algorithm) {
|
val algorithmName = when (certs[0].publicKey.algorithm) {
|
||||||
"EC" -> "EC"
|
"EC", "ECDSA" -> "EC"
|
||||||
"RSA" -> "RSA"
|
"RSA" -> "RSA"
|
||||||
else -> certs[0].publicKey.algorithm
|
else -> certs[0].publicKey.algorithm
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,27 +91,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,10 +164,37 @@ object AndroidDeviceUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Generates a cryptographically random byte array of a specified length. */
|
|
||||||
private fun generateRandomBytes(size: Int): ByteArray =
|
private fun generateRandomBytes(size: Int): ByteArray =
|
||||||
ByteArray(size).also { ThreadLocalRandom.current().nextBytes(it) }
|
ByteArray(size).also { ThreadLocalRandom.current().nextBytes(it) }
|
||||||
|
|
||||||
|
private val PERSIST_DIR = File("/data/adb/tricky_store")
|
||||||
|
|
||||||
|
private fun fileForProperty(propertyName: String): File = when (propertyName) {
|
||||||
|
"ro.boot.vbmeta.digest" -> File(PERSIST_DIR, "boot_hash.bin")
|
||||||
|
"ro.boot.vbmeta.public_key_digest" -> File(PERSIST_DIR, "boot_key.bin")
|
||||||
|
else -> File(PERSIST_DIR, "${propertyName.replace('.', '_')}.bin")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun persistToFile(propertyName: String, bytes: ByteArray) {
|
||||||
|
try {
|
||||||
|
fileForProperty(propertyName).writeBytes(bytes)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("Failed to persist $propertyName to file.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readFromFile(propertyName: String, expectedSize: Int): ByteArray? {
|
||||||
|
return try {
|
||||||
|
val file = fileForProperty(propertyName)
|
||||||
|
if (!file.exists()) return null
|
||||||
|
val bytes = file.readBytes()
|
||||||
|
if (bytes.size == expectedSize) bytes else null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("Failed to read $propertyName from file.", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Patch Level Properties ---
|
// --- Patch Level Properties ---
|
||||||
|
|
||||||
fun getPatchLevel(uid: Int): Int {
|
fun getPatchLevel(uid: Int): Int {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,105 @@
|
|||||||
|
## 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
|
## 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.
|
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.
|
||||||
|
|||||||
+1
-1
@@ -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 ---
|
||||||
|
|||||||
+2
-2
@@ -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, Enginex0
|
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/Enginex0/TEESimulator/main/module/update.json
|
updateJson=https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/update.json
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ done
|
|||||||
|
|
||||||
rm -rf "$CONFIG_DIR/persistent_keys"
|
rm -rf "$CONFIG_DIR/persistent_keys"
|
||||||
rm -f "$CONFIG_DIR/tee_status.txt"
|
rm -f "$CONFIG_DIR/tee_status.txt"
|
||||||
|
rm -f "$CONFIG_DIR/boot_hash.bin" "$CONFIG_DIR/boot_key.bin"
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "v4.0",
|
"version": "v4.5",
|
||||||
"versionCode": 90,
|
"versionCode": 111,
|
||||||
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.0/TEESimulator-v4.0-Release.zip",
|
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.5/TEESimulator-v4.5-Release.zip",
|
||||||
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md"
|
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ fn build_leaf_cert(
|
|||||||
let subject_dn_der = if let Some(ref subject) = params.cert_subject {
|
let subject_dn_der = if let Some(ref subject) = params.cert_subject {
|
||||||
subject.clone()
|
subject.clone()
|
||||||
} else {
|
} else {
|
||||||
encode_simple_cn_dn("Android KeyStore Key")
|
encode_simple_cn_dn("Android Keystore Key")
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validity
|
// Validity
|
||||||
|
|||||||
+1
-1
@@ -235,7 +235,7 @@ print_summary() {
|
|||||||
|
|
||||||
# --- Main ---
|
# --- Main ---
|
||||||
echo ""
|
echo ""
|
||||||
bold "TEESimulator package pipeline"
|
bold "TEESimulator-RS package pipeline"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
[[ "$BUILD_RUST" == true ]] && build_rust
|
[[ "$BUILD_RUST" == true ]] && build_rust
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "TEESimulator"
|
rootProject.name = "TEESimulator-RS"
|
||||||
|
|
||||||
include(":stub")
|
include(":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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user