Compare commits

...
13 Commits
Author SHA1 Message Date
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
7 changed files with 157 additions and 149 deletions
@@ -116,12 +116,21 @@ object InterceptorUtils {
fun <T : Parcelable?> createTypedObjectReply( fun <T : Parcelable?> createTypedObjectReply(
obj: T, obj: T,
flags: Int = 0, flags: Int = 0,
diagnosticTag: String? = null,
): BinderInterceptor.TransactionResult.OverrideReply { ): BinderInterceptor.TransactionResult.OverrideReply {
val parcel = val parcel =
Parcel.obtain().apply { Parcel.obtain().apply {
writeNoException() writeNoException()
writeTypedObject(obj, flags) writeTypedObject(obj, flags)
} }
if (diagnosticTag != null && SystemLogger.isDebugBuild) {
val savedPos = parcel.dataPosition()
val wire = parcel.marshall()
parcel.setDataPosition(savedPos)
val path = "/data/local/tmp/teesim-$diagnosticTag-${System.nanoTime()}.bin"
runCatching { java.io.File(path).writeBytes(wire) }
SystemLogger.debug("[$diagnosticTag] reply len=${wire.size} path=$path")
}
return BinderInterceptor.TransactionResult.OverrideReply(parcel) return BinderInterceptor.TransactionResult.OverrideReply(parcel)
} }
@@ -21,12 +21,10 @@ 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.Date import java.util.Date
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedDeque import java.util.concurrent.ConcurrentLinkedDeque
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.LockSupport
import org.matrix.TEESimulator.attestation.AttestationBuilder import org.matrix.TEESimulator.attestation.AttestationBuilder
import org.matrix.TEESimulator.attestation.AttestationConstants import org.matrix.TEESimulator.attestation.AttestationConstants
@@ -60,10 +58,6 @@ class KeyMintSecurityLevelInterceptor(
val keyParams: KeyMintAttestation? = null, 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 activeOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<SoftwareOperation>>()
private val recentOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<Long>>() private val recentOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<Long>>()
@@ -76,18 +70,16 @@ class KeyMintSecurityLevelInterceptor(
callingPid: Int, callingPid: Int,
data: Parcel, data: Parcel,
): TransactionResult { ): TransactionResult {
val shouldSkip = ConfigurationManager.shouldSkipUid(callingUid)
when (code) { when (code) {
GENERATE_KEY_TRANSACTION -> { GENERATE_KEY_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (!shouldSkip) return handleGenerateKey(txId, callingUid, callingPid, data) return handleGenerateKey(txId, callingUid, callingPid, data)
} }
CREATE_OPERATION_TRANSACTION -> { CREATE_OPERATION_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (!shouldSkip) return handleCreateOperation(txId, callingUid, data) return handleCreateOperation(txId, callingUid, data)
} }
IMPORT_KEY_TRANSACTION -> { IMPORT_KEY_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
@@ -424,6 +416,14 @@ class KeyMintSecurityLevelInterceptor(
} }
private fun handleGenerateKey(txId: Long, callingUid: Int, callingPid: Int, data: Parcel): TransactionResult { 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 val oversized = data.dataSize() > MAX_ALIAS_LENGTH
return runCatching { return runCatching {
@@ -436,6 +436,12 @@ class KeyMintSecurityLevelInterceptor(
) )
val params = data.createTypedArray(KeyParameter.CREATOR)!! val params = data.createTypedArray(KeyParameter.CREATOR)!!
val parsedParams = KeyMintAttestation(params) val 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}" } 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 -> if (SystemLogger.isDebugBuild) params.forEach { p ->
@@ -496,26 +502,17 @@ class KeyMintSecurityLevelInterceptor(
} }
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
val isAttestKeyRequest = parsedParams.isAttestKey()
val forceGenerate = val forceGenerate =
oversized || oversized ||
ConfigurationManager.shouldGenerate(callingUid) || ConfigurationManager.shouldGenerate(callingUid) ||
(ConfigurationManager.shouldPatch(callingUid) && isAttestKeyRequest) || isAttestKeyRequest ||
(attestationKey != null && attestationKey != null
(attestationKey.alias?.let { isAttestationKey(KeyIdentifier(callingUid, it)) }
?: attestationKeys.any { kid -> kid.uid == callingUid && generatedKeys[kid]?.nspace == attestationKey.nspace }))
val isAuto = ConfigurationManager.isAutoMode(callingUid) SystemLogger.trace { "[TRACE-$txId] dispatch: forceGen=$forceGenerate hasChallenge=${challenge != null} isSymmetric=$isSymmetric isAttestKey=$isAttestKeyRequest" }
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" }
when { when {
forceGenerate -> doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest) 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 parsedParams.attestationChallenge != null -> TransactionResult.Continue
else -> { else -> {
cleanupKeyData(keyId) cleanupKeyData(keyId)
@@ -547,11 +544,17 @@ class KeyMintSecurityLevelInterceptor(
parsedParams.algorithm != Algorithm.RSA parsedParams.algorithm != Algorithm.RSA
if (isSymmetric) { 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) { val algoName = when (parsedParams.algorithm) {
Algorithm.AES -> "AES" Algorithm.AES -> "AES"
Algorithm.HMAC -> "HmacSHA256" Algorithm.HMAC -> "HmacSHA256"
else -> throw android.os.ServiceSpecificException( else -> throw android.os.ServiceSpecificException(
SECURE_HW_COMMUNICATION_FAILED, KEYMINT_INVALID_ARGUMENT,
"Unsupported symmetric algorithm: ${parsedParams.algorithm}", "Unsupported symmetric algorithm: ${parsedParams.algorithm}",
) )
} }
@@ -620,7 +623,7 @@ class KeyMintSecurityLevelInterceptor(
TeeLatencySimulator.simulateGenerateKeyDelay(parsedParams.algorithm, System.nanoTime() - genStartNanos) 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) { val keyData = if (NativeCertGen.isAvailable && attestationKey == null) {
@@ -693,94 +696,7 @@ class KeyMintSecurityLevelInterceptor(
TeeLatencySimulator.simulateGenerateKeyDelay(parsedParams.algorithm, System.nanoTime() - genStartNanos) TeeLatencySimulator.simulateGenerateKeyDelay(parsedParams.algorithm, System.nanoTime() - genStartNanos)
} }
return InterceptorUtils.createTypedObjectReply(response.metadata) return InterceptorUtils.createTypedObjectReply(response.metadata, diagnosticTag = "gen-mode-asym")
}
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)
}
}
} }
private fun generateAttestedKeyPairNative( private fun generateAttestedKeyPairNative(
@@ -1329,15 +1245,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.ALGORITHM, KeyParameterValue.algorithm(this.algorithm)))
authList.add(createAuth(Tag.KEY_SIZE, KeyParameterValue.integer(this.keySize)))
if (this.ecCurve != null) { if (this.ecCurve != null) {
authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve))) 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.blockMode.forEach { authList.add(createAuth(Tag.BLOCK_MODE, KeyParameterValue.blockMode(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))) } 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) { if (this.rsaPublicExponent != null) {
authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong()))) authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong())))
} }
@@ -101,18 +101,19 @@ object CertificateGenerator {
val keybox = getKeyboxForAlgorithm(uid, params.algorithm) val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
val (signingKey, issuer) = val attestKeyInfo =
if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second } getAttestationKeyInfo(uid, attestKeyAlias)
?: (keybox.keyPair to getIssuerFromKeybox(keybox)) } else null
} else {
keybox.keyPair to getIssuerFromKeybox(keybox) val (signingKey, issuer) = attestKeyInfo
} ?.let { it.first to it.second }
?: (keybox.keyPair to getIssuerFromKeybox(keybox))
val leafCert = val leafCert =
buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel) buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel)
if (attestKeyAlias != null) { if (attestKeyInfo != null) {
listOf(leafCert) listOf(leafCert)
} else { } else {
listOf(leafCert) + keybox.certificates listOf(leafCert) + keybox.certificates
+11 -25
View File
@@ -17,36 +17,22 @@ echo " "
confirm() { confirm() {
vol_tmp="${TMPDIR:-/data/local/tmp}/teesim_vol_key" vol_tmp="${TMPDIR:-/data/local/tmp}/teesim_vol_key"
seconds=10
: > "$vol_tmp" : > "$vol_tmp"
getevent -qlc 1 > "$vol_tmp" 2>/dev/null &
ge_pid=$!
while [ "$seconds" -gt 0 ]; do # Stream getevent and match VOLUME DOWN inline. Single-event sampling
sleep 1 # (`getevent -c 1`) races with EV_SYN/EV_MSC noise on Magisk's BusyBox ash.
if ! kill -0 "$ge_pid" 2>/dev/null; then /system/bin/timeout 10 /system/bin/sh -c '
key=$(awk '/KEY_/{print $3}' "$vol_tmp" 2>/dev/null) /system/bin/getevent -lq 2>/dev/null | while IFS= read -r line; do
case "$key" in case "$line" in
KEY_VOLUMEUP) *KEY_VOLUMEUP*DOWN*) echo UP > "$1"; exit 0 ;;
rm -f "$vol_tmp" *KEY_VOLUMEDOWN*DOWN*) echo DOWN > "$1"; exit 0 ;;
return 0
;;
KEY_VOLUMEDOWN)
rm -f "$vol_tmp"
return 1
;;
esac esac
: > "$vol_tmp" done
getevent -qlc 1 > "$vol_tmp" 2>/dev/null & ' _ "$vol_tmp"
ge_pid=$!
fi
seconds=$((seconds - 1))
done
kill "$ge_pid" 2>/dev/null key=$(cat "$vol_tmp" 2>/dev/null)
wait "$ge_pid" 2>/dev/null
rm -f "$vol_tmp" rm -f "$vol_tmp"
[ "$key" = "UP" ] && return 0
return 1 return 1
} }
+77
View File
@@ -1,3 +1,80 @@
## 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 ## TEESimulator-RS v6.0.0
Repository consolidation release. All tee-rebuild work merged as the new main branch. 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 # Fork-based supervisor for instant restart
./supervisor ./daemon "$MODDIR" & ./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", "version": "v6.0.0-235",
"versionCode": 211, "versionCode": 235,
"zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/download/v6.0.0-211/TEESimulator-RS-v6.0.0-211-Release.zip", "zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/download/v6.0.0-235/TEESimulator-RS-v6.0.0-235-Release.zip",
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/changelog.md" "changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/changelog.md"
} }