Compare commits

...
20 Commits
Author SHA1 Message Date
Enginex0 d2b8a92fbd chore(version): bump to v4.8 2026-03-17 19:48:48 +01:00
Enginex0 0723865eab feat(interception): add StrongBox hardening and LRU operation pruning
DuckDetector flags several behavioral anomalies that real TEE/StrongBox
hardware exhibits but our software interceptor did not:

- LRU pruning: cap concurrent ops at 15 (TEE) / 4 (StrongBox) per UID,
  aborting oldest when exceeded — matches AOSP keystore2 malus scoring
- StrongBox param guard: forward unsupported params (RSA>2048, non-P256)
  to real HAL for proper rejection instead of generating in software
- StrongBox latency floors: 250ms keygen, 80ms sign to match real SE
  timing characteristics
- Sliding-window op limit for hardware-generated StrongBox keys that
  bypass the software pruning path
- Domain.APP lookup path for createOperation to find software-generated
  keys that never reach keystore2's database
2026-03-17 19:48:40 +01:00
Enginex0 7cb44b9999 feat(operation): add LRU pruning support and latency floor to SoftwareOperation
Expose finalized state for pruning, add latency floor parameter for
StrongBox timing simulation, and add trace logging for 32KB test
diagnosis.
2026-03-17 19:48:29 +01:00
Enginex0 eddd9908af fix(certgen): accept ECDSA as EC algorithm alias in JCA key type matching
Some Android 10 devices (e.g. Sony H8296) report EC private key
algorithm as "ECDSA" instead of "EC", causing IllegalArgumentException
in certificate signing and a SIGSEGV crash in the keystore process.

Closes #4
2026-03-17 19:48:19 +01:00
Enginex0 36ccd22cdc fix(operation): correct TOO_MUCH_DATA fallback to match AOSP ResponseCode
AOSP ResponseCode.TOO_MUCH_DATA = 21, not 29.
2026-03-17 14:16:00 +01:00
Enginex0 81e6fbf97e fix(keygen): forward symmetric algorithms to HAL and add missing JCA mappings
Symmetric keys (AES/HMAC/3DES) don't have KeyPairs or attestation
certs — routing them through doSoftwareKeyGen crashes with
"Unsupported algorithm: 32". Skip the software path entirely and
let the real HAL handle them.

Also adds CTR block mode, RSA_PKCS1_1_5_SIGN cipher padding, and
RSA_PSS signature padding to JcaAlgorithmMapper.
2026-03-17 13:44:25 +01:00
Enginex0andGitHub 7f63713f07 fix(interception): add permission checks for device ID attestation tags
fix(interception): Add permission checks for KeyMintSecurityLevelInterceptor and fix some regression
2026-03-17 13:05:36 +01:00
fatalcoder524 5df76eacd1 fix(interception): Add permission checks for KeyMintSecurityLevelInterceptor and fix some regression
1. Add permission checks for KeyMintSecurityLevelInterceptor to ensure that only authorized users can access sensitive information about the security level of the key mint.
2. Fix regression where device id attestation was allowed for all users by adding appropriate permission checks.
3. Update .gitignore to exclude build artifacts and generated files to keep the repository clean and prevent accidental commits of unnecessary files.
2026-03-17 11:49:54 +00:00
Enginex0 ca3978888e docs(release): bump to v4.7 with operation and attestation fixes changelog 2026-03-17 07:12:30 +01:00
Enginex0 023d7f929d fix(operation): match AOSP error-path semantics for software operations
KeyDetector's OperationErrorPathChecker (flag 0x400000) probes three
error-path behaviors that real keystore2 operations expose. Our
SoftwareOperationBinder was missing all three, plus had no updateAad
implementation which caused AbstractMethodError on Android 16 where
the runtime Stub declares it abstract.

SoftwareOperation changes:
- Add finalized state tracking; post-abort calls now throw
  INVALID_OPERATION_HANDLE (-28) matching AOSP operation.rs
- Add input length guard (0x8000) throwing TOO_MUCH_DATA (29)
  matching AOSP operation.rs MAX_RECEIVE_DATA
- Add updateAad to CryptoPrimitive interface and SoftwareOperationBinder
- Add KeystoreErrorCodes with runtime reflection + AOSP fallback values

KeyMintSecurityLevelInterceptor changes:
- Infer algorithm from stored key pair when operation params omit
  ALGORITHM tag, matching AOSP behavior where createOperation uses
  the key's stored algorithm rather than requiring it in op params

Stub addition:
- ServiceSpecificException compile stub (framework-internal class
  resolved at runtime on device)

Tested on OnePlus Android 16 (SDK 36) — KeyDetector passes all three
probes: updateAad succeeds, TOO_MUCH_DATA returns code=21,
INVALID_OPERATION_HANDLE returns after abort.
2026-03-17 07:04:59 +01:00
Enginex0 bd40f4b950 fix(attestation): encode PADDING as SET OF INTEGER per AOSP schema
PADDING (tag 6) is ENUM_REP in Tag.aidl, meaning SET OF INTEGER in the
attestation extension ASN.1 — same as PURPOSE and DIGEST. Commit f8bfa0d
added it as individual [6] INTEGER entries, causing parsers to fail with
CertificateParsingException on any RSA key attestation.
2026-03-17 04:36:49 +01:00
Enginex0 23696d2f61 ci(release): fetch full history for accurate commit count 2026-03-17 03:43:16 +01:00
Enginex0 dfacb34cf9 chore(brand): rebrand to TEESimulator-RS with simplified versioning
Fork identity: rename across module metadata, CI pipeline, and build
scripts. Version scheme changed from v4.5-115-7e87766 to v4.6-117
format — commit count auto-increments, git hash dropped from filenames.
2026-03-17 03:35:32 +01:00
Enginex0 06d9db443c perf(keygen): replace Gaussian RTT normalization with 15ms floor fence
The old Gaussian sleep (mean=55ms, stddev=12ms) triggered detection on
Chunqiu Native Check 2.8. A flat 15ms floor satisfies the minimum RTT
threshold without creating a detectable delay pattern — both attested
and non-attested paths get identical treatment, keeping the D50 ratio
at ~1.0 while staying above the >=15ms requirement.
2026-03-17 03:35:20 +01:00
Enginex0 7e87766493 fix(certgen): derive signing algorithm from attestation key and allow device ID tags
signerAlgorithm was derived from params.algorithm (the generated key)
instead of the signing key, causing BouncyCastle to throw when signing
RSA keys with an EC attestation key. Now reads signingKeyPair.private.algorithm.

Device ID tags (serial/imei/meid/secondImei) were blanket-rejected
instead of flowing through to software cert gen like AOSP does.
Narrowed rejection to DEVICE_UNIQUE_ATTESTATION only.
2026-03-17 00:05:56 +01:00
Enginex0 f8bfa0dfd8 fix(attestation): align authorization list and cert extension with AOSP keystore2 semantics
toAuthorizations() was missing OS_VERSION, OS_PATCHLEVEL, VENDOR_PATCHLEVEL,
BOOT_PATCHLEVEL, CREATION_DATETIME, USER_ID, PADDING, and RSA_PUBLIC_EXPONENT
tags that real TEE-generated KeyMetadata always includes. EC_CURVE was also
hardcoded unconditionally, producing invalid authorizations for RSA keys.

Additionally, live-patched certificate chains in getKeyEntry weren't cached,
causing re-patching on every call with potentially different signatures.

Ports upstream JingMatrix/TEESimulator#148 and #150.
2026-03-16 23:01:28 +01:00
Enginex0 90ff59e0aa fix(interception): check generatedKeys before deletedSoftwareKeys on getKeyEntry
The deletion guard must not shadow re-generated keys. If an app
deletes a key then re-creates it, getKeyEntry was still returning
KEY_NOT_FOUND because deletedSoftwareKeys was checked first.
2026-03-16 22:36:02 +01:00
Enginex0 8bdf0d59fa docs(release): bump to v4.5 with detection hardening changelog 2026-03-16 22:07:56 +01:00
Enginex0 6ab09f4889 fix(interception): prevent ghost key responses after software key deletion
After deleting a software-generated key, getKeyEntry was falling
through to the real keystore2 service which could return a stale
hardware key with the same alias. The post-transact live-patch
fallback would then resurrect the key with a patched chain —
detectors flag this as binder inconsistency.

Track deleted software key aliases and return KEY_NOT_FOUND (7) for
subsequent getKeyEntry calls. Also always invoke cleanupKeyData on
delete to clear stale patchedChains entries for hardware keys.
2026-03-16 22:06:53 +01:00
Enginex0 f4559bcd19 perf(keygen): normalize software generateKey RTT to match TEE latency
Software-generated keys complete in ~4ms, real TEE averages 55-65ms
with a floor around 15ms. Detectors measure this RTT to distinguish
software from hardware paths. Gaussian delay sampling (mean=55ms,
σ=12ms, floor=15ms) brings total RTT into the expected range.
2026-03-16 22:06:38 +01:00
19 changed files with 468 additions and 125 deletions
+20 -28
View File
@@ -70,38 +70,25 @@ jobs:
id: ver id: ver
run: | run: |
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/') ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
echo "version=${ver}" >> "$GITHUB_OUTPUT" count=$(git rev-list HEAD --count)
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
- name: Rename ZIPs for release - name: List build artifacts
run: | run: |
RELEASE_FILE=$(find out -name "*Release*.zip" | head -1) echo "Release: $(ls out/*Release*.zip | head -1) ($(du -h out/*Release*.zip | head -1 | cut -f1))"
DEBUG_FILE=$(find out -name "*Debug*.zip" | head -1) echo "Debug: $(ls out/*Debug*.zip | head -1) ($(du -h out/*Debug*.zip | head -1 | cut -f1))"
if [[ -z "$RELEASE_FILE" || -z "$DEBUG_FILE" ]]; then
echo "::error::Could not find release or debug ZIPs in out/"
ls -la out/ || echo "out/ does not exist"
exit 1
fi
mv "$RELEASE_FILE" "out/TEESimulator-${VER}-Release.zip"
mv "$DEBUG_FILE" "out/TEESimulator-${VER}-Debug.zip"
echo "Release: TEESimulator-${VER}-Release.zip ($(du -h "out/TEESimulator-${VER}-Release.zip" | cut -f1))"
echo "Debug: TEESimulator-${VER}-Debug.zip ($(du -h "out/TEESimulator-${VER}-Debug.zip" | cut -f1))"
env:
VER: ${{ steps.ver.outputs.version }}
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: TEESimulator-release-zip name: TEESimulator-RS-release-zip
path: out/TEESimulator-*-Release.zip path: out/TEESimulator-RS-*-Release.zip
retention-days: 30 retention-days: 30
compression-level: 0 compression-level: 0
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: TEESimulator-debug-zip name: TEESimulator-RS-debug-zip
path: out/TEESimulator-*-Debug.zip path: out/TEESimulator-RS-*-Debug.zip
retention-days: 7 retention-days: 7
compression-level: 0 compression-level: 0
@@ -120,27 +107,30 @@ jobs:
contents: write contents: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read version - name: Read version
id: ver id: ver
run: | run: |
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/') ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
echo "version=${ver}" >> "$GITHUB_OUTPUT" count=$(git rev-list HEAD --count)
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: TEESimulator-release-zip name: TEESimulator-RS-release-zip
path: zips path: zips
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: TEESimulator-debug-zip name: TEESimulator-RS-debug-zip
path: zips path: zips
- name: Extract changelog - name: Extract changelog
run: | run: |
ver="${VER#v}" ver="${VER#v}"
awk "/^## TEESimulator v${ver}/{flag=1; next} /^## TEESimulator v/{if(flag) exit} flag" module/changelog.md > /tmp/notes.md 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 cat /tmp/notes.md
env: env:
VER: ${{ steps.ver.outputs.version }} VER: ${{ steps.ver.outputs.version }}
@@ -148,12 +138,14 @@ jobs:
- name: Create release - name: Create release
run: | run: |
gh release delete "$VER" --yes 2>/dev/null || true 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" \ gh release create "$VER" \
--title "$VER" \ --title "$VER" \
--latest \ --latest \
--notes-file /tmp/notes.md \ --notes-file /tmp/notes.md \
"zips/TEESimulator-${VER}-Release.zip" \ "$RELEASE" \
"zips/TEESimulator-${VER}-Debug.zip" "$DEBUG"
env: env:
VER: ${{ steps.ver.outputs.version }} VER: ${{ steps.ver.outputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+6
View File
@@ -1 +1,7 @@
out out
.gradle
.kotlin
app/build
build
native-certgen/target
app/src/main/jniLibs
+9 -10
View File
@@ -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.4" 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,13 +108,13 @@ androidComponents {
// --- Define output locations and file names --- // --- Define output locations and file names ---
// Stage all files in a temporary directory inside 'build' before zipping // Stage all files in a temporary directory inside 'build' before zipping
val tempModuleDir = project.layout.buildDirectory.dir("module/${variant.name}") val tempModuleDir = project.layout.buildDirectory.dir("module/${variant.name}")
val zipFileName = "TEESimulator-$verName-$gitCommitCount-$gitCommitHash-$capitalized.zip" val zipFileName = "TEESimulator-RS-$verName-$gitCommitCount-$capitalized.zip"
// Task 1: Prepare all module files in the temporary build directory. // Task 1: Prepare all module files in the temporary build directory.
// Using Sync ensures that stale files from previous runs are removed. // Using Sync ensures that stale files from previous runs are removed.
val prepareModuleFilesTask = val prepareModuleFilesTask =
tasks.register<Sync>("prepareModuleFiles${capitalized}") { tasks.register<Sync>("prepareModuleFiles${capitalized}") {
group = "TEESimulator Module Packaging" group = "TEESimulator-RS Module Packaging"
description = "Prepares all files for the ${variant.name} module zip." description = "Prepares all files for the ${variant.name} module zip."
if (isDebug) { if (isDebug) {
@@ -162,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})",
) )
} }
@@ -174,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)
@@ -187,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)
@@ -201,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(
@@ -214,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")
@@ -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)]
@@ -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,9 +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.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
@@ -17,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
@@ -33,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,
@@ -45,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,
@@ -192,35 +199,90 @@ 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 SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for uid=$callingUid.")
val generatedKeyInfo = findGeneratedKeyByKeyId(callingUid, nspace)
if (generatedKeyInfo == null) {
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 =
@@ -260,13 +322,38 @@ class KeyMintSecurityLevelInterceptor(
return InterceptorUtils.createErrorReply(RESPONSE_INVALID_ARGUMENT) return InterceptorUtils.createErrorReply(RESPONSE_INVALID_ARGUMENT)
} }
if (parsedParams.serial != null || parsedParams.imei != null || if (params.any { it.tag == Tag.DEVICE_UNIQUE_ATTESTATION } && !AndroidPermissionUtils.hasUniqueIdAttestationPermission(callingUid)) {
parsedParams.meid != null || parsedParams.secondImei != null || SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_UNIQUE_ATTESTATION for uid=$callingUid")
params.any { it.tag == Tag.DEVICE_UNIQUE_ATTESTATION }) {
SystemLogger.warning("[TX_ID: $txId] Rejecting device ID attestation for uid=$callingUid")
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS) return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
} }
val hasDeviceIdAttestation = params.any {
it.tag == Tag.ATTESTATION_ID_IMEI ||
it.tag == Tag.ATTESTATION_ID_MEID ||
it.tag == Tag.ATTESTATION_ID_SERIAL ||
it.tag == Tag.DEVICE_UNIQUE_ATTESTATION ||
it.tag == Tag.ATTESTATION_ID_SECOND_IMEI
}
if(hasDeviceIdAttestation && !AndroidPermissionUtils.hasDeviceAttestationPermission(callingUid)) {
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_ID_ATTESTATION for uid=$callingUid")
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
}
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()
@@ -317,6 +404,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}].")
@@ -332,7 +420,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)
@@ -344,12 +432,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)
} }
@@ -378,7 +471,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(),
@@ -422,6 +515,7 @@ class KeyMintSecurityLevelInterceptor(
} }
private fun buildKeyEntryResponse( private fun buildKeyEntryResponse(
callingUid: Int,
chain: List<Certificate>, chain: List<Certificate>,
params: KeyMintAttestation, params: KeyMintAttestation,
descriptor: KeyDescriptor, descriptor: KeyDescriptor,
@@ -438,7 +532,7 @@ class KeyMintSecurityLevelInterceptor(
keySecurityLevel = securityLevel keySecurityLevel = securityLevel
key = normalizedKeyDescriptor 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() modificationTimeMs = System.currentTimeMillis()
} }
return KeyEntryResponse().apply { return KeyEntryResponse().apply {
@@ -516,7 +610,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)
@@ -537,16 +631,28 @@ class KeyMintSecurityLevelInterceptor(
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 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 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) }
@@ -592,8 +698,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>()
@@ -653,7 +758,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 {
@@ -668,19 +776,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( authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong())))
createAuth( }
Tag.ORIGIN,
KeyParameterValue.origin(this.origin ?: KeyOrigin.GENERATED),
)
)
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()
} }
@@ -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()
} }
@@ -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
} }
@@ -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")
}
}
+31
View File
@@ -1,3 +1,34 @@
## 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 ## 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. - **Binder error reply format** — Aligned EX_SERVICE_SPECIFIC wire layout with AOSP Status.cpp, including the remote stack trace header field.
+1 -1
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"version": "v4.4", "version": "v4.5",
"versionCode": 109, "versionCode": 111,
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.4/TEESimulator-v4.4-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"
} }
+1 -1
View File
@@ -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
View File
@@ -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;
}
}