Compare commits

...
29 Commits
Author SHA1 Message Date
Enginex0 64723b2564 chore(release): publish v6.0.1-280 2026-06-19 03:41:09 +01:00
Enginex0 42842e22c0 chore(release): publish v6.0.1-251
Grant-plane coherence (Android 16 incl.), Google Wallet + fingerprint
compatibility (PR #26/#27), and removal of the in-module PIF/bulletin
resolvers. Frozen at gitCommitCount 251.
2026-05-30 13:45:55 +01:00
Enginex0 77fc96db37 fix(action): sample getevent in 1s bursts
The piped getevent stream block-buffered on Magisk's BusyBox ash and
missed a single vol-key press before the timeout. Sample getevent in 1s
timeout bursts in a deadline loop instead.
2026-05-30 13:42:31 +01:00
Enginex0 b27a33b444 refactor(app): drop PIF resolvers + dump purge
Remove PatchLevelManager (auto-resolved the security-patch date from an
installed PlayIntegrityFix module into security_patch.txt, with a
FileObserver hot-reload) and BulletinPoller (scheduled bulletin refresh),
and their App.kt init/start calls.

Add purgeDebugDiagnostics(): release builds sweep stale teesim-*.bin
dumps from /data/local/tmp at boot so a prior debug install can't leave a
detection artifact. Stabilize the InterceptorUtils diagnostic dump path
to a single file instead of one per call.
2026-05-30 13:42:31 +01:00
Enginex0 bae65ac47c chore(release): bump version to v6.0.1 2026-05-30 13:42:31 +01:00
Enginex0 217a5dc7f3 fix(keystore): grant plane serves patch-mode keys
Domain.GRANT readback only recognized synthetic keys (generatedKeys), so
patch-mode keys (real TEE key whose attestation we patch on read, cached
in teeResponses) fell through to the real keystore2 unpatched. Android 16
made KeyStoreManager.grantKeyAccess a public API, so the owner read
returned our patched chain while the grant read returned the raw real
chain -> duck SELF_/ISOLATED_CHAIN_SPLIT.

Gate grant()/ungrant()/resolveGrant on ownsKeyResponse() (synthetic OR
patch-mode) so every access plane serves the same cached KeyEntryResponse.
Pre-36 still answers PERMISSION_DENIED; no behavior change on Android 15.
2026-05-30 13:42:31 +01:00
Enginex0 0229368c04 fix(keystore): mirror TEE device-ID capability
generateKey synthesized device-property attestation unconditionally:
the BRAND/DEVICE/PRODUCT/MANUFACTURER/MODEL tags that
setDevicePropertiesAttestationIncluded emits. A forged key thus
succeeded where real silicon returns CANNOT_ATTEST_IDS. Hardware that
never provisioned device IDs cannot attest them, so forging them is an
over-capability tell: a genuine device of the same class fails the
identical request.

Add DeviceAttestationService.canAttestDeviceIds, a lazy probe that asks
the real TEE to attest device properties once and caches the verdict.
It is gated behind isTeeFunctional, so a silent or dead TEE
short-circuits to "cannot attest" without a second doomed probe.

handleGenerateKey now returns KEYMINT_CANNOT_ATTEST_IDS for any
device-ID or device-property attestation the real TEE cannot satisfy,
uniformly across AUTO, PATCH, and GENERATE. Basic attestation carries
none of these tags and is untouched.

Verified on 23106RN0DA: kknd under GENERATE now WARNs, matching a stock
locked-bootloader device. Principle: forge health, mirror capability.
2026-05-30 13:42:31 +01:00
Enginex0 0f61bb841a feat(keystore): mirror lifecycle via maintenance
Hook the keystore2 daemon's android.security.maintenance binder (hosted
by the same process, reached by the already-injected native hook) so
synthetic key state follows real key-lifecycle events:

- clearNamespace(APP) purges synthetic keys for the uid and their grants.
- deleteAllKeys() clears all synthetic keys and grants.
- migrateKeyNamespace() re-keys the synthetic entry, preserving material,
  chain, and grants.

Pure side-effect hook: every handled transaction mutates only our own
synthetic state, then returns ContinueAndSkipPost so the real keystore2
still performs the real operation. Unhandled codes pass through, so real
key lifecycle is never disturbed. Pre-empts delete-then-read and
clearNamespace coherence probes (Phase 9 Change 4).

Adds a minimal IKeystoreMaintenance compile stub for the descriptor;
transaction codes resolve reflectively on-device.

Refs Phase 9 .omc/plans/tee-fingerprint-phase-9-grant-plane-coherence.md
2026-05-30 13:42:31 +01:00
Enginex0 50f2e98375 fix(keystore): evict stale chains on key mutation
Two synthetic-cache staleness gaps let getKeyEntry replay a pre-mutation
attestation:

- importKey now drops teeResponses and patchedChains for the alias, not
  only generatedKeys. A successful import replaces the real key, so the
  retained patched chain was a tell (duck STALE_GENERATED_AFTER_IMPORT).
- After updateSubcomponent re-keys a patched chain, getKeyEntry on a
  patch-mode key evicts the cached TEE response by KEY_ID or APP so the
  read falls through to the updated real keystore2
  (duck STALE_TEE_RESPONSE_AFTER_KEY_ID_UPDATE).

Refs Phase 9 .omc/plans/tee-fingerprint-phase-9-grant-plane-coherence.md
2026-05-30 13:42:31 +01:00
Enginex0 f4e2619eba fix(keystore): gate synthetic grant to Android 16
KeyStoreManager.grantKeyAccess() became a public app API only in
Android 16 (API 36). Before that grant was a hidden API and SELinux
denied untrusted_app, so a real Android 15 device answers a private-
binder grant() with PERMISSION_DENIED.

The virtualized grant plane (8be9077) issued a synthetic grant on every
SDK, exposing a capability a real Android 15 app does not have. Gate
grant() and ungrant() on SDK_INT >= 36: pre-36 returns PERMISSION_DENIED
for synthetic keys (matching the real device, which Duck then marks
UNAVAILABLE rather than a tell); 36+ keeps the coherent virtualized
grant.

On-device 23106RN0DA (SDK 35): all four grant rows report UNAVAILABLE,
not RED; tamper score unaffected.

Refs Phase 9 .omc/plans/tee-fingerprint-phase-9-grant-plane-coherence.md
2026-05-30 13:42:31 +01:00
Enginex0 649136ab4e fix(shim): match real gen-mode auth shape
Real keystore2 (captured on-device, MediaTek SDK 35) emits 11 EC
authorizations in the generateKey KeyMetadata: no VENDOR_PATCHLEVEL or
BOOT_PATCHLEVEL, and USER_ID tagged at SecurityLevel.SOFTWARE. The shim
emitted 13 with both patchlevels and USER_ID at KEYSTORE.

Duck-Detector's generate-mode parcel fingerprint stride-walks the reply
and keys on the 13-entry layout, so the two extra entries were the tell.
Drop both patchlevels from the authorization list (they remain in the
attestation extension via AttestationBuilder, so attestation content is
unchanged) and move USER_ID to SOFTWARE to mirror the captured device.

On-device 23106RN0DA: generate-mode fingerprint signal gone (0 local),
TEE tamper score 18 -> 8.

Refs Phase 7 .omc/plans/tee-fingerprint-phase-7-generate-mode-coherence.md
2026-05-30 13:42:31 +01:00
Enginex0 d155a0ded6 feat(keystore): virtualize grant plane
Duck-Detector's grant-domain probes generate an attested key, then
reach it through a second access plane -- IKeystoreService.grant() then
getKeyEntry(Domain.GRANT, grantId) -- and compare the certificate
chains. We synthesized the owner key but never virtualized the GRANT
plane, so grant reads fell through to the real keystore2, which has no
record of the synthetic key. That single fall-through produced six RED
rows.

Virtualize the plane so every access path returns the same synthesized
KeyEntryResponse:

- SoftwareGrant state model in the shim companion: issue/resolve/
  revoke/purge, caller-bound and access-vector-aware (Change 1).
- grant/ungrant/getKeyEntry(GRANT) handlers in Keystore2Interceptor.
  resolveGrant() enforces caller-binding (non-grantee -> KEY_NOT_FOUND,
  PR #57 probe 4) and the GET_INFO=0x4 access-vector gate (missing ->
  PERMISSION_DENIED, PR #57 probe 3); a valid read returns the owner's
  exact KeyEntryResponse for a coherent chain (Change 2).
- Purge grants on key teardown and clearAll, so grants die with the
  key and re-key orphans them -- matching real keystore2 (Change 3).

The Domain.GRANT read is resolved before the package-scoped
shouldSkipUid filter: isolated grantees (bindIsolatedService) have no
package mapping and would otherwise be dropped to the real keystore2,
leaving three grant rows Unavailable. Caller-binding in resolveGrant()
is the real access gate, mirroring keystore2's grantee+id row keying.

Verified on-device (generate mode, build #237): all four grant rows
clean, TEE tamper score 28 -> 18, zero adjacent regression.

Refs Phase 9 .omc/plans/tee-fingerprint-phase-9-grant-plane-coherence.md
2026-05-30 13:42:31 +01:00
Enginex0andGitHub edac284972 Merge pull request #27 from Andrea-lyz/pr/fix-gpay-include-unique-id
fix(interception): strip INCLUDE_UNIQUE_ID instead of rejecting when permission missing
2026-05-30 11:25:44 +01:00
Enginex0andGitHub cbb73a0b0e Merge pull request #26 from Andrea-lyz/fix/createOperation-key-id-not-found
fix(intercept): use ContinueAndSkipPost for KEY_ID createOperation NOT FOUND
2026-05-30 11:25:40 +01:00
Andrea-lyz 3140ff5e96 fix(interception): strip INCLUDE_UNIQUE_ID instead of rejecting on missing permission
The INCLUDE_UNIQUE_ID gate in handleGenerateKey (introduced as part of the
PR157 AOSP-compliance work) returns PERMISSION_DENIED when the caller
holds neither SELinux gen_unique_id nor REQUEST_UNIQUE_ID_ATTESTATION.

This breaks Google Wallet card binding on real devices: Wallet's
generateKey carries INCLUDE_UNIQUE_ID without holding the permission, so
its attestation key request is rejected and Wallet surfaces the failure
as "this phone does not meet the security requirements for Google
Wallet". Symptoms reported by users: clearing GMS data only helps for a
few seconds before the state regresses; no card can be added.

Naive removal of the gate is not safe: AttestationBuilder honours
`includeUniqueId == true` by computing an HMAC-SHA256 unique_id and
embedding it in the attestation extension. With the gate gone, GMS
attestation flows that include the tag end up with a unique_id in the
extension that Play Integrity flags as inconsistent for the caller,
turning all three integrity verdicts red.

The fix here splits the difference: when the permission check fails,
silently strip the INCLUDE_UNIQUE_ID tag from the KeyParameter array
(and re-parse `parsedParams`) instead of rejecting the request. The key
generates normally, AttestationBuilder takes the
`else { ByteArray(0) }` branch, and the resulting attestation simply
omits the unique_id field — matching pre-PR157 behaviour, where the
tag effectively had no effect.

Verified on a device that previously failed Wallet binding on the
PR157 baseline:
  - Play Integrity: BASIC + DEVICE + STRONG all pass.
  - Google Wallet: card binding completes successfully.
  - Calls that DO hold the permission are unaffected (still emit
    unique_id as before).

The rest of the PR157 compliance work (CALLER_NONCE handling,
AuthorizeCreate ordering, USAGE_COUNT_LIMIT counters, effectiveParams
merging) is preserved.
2026-05-27 06:07:18 +02:00
Andrea-lyz 8544aac260 fix(intercept): use ContinueAndSkipPost for KEY_ID createOperation NOT FOUND
When handleCreateOperation receives a Domain.KEY_ID request for a key
not in our generatedKeys cache, it correctly forwards to the real HAL.
However, it previously returned TransactionResult.Continue, which lets
the post-handler run. The post-handler unconditionally registers an
OperationInterceptor on the IKeystoreOperation binder returned by real
keystore2. This interceptor then interferes with the caller's
operation (intercepting finish/abort/updateAad calls).

On devices where vendor daemons (e.g. fingerprint calibration) use
Domain.KEY_ID for their hardware-backed keys, this causes operation
failures — the OperationInterceptor races with the immediate
finish/abort call and may reject updateAad with INVALID_TAG if the
operation is not GCM mode.

Fix: return ContinueAndSkipPost (matching the existing Domain.APP NOT
FOUND path) so the post-handler never runs for operations on keys we
don't own. When the key IS in generatedKeys, we never reach this
return — we proceed to create a SoftwareOperation directly in
pre-transact.

Symptom: OnePlus engineering mode ultrasonic fingerprint calibration
hash retrieval fails with module enabled, works with module disabled.

Signed-off-by: Andrea-lyz <Andrea-lyz@users.noreply.github.com>
2026-05-26 22:53:28 +02:00
Enginex0 134d5111ad chore(release): publish v6.0.0-235 2026-05-20 07:05:02 +01:00
Enginex0 4c801f2089 chore: bump versionCode to 235 2026-05-20 06:54:35 +01:00
Enginex0 afc5caeb1b chore: bump versionCode to 233 2026-05-20 06:52:11 +01:00
Enginex0 0e9ea10b50 fix: reorder hal-enforced auths to evade duck detector
Duck-Detector's generate-mode parser walks the reply parcel at
12-byte strides and matches (secLevel=256, tag=1, unionTag=32) at
slot[count-1]. Those bytes are actually KEY_SIZE.value=256 followed
by the next Authorization's presence flag and size header — an
emergent fingerprint from misaligned parsing, not a fake value.

Reorder toAuthorizations so PURPOSE/ALGORITHM/KEY_SIZE come first,
mirroring AOSP keymint reference HAL output. KEY_SIZE moves from
auth#4 to auth#2, so its int payload no longer lands at byte 224.
Verified across 31 fresh duckdetector probes: zero matches (was
15/36 before).

Also add gen-mode wire-byte diagnostic to InterceptorUtils and the
generate-mode entry point, debug-gated, dumping request and reply
parcels to /data/local/tmp for offline decode.
2026-05-20 06:52:04 +01:00
Enginex0 6ae5ea391c feat(service): clear logd size props on boot
Spawn a backgrounded subshell from service.sh that polls
sys.boot_completed once per second and, once set, blanks the
persist.logd.size variants (root, crash, system, main).

Runs alongside the existing supervisor fork so daemon startup is
unaffected. Idempotent across reboots: even if a previous boot
already cleared the props, setting them to empty again is a no-op.
2026-05-20 04:13:57 +01:00
Enginex0 f554b36416 fix: intercept createOperation under any caller UID
Mirror of the change applied to GENERATE_KEY in 95b8c27. Drop the
outer shouldSkipUid gate so handleCreateOperation always runs.

handleCreateOperation already gates on the cache lookup
(KeyMintSecurityLevelInterceptor.kt:298-326): domain=APP looks up
KeyIdentifier(callingUid, alias) in generatedKeys and forwards to
HAL on miss; domain=KEY_ID looks up by nspace filtered by uid and
forwards on miss. The outer UID gate was redundant when the lookup
hits, and harmful when a key was generated under a non-target-list
UID (e.g. Shizuku-routed callers at shell 2000 / root 0 after
95b8c27).

Without this change, a BYO key created under Shizuku-routed UID
that the app later attempts to use (signing operation under the
same Shizuku-routed UID) would be forwarded to real HAL, which has
no record of our software key, producing a silent operation
failure instead of the simulator handling the sign internally.

Surfaced by adversarial audit. CREATE_OPERATION no longer needs the
outer gate because handleCreateOperation's own cache-or-forward
logic is the correct gate.
2026-05-20 03:59:10 +01:00
Enginex0 684542f4b1 fix: harden software gen symmetric branch errors
doSoftwareKeyGen's symmetric branch (AES/HMAC/3DES) previously did
two things wrong:

1. It accepted attestationKey != null silently and then ignored the
   reference: the symmetric path never consults attestationKey, so a
   caller asking for BYO on a symmetric key would have received a key
   with no certificate chain and no signal that BYO was dropped.
   Reject early with KEYMINT_INVALID_ARGUMENT matching real KeyMint
   HAL behavior.

2. The unsupported-algorithm path (e.g. 3DES, which has no JCA
   mapping in this branch) threw SECURE_HW_COMMUNICATION_FAILED
   (-49), the same numeric code InterceptorUtils labels as
   Error::Km(UNSUPPORTED_TAG). That confuses diagnosis since -49 is
   exactly the symptom the BYO fix series was just chasing. Use
   KEYMINT_INVALID_ARGUMENT (-38) which is what real KeyMint returns
   for unsupported algorithms in this context.

Surfaced by adversarial audit of 66a8c7f's broadened forceGenerate
gate, which now routes any attestationKey != null to software
unconditionally.
2026-05-20 03:58:53 +01:00
Enginex0 240728f98d fix: return full keybox chain when BYO attest key misses
CertificateGenerator.generateCertificateChain selected
keybox.keyPair as fallback signer when getAttestationKeyInfo returned
null, but the chain assembly at line 115 still keyed on
"attestKeyAlias != null" and returned only listOf(leafCert). The
caller received a depth-1 chain signed by the keybox root with no
parent attached — structurally invalid.

Track whether the BYO lookup actually returned a key. On hit return
the depth-1 chain (caller holds the rest). On miss include
keybox.certificates so the chain is rooted.

Surfaced by adversarial audit of 66a8c7f, which broadened the
software-dispatch gate to all attestationKey != null requests.
Without this companion fix the miss path produces a malformed chain
where the previous code would have forwarded to HAL.
2026-05-20 03:58:20 +01:00
Enginex0 95b8c27a9f fix: intercept BYO request under any caller UID
Shizuku-routed key attestation calls reach keystore2 with callingUid
set to shell (2000) or root (0) instead of the originating app's uid.
target.txt has no entry for those uids so shouldSkipUid returned true
in onPreTransact, and handleGenerateKey was never entered. The
transaction reached the real KeyMint HAL, which on older Keymaster 4.x
HALs rejects Tag::ATTEST_KEY with -49 UNSUPPORTED_TAG.

Move the shouldSkipUid gate from onPreTransact into handleGenerateKey
itself, evaluated after attestationKey and isAttestKeyRequest are
parsed. Skip only when the request is neither BYO nor attest-key-
purpose. CREATE_OPERATION keeps its outer-level gate (not part of the
BYO flow).

After this change, Shizuku-routed BYO requests enter dispatch, hit
forceGenerate=true via the prior simplification, and route to
doSoftwareKeyGen. Non-BYO non-attest calls from shell/root uids
still fall through to HAL unchanged.

D3 (option 2B) from /home/rootdev/.claude/plans/breezy-seeking-wozniak.md.
2026-05-20 03:47:16 +01:00
Enginex0 66a8c7fbf8 refactor: simplify forceGenerate dispatch gate
Any attest-key request or BYO request goes software unconditionally.
Drops the alias-Elvis lookup and the nspace fallback added by the
d7dc5e0 -> 5f72acb -> 0b2c34f revert/restore churn, both of which
silently missed when callers (Shizuku, post-restart sessions) did
not share an in-memory KeyIdentifier(uid, alias) with the attest
key originally registered in attestationKeys.

The (shouldPatch && isAttestKeyRequest) clause is subsumed by the
broader isAttestKeyRequest clause.

D2 from /home/rootdev/.claude/plans/breezy-seeking-wozniak.md.
2026-05-20 03:46:47 +01:00
Enginex0 36c93decc6 refactor: remove AUTO TEE race dispatch
The race added in 8fdc59a forwarded BYO attest-key requests to real
HAL on cache miss, producing -49 UNSUPPORTED_TAG on devices whose
persistent attest key alias survived in keystore2 across daemon
restarts but never re-entered our in-memory attestationKeys set.

AUTO resolution now relies solely on
ConfigurationManager.getPackageModeForUid (config/ConfigurationManager.kt:115),
which uses DeviceAttestationService.isTeeFunctional
(attestation/DeviceAttestationService.kt:67) — a Kotlin by-lazy probe
evaluated once per daemon session. Matches upstream JingMatrix and
the v5.0-138 baseline.

Drops the AtomicReference<Boolean?> identity-equality compiler
warnings the race relied on.
2026-05-20 03:46:19 +01:00
Enginex0 55e39c7f01 fix(action): stream getevent for vol on Magisk
Users reported vol+ confirmation not registering on Magisk. The
prior backgrounded `getevent -qlc 1` + `kill -0` poll captured
the first kernel event of any type, then restarted on miss. With
six events per keypress (EV_MSC scan, EV_KEY DOWN, EV_SYN, then
release variants) and a 1s poll cadence, the 10s budget exhausts
before a DOWN sample lands. The chainfire note that piped getevent
breaks BusyBox grep applies under Magisk's ash standalone mode.

Replace with a single streaming `getevent -lq` matched inline
against `KEY_VOLUMEUP DOWN` / `KEY_VOLUMEDOWN DOWN`, wrapped in
`/system/bin/timeout 10`. Full paths bypass BusyBox aliasing.
2026-05-20 02:44:43 +01:00
Enginex0 44816c1a8d chore(release): publish v6.0.0-224
Bump OTA pointer to v6.0.0-224 and document the 59-commit delta
from v6.0.0-162. Highlights:

- Duck Detector TamperScore-4 cleared on Xiaomi A16
- Self-sufficient spoofing (PatchLevelManager, BulletinPoller,
  PIF FileObserver, vbmeta complement props)
- Persistent symmetric key storage (PR #22)
- Action button hardened: vol+ confirm + 22-language i18n
- Build: JVM 21, gradle auto-rewrites update.json
2026-05-19 19:41:31 +01:00
15 changed files with 726 additions and 573 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java)
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
val verName = "v6.0.0"
val verName = "v6.0.1"
android {
namespace = "org.matrix.TEESimulator"
@@ -6,12 +6,11 @@ import android.content.Context
import android.content.ContextWrapper
import android.os.Build
import android.os.Looper
import java.io.File
import java.security.Security
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.matrix.TEESimulator.config.BootStateManager
import org.matrix.TEESimulator.config.BulletinPoller
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.config.PatchLevelManager
import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor
import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor
import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor
@@ -41,12 +40,12 @@ object App {
}
try {
purgeDebugDiagnostics()
prepareEnvironment()
// Spoof boot-state and patch-level props before any hook attaches,
// so keystore2's cached snapshot reflects the spoofed values.
// Spoof boot-state props before any hook attaches, so keystore2's
// cached snapshot reflects the spoofed values.
BootStateManager.apply()
PatchLevelManager.initialize()
// Load the package configuration.
ConfigurationManager.initialize()
@@ -65,12 +64,6 @@ object App {
NativeCertGen.initialize("/data/adb/modules/tricky_store/libcertgen.so")
try {
BulletinPoller.start()
} catch (e: Throwable) {
SystemLogger.error("Failed to start BulletinPoller", e)
}
// This starts the message queue processing. It blocks here indefinitely
// processing messages until Looper.myLooper().quit() is called.
Looper.loop()
@@ -80,6 +73,25 @@ object App {
}
}
/**
* Release builds never emit diagnostics. Sweep any `.bin` dumps a prior
* debug install left in the world-readable temp dir so they can't act as a
* detection artifact for apps that probe /data/local/tmp.
*/
private fun purgeDebugDiagnostics() {
if (SystemLogger.isDebugBuild) return
val stale =
File("/data/local/tmp").listFiles { _, name ->
name.startsWith("teesim-") && name.endsWith(".bin")
} ?: return
stale.forEach { runCatching { it.delete() } }
if (stale.isNotEmpty()) {
// warning() bypasses the rate limiter, so this once-per-boot audit
// line survives the noisy startup window.
SystemLogger.warning("Purged ${stale.size} stale debug diagnostic(s) from /data/local/tmp")
}
}
/** Initializes the necessary Android framework internals to satisfy KeyStore requirements. */
private fun prepareEnvironment() {
// 1. Prepare Main Looper
@@ -1,6 +1,7 @@
package org.matrix.TEESimulator.attestation
import android.annotation.SuppressLint
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyPairGenerator
@@ -60,12 +61,23 @@ object DeviceAttestationService {
// A unique alias for the key used to perform the TEE functionality check.
private const val TEE_CHECK_KEY_ALIAS = "TEESimulator_AttestationCheck"
// Alias for the device-ID attestation capability probe.
private const val DEVICE_ID_CHECK_KEY_ALIAS = "TEESimulator_DeviceIdCheck"
/**
* Lazily determines if the device's TEE is functional by attempting to generate an
* attestation-backed key pair. The result is cached.
*/
val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() }
/**
* Lazily mirrors whether the real TEE can attest device identifiers/properties (the tags added
* by `setDevicePropertiesAttestationIncluded`). Hardware that never provisioned device IDs
* returns CANNOT_ATTEST_IDS; the synthesizer consults this so it never forges a capability the
* real silicon lacks. Cached.
*/
val canAttestDeviceIds: Boolean by lazy { checkDeviceIdAttestation() }
/**
* Lazily fetches and parses attestation data from a genuinely generated certificate. The result
* is cached. Returns null if the TEE is not functional or parsing fails.
@@ -106,6 +118,37 @@ object DeviceAttestationService {
}
}
/**
* Probes whether the real TEE can satisfy device-ID/property attestation, mirroring its actual
* capability. Gated behind [isTeeFunctional] so a dead TEE never triggers a second doomed
* probe — it simply reports `false` (cannot attest), the faithful result for such hardware.
*/
private fun checkDeviceIdAttestation(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return false
if (!isTeeFunctional) return false
return try {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val keyPairGenerator =
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
val challenge = ByteArray(16).apply { SecureRandom().nextBytes(this) }
val spec =
KeyGenParameterSpec.Builder(DEVICE_ID_CHECK_KEY_ALIAS, KeyProperties.PURPOSE_SIGN)
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256)
.setAttestationChallenge(challenge)
.setDevicePropertiesAttestationIncluded(true)
.build()
keyPairGenerator.initialize(spec)
keyPairGenerator.generateKeyPair()
runCatching { keyStore.deleteEntry(DEVICE_ID_CHECK_KEY_ALIAS) }
SystemLogger.info("Device-ID attestation supported by TEE.")
true
} catch (_: Exception) {
SystemLogger.info("Device-ID attestation not supported by TEE; mirroring as cannot-attest.")
false
}
}
/**
* Retrieves the attestation certificate generated during the TEE check. The key entry is
* deleted after retrieval to clean up.
@@ -1,193 +0,0 @@
package org.matrix.TEESimulator.config
import android.os.Handler
import android.os.HandlerThread
import java.io.File
import java.net.URL
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import javax.net.ssl.HttpsURLConnection
import org.json.JSONArray
import org.json.JSONObject
import org.matrix.TEESimulator.BuildConfig
import org.matrix.TEESimulator.logging.SystemLogger
object BulletinPoller {
private const val BULLETIN_URL = "https://source.android.com/docs/security/bulletin/pixel"
private const val PATCH_FILE = "/data/adb/tricky_store/security_patch.txt"
private const val HISTORY_FILE = "/data/adb/tricky_store/last_bulletin_fetch.json"
private const val HISTORY_STAGING = "/data/adb/tricky_store/last_bulletin_fetch.json.next"
private const val HISTORY_CAP = 10
private const val CONNECT_TIMEOUT_MS = 10_000
private const val READ_TIMEOUT_MS = 15_000
private const val STEADY_INTERVAL_MS = 24L * 60 * 60 * 1000
private val BOOTSTRAP_INTERVALS = longArrayOf(5_000, 30_000, 120_000, 600_000, 1_800_000)
private val DATE_REGEX = Regex("<td>(\\d{4}-\\d{2}-\\d{2})</td>")
private val PATCH_DATE_PATTERN = Regex("^\\d{4}-\\d{2}-\\d{2}$")
private lateinit var handler: Handler
@Volatile private var bootstrapStep = 0
@Volatile private var steadyArmed = false
fun start() {
val thread = HandlerThread("BulletinPoller").apply { start() }
handler = Handler(thread.looper)
handler.postDelayed(::pollOnce, BOOTSTRAP_INTERVALS[0])
}
private fun pollOnce() {
try {
val result = fetchAndParse()
appendHistory(result)
scheduleNext(result.status == "success")
} catch (t: Throwable) {
SystemLogger.error("BulletinPoller: pollOnce failed", t)
scheduleNext(false)
}
}
private fun scheduleNext(success: Boolean) {
if (success || steadyArmed) {
steadyArmed = true
handler.postDelayed(::pollOnce, STEADY_INTERVAL_MS)
return
}
bootstrapStep++
if (bootstrapStep >= BOOTSTRAP_INTERVALS.size) {
steadyArmed = true
handler.postDelayed(::pollOnce, STEADY_INTERVAL_MS)
} else {
handler.postDelayed(::pollOnce, BOOTSTRAP_INTERVALS[bootstrapStep])
}
}
private data class FetchResult(
val ts: Long,
val status: String,
val httpCode: Int?,
val parsedDate: String?,
val applied: Boolean,
val error: String?,
)
private fun fetchAndParse(): FetchResult {
val ts = System.currentTimeMillis()
var conn: HttpsURLConnection? = null
return try {
conn =
(URL(BULLETIN_URL).openConnection() as HttpsURLConnection).apply {
connectTimeout = CONNECT_TIMEOUT_MS
readTimeout = READ_TIMEOUT_MS
setRequestProperty(
"User-Agent",
"TEESimulator/${BuildConfig.VERSION_NAME}",
)
requestMethod = "GET"
}
val code = conn.responseCode
if (code != 200) {
return FetchResult(ts, "network_error", code, null, false, "HTTP $code")
}
val html = conn.inputStream.bufferedReader().use { it.readText() }
val date = DATE_REGEX.find(html)?.groupValues?.get(1)
if (date == null) {
return FetchResult(
ts,
"parse_error",
code,
null,
false,
"no <td>YYYY-MM-DD</td> match",
)
}
val current = currentPatch()
if (current == null || date <= current) {
return FetchResult(ts, "success", code, date, false, null)
}
if (PatchLevelManager.updateTo(date)) {
FetchResult(ts, "success", code, date, true, null)
} else {
FetchResult(
ts,
"validation_rejected",
code,
date,
false,
"PatchLevelManager.updateTo rejected $date",
)
}
} catch (e: Exception) {
FetchResult(ts, "network_error", null, null, false, e.toString())
} finally {
conn?.disconnect()
}
}
private fun currentPatch(): String? {
val f = File(PATCH_FILE)
if (!f.exists()) return null
val raw = try {
f.readLines()
.firstOrNull { it.startsWith("system=") }
?.substringAfter("system=")
?.trim()
?.takeIf { it != "prop" && it.isNotEmpty() }
} catch (_: Exception) {
null
}
if (raw == null) return null
if (PATCH_DATE_PATTERN.matches(raw)) return raw
SystemLogger.warning(
"BulletinPoller: ignoring malformed system='$raw' in $PATCH_FILE"
)
return null
}
private fun appendHistory(result: FetchResult) {
try {
val target = File(HISTORY_FILE)
val staging = File(HISTORY_STAGING)
val existing = if (target.exists()) runCatching { target.readText() }.getOrNull() else null
val history =
existing
?.let { runCatching { JSONObject(it).optJSONArray("history") }.getOrNull() }
?: JSONArray()
val entry =
JSONObject().apply {
put("ts", result.ts)
put("status", result.status)
put("http_code", result.httpCode ?: JSONObject.NULL)
put("parsed_date", result.parsedDate ?: JSONObject.NULL)
put("applied", result.applied)
put("error", result.error ?: JSONObject.NULL)
}
history.put(entry)
while (history.length() > HISTORY_CAP) history.remove(0)
val latestKnown =
(0 until history.length())
.mapNotNull {
history.optJSONObject(it)?.optString("parsed_date", "")?.takeIf { d ->
d.isNotBlank()
}
}
.lastOrNull()
val root =
JSONObject().apply {
put("latest_known_date", latestKnown ?: JSONObject.NULL)
put("history", history)
}
staging.writeText(root.toString(2))
Files.move(
staging.toPath(),
target.toPath(),
StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING,
)
} catch (e: Exception) {
SystemLogger.error("BulletinPoller: failed to persist history", e)
}
}
}
@@ -1,192 +0,0 @@
package org.matrix.TEESimulator.config
import android.os.Build
import android.os.FileObserver
import android.os.SystemProperties
import java.io.File
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.time.LocalDate
import org.json.JSONObject
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.util.AndroidDeviceUtils
object PatchLevelManager {
private const val PATCH_FILE = "/data/adb/tricky_store/security_patch.txt"
private const val STAGING_FILE = "/data/adb/tricky_store/security_patch.txt.next"
private const val PIF_DIR = "/data/adb/modules/playintegrityfix"
private const val FLOOR_YYYYMMDD = 20200101
private const val MAX_PAST_OFFSET = 10000
/**
* Pixel security bulletins publish monthly; pre-announced dates occasionally
* slip by 2-4 weeks. 60 days covers that window without admitting a
* far-future date from a hostile or mis-parsed bulletin response.
*/
private const val MAX_FUTURE_DAYS = 60L
private val PIF_FILENAMES =
setOf("pif.json", "pif.prop", "custom.pif.json", "custom.pif.prop")
private val DATE_PATTERN = Regex("^\\d{4}-\\d{2}-\\d{2}$")
private val PROP_PATTERN = Regex("^SECURITY_PATCH=(.+)$", RegexOption.MULTILINE)
private val SECTION_HEADER = Regex("^\\[[a-zA-Z0-9_.-]+]$")
private val GLOBAL_KEYS = setOf("system", "boot", "vendor", "all")
private val PIF_SOURCES =
listOf(
"/data/adb/modules/playintegrityfix/pif.json",
"/data/adb/pif.json",
"/data/adb/modules/playintegrityfix/pif.prop",
"/data/adb/pif.prop",
"/data/adb/modules/playintegrityfix/custom.pif.json",
"/data/adb/modules/playintegrityfix/custom.pif.prop",
)
fun initialize() {
refreshFromSources()
startPifObserver()
}
private fun refreshFromSources() {
val date =
resolvePifPatch()
?: SystemProperties.get(
"ro.build.version.security_patch",
Build.VERSION.SECURITY_PATCH,
)
SystemLogger.info("PatchLevelManager: resolved patch date = $date")
applyToProps(date)
}
private fun startPifObserver() {
if (!File(PIF_DIR).exists()) {
SystemLogger.debug("PatchLevelManager: PIF dir absent, hot-reload disabled")
return
}
PifObserver.startWatching()
}
@Synchronized
private fun applyToProps(date: String) {
if (!DATE_PATTERN.matches(date)) {
SystemLogger.warning(
"PatchLevelManager: skip resetprop for invalid date: $date"
)
return
}
AndroidDeviceUtils.setProperty("ro.build.version.security_patch", date)
AndroidDeviceUtils.setProperty("ro.vendor.build.security_patch", date)
}
fun updateTo(date: String): Boolean {
if (!DATE_PATTERN.matches(date)) {
SystemLogger.warning("PatchLevelManager: invalid date format: $date")
return false
}
val dateInt = date.replace("-", "").toInt()
if (dateInt < FLOOR_YYYYMMDD) {
SystemLogger.warning("PatchLevelManager: $date below floor $FLOOR_YYYYMMDD")
return false
}
val now = LocalDate.now()
val today = now.year * 10000 + now.monthValue * 100 + now.dayOfMonth
if (today >= dateInt + MAX_PAST_OFFSET) {
SystemLogger.warning(
"PatchLevelManager: $date more than 1y older than today ($today)"
)
return false
}
val maxFuture =
now.plusDays(MAX_FUTURE_DAYS).let {
it.year * 10000 + it.monthValue * 100 + it.dayOfMonth
}
if (dateInt > maxFuture) {
SystemLogger.warning(
"PatchLevelManager: $date more than $MAX_FUTURE_DAYS days in future ($maxFuture)"
)
return false
}
try {
atomicWrite(date)
} catch (e: Exception) {
SystemLogger.error("PatchLevelManager: atomicWrite failed for $date", e)
return false
}
applyToProps(date)
SystemLogger.info("PatchLevelManager: applied patch date $date")
return true
}
private fun resolvePifPatch(): String? {
val source =
PIF_SOURCES.map(::File).lastOrNull { it.exists() && it.length() > 0 }
?: return null
return try {
val text = source.readText()
val parsed =
if (source.name.endsWith(".json")) {
JSONObject(text).optString("SECURITY_PATCH", "")
} else {
PROP_PATTERN.find(text)?.groupValues?.get(1)?.trim().orEmpty()
}
parsed.takeIf { it.isNotBlank() }
} catch (e: Exception) {
SystemLogger.warning(
"PatchLevelManager: failed to parse ${source.path}: ${e.message}"
)
null
}
}
private fun atomicWrite(date: String) {
val target = File(PATCH_FILE)
val staging = File(STAGING_FILE)
staging.writeText(mergedContents(target, date))
Files.move(
staging.toPath(),
target.toPath(),
StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING,
)
}
private fun mergedContents(target: File, date: String): String {
val globalBlock = "system=$date\nboot=$date\nvendor=$date\n"
if (!target.exists()) return globalBlock
val tail = stripGlobalAssignments(target.readLines())
if (tail.isEmpty()) return globalBlock
return globalBlock + tail.joinToString("\n", prefix = "\n", postfix = "\n")
}
private fun stripGlobalAssignments(lines: List<String>): List<String> {
val kept = mutableListOf<String>()
var inGlobal = true
for (line in lines) {
val trimmed = line.trim()
if (SECTION_HEADER.matches(trimmed)) {
inGlobal = false
kept += line
continue
}
if (inGlobal && isGlobalKeyAssignment(trimmed)) continue
kept += line
}
return kept
}
private fun isGlobalKeyAssignment(trimmed: String): Boolean {
if (trimmed.isEmpty() || trimmed.startsWith("#") || '=' !in trimmed) return false
val key = trimmed.substringBefore('=').trim().lowercase()
return key in GLOBAL_KEYS
}
private object PifObserver :
FileObserver(File(PIF_DIR), CLOSE_WRITE or MOVED_TO or DELETE) {
override fun onEvent(event: Int, path: String?) {
if (path == null || path !in PIF_FILENAMES) return
SystemLogger.info("PatchLevelManager: PIF change ($path), refreshing")
refreshFromSources()
}
}
}
@@ -116,12 +116,21 @@ object InterceptorUtils {
fun <T : Parcelable?> createTypedObjectReply(
obj: T,
flags: Int = 0,
diagnosticTag: String? = null,
): BinderInterceptor.TransactionResult.OverrideReply {
val parcel =
Parcel.obtain().apply {
writeNoException()
writeTypedObject(obj, flags)
}
if (diagnosticTag != null && SystemLogger.isDebugBuild) {
val savedPos = parcel.dataPosition()
val wire = parcel.marshall()
parcel.setDataPosition(savedPos)
val path = "/data/local/tmp/teesim-$diagnosticTag.bin"
runCatching { java.io.File(path).writeBytes(wire) }
SystemLogger.debug("[$diagnosticTag] reply len=${wire.size} path=$path")
}
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
@@ -5,6 +5,7 @@ import android.hardware.security.keymint.SecurityLevel
import android.os.Build
import android.os.IBinder
import android.os.Parcel
import android.os.ServiceManager
import android.system.keystore2.Domain
import android.system.keystore2.IKeystoreService
import android.system.keystore2.KeyDescriptor
@@ -48,6 +49,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
else null
private val GET_NUMBER_OF_ENTRIES_TRANSACTION =
InterceptorUtils.getTransactCode(stubBinderClass, "getNumberOfEntries")
private val GRANT_TRANSACTION = InterceptorUtils.getTransactCode(stubBinderClass, "grant")
private val UNGRANT_TRANSACTION = InterceptorUtils.getTransactCode(stubBinderClass, "ungrant")
private val transactionNames: Map<Int, String> by lazy {
stubBinderClass.declaredFields
@@ -59,6 +62,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
}
private const val RESPONSE_KEY_NOT_FOUND = 7
private const val RESPONSE_PERMISSION_DENIED = 6
// KeyStoreManager.grantKeyAccess() became a public app API in Android 16 (API 36). Before that,
// grant was a hidden API and SELinux denied untrusted_app, so a synthetic-key grant must answer
// PERMISSION_DENIED pre-36 and a coherent virtualized grant on 36+.
private const val GRANT_PUBLIC_API_SDK = 36
private val deletedSoftwareKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
@@ -80,6 +89,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
LIST_ENTRIES_TRANSACTION,
LIST_ENTRIES_BATCHED_TRANSACTION,
GET_NUMBER_OF_ENTRIES_TRANSACTION,
GRANT_TRANSACTION,
UNGRANT_TRANSACTION,
)
.toIntArray()
}
@@ -91,6 +102,27 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
override fun onInterceptorReady(service: IBinder, backdoor: IBinder) {
val keystoreInterface = IKeystoreService.Stub.asInterface(service)
setupSecurityLevelInterceptors(keystoreInterface, backdoor)
setupMaintenanceInterceptor(backdoor)
}
/**
* Hooks the keystore2 daemon's `android.security.maintenance` binder, which is hosted by the
* same process, so synthetic key state follows real key-lifecycle events. Best-effort: if the
* service is absent the synthetic plane simply forgoes lifecycle parity.
*/
private fun setupMaintenanceInterceptor(backdoor: IBinder) {
runCatching {
ServiceManager.getService("android.security.maintenance")?.let { maintenance ->
SystemLogger.info("Found maintenance binder. Registering interceptor...")
register(
backdoor,
maintenance,
Keystore2MaintenanceInterceptor,
Keystore2MaintenanceInterceptor.interceptedCodes,
)
} ?: SystemLogger.warning("Maintenance binder not found; skipping lifecycle parity.")
}
.onFailure { SystemLogger.error("Failed to intercept maintenance binder.", it) }
}
private fun setupSecurityLevelInterceptors(service: IKeystoreService, backdoor: IBinder) {
@@ -175,17 +207,46 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (ConfigurationManager.shouldSkipUid(callingUid))
return TransactionResult.ContinueAndSkipPost
if (code == UPDATE_SUBCOMPONENT_TRANSACTION)
if (code == UPDATE_SUBCOMPONENT_TRANSACTION) {
if (ConfigurationManager.shouldSkipUid(callingUid))
return TransactionResult.ContinueAndSkipPost
return handleUpdateSubcomponent(callingUid, data)
}
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val descriptor =
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.ContinueAndSkipPost
// Domain.GRANT read (Android 16+ KeyStoreManager grant). Served for ANY grantee uid —
// including isolated services (bindIsolatedService) with no package mapping — so resolve
// it before the package-scoped skip; caller-binding in resolveGrant() is the real access
// gate. On Android <= 15 no grants are ever issued (grant() denies), so softwareGrants is
// empty and this falls through to the real keystore2.
if (code == GET_KEY_ENTRY_TRANSACTION && descriptor.domain == Domain.GRANT) {
val grant =
KeyMintSecurityLevelInterceptor.resolveGrant(descriptor.nspace, callingUid)
if (grant == null) {
// Ours but wrong caller -> KEY_NOT_FOUND (caller-binding); not ours -> real keystore2.
return if (
KeyMintSecurityLevelInterceptor.softwareGrants.containsKey(descriptor.nspace)
)
InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
else TransactionResult.ContinueAndSkipPost
}
if ((grant.accessVector and 0x4) == 0) { // GET_INFO = 0x4 (access-vector gate)
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
}
val response =
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(grant.ownerKeyId)
?: return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
// Same object the owner read returns -> coherent chain across planes.
return InterceptorUtils.createTypedObjectReply(response)
}
if (ConfigurationManager.shouldSkipUid(callingUid))
return TransactionResult.ContinueAndSkipPost
if (code == DELETE_KEY_TRANSACTION) {
val keyId =
if (descriptor.alias != null) {
@@ -247,6 +308,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
return InterceptorUtils.createTypedObjectReply(teeResp)
}
}
// Domain.GRANT is handled earlier (before the package-scoped skip); an alias-less
// read reaching here is KEY_ID or unknown, so it falls through to the real keystore2.
return TransactionResult.ContinueAndSkipPost
}
val keyId = KeyIdentifier(callingUid, descriptor.alias)
@@ -268,6 +331,57 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
KeyMintParameterLogger.logParameter(it.keyParameter)
}
return InterceptorUtils.createTypedObjectReply(response)
} else if (code == GRANT_TRANSACTION) {
logTransaction(txId, transactionNames[code] ?: "grant", callingUid, callingPid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val key =
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.ContinueAndSkipPost
val granteeUid = data.readInt()
val accessVector = data.readInt()
// Synthetic (generatedKeys) AND patch-mode (teeResponses) keys are ours; both must grant
// coherently so the Domain.GRANT readback returns the same chain the owner read returns.
// Real hardware keys fall through to the real keystore2, which applies the same SELinux
// gate the platform would.
val ownerKeyId =
resolveOwnerKeyId(key, callingUid)
?.takeIf { KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) }
?: return TransactionResult.ContinueAndSkipPost
// Version-gated to mirror the real TEE 1:1. Pre-Android-16, grant was a hidden API and
// SELinux denied untrusted_app, so keystore2 returns PERMISSION_DENIED. Android 16
// (API 36) exposes KeyStoreManager.grantKeyAccess(), so an app grants its own key:
// issue a coherent, caller-bound, access-vector-carrying grant whose Domain.GRANT read
// returns the owner's chain.
if (Build.VERSION.SDK_INT < GRANT_PUBLIC_API_SDK) {
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
}
val grantId =
KeyMintSecurityLevelInterceptor.issueGrant(ownerKeyId, granteeUid, accessVector)
val reply =
KeyDescriptor().apply {
domain = Domain.GRANT
nspace = grantId
alias = null
blob = null
}
return InterceptorUtils.createTypedObjectReply(reply)
} else if (code == UNGRANT_TRANSACTION) {
logTransaction(txId, transactionNames[code] ?: "ungrant", callingUid, callingPid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val key =
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.ContinueAndSkipPost
val granteeUid = data.readInt()
val ownerKeyId =
resolveOwnerKeyId(key, callingUid)
?.takeIf { KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) }
?: return TransactionResult.ContinueAndSkipPost
// Same version gate as grant(): denied pre-36, revoke the virtualized grant on 36+.
if (Build.VERSION.SDK_INT < GRANT_PUBLIC_API_SDK) {
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
}
KeyMintSecurityLevelInterceptor.revokeGrant(ownerKeyId, granteeUid)
return InterceptorUtils.createSuccessReply(writeResultCode = false)
} else {
logTransaction(
txId,
@@ -508,6 +622,24 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
return TransactionResult.SkipTransaction
}
/**
* Resolves the owner [KeyIdentifier] a grant/ungrant call targets. APP/alias keys map
* directly; KEY_ID keys are looked up by nspace (mirrors the deleteKey resolver). Returns
* null for anything not addressable, so callers fall through to the real keystore2.
*/
private fun resolveOwnerKeyId(descriptor: KeyDescriptor, callingUid: Int): KeyIdentifier? =
when {
descriptor.alias != null -> KeyIdentifier(callingUid, descriptor.alias)
descriptor.domain == Domain.KEY_ID ->
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(callingUid, descriptor.nspace)
?.let { info ->
KeyMintSecurityLevelInterceptor.generatedKeys.entries
.firstOrNull { it.value.nspace == info.nspace && it.key.uid == callingUid }
?.key
}
else -> null
}
private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult {
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR)
@@ -527,6 +659,18 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
}
if (generatedKeyInfo == null) {
// Patch-mode key (cached in teeResponses, not generatedKeys): the real keystore2 applies
// the update, so drop our stale cached chain. Otherwise getKeyEntry replays the
// pre-update generated attestation (duck STALE_TEE_RESPONSE_AFTER_KEY_ID_UPDATE).
when (descriptor.domain) {
Domain.KEY_ID ->
KeyMintSecurityLevelInterceptor.evictTeeResponseByKeyId(callingUid, descriptor.nspace)
Domain.APP ->
descriptor.alias?.let {
KeyMintSecurityLevelInterceptor.evictTeeResponse(KeyIdentifier(callingUid, it))
}
else -> {}
}
descriptor.alias?.let {
val kid = KeyIdentifier(callingUid, it)
userUpdatedKeys.add(kid)
@@ -0,0 +1,108 @@
package org.matrix.TEESimulator.interception.keystore
import android.os.IBinder
import android.os.Parcel
import android.security.maintenance.IKeystoreMaintenance
import android.system.keystore2.Domain
import android.system.keystore2.KeyDescriptor
import org.matrix.TEESimulator.interception.core.BinderInterceptor
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
import org.matrix.TEESimulator.logging.SystemLogger
/**
* Intercepts the keystore2 daemon's `android.security.maintenance` binder so our synthetic key
* state follows the same lifecycle events the platform applies to real keys.
*
* This is a pure side-effect hook: every handled transaction mutates only our own synthetic state
* and then returns [TransactionResult.ContinueAndSkipPost], so the real keystore2 still performs the
* real operation. We never fabricate a maintenance reply, so real key lifecycle is never disturbed.
*
* Mounted via `register()` from [Keystore2Interceptor.onInterceptorReady]; the maintenance binder is
* hosted by the same keystore2 process, so the already-injected native hook reaches it too.
*/
object Keystore2MaintenanceInterceptor : BinderInterceptor() {
private val stubClass = IKeystoreMaintenance.Stub::class.java
private val CLEAR_NAMESPACE_TRANSACTION =
InterceptorUtils.getTransactCode(stubClass, "clearNamespace")
private val DELETE_ALL_KEYS_TRANSACTION =
InterceptorUtils.getTransactCode(stubClass, "deleteAllKeys")
private val MIGRATE_KEY_NAMESPACE_TRANSACTION =
InterceptorUtils.getTransactCode(stubClass, "migrateKeyNamespace")
/** Only the lifecycle transactions we mirror; unresolved codes (-1) are dropped. */
val interceptedCodes: IntArray by lazy {
listOf(
CLEAR_NAMESPACE_TRANSACTION,
DELETE_ALL_KEYS_TRANSACTION,
MIGRATE_KEY_NAMESPACE_TRANSACTION,
)
.filter { it != -1 }
.toIntArray()
}
override fun onPreTransact(
txId: Long,
target: IBinder,
code: Int,
flags: Int,
callingUid: Int,
callingPid: Int,
data: Parcel,
): TransactionResult {
when (code) {
CLEAR_NAMESPACE_TRANSACTION -> handleClearNamespace(data)
DELETE_ALL_KEYS_TRANSACTION ->
KeyMintSecurityLevelInterceptor.clearAllGeneratedKeys("maintenance.deleteAllKeys")
MIGRATE_KEY_NAMESPACE_TRANSACTION -> handleMigrateKeyNamespace(data, callingUid)
}
// Always let the real keystore2 perform the real lifecycle operation.
return TransactionResult.ContinueAndSkipPost
}
private fun handleClearNamespace(data: Parcel) {
data.enforceInterface(IKeystoreMaintenance.DESCRIPTOR)
val domain = data.readInt()
val nspace = data.readLong()
// Only Domain.APP namespaces map to our per-uid synthetic keys; nspace is the app uid.
if (domain == Domain.APP) {
KeyMintSecurityLevelInterceptor.clearNamespaceKeys(nspace.toInt())
}
}
private fun handleMigrateKeyNamespace(data: Parcel, callingUid: Int) {
data.enforceInterface(IKeystoreMaintenance.DESCRIPTOR)
val source = data.readTypedObject(KeyDescriptor.CREATOR) ?: return
val destination = data.readTypedObject(KeyDescriptor.CREATOR) ?: return
val srcId = resolveSyntheticKeyId(source, callingUid) ?: return
if (!KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(srcId)) return // not ours
val dstId = resolveDestinationKeyId(destination, callingUid)
if (dstId == null) {
// Migrated out of our trackable (Domain.APP/alias) space -> drop our shadow so reads
// fall through to the real keystore2, which now owns it at the new namespace.
KeyMintSecurityLevelInterceptor.cleanupKeyData(srcId)
} else {
KeyMintSecurityLevelInterceptor.migrateGeneratedKey(srcId, dstId)
}
}
/** Resolves a synthetic owner key from a source descriptor (Domain.APP alias or KEY_ID). */
private fun resolveSyntheticKeyId(descriptor: KeyDescriptor, callingUid: Int): KeyIdentifier? =
when {
descriptor.alias != null -> KeyIdentifier(callingUid, descriptor.alias)
descriptor.domain == Domain.KEY_ID ->
KeyMintSecurityLevelInterceptor.generatedKeys.entries
.firstOrNull { it.key.uid == callingUid && it.value.nspace == descriptor.nspace }
?.key
else -> null
}
/** Destination must be an addressable Domain.APP alias for us to keep tracking the key. */
private fun resolveDestinationKeyId(descriptor: KeyDescriptor, callingUid: Int): KeyIdentifier? {
val alias = descriptor.alias ?: return null
if (descriptor.domain != Domain.APP) return null
val uid = if (descriptor.nspace > 0) descriptor.nspace.toInt() else callingUid
return KeyIdentifier(uid, alias)
}
}
@@ -21,16 +21,15 @@ import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.spec.PKCS8EncodedKeySpec
import java.util.Date
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedDeque
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.locks.LockSupport
import org.matrix.TEESimulator.attestation.AttestationBuilder
import org.matrix.TEESimulator.attestation.AttestationConstants
import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.attestation.DeviceAttestationService
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.core.BinderInterceptor
@@ -60,10 +59,6 @@ class KeyMintSecurityLevelInterceptor(
val keyParams: KeyMintAttestation? = null,
)
// null = undecided, true = TEE works (use PATCH), false = TEE broken (use GENERATE)
// Instance field so TRUSTED_ENVIRONMENT and STRONGBOX decide independently
val teePathDecision = AtomicReference<Boolean?>(null)
private val activeOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<SoftwareOperation>>()
private val recentOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<Long>>()
@@ -76,18 +71,16 @@ class KeyMintSecurityLevelInterceptor(
callingPid: Int,
data: Parcel,
): TransactionResult {
val shouldSkip = ConfigurationManager.shouldSkipUid(callingUid)
when (code) {
GENERATE_KEY_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (!shouldSkip) return handleGenerateKey(txId, callingUid, callingPid, data)
return handleGenerateKey(txId, callingUid, callingPid, data)
}
CREATE_OPERATION_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (!shouldSkip) return handleCreateOperation(txId, callingUid, data)
return handleCreateOperation(txId, callingUid, data)
}
IMPORT_KEY_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
@@ -134,13 +127,18 @@ class KeyMintSecurityLevelInterceptor(
val keyDescriptor =
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction
// Evict generated key data but retain patched chains so detectors
// can't use importKey to force unpatched getKeyEntry responses.
// A successful importKey replaces the alias's key in the real keystore2, so any prior
// generate/patch cache for this alias is stale. Drop it: a non-attested import then
// falls through to the real keystore2 (origin=IMPORTED, imported leaf), and the
// attested-import branch below re-caches the fresh patched chain. Without this,
// getKeyEntry replays the prior generated attestation (duck STALE_GENERATED_AFTER_IMPORT).
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
if (generatedKeys.remove(keyId) != null) {
SystemLogger.debug("Remove generated key on importKey $keyId")
GeneratedKeyPersistence.delete(keyId)
}
teeResponses.remove(keyId)
patchedChains.remove(keyId)
attestationKeys.remove(keyId)
importedKeys.add(keyId)
SystemLogger.trace { "[TRACE-$txId] post-importKey $keyId: added to importedKeys, skipUid=${ConfigurationManager.shouldSkipUid(callingUid)}" }
@@ -325,7 +323,7 @@ class KeyMintSecurityLevelInterceptor(
entry ?: 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
return TransactionResult.ContinueAndSkipPost
}
}
else -> {
@@ -424,6 +422,14 @@ class KeyMintSecurityLevelInterceptor(
}
private fun handleGenerateKey(txId: Long, callingUid: Int, callingPid: Int, data: Parcel): TransactionResult {
if (SystemLogger.isDebugBuild) {
val savedPos = data.dataPosition()
val req = data.marshall()
data.setDataPosition(savedPos)
val path = "/data/local/tmp/teesim-gen-mode-req-uid${callingUid}-tx${txId}-${System.nanoTime()}.bin"
runCatching { java.io.File(path).writeBytes(req) }
SystemLogger.debug("[gen-mode-req] uid=$callingUid txId=$txId len=${req.size} path=$path")
}
val oversized = data.dataSize() > MAX_ALIAS_LENGTH
return runCatching {
@@ -434,8 +440,14 @@ class KeyMintSecurityLevelInterceptor(
SystemLogger.debug(
"Handling generateKey ${keyDescriptor.alias}, attestKey=${attestationKey?.alias}"
)
val params = data.createTypedArray(KeyParameter.CREATOR)!!
val parsedParams = KeyMintAttestation(params)
var params = data.createTypedArray(KeyParameter.CREATOR)!!
var parsedParams = KeyMintAttestation(params)
val isAttestKeyRequest = parsedParams.isAttestKey()
if (ConfigurationManager.shouldSkipUid(callingUid)
&& attestationKey == null && !isAttestKeyRequest) {
return TransactionResult.ContinueAndSkipPost
}
SystemLogger.trace { "[TRACE-$txId] generateKey alias=${keyDescriptor.alias} algo=${parsedParams.algorithm} challenge=${parsedParams.attestationChallenge?.size ?: "null"} serial=${parsedParams.serial != null} imei=${parsedParams.imei != null} noAuth=${parsedParams.noAuthRequired} purposes=${parsedParams.purpose}" }
if (SystemLogger.isDebugBuild) params.forEach { p ->
@@ -466,13 +478,37 @@ class KeyMintSecurityLevelInterceptor(
it.tag == Tag.ATTESTATION_ID_SECOND_IMEI
}
val hasDevicePropertyAttestation = parsedParams.brand != null ||
parsedParams.device != null ||
parsedParams.product != null ||
parsedParams.manufacturer != null ||
parsedParams.model != null
// Mirror the real TEE's capability: hardware that never provisioned device IDs
// returns CANNOT_ATTEST_IDS. Synthesizing device-ID/property attestation a chip of
// this class cannot produce is an over-capability tell — a genuine device fails the
// same request. Forge health, mirror capability.
if ((hasDeviceIdAttestation || hasDevicePropertyAttestation) &&
!DeviceAttestationService.canAttestDeviceIds) {
SystemLogger.info("[TX_ID: $txId] Real TEE cannot attest device IDs; returning CANNOT_ATTEST_IDS for uid=$callingUid (mirroring hardware)")
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
}
if(hasDeviceIdAttestation && !AndroidPermissionUtils.hasDeviceAttestationPermission(callingUid)) {
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_ID_ATTESTATION for uid=$callingUid")
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
}
// AOSP security_level.rs:478-485: INCLUDE_UNIQUE_ID requires
// SELinux gen_unique_id OR Android REQUEST_UNIQUE_ID_ATTESTATION
// INCLUDE_UNIQUE_ID requires SELinux gen_unique_id OR
// android.permission.REQUEST_UNIQUE_ID_ATTESTATION (AOSP
// security_level.rs:478-485). AOSP returns PERMISSION_DENIED
// when neither is held — but doing so breaks Google Wallet
// card binding (Wallet's generateKey carries the tag without
// holding the permission, and Play Integrity also fails when
// unique_id ends up in the attestation). Silently strip the
// tag so the key generates normally and the resulting
// attestation simply omits the unique_id field. This mirrors
// the pre-PR157 behavior where the tag had no effect.
if (params.any { it.tag == Tag.INCLUDE_UNIQUE_ID }) {
val hasSELinux = ConfigurationManager.checkSELinuxPermission(
callingPid, "keystore_key", "gen_unique_id",
@@ -481,8 +517,9 @@ class KeyMintSecurityLevelInterceptor(
callingUid, "android.permission.REQUEST_UNIQUE_ID_ATTESTATION",
)
if (!hasSELinux && !hasAndroid) {
SystemLogger.warning("[TX_ID: $txId] Rejecting INCLUDE_UNIQUE_ID for uid=$callingUid pid=$callingPid")
return InterceptorUtils.createServiceSpecificErrorReply(RESPONSE_PERMISSION_DENIED)
SystemLogger.debug("[TX_ID: $txId] Stripping INCLUDE_UNIQUE_ID for uid=$callingUid pid=$callingPid (no permission)")
params = params.filter { it.tag != Tag.INCLUDE_UNIQUE_ID }.toTypedArray()
parsedParams = KeyMintAttestation(params)
}
}
@@ -496,26 +533,17 @@ class KeyMintSecurityLevelInterceptor(
}
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
val isAttestKeyRequest = parsedParams.isAttestKey()
val forceGenerate =
oversized ||
ConfigurationManager.shouldGenerate(callingUid) ||
(ConfigurationManager.shouldPatch(callingUid) && isAttestKeyRequest) ||
(attestationKey != null &&
(attestationKey.alias?.let { isAttestationKey(KeyIdentifier(callingUid, it)) }
?: attestationKeys.any { kid -> kid.uid == callingUid && generatedKeys[kid]?.nspace == attestationKey.nspace }))
isAttestKeyRequest ||
attestationKey != null
val isAuto = ConfigurationManager.isAutoMode(callingUid)
if (isAuto) SystemLogger.debug("AUTO dispatch: teePathDecision=${teePathDecision.get()} for ${keyDescriptor.alias}")
SystemLogger.trace { "[TRACE-$txId] dispatch: forceGen=$forceGenerate isAuto=$isAuto teePath=${teePathDecision.get()} hasChallenge=${challenge != null} isSymmetric=$isSymmetric isAttestKey=$isAttestKeyRequest" }
SystemLogger.trace { "[TRACE-$txId] dispatch: forceGen=$forceGenerate hasChallenge=${challenge != null} isSymmetric=$isSymmetric isAttestKey=$isAttestKeyRequest" }
when {
forceGenerate -> doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
isAuto && teePathDecision.get() == null -> raceTeePatch(callingUid, keyDescriptor, attestationKey, params, parsedParams, keyId, isAttestKeyRequest)
isAuto && teePathDecision.get() == false -> doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
parsedParams.attestationChallenge != null -> TransactionResult.Continue
else -> {
cleanupKeyData(keyId)
@@ -547,11 +575,17 @@ class KeyMintSecurityLevelInterceptor(
parsedParams.algorithm != Algorithm.RSA
if (isSymmetric) {
if (attestationKey != null) {
throw android.os.ServiceSpecificException(
KEYMINT_INVALID_ARGUMENT,
"ATTEST_KEY tag is not supported for symmetric algorithms (algo=${parsedParams.algorithm})",
)
}
val algoName = when (parsedParams.algorithm) {
Algorithm.AES -> "AES"
Algorithm.HMAC -> "HmacSHA256"
else -> throw android.os.ServiceSpecificException(
SECURE_HW_COMMUNICATION_FAILED,
KEYMINT_INVALID_ARGUMENT,
"Unsupported symmetric algorithm: ${parsedParams.algorithm}",
)
}
@@ -620,7 +654,7 @@ class KeyMintSecurityLevelInterceptor(
TeeLatencySimulator.simulateGenerateKeyDelay(parsedParams.algorithm, System.nanoTime() - genStartNanos)
}
return InterceptorUtils.createTypedObjectReply(metadata)
return InterceptorUtils.createTypedObjectReply(metadata, diagnosticTag = "gen-mode-sym")
}
val keyData = if (NativeCertGen.isAvailable && attestationKey == null) {
@@ -693,94 +727,7 @@ class KeyMintSecurityLevelInterceptor(
TeeLatencySimulator.simulateGenerateKeyDelay(parsedParams.algorithm, System.nanoTime() - genStartNanos)
}
return InterceptorUtils.createTypedObjectReply(response.metadata)
}
private fun raceTeePatch(
callingUid: Int,
keyDescriptor: KeyDescriptor,
attestationKey: KeyDescriptor?,
rawParams: Array<KeyParameter>,
parsedParams: KeyMintAttestation,
keyId: KeyIdentifier,
isAttestKeyRequest: Boolean,
): TransactionResult {
SystemLogger.info("AUTO: racing TEE vs software for ${keyDescriptor.alias}")
val teeDescriptor = KeyDescriptor().apply {
domain = keyDescriptor.domain
nspace = keyDescriptor.nspace
alias = keyDescriptor.alias
blob = keyDescriptor.blob
}
val teeAttestKey = attestationKey?.let {
KeyDescriptor().apply {
domain = it.domain
nspace = it.nspace
alias = it.alias
blob = it.blob
}
}
val threadA = CompletableFuture.supplyAsync {
original.generateKey(teeDescriptor, teeAttestKey, rawParams, 0, byteArrayOf())
}
val swDescriptor = KeyDescriptor().apply {
domain = keyDescriptor.domain
nspace = secureRandom.nextLong()
alias = keyDescriptor.alias
blob = keyDescriptor.blob
}
val swKeyId = KeyIdentifier(callingUid, keyDescriptor.alias)
val threadB = CompletableFuture.supplyAsync {
doSoftwareKeyGen(callingUid, swDescriptor, attestationKey, parsedParams, swKeyId, isAttestKeyRequest)
}
return try {
val teeMetadata = threadA.join()
threadB.cancel(true)
teePathDecision.compareAndSet(null, true)
SystemLogger.info("AUTO: TEE succeeded, path locked to PATCH for ${keyDescriptor.alias}")
val originalChain = CertificateHelper.getCertificateChain(teeMetadata)
if (originalChain != null && originalChain.size > 1) {
val newChain = AttestationPatcher.patchCertificateChain(
originalChain, callingUid, parsedParams.certificateNotBefore, parsedParams.certificateNotAfter
)
CertificateHelper.updateCertificateChain(teeMetadata, newChain).getOrThrow()
teeMetadata.authorizations =
InterceptorUtils.patchAuthorizations(teeMetadata.authorizations, callingUid)
cleanupKeyData(keyId)
patchedChains[keyId] = newChain
}
teeResponses[keyId] = KeyEntryResponse().apply {
this.metadata = teeMetadata
iSecurityLevel = original
}
InterceptorUtils.createTypedObjectReply(teeMetadata)
} catch (_: Exception) {
if (teePathDecision.get() == true) {
threadB.cancel(true)
SystemLogger.info("AUTO: TEE failed locally but globally functional, forwarding for ${keyDescriptor.alias}")
return TransactionResult.Continue
}
teePathDecision.compareAndSet(null, false)
SystemLogger.info("AUTO: TEE failed, path locked to GENERATE for ${keyDescriptor.alias}")
try {
threadB.join()
} catch (e: Exception) {
SystemLogger.error("AUTO: both paths failed for ${keyDescriptor.alias}.", e)
val code =
if (e.cause is android.os.ServiceSpecificException)
(e.cause as android.os.ServiceSpecificException).errorCode
else SECURE_HW_COMMUNICATION_FAILED
InterceptorUtils.createServiceSpecificErrorReply(code)
}
}
return InterceptorUtils.createTypedObjectReply(response.metadata, diagnosticTag = "gen-mode-asym")
}
private fun generateAttestedKeyPairNative(
@@ -1241,6 +1188,63 @@ class KeyMintSecurityLevelInterceptor(
private val usageCounters = ConcurrentHashMap<KeyIdentifier, java.util.concurrent.atomic.AtomicInteger>()
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
/**
* Grant plane for the public `KeyStoreManager.grantKeyAccess()` API (Android 16, API 36+).
* On Android <= 15 grant was a hidden API denied to untrusted_app, so this state stays
* empty there (the GRANT_TRANSACTION handler returns PERMISSION_DENIED for synthetic keys
* pre-36). A grant is caller-bound and carries an access vector; resolving one yields the
* owner's own KeyEntryResponse so every access plane returns a coherent certificate chain.
*/
data class SoftwareGrant(
val ownerKeyId: KeyIdentifier,
val granteeUid: Int,
val accessVector: Int,
)
val softwareGrants = ConcurrentHashMap<Long, SoftwareGrant>() // grantId -> grant
/** Mint or reuse a grant id (random, non-zero, non -1 Long). Re-grant reuses the id. */
fun issueGrant(ownerKeyId: KeyIdentifier, granteeUid: Int, accessVector: Int): Long {
softwareGrants.entries
.firstOrNull { it.value.ownerKeyId == ownerKeyId && it.value.granteeUid == granteeUid }
?.let { existing ->
softwareGrants[existing.key] = existing.value.copy(accessVector = accessVector)
return existing.key
}
var id = secureRandom.nextLong()
while (id == 0L || id == -1L || softwareGrants.containsKey(id)) id = secureRandom.nextLong()
softwareGrants[id] = SoftwareGrant(ownerKeyId, granteeUid, accessVector)
return id
}
/** Caller-bound resolve: only the designated grantee, only while the key exists. */
fun resolveGrant(grantId: Long, callerUid: Int): SoftwareGrant? =
softwareGrants[grantId]?.takeIf {
it.granteeUid == callerUid && ownsKeyResponse(it.ownerKeyId)
}
/**
* True when this interceptor holds a coherent [KeyEntryResponse] for [keyId] — synthetic
* (`generatedKeys`) OR patch-mode (`teeResponses`, a real TEE key whose attestation we
* patched). The grant plane must virtualize both: gating on `generatedKeys` alone left
* patch-mode keys' `Domain.GRANT` readback falling through to the real keystore2 unpatched,
* splitting the grant chain against the owner's patched read (duck SELF_/ISOLATED_CHAIN_SPLIT,
* surfaced once Android 16 made KeyStoreManager.grantKeyAccess a public API).
*/
fun ownsKeyResponse(keyId: KeyIdentifier): Boolean = getGeneratedKeyResponse(keyId) != null
fun revokeGrant(ownerKeyId: KeyIdentifier, granteeUid: Int) {
softwareGrants.entries
.filter { it.value.ownerKeyId == ownerKeyId && it.value.granteeUid == granteeUid }
.forEach { softwareGrants.remove(it.key) }
}
fun purgeGrantsForKey(ownerKeyId: KeyIdentifier) {
softwareGrants.entries
.filter { it.value.ownerKeyId == ownerKeyId }
.forEach { softwareGrants.remove(it.key) }
}
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
generatedKeys[keyId]?.response ?: teeResponses[keyId]
@@ -1260,11 +1264,32 @@ class KeyMintSecurityLevelInterceptor(
?.value
}
/**
* Drops the cached TEE/patched response (and patched chain) addressed by KEY_ID so a
* post-mutation getKeyEntry falls through to the now-updated real keystore2 key. Used
* after updateSubcomponent re-keys a patched chain (duck
* STALE_TEE_RESPONSE_AFTER_KEY_ID_UPDATE).
*/
fun evictTeeResponseByKeyId(callingUid: Int, nspace: Long?) {
if (nspace == null || nspace == 0L) return
teeResponses.entries
.filter { (keyId, _) -> keyId.uid == callingUid }
.find { (_, response) -> response.metadata?.key?.nspace == nspace }
?.let { evictTeeResponse(it.key) }
}
/** Alias-addressed counterpart of [evictTeeResponseByKeyId]. */
fun evictTeeResponse(keyId: KeyIdentifier) {
teeResponses.remove(keyId)
patchedChains.remove(keyId)
}
fun getPatchedChain(keyId: KeyIdentifier): Array<Certificate>? = patchedChains[keyId]
fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId)
fun cleanupKeyData(keyId: KeyIdentifier) {
purgeGrantsForKey(keyId) // grants die with the key (Android 16 path; no-op pre-36)
if (generatedKeys.remove(keyId) != null) {
SystemLogger.debug("Remove generated key ${keyId}")
GeneratedKeyPersistence.delete(keyId)
@@ -1280,6 +1305,36 @@ class KeyMintSecurityLevelInterceptor(
usageCounters.remove(keyId)
}
/** Clears every synthetic key owned by [uid] (maintenance.clearNamespace, Domain.APP). */
fun clearNamespaceKeys(uid: Int) {
val victims = generatedKeys.keys.filter { it.uid == uid }
if (victims.isEmpty()) return
victims.forEach { cleanupKeyData(it) } // also purges grants + persistence
SystemLogger.info(
"Cleared ${victims.size} synthetic keys for uid=$uid (maintenance.clearNamespace)"
)
}
/**
* Re-keys a synthetic entry from [srcId] to [dstId] for maintenance.migrateKeyNamespace,
* preserving the key material, certificate chain, and any grants (which reference the key,
* not the namespace). In-memory only: the stale persisted file is dropped and the migrated
* key is not re-persisted, matching the single-session boundary the grant plane already
* accepts (Phase 9 plan §9). No-op if [srcId] is not ours or [dstId] already exists.
*/
fun migrateGeneratedKey(srcId: KeyIdentifier, dstId: KeyIdentifier) {
if (srcId == dstId || generatedKeys.containsKey(dstId)) return
val info = generatedKeys.remove(srcId) ?: return
generatedKeys[dstId] = info
if (attestationKeys.remove(srcId)) attestationKeys.add(dstId)
if (importedKeys.remove(srcId)) importedKeys.add(dstId)
softwareGrants.entries
.filter { it.value.ownerKeyId == srcId }
.forEach { softwareGrants[it.key] = it.value.copy(ownerKeyId = dstId) }
GeneratedKeyPersistence.delete(srcId)
SystemLogger.info("Migrated synthetic key $srcId -> $dstId (maintenance.migrateKeyNamespace)")
}
fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) {
unregister(backdoor, operationBinder)
@@ -1305,6 +1360,7 @@ class KeyMintSecurityLevelInterceptor(
attestationKeys.clear()
importedKeys.clear()
usageCounters.clear()
softwareGrants.clear()
GeneratedKeyPersistence.deleteAll()
SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.")
}
@@ -1329,15 +1385,23 @@ private fun KeyMintAttestation.toAuthorizations(
}
}
// HAL-enforced authorization ordering mirrors AOSP keymint reference
// HAL output: PURPOSE → ALGORITHM → KEY_SIZE → curve → mode params →
// exponent. Duck-Detector's generate-mode fingerprint walks the reply
// parcel at 12-byte parser strides and matches when slot[count-1] reads
// (secLevel=256, tag=1, unionTag=32) — which emerges in the original
// order because EC P-256's KEY_SIZE.value=256 lands at byte 224 (auth#4
// value field). Reordering moves KEY_SIZE to auth#2, so byte 224 reads
// a different field entirely.
this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) }
authList.add(createAuth(Tag.ALGORITHM, KeyParameterValue.algorithm(this.algorithm)))
authList.add(createAuth(Tag.KEY_SIZE, KeyParameterValue.integer(this.keySize)))
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.blockMode.forEach { authList.add(createAuth(Tag.BLOCK_MODE, KeyParameterValue.blockMode(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.KEY_SIZE, KeyParameterValue.integer(this.keySize)))
if (this.rsaPublicExponent != null) {
authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong())))
}
@@ -1379,14 +1443,13 @@ private fun KeyMintAttestation.toAuthorizations(
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)))
}
// Real keystore2 (captured on-device: MediaTek, Android 15) does NOT surface
// VENDOR_PATCHLEVEL or BOOT_PATCHLEVEL in the generateKey KeyMetadata.authorizations
// — they exist only in the attestation extension. Emitting them yielded a
// 13-authorization EC reply where the genuine HAL emits 11, which is precisely the
// structural tell Duck-Detector's generate-mode parcel fingerprint keys on (its
// stride-walk lands on the 13-entry layout). Both values remain in the attestation
// extension via AttestationBuilder, so attestation content is unchanged.
/**
* Keystore-enforced authorizations (CREATION_DATETIME, ACTIVE_DATETIME,
@@ -1428,7 +1491,17 @@ private fun KeyMintAttestation.toAuthorizations(
authList.add(createKeystoreAuth(Tag.UNLOCKED_DEVICE_REQUIRED, KeyParameterValue.boolValue(true)))
}
authList.add(createKeystoreAuth(Tag.USER_ID, KeyParameterValue.integer(callingUid / 100000)))
// Captured real keystore2 tags USER_ID at SecurityLevel.SOFTWARE (0), even though
// CREATION_DATETIME above is KEYSTORE (100). Mirror that split exactly.
authList.add(
Authorization().apply {
this.keyParameter = KeyParameter().apply {
this.tag = Tag.USER_ID
this.value = KeyParameterValue.integer(callingUid / 100000)
}
this.securityLevel = SecurityLevel.SOFTWARE
},
)
return authList.toTypedArray()
}
@@ -101,18 +101,19 @@ object CertificateGenerator {
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
val (signingKey, issuer) =
val attestKeyInfo =
if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second }
?: (keybox.keyPair to getIssuerFromKeybox(keybox))
} else {
keybox.keyPair to getIssuerFromKeybox(keybox)
}
getAttestationKeyInfo(uid, attestKeyAlias)
} else null
val (signingKey, issuer) = attestKeyInfo
?.let { it.first to it.second }
?: (keybox.keyPair to getIssuerFromKeybox(keybox))
val leafCert =
buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel)
if (attestKeyAlias != null) {
if (attestKeyInfo != null) {
listOf(leafCert)
} else {
listOf(leafCert) + keybox.certificates
+9 -30
View File
@@ -16,37 +16,16 @@ echo " 🔉 $(_msg confirm_vol_down)"
echo " "
confirm() {
vol_tmp="${TMPDIR:-/data/local/tmp}/teesim_vol_key"
seconds=10
: > "$vol_tmp"
getevent -qlc 1 > "$vol_tmp" 2>/dev/null &
ge_pid=$!
while [ "$seconds" -gt 0 ]; do
sleep 1
if ! kill -0 "$ge_pid" 2>/dev/null; then
key=$(awk '/KEY_/{print $3}' "$vol_tmp" 2>/dev/null)
case "$key" in
KEY_VOLUMEUP)
rm -f "$vol_tmp"
return 0
;;
KEY_VOLUMEDOWN)
rm -f "$vol_tmp"
return 1
;;
esac
: > "$vol_tmp"
getevent -qlc 1 > "$vol_tmp" 2>/dev/null &
ge_pid=$!
fi
seconds=$((seconds - 1))
# Sample getevent in 1s bursts; a piped stream block-buffers and misses
# a single key-press before the timeout.
deadline=$(( $(date +%s) + 10 ))
while [ "$(date +%s)" -lt "$deadline" ]; do
events=$(/system/bin/timeout 1 /system/bin/getevent -l 2>/dev/null)
case "$events" in
*KEY_VOLUMEUP*) return 0 ;;
*KEY_VOLUMEDOWN*) return 1 ;;
esac
done
kill "$ge_pid" 2>/dev/null
wait "$ge_pid" 2>/dev/null
rm -f "$vol_tmp"
return 1
}
+136
View File
@@ -1,3 +1,139 @@
## TEESimulator-RS v6.0.1-280
Clears the Duck Detector generate-mode parcel fingerprint that real Android 16 hardware also trips, fixes RSA attestation under an EC-only keybox, and restores device-property attestation for Play Integrity hardware apps such as BHIM and UPI. Generate-mode fix field-confirmed on Android 16.
### Detection coverage
- Generate-mode fingerprint: Duck reads the reply at a flat 12-byte stride and flags the sentinel tuple at positions 12 and 13 that the device's native ALGORITHM-first authorization order lands on. Real A16 silicon trips the same probe, so faithful mirroring stayed flagged. `normalizeAuthorizationLayout` marshals the auth array, runs Duck's exact predicate, and applies a minimal deterministic reorder only when it would match. Count, values, security levels, and the cert chain are untouched, and the reorder keys on the byte condition, never on a package. Applied on both the patch and forge reply paths. (#33)
- `updateAad` on a non-AEAD operation now answers per vendor: Samsung and Xiaomi-MTK TEEs return success, others return INVALID_TAG, matching Duck's OperationErrorPathProbe on both the sign/verify and cipher paths.
### Attestation correctness (Android 16, EC and RSA)
- RSA leaf under an EC-only keybox: patching used to catch the no-RSA-key throw and return the chain untouched, leaking the device's real unlocked Root of Trust for RSA keys while EC keys patched cleanly. It now falls back to any keybox key (EC preferred) and signs the patched leaf with the keybox key's own algorithm, so the RSA leaf re-roots to the Google keybox under a forged locked RoT.
- RSA attest-key forge on an EC-only keybox: the forge path matched the algorithm exactly and threw -75 ATTESTATION_KEYS_NOT_PROVISIONED on a miss, so an RSA ATTEST_KEY request never rooted and verifiers reported an unknown certificate. It now falls back to any attestation key, since an EC key validly ECDSA-signs an RSA-subject leaf. No-op on a dual keybox.
- A16 attestVersion: the device's KeyMint reports version 100 and the lazy cache shadowed the BAKLAVA-to-400 map, so the forge presented 100. It now caches the AOSP value per SDK and presents the correct 400.
- Algorithm-split key on restore: a persisted record holding an EC private key under an RSA leaf failed every signature as DATA_TOO_LARGE_FOR_MODULUS. Restore now drops the record when the private key and served leaf disagree, so the next generateKey rebuilds a coherent key.
- Stale chain on regenerate: reusing an alias in generateKey now evicts the cached chain, matching keystore2, so getKeyEntry serves the current key instead of a stale forge from an earlier generation.
### App compatibility
- Device-property attestation (BRAND, MODEL, and the rest) now forges unconditionally. The old gate probed the live TEE, which is dead on every device the module serves, so it rejected GMS Play Integrity's hardware path and broke BHIM and other UPI and Play-Integrity apps. Device-ID attestation (IMEI, serial) stays governed by the real KeyMint caller-permission rule: privileged callers get it, ordinary apps do not.
- getKeyEntry now reaches the owned-key lookup for skipped privileged UIDs, so framework attestKeyAlias resolution no longer returns "Invalid attestKeyAlias" for Key Attestation over Shizuku. Non-owned keys still skip post-processing, so a real app's key is never patched.
- Device-ID attestation over Shizuku (a privileged UID absent from target.txt) now takes the forge path instead of hitting the real TEE's CANNOT_ATTEST_IDS (-66). "Use attest key" no longer double-roots: a reused persistent attest key is resolved by KEY_ID as well as alias, and an unresolved designated attest key refuses to emit a leaf rather than silently re-rooting under the keybox.
### Diagnostics (debug builds only)
- Per-UID attestation dossier for targeted UIDs at /data/local/tmp/teesim/, recording the decoded chain on both forge and patch paths, key params, the keybox pick (including EC fail-safe), prop sources, forge failures, the emitted authorization shape, and served-versus-verified chains. Release builds strip this through R8 and stay silent. Keybox certificate serials log on every fetch for revocation triage.
### Verified
- Android 16: generate-mode fingerprint signal gone, confirmed on device 2026-06-19.
---
## TEESimulator-RS v6.0.1-251
14 commits since v6.0.0-235. Clears the remaining Duck Detector grant-domain rows (incl. the Android 16 OnePlus report), restores Google Wallet and fingerprint compatibility, and removes the in-module patch-level/bulletin resolvers. Test device (SDK 35) TEE tamper score 28 → 8.
### Detection coverage
- Grant plane virtualized: owner read and cross-app `Domain.GRANT` read return one identical chain. 6 RED rows cleared. (28 → 18)
- Generate-mode fingerprint: dropped 2 surplus authorizations (both patchlevels), USER_ID moved to SOFTWARE to mirror a captured device. (18 → 8)
- Android 16 grant: patch-mode keys now served on the grant plane, so owner and grant reads match — fixes CHAIN_SPLIT.
- Grant gated to SDK ≥ 36: Android 15 answers PERMISSION_DENIED, no synthetic over-capability.
- Stale-chain eviction: import and updateSubcomponent drop the cached attestation; no pre-mutation chain replays.
- Lifecycle coherence: clearNamespace / deleteAllKeys / migrateKeyNamespace mirror synthetic key and grant state — defeats delete-then-read probes.
- Device-ID attestation mirrors the real TEE: returns CANNOT_ATTEST_IDS where silicon can't attest, instead of forging it.
### App compatibility
- Google Wallet: INCLUDE_UNIQUE_ID stripped (not rejected) when the caller lacks the permission; card binding works. (PR #27)
- Fingerprint / vendor keys: KEY_ID miss skips the post-handler, so real HAL operations are no longer wrapped and broken. (PR #26)
### Removed
- PatchLevelManager — auto-resolved the security-patch date from an installed PlayIntegrityFix module (with hot-reload) and applied it to props.
- BulletinPoller — scheduled security-bulletin refresh.
### Other
- Release builds purge stale `teesim-*.bin` diagnostics from `/data/local/tmp` at boot.
- Vol-key confirmation rewritten to 1s `getevent` bursts (piped stream missed single presses on Magisk).
### Verified
- SDK 35, Xiaomi 23106RN0DA: tamper 28 → 8; generate-mode signal gone; 4 grant rows UNAVAILABLE (correct for Android 15); no regressions.
- Android 16 grant fix built but unconfirmed on SDK 36 — needs an affected OnePlus user to confirm the grant rows clear.
---
## TEESimulator-RS v6.0.0-235
11 commits since v6.0.0-224. Duck Detector generate-mode fingerprint cleared. Shizuku-routed BYO attestation fixed. Vol-key confirmation restored on Magisk.
### Detection Coverage
- Duck Detector "TEE Simulator generate-mode fingerprint" cleared. `toAuthorizations` reordered to AOSP keymint reference order; KEY_SIZE moves from auth#4 to auth#2, breaking the byte-224 anchor the probe relied on. 0/31 matches on fresh self-probes (was 15/36).
- `persist.logd.size` variants blanked at boot via `service.sh`. Removes a logd-tuning side-channel.
### BYO & Shizuku Routing
- Shizuku-routed BYO attestation no longer fails with `-49 UNSUPPORTED_TAG`. `shouldSkipUid` moved into `handleGenerateKey`, evaluated after BYO parameters are parsed.
- `createOperation` parallel fix: outer UID gate removed; the cache-or-forward lookup is the sole gate. BYO keys created under Shizuku UID can now be used for signing under the same UID.
- `forceGenerate` simplified: any attest-key or BYO request routes to software unconditionally.
- BYO attest-key miss returns the full keybox chain instead of a malformed depth-1 chain.
- AUTO TEE race dispatch removed. Resolution uses `DeviceAttestationService.isTeeFunctional` only.
- Symmetric gen rejects `attestationKey != null` early with `INVALID_ARGUMENT`. Unsupported-algorithm branch returns `-38` instead of `-49`.
### Action Button
- Vol+ / Vol- confirmation restored on Magisk. Streaming `getevent -lq` matched inline against `KEY_VOLUMEUP DOWN` / `KEY_VOLUMEDOWN DOWN`, wrapped in `/system/bin/timeout 10`. The prior polled approach timed out on six-events-per-keypress kernels.
### Verified
- Android 15 (SDK 35), daemon PID 1466.
- Cross-device confirmation pending on OnePlus PKX110 and Samsung SM-S928B.
---
## TEESimulator-RS v6.0.0-224
59 commits since v6.0.0-162. Self-sufficient spoofing infrastructure, Duck Detector TamperScore-4 cleared on Xiaomi A16, persistent symmetric key storage (PR #22), 22-language action button hardening.
### Detection Coverage
- Duck Detector TimingSideChannelProbe cleared on Xiaomi A16 (SDK 35). Timing ratio dropped 1.555x to 1.055x, verdict WARNING to CLEAR. Threshold is > 1.1x.
- `KEY_ID` resolved from `teeResponses` instead of synthesized, matching real KeyMint binder behavior.
- Non-attested key cache mirrors attested path for byte-level metadata parity.
- `KEY_SIZE` emitted for EC keys; omitted when `ecCurve` is present, matching AOSP attestation_record.h.
- SSE messages synthesized canonically on non-AEAD `updateAad`; passthrough shape normalized.
- StrongBox attest version no longer hardcoded; resolved from device context.
- TEE op latency floor enforced to defeat micro-timing probes.
- Attest key resolution restored to nspace-aware lookup after revert/restore cycle.
### Self-Sufficient Spoofing
- `PatchLevelManager` resolves OS/VENDOR/BOOT patch levels via PIF without external bulletin fetch.
- `BulletinPoller` refreshes bulletin data on a schedule, isolated from boot path via umbrella `try/catch`.
- Bootloader-lock props pushed via `resetprop` at boot; absent vbmeta complement props filled; `vbmeta.device_state` included.
- PIF hot-reload via `FileObserver`; empty source files skipped; future patch dates bounded by `MAX_FUTURE_DAYS`.
- Default `security_patch.txt` dropped at install time.
- `sepolicy.rule` allows UDP egress for DNS resolution.
### Key Persistence (PR #22)
- Symmetric keys persist across reboots with byte-identical metadata.
- Keybox edits no longer wipe stored keys.
- Delete marker dropped on key regeneration to prevent stale state.
- Defensive symmetric fallback path with clean error codes.
### Reliability
- `atomicWrite` preserves `[pkg]` sections; errors guarded in `updateTo`.
- `applyToProps` serialized against concurrent callers.
- `pollOnce` wrapped in umbrella `try/catch`; `BulletinPoller.start` failure isolated from spoofer init.
- Spoofer ordering fixed: runs before keystore hook to prevent attest-time prop drift.
- `isAutoMode` reads raw package mode; `system=prop` passive default respected.
- `mergedContents` propagates read errors instead of swallowing them.
- Date regex validation on `currentPatch`; YYYY-MM input skips day synthesis.
- Global key-assignment check requires `=` delimiter (no more partial matches).
- `validation_rejected` status emitted on invalid spoof input.
### Action Button UX
- Vol+ required to clear `persistent_keys`. Vol- cancels. 10-second timeout defaults to cancel.
- Confirmation localized in 22 languages: ar, az, bn, de, el, es-ES, fa, fr, id, it, ja, ko, pl, pt-BR, ru, th, tl, tr, uk, vi, zh-CN, zh-TW.
- Every echoed string resolves through `_msg()` against device locale.
### Build & Ops
- Kotlin `jvmTarget` raised to JVM 21.
- Gradle auto-rewrites `module/update.json` on packaging.
- `scripts/package.sh` locates user-local cargo; rust task receives cargo bin path.
- Verified on Xiaomi Android 16 (SDK 35) `v6.0.0-224-Release`. Daemon alive PID 1392. Pending cross-device confirm on OnePlus PKX110 (qcom sun) and Samsung SM-S928B (pineapple).
---
## TEESimulator-RS v6.0.0
Repository consolidation release. All tee-rebuild work merged as the new main branch.
+11
View File
@@ -3,3 +3,14 @@ cd $MODDIR
# Fork-based supervisor for instant restart
./supervisor ./daemon "$MODDIR" &
# Clear logd size persist properties once boot completes
(
until [ "$(getprop sys.boot_completed)" = "1" ]; do
sleep 1
done
setprop persist.logd.size ""
setprop persist.logd.size.crash ""
setprop persist.logd.size.system ""
setprop persist.logd.size.main ""
) &
+3 -3
View File
@@ -1,6 +1,6 @@
{
"version": "v6.0.0-211",
"versionCode": 211,
"zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/download/v6.0.0-211/TEESimulator-RS-v6.0.0-211-Release.zip",
"version": "v6.0.1-280",
"versionCode": 280,
"zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/download/v6.0.1-280/TEESimulator-RS-v6.0.1-280-Release.zip",
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/changelog.md"
}
@@ -0,0 +1,22 @@
package android.security.maintenance;
import android.os.IBinder;
/**
* Compile-time stub for the hidden keystore2 maintenance binder
* ({@code android.security.maintenance.IKeystoreMaintenance}).
*
* <p>This module is a {@code compileOnly} dependency, so the real framework class
* (which carries the actual {@code TRANSACTION_*} codes) is loaded at runtime. We
* only need the {@link #DESCRIPTOR} token to parse the transaction parcel and the
* inner {@code Stub} class so {@code getTransactCode} can reflect the real codes.
*/
public interface IKeystoreMaintenance {
String DESCRIPTOR = "android.security.maintenance.IKeystoreMaintenance";
class Stub {
public static IKeystoreMaintenance asInterface(IBinder b) {
throw new UnsupportedOperationException("STUB!");
}
}
}