Commit Graph
100 Commits
Author SHA1 Message Date
Enginex0 422f78ebcf fix(keystore): handle grant subcomponent updates
Extend our grant model to the updateSubcomponent path. A grantee holding
a grant with the UPDATE access-vector bit can now update the cert/chain
subcomponent of the owner's synthetic key; previously a Domain.GRANT
updateSubcomponent fell through to the real keystore2 and failed for
synthetic keys.

Reuse the existing grant machinery (resolveGrant / softwareGrants /
getGeneratedKeyResponse) already backing the getKeyEntry Domain.GRANT
read, gated on KEY_PERMISSION_UPDATE (0x80) instead of GET_INFO (0x4).
Extract a shared updateResponseSubcomponent() helper so the grant and
owner paths share one cert-swap plus re-persist body.
2026-07-11 13:06:22 +01:00
Enginex0 dc2d894647 feat(spoof): add boot_props_mode Oplus carve-out
Add a boot_props_mode control so global ro.boot.* property spoofing
can be tuned per device.

BootStateManager reads /data/adb/tricky_store/boot_props_mode
(auto/force/disable). In auto, Oplus-family devices
(OnePlus/OPPO/realme/Oplus) skip the global ro.boot.* spoof so vendor
TEE services such as ultrasonic fingerprint calibration keep working.
AndroidDeviceUtils routes the bootKey/bootHash resetprop writes
through a setBootProperty() gate honoring the same mode; the forged
value is still persisted and returned, so attestation is unaffected.

Tradeoff: in auto, skipping ro.boot.* leaves direct system-property
boot-state checks truthful on Oplus devices (compatibility over
stealth). Latent, not a reported issue.
2026-07-11 12:50:07 +01:00
Enginex0 a811919d0c fix(attestation): align KeyMint version to VINTF
getAttestVersion/getKeymasterVersion now read the device VINTF manifest
and derive attestationVersion=keymasterVersion=aidl*100 (KeyMint) or the
legacy HIDL Keymaster pair, so the forged attestation certificate's
version matches the device's declared IKeyMintDevice interface. This
removes the version MISMATCH that Duck Detector flags (attested 400 vs
declared 300). Falls back to the existing cache/SDK-map/400 chain when
VINTF is absent or unreadable.

Refs #40
2026-07-11 12:08:00 +01:00
Enginex0 0f1143a445 chore: prepare source tree for public release
- Freeze changelog.md and update.json at v6.0.1-282
- Add refactoring note to changelog top
- Untrack local planning files (CLAUDE.md, bucket/) via gitignore
2026-07-08 13:45:33 +01:00
Enginex0 4429f0ed60 docs(soter): initSigh resultCode is load-bearing
The probe's signSessionOk requires resultCode==0 (SoterCapabilityProbe
.kt:107), so writeInt(SOTER_OK) for initSigh is not optional. Cite the
probe line in the comment so a future cleanup does not drop it on the
false "detector ignores resultCode" belief, true only for the export
path.
2026-06-26 16:57:12 +01:00
Enginex0 5ddd8137df fix(soter): harden on-demand mount recovery
The supervisor mounted the forge on the happy path but could not
re-attempt: mount() returned silently on inject/handshake failure, and a
live-but-uninjected binding never died to trigger a rebind, stranding the
forge for the life of that soterserver process (audit F1).

Route every unmounted outcome through scheduleRetry(): inject failure,
post-inject handshake-null, and register failure now schedule a re-bind
instead of returning. Add onNullBinding (F2) and exponential backoff
capped at 30s, reset on a clean mount (F3). register() now returns
whether the transact succeeded so mount() retries on a false reply (F4);
existing keystore callers ignore the new return.

Audit remediation. compileDebugKotlin clean.
2026-06-26 16:57:12 +01:00
Enginex0 a4875dc200 feat(soter): wire supervisor into App startup
Start SoterProcessSupervisor from App.main() so the Layer-A forge
mounts on the on-demand soterserver process. prepareEnvironment() now
returns the system Context it previously discarded; main() hands it to
start() after keystore init and before Looper.loop(). start() runs on
its own HandlerThread and returns at once, so neither the blocking
keystore loop nor the message loop is affected.

The stub types ActivityThread.getSystemContext() as a bare ContextImpl,
so the Context cast warns CAST_NEVER_SUCCEEDS; the real ContextImpl does
extend Context, so it is runtime-safe and the warning is suppressed.

Checkpoint 10.W. compileDebugKotlin clean.
2026-06-26 16:13:03 +01:00
Enginex0 6b84c76f10 fix(module): detect diag.sh by file not exit code
The release build is meant to nuke any debug NDJSON directory on
install, but the sweep never ran. customize.sh keyed the debug vs
release decision on unzip's exit code:

  if unzip -qqjo "$ZIPFILE" "diag.sh" ...; then ...

Info-ZIP returns 11 when the entry is absent, but the busybox/toybox
unzip in the Magisk/KSU install environment exits 0, so on a release
ZIP (which correctly omits diag.sh) the branch was wrongly taken and
the rm -rf in else never fired.

Detect presence by the extracted file instead: run unzip, then test
[ -f "$MODPATH/diag.sh" ]. Robust to any unzip implementation.
2026-06-26 15:54:19 +01:00
Enginex0 43a2301982 feat(soter): sepolicy grants for ptrace injection
Injection into the soterserver app (platform_app domain, per recon)
needs ptrace under SELinux enforcing. Add the grant mirroring the
keystore one, in the base rule so it applies to both variants:

  allow crash_dump platform_app process *

The per-UID NDJSON write grant is debug-only: appended for debug
builds in build.gradle.kts's isDebug doLast, mirroring the existing
keystore media_rw_data_file grant. Keeping it out of the base rule
stops an external-storage write from leaking into release.

No soter_server SELinux type exists; platform_app is the soterserver
app domain. Runtime policy (KSU/magiskpolicy) grants this past the
compile-time neverallow; on-device avc verification is 10.V.

Checkpoint 10.C.
2026-06-26 15:54:19 +01:00
Enginex0 7de3ef1ba3 feat(soter): supervise on-demand injection
soterserver is Intent-bound and on-demand, so the one-shot pidof +
inject the always-alive keystore path uses never lands. Bind the
service to both poke its start and obtain the ISoterService binder
(the identity the native MITM registry keys on), inject
libTEESimulator.so, confirm the landing via the 0xdeadbeef handshake,
then register the forge.

Re-binds and re-injects on every respawn instead of exiting like the
keystore one-shot. Runs on its own HandlerThread so it never stalls
keystore init or the daemon looper; lifecycle logging is debug-gated.
Not yet wired into App.kt (that is 10.W).

Checkpoint 10.B.
2026-06-26 15:31:11 +01:00
Enginex0 9a8c06bca6 feat(soter): forge ISoterService Layer-A replies
Forge healthy com.tencent.soter.soterserver.ISoterService AIDL replies
from inside the injected soterserver process so the SOTER capability
probe reads available=true / damaged=false on a bootloader-unlocked
device whose SOTER TA can no longer use its factory ATTK.

Hardcodes the 13 obfuscation-stable transaction codes (R8 stripped the
Stub) and fills the 5 parcelable payloads with detector-valid values:
the export blob is a little-endian length-framed SOTER pubkey envelope
that the SDK's retrieveJsonFromExportedData parses to a non-null model.
Every request and forged reply is captured to per-UID NDJSON,
debug-gated.

Checkpoints 10.A (forge) and 10.M (reply marshalling).
2026-06-26 15:31:11 +01:00
Enginex0 4e61f1523d chore(release): sync update.json to v6.0.1-292 2026-06-26 11:40:57 +01:00
Enginex0 ab40677dac fix(attestation): per-security-level RSA/EC probe
c2552ba gated AUTO forge on isRsaAttestable, but the probe minted its
key without setIsStrongBoxBacked, so it measured only the TEE. A device
whose TEE provisions an RSA attestation key while its StrongBox cannot
(OnePlus PJZ110, Android 16) had StrongBox RSA requests PATCHed against
the real keystore, which has no StrongBox attestation key and returns
-74 (ATTESTATION_KEYS_NOT_PROVISIONED).

Probe each (algorithm, security-level) pair independently and have
dispatch consult the verdict matching the request's security level, for
both RSA and EC. StrongBox-incapable requests forge; capable ones keep
the genuine TEE chain via PATCH.

Refs #37
2026-06-26 11:38:39 +01:00
Enginex0 07dde7c756 fix(build): revert NDK to 27.3.13750724
NDK 29's Clang-21 libc++ makes libTEESimulator.so reference
__cxa_init_primary_exception, which the platform libc++ inside
keystore2 does not export. The injected lib resolves its C++ ABI
symbols against the target process at dlopen time, so injection
failed with "cannot locate symbol" on every retry. keystore2 ran
unhooked and every app saw the raw TEE chain (KeyAttestation showed
the real unlocked bootloader; per-UID NDJSON never created).

libc++ began emitting that symbol from std::exception_ptr
construction in Clang 19, so 27.3 (Clang 18) is the last toolchain
that builds a loadable lib. The exception_ptr machinery enters via
the AOSP/binder stub headers, not module code.

Verified on device: lib injects (3 maps in keystore2), KeyAttestation
generateKey -> PATCH with deviceLocked=true, verifiedBootState=Verified.
2026-06-25 17:14:15 +01:00
Enginex0 718c80a68b chore: bump NDK to 29.0.14206865
NDK 27.3.13750724's sysroot is corrupted (missing sys/cdefs.h and the
aarch64 asm headers), breaking the native-certgen build. NDK 29 is
installed and healthy, so move the toolchain to it.
2026-06-25 03:05:39 +01:00
Enginex0 9ffa00332b docs(readme): rewrite in plain language
Define load-bearing terms on first use, cut marketing phrasing, and
remove every em dash. Add an ASCII flow diagram to "How it works".
Trim credits to JingMatrix, ring, fatalcoder524, and huguangares.
Reword the tagline and update the build requirement to NDK 29.
2026-06-25 03:05:39 +01:00
Enginex0 5c36288461 refactor(certgen): drop dead native logging code
Remove orphaned native logging that nothing reached:

- The /sdcard/Download zip dump (NativeCertGen.dump and the dumpLogs
  JNI, dump_logs_inner, dump.rs, pub mod dump), superseded by the
  diag.sh export.
- The verbose-marker helpers (sysfs.rs, pub mod sysfs); the manual
  .verbose toggle still works via mod.rs::init's inline check.

Drop the now-unused direct deps zip and libc and the orphaned jstring
import. cargo ndk build is warning-clean.
2026-06-25 02:31:35 +01:00
Enginex0 d0139003a6 feat(logging): per-UID NDJSON on external storage
Move debug diagnostics off /data/local/tmp/teesim to
/data/media/0/TEESimulator (visible at /sdcard/TEESimulator), so users
can pull them without a root explorer. The logging code runs in the
keystore SELinux domain, so a debug-only media_rw_data_file grant plus
a debug-only diag.sh fragment gate the plane: diag.sh's presence is the
signal service.sh (setup) and action.sh (export) test. customize.sh
extracts diag.sh on debug installs or sweeps the dir on release, since
the release keystore domain cannot remove it itself.

Replace the per-call .bin parcel dumps (a fresh undecodable file per
generateKey) with one NDJSON record per event on the UID's own file,
carrying decoded fields plus the raw parcel as base64 for the offline
parsers. computeIfAbsent makes per-UID writer creation atomic.
2026-06-25 02:31:35 +01:00
Enginex0 28f48f2e01 fix(attestation): harden RSA capability probe
The RSA capability probe cached any first-call failure for the process
lifetime via by-lazy plus a catch-all false, so a transient keystore
hiccup could freeze the device into forging an attestation the real TEE
could serve, silently re-creating the issue #37 regression with no
self-heal.

Memoize only a definitive verdict: a successful probe, or a permanent
KeyStoreException per the framework's own isTransientFailure(). Transient
and non-keystore failures fail open, reporting the device attestable so
dispatch PATCHes the genuine chain, and re-probe on the next read. An
AtomicBoolean guard keeps at most one probe in flight with no lock held
across the keygen. Delete the probe key best-effort in finally.

Refs #37
2026-06-25 02:19:51 +01:00
Enginex0 c2552ba164 fix(dispatch): gate AUTO forge on RSA capability
v282 forged every AUTO attestation that carried a challenge, so a
strict app that validates attestation server-side, such as Kraken,
rejected the software-forged chain where it accepted a patched
real-TEE chain, breaking login. The trigger was algorithm-blind: the
AUTO capability probe only mints an EC key, so it could not tell an
EC-capable TEE from one that cannot provision RSA attestation keys.

Add an isRsaAttestable probe and forge AUTO attestation only for RSA
the real TEE cannot provision. EC and RSA-capable devices keep their
genuine TEE chain via PATCH, restoring the v280 behavior strict apps
depend on while preserving the RSA red fix on incapable devices.

Refs #37
2026-06-25 00:58:13 +01:00
Enginex0 e2dc7aa210 fix(keystore): vendor-gate real-op updateAad
OperationInterceptor rejected non-AEAD updateAad with INVALID_TAG
unconditionally, while SoftwareOperation's VendorQuirks gate returns
success on Samsung and Xiaomi-MTK. On those devices the real-key and
forged-key paths disagreed, and the genuine TEE accepts the call, so
the inconsistency fingerprinted the injection layer through Duck
Detector's operation error-path probe.

Apply the same gate to the real-op path: a void success reply where
nonAeadUpdateAadSucceeds(), else the INVALID_TAG reply. Promote
VendorQuirks to internal so both paths share one decision.

Refs #36
2026-06-25 00:58:00 +01:00
Enginex0 9193b79a6a chore(release): sync update.json to v6.0.1-282 2026-06-19 16:45:01 +01:00
Enginex0 b2d84b4661 fix(dispatch): forge AUTO attestation requests
Plain attestation (Use-attest-key OFF, challenge present) on an AUTO-mode target was routed to PATCH, deferring to the real TEE. The AUTO probe (checkTeeFunctionality) only proves the device can mint one EC key, so devices that cannot attest RSA or device-ID, or whose patched chain fails RSA verify, surfaced as KeyAttestation reds (ATTESTATION_KEYS_NOT_PROVISIONED/-49, BLOCK_TYPE_IS_NOT_01).

Forge these from the keybox instead, gated on isAutoMode + attestationChallenge, matching the attest-key-ON path that already yields a green Google-rooted chain. Non-attestation keys still pass through to real hardware, so KeyDetector hardware-backed checks are unaffected.

Verified offline against real FORGE captures with scripts/keyatt_conformance.py: uid10389/uid10154 chains are GREEN and the root SPKI byte-matches GOOGLE_ROOT_PUBLIC_KEY.
2026-06-19 14:22:56 +01:00
Enginex0 af3c27451d chore(release): sync update.json to v6.0.1-280 2026-06-19 01:46:06 +01:00
Enginex0 0586db18d9 fix(keystore): reorder auths off genmode sentinel
Duck's generate-mode parcel fingerprint reads the reply with a flat
12-byte stride and flags the sentinel tuple the device's native
ALGORITHM-first auth order lands on, at count 12 and 13. Real A16
hardware trips it too, so faithful mirroring stays flagged.

Add InterceptorUtils.normalizeAuthorizationLayout: marshal the auth
array, run Duck's exact predicate, and only when it would match,
reorder by a deterministic minimal move until it clears. Order
carries no keystore semantics and the cert chain is a separate
field, so count, values, security levels, and attestation are all
preserved. Applied on both the patch and forge reply paths; it keys
on the byte condition, never on any package.
2026-06-19 01:44:04 +01:00
Enginex0 ec5df08574 chore(release): sync update.json to v6.0.1-277
The packaging task rewrites update.json to gitCommitCount on every
build; this records the v6.0.1-277 artifacts and supersedes the manual
271 bump made before the build counter was understood.
2026-06-17 20:34:17 +01:00
Enginex0 d65526aaa5 chore(release): bump module to v6.0.1-271 2026-06-17 20:28:28 +01:00
Enginex0 ca226bd7de chore(pki): log attest-sign signer vs leaf algo
At the attest-key signing instant, log signer key algorithm, served
leaf algorithm, chain depth, and issuer (debug, targeted uid) so an EC
attest-key run pins the mismatched edge of the two-root chain.

Bucket a16-ec-attestkey-red, task T01.
2026-06-17 20:28:28 +01:00
Enginex0 e5d24c3907 fix(keystore): drop algo-split key on restore
A persisted record whose private-key algorithm disagrees with its
served leaf public key (EC private under an RSA leaf) makes every
signature fail as DATA_TOO_LARGE_FOR_MODULUS: the A16 EC two-root.
Require the two to match on restore and drop the record otherwise, so
the next generateKey rebirths a coherent key. Awaiting an EC device
capture to confirm the red originates from a restored record.

Bucket a16-ec-attestkey-red, task T01.
2026-06-17 20:28:28 +01:00
Enginex0 a54e8a5315 fix(attestation): cache AOSP attestVersion on A16
The A16 test device's KeyMint HAL reports attestVersion 100 (KeyMint
1.0); the lazy cache stored that and it shadowed the map's BAKLAVA->400
in getAttestVersion. fetchAttestationData now caches
AndroidDeviceUtils.aospAttestVersion (attestVersionMap[SDK_INT]),
falling back to the parsed device value only when the SDK is unmapped,
so the forge presents the AOSP-correct 400. attestVersionMap unchanged.

Bucket a16-ec-attestkey-red, task T02.
2026-06-17 20:28:27 +01:00
Enginex0 0b67700763 fix(keystore): evict stale cached key on regen
keystore2 replaces a key when generateKey reuses an alias. Mirror that:
drop any cached chain for the alias so a later getKeyEntry serves the
current key, not a stale FORGE from a prior generation (an
attest-key-mode leaf cached, then re-generated without an attest key).
2026-06-17 14:54:22 +01:00
Enginex0 fbee59688d feat(logging): log served and verified chains
Log each cert chain the module hands the app so an attestation
verification failure is provable from the per-UID log, not inferred.

- formatChainVerification verifies every edge of a produced chain and
  reports RSA signature-vs-modulus sizes (the DATA_TOO_LARGE condition).
- formatChainKeys and logServedChain record the chain served back on
  each getKeyEntry, keyed by alias, since the app reassembles its chain
  from the leaf alias plus the attest-key alias.

Debug-build only, gated by isUidLogged.
2026-06-17 14:54:22 +01:00
Enginex0 2e5155fb75 fix(keystore): serve getKeyEntry for skipped UIDs
Un-targeted privileged callers (e.g. KeyAttestation via Shizuku) have
their attest-key and device-id generateKey requests force-forged, but
getKeyEntry blanket-skipped those UIDs before the owned-key lookup, so
the framework's attestKeyAlias resolution in
AndroidKeyStoreKeyPairGeneratorSpi.initialize() returned KEY_NOT_FOUND
and surfaced as "Invalid attestKeyAlias".

Let getKeyEntry reach the owned-key lookup for skipped UIDs; a non-owned
key still skips post-processing so an un-targeted app's real key is
never patched.
2026-06-17 11:34:22 +01:00
Enginex0 ce22327147 refactor(keystore): strip unique-id at parse time
Decide the effective generateKey params once via .let when the caller lacks gen_unique_id / REQUEST_UNIQUE_ID_ATTESTATION, instead of mutating var params/parsedParams deep in handleGenerateKey and re-parsing KeyMintAttestation a second time. isAttestKeyRequest now derives from the final parsedParams, closing the staleness flagged in PR #27 review r3308356496.

Behavior is unchanged: no gate between the parse and the old strip site reads INCLUDE_UNIQUE_ID, and the && short-circuits so the permission lookups still run only when the tag is present.
2026-06-17 10:38:32 +01:00
Enginex0 f69fee21a8 chore(debug): co-locate per-UID logs with dumps
Per-UID dossier logs moved from /data/adb/tricky_store/logs to
/data/local/tmp/teesim, beside the .bin dumps, so the whole debug
trail comes off the device in one `adb pull /data/local/tmp/teesim/`
with no root and no /sdcard hop.

The release purge now sweeps .log/.log.1 from the diagnostic dir and
keeps legacy sweeps of both old locations (loose /data/local/tmp and
the module config dir) so upgrading to a release build leaves nothing
behind. Repoint package.sh --clear-logs to the new path.
2026-06-04 21:15:34 +01:00
Enginex0 9561f7d9c0 feat(logging): per-UID forge diagnostics
The attestation dossier only fired on a successfully produced chain, so
the StrongBox/BHIM failures left nothing on the per-UID plane and had to
be reconstructed from marshalled .bin dumps offline. Add three records,
all debug- and target-gated like the existing dossier:

- keybox-pick: which keybox signs the forge (requested algo, exact match
  vs EC fail-safe, signer subject) -- makes an EC-only-keybox RSA
  fallback visible instead of silent.
- forge-fail: emit the failure reason on the per-UID plane when a forge
  throws (e.g. ATTESTATION_KEYS_NOT_PROVISIONED), paired with dispatch.
- auth-shape: the emitted authorization list (count, ordered tags,
  per-auth securityLevel) -- the surface the duck generate-mode parcel
  fingerprint stride-walks, readable without offline decode.
2026-06-04 20:39:49 +01:00
Enginex0 79e4fe905e chore(debug): per-request gen-mode result dumps
The asymmetric and symmetric result dumps wrote a single fixed
filename (teesim-gen-mode-asym.bin / -sym.bin), so each forge
overwrote the previous one and only the final reply survived a
capture -- which is why a tester's zip showed one app's good chain
while the failing chain was already gone.

Tag both dumps with uid and tx, matching the request dumps, so every
forged chain is retained and correlatable with its request.
2026-06-04 20:00:35 +01:00
Enginex0 5bbb0bffe0 fix(pki): root RSA forge on EC-only keybox
The forge keybox selector matched the requested algorithm exactly and
threw -75 ATTESTATION_KEYS_NOT_PROVISIONED on a miss, while the patch
path already falls back to any usable key (EC preferred). An RSA
ATTEST_KEY request on an EC-only keybox therefore never rooted: the
caller's attest-key chain could not reach the Google root and verifiers
reported "unknown certificate".

Fall back to getAnyAttestationKey when no algorithm-matching keybox
exists. An EC attestation key validly ECDSA-signs an RSA-subject leaf,
so the EC keybox roots the RSA forge. No-op when the keybox is dual.
2026-06-04 20:00:08 +01:00
Enginex0 59a2357312 fix(keystore): repair attestation generation gaps
Two gaps in attestation generation surfaced by a tester's Key Attestation
app runs on build 259.

Device-ID attestation (IMEI/serial) via Shizuku arrives as a privileged
UID (shell/system) absent from target.txt, so it was skipped and the real
TEE rejected it with CANNOT_ATTEST_IDS (-66). Stop skipping requests that
carry device-ID tags, and force the forge path for them (the real TEE
cannot attest IDs, so there is no chain to patch). The permission gate
still rejects ordinary apps, mirroring a real device.

'Use attest key' produced WRONG_PUBLIC_KEY_TYPE: a reused persistent attest
key is designated by KEY_ID with a null alias, so the lookup missed and the
leaf was silently re-rooted under the keybox, double-rooting the chain the
caller assembles. Resolve the attest key by KEY_ID as well as alias, and
refuse to emit a leaf rather than fall back to the keybox when a designated
attest key cannot be resolved.
2026-06-04 19:07:42 +01:00
Enginex0 7f33bd737d chore(debug): move diagnostic dumps to subfolder
The debug-only .bin dumps wrote loose into /data/local/tmp, cluttering a
directory shared with every other tool. Route both writers through a
shared DIAGNOSTIC_DIR (/data/local/tmp/teesim) with mkdir-on-write, and
extend the release purge to sweep the new folder plus any loose leftovers
from older debug installs.
2026-06-04 19:05:26 +01:00
Enginex0 b90af0b716 fix(attestation): patch RSA leaf under EC keybox
An EC-only Google keybox could not re-root an RSA-keyed attestation: the keybox lookup asked for an RSA signing key, got null, and threw. patchCertificateChain caught the throw and returned the original chain untouched, leaking the device's real unlocked Root of Trust for RSA keys while EC keys patched correctly.

Fall back to any available keybox key (preferring EC) and sign the patched leaf with the keybox key's own algorithm rather than the original leaf's. A leaf's signature algorithm is independent of its subject key, so the RSA leaf re-signs validly under the EC keybox and the chain still roots to the Google keybox with the forged, locked RoT.

Add KeyBoxManager.getAnyAttestationKey; drop the now-dead sigAlgName param and normalizeSignatureAlgorithm helper.
2026-06-04 17:43:19 +01:00
Enginex0 e5483afc70 feat(logging): UID-keyed attestation dossier
Add a debug-only per-UID diagnostic plane gated on BuildConfig.DEBUG.
For apps in target.txt it records every keystore interaction and the
forged attestation it produces to teesim-uid-<uid>.log: decoded cert
chain (FORGE and PATCH paths), key params, keybox, and prop sources,
with the calling UID threaded through the C++ binder hook and Rust
certgen. Release builds stay silent (R8 strips the write plane and the
runtime gate short-circuits). Adds --clear-logs to package.sh.
2026-06-04 15:53:06 +01:00
Enginex0 f826312fc4 fix(pki): log keybox serial on every fetch
The serial log added previously lived in parseKeysFromXml, which
getAttestationKey runs only on a cache miss -- so it emitted at most
once per boot and scrolled off the buffer before it could be read.
Move it into getAttestationKey so the keybox attestation cert serials
are logged on every fetch, on the live native cert-gen path.

Verified on device: "Using RSA keybox keybox.xml; attestation cert
serials (hex): ..." now prints on each forge.
2026-06-04 13:58:43 +01:00
Enginex0 40519b94ea feat(keystore): vendor-gate non-AEAD updateAad
Mirror Duck Detector's OperationErrorPathProbe: real Samsung and
Xiaomi-MTK TEEs return success for updateAad on a non-AEAD operation,
while other vendors reject it with INVALID_TAG. The shim reads the same
Build identity the probe reads and answers accordingly, in both the
CryptoPrimitive default (sign/verify) and CipherPrimitive paths.

Forward hardening for the #28 detector: the prior unconditional
ServiceSpecificException throw already passes the probe on every vendor,
so this guards against stricter future probes rather than fixing a
current failure.
2026-06-04 13:14:43 +01:00
Enginex0 37e9d007de feat(pki): log keybox attestation cert serials
Emit each loaded keybox's certificate-chain serials (lowercase hex) at
parse time. A revoked or leaked keybox is then visible from logcat
alone, since Google's CRL and Duck Detector's "mass abuse" check both
match by certificate serial.

Diagnostic aid for the revoked-keybox danger in #28; the actual fix is
rotating to a non-revoked keybox.
2026-06-04 13:00:38 +01:00
Enginex0 5c300ff47b style: apply ktfmt formatting pass
Run the project ktfmt kotlinLangStyle formatter over app/ to bring the
tree into canonical form. Formatting only -- no logic change.

Verified semantic-neutral: ktfmt(working tree) is byte-identical to
ktfmt(committed HEAD) across all of app/src, so the prior uncommitted
WIP carried zero behavioral change.
2026-06-04 12:55:07 +01:00
Enginex0 5bd563d8db feat(keystore): probe trail for generateKey
Add a debug-only structured log line per generateKey, emitted at every
outcome (SKIP, the four REJECTs, FORWARD_HAL, FORGE, PATCH, PASSTHROUGH).
Each line carries the resolving package (via the cached
ConfigurationManager.getPackagesForUid), alias, algorithm, StrongBox
flag, the attestation tag set, and the verdict, so triaging "app X
broke" becomes a single logcat grep instead of decoding parcel dumps.

Gated on SystemLogger.isDebugBuild: release builds return before any
string is built, keeping the path silent and artifact-free. Logs to
logcat only, never to files.
2026-05-30 14:54:57 +01:00
Enginex0 254fb0f0a9 fix(keystore): forge device-property attestation
The canAttestDeviceIds gate (3575c74) probed the live TEE to decide
whether to honor device-property/ID attestation. That probe is gated on
isTeeFunctional, which is false on every dead-TEE device the module
serves, so GENERATE mode rejected all such requests with
CANNOT_ATTEST_IDS, including GMS Play Integrity's hardware path, which
broke BHIM and any UPI/Play-Integrity-gated app.

Remove the gate. Device-property attestation (BRAND/MODEL/...) now forges
unconditionally, as genuine devices universally attest it. Device-ID
attestation stays governed by the pre-existing caller-permission check,
the real KeyMint rule: privileged callers get it, ordinary apps do not.
Drop the now-unused DeviceAttestationService.canAttestDeviceIds probe.
2026-05-30 14:51:48 +01:00
Enginex0 8d195010fd 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 d6ddc925ce 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 a67efa4102 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 00e8cc36fe chore(release): bump version to v6.0.1 2026-05-30 13:42:31 +01:00
Enginex0 ca857c30ac 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 a58c4798c3 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 8cebcf14a8 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 ab58b11e4d 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 bed32b7454 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 (5579b16) 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 43e948efb3 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 cf4fb127f2 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
Enginex0 67c283b75b chore(release): publish v6.0.0-235 2026-05-20 07:05:02 +01:00
Enginex0 40f652f725 chore: bump versionCode to 235 2026-05-20 06:54:35 +01:00
Enginex0 25fd28a32b chore: bump versionCode to 233 2026-05-20 06:52:11 +01:00
Enginex0 108e027a98 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 be4c418893 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 0829bc98ca fix: intercept createOperation under any caller UID
Mirror of the change applied to GENERATE_KEY in 76e0337. 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
76e0337).

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 49763cdca7 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 f384871's broadened forceGenerate
gate, which now routes any attestationKey != null to software
unconditionally.
2026-05-20 03:58:53 +01:00
Enginex0 937058a7ff 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 f384871, 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 76e033700c 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 f384871f5a 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
17c6312 -> eea6001 -> be04f16 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 1cea3ab8f1 refactor: remove AUTO TEE race dispatch
The race added in b3aa795 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 a6452c3a26 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 da9a99bfbf 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
Enginex0 b9defa70ae feat(action): i18n the clear-keys confirmation
Add action_i18n.sh with 22 language arms (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) plus an English default. Detect device locale via
persist.sys.locale with ro.product.locale and ro.system.locale
fallbacks; split Chinese on Hans/Hant.

action.sh sources the helper at top and substitutes every echoed
literal with $(_msg <key>). The confirm() flow stays unchanged.

Wire action_i18n.sh into customize.sh's per-file extraction list so
the helper lands alongside action.sh in /data/adb/modules/tricky_store
at install time.

Pattern mirrors tricky-addon-enhanced/install_i18n.sh.
2026-05-19 18:43:46 +01:00
Enginex0 60fee55a0f feat(action): require vol+ confirm to clear keys
Users reported accidentally triggering the module action button from
the root manager UI, which wiped /data/adb/tricky_store/persistent_keys
and forced every attestation-dependent app to re-enroll.

Gate the destructive operation behind an explicit Vol+ confirmation
with a 10-second timeout. Vol- or timeout aborts and preserves keys.
Pattern mirrors tricky-addon-enhanced/install_func.sh choose_automation
(getevent -qlc 1 polled per second), but defaults to cancel on timeout
because the destructive default of the previous script is the
behavior we are correcting.
2026-05-19 18:31:16 +01:00
Enginex0 182450d3b4 fix(intercept): cache non-attested keys for parity
After PR #22 and the AUTO-mode extension started caching attested
generateKey responses in teeResponses, KEY_ID getKeyEntry lookups for
attested keys returned from memory in ~1ms while non-attested keys
forwarded to real keystore2 took ~1.5ms.
TimingSideChannelProbe measured the 1.55x ratio against its 1.1x
threshold and flagged the asymmetry.

Forward non-attested generateKey to real keystore2 with post-hook
enabled (Continue instead of ContinueAndSkipPost), and extend the
GENERATE_KEY post-hook to cache no-chain responses into teeResponses.
The KEY_ID lookup added in the previous commit now resolves both
paths from memory at matched latency. Cert-chain patching is skipped
for the no-chain branch because there is no attestation extension to
rewrite.
2026-05-19 18:01:06 +01:00
Enginex0 6d11362034 feat(intercept): resolve KEY_ID via teeResponses
PR #22's KEY_ID lookup at Keystore2Interceptor.onPreTransact only
scanned generatedKeys, which is populated exclusively by
doSoftwareKeyGen (GENERATE mode and the attest-key override path).
For AUTO packages on TEE-good devices the real TEE handles
generateKey and the response lands in teeResponses via the existing
GENERATE_KEY_TRANSACTION post-hook, so getKeyEntry(KEY_ID) by
TimingSideChannelProbe missed and the call leaked SSE.

Add findTeeResponseByKeyId companion helper that mirrors
findGeneratedKeyByKeyId's shape but scans teeResponses keyed by
response.metadata.key.nspace. Wire it as a fallback after the
existing PR #22 lookup. Behavior unchanged for GENERATE packages.
2026-05-19 17:45:54 +01:00
Enginex0 20a58f8a58 fix(config): isAutoMode reads raw package mode
getPackageModeForUid collapses AUTO -> PATCH/GENERATE at the call site
based on DeviceAttestationService.isTeeFunctional, so isAutoMode never
observed Mode.AUTO and always returned false. That made the
isAuto-gated dispatch arms in KeyMintSecurityLevelInterceptor and the
entire raceTeePatch path unreachable.

Iterate packageModes directly with the same priority order as
getPackageModeForUid: first non-null mode wins. AUTO returns true,
PATCH and GENERATE return false. Activates raceTeePatch for AUTO
packages on the first generateKey per security level.
2026-05-19 17:45:37 +01:00
Enginex0 5a12970426 Merge PR #22: persist symmetric keys + byte-identical metadata 2026-05-19 17:00:14 +01:00
Enginex0 cfbba4cddb chore(release): bump update.json to v6.0.0-211 2026-05-19 17:00:04 +01:00
Enginex0 c4ea3e0bb2 fix(intercept): normalize passthrough SSE shape
Real keystore2 SSE replies passed through SkipTransaction kept
the daemon's anyhow chain in the parcel string. Run those replies
through the same synthesizer used for module-generated SSEs so
wire shape stays consistent regardless of source.
2026-05-19 17:00:04 +01:00
Enginex0 ea37792653 fix(util): drop StrongBox attest version hardcode
StrongBox was pinned to attest/keymaster v300 regardless of SDK,
which mismatches devices shipping KeyMint v400 on Android 16.
Fall through to the same SDK_INT->version map used by the TEE
path so StrongBox reports the device-correct tier.
2026-05-19 17:00:04 +01:00
Enginex0 80f65b02ac fix(intercept): synthesize canonical SSE messages
writeString(null) on service-specific exception replies left the
message word as 0xFFFFFFFF, which diverges from AOSP keystore2's
anyhow-formatted "Error::Rc(NAME)" / "Error::Km(NAME)" strings.
Map known ResponseCode/KeyMint codes to their canonical names so
the wire shape matches a stock TEE reply.
2026-05-19 17:00:04 +01:00
Enginex0 be04f16a50 fix(shim): restore nspace attest key lookup
Reverts eea6001. The nspace attestation key lookup was part of
the score-4 baseline and closes a real NPE on KEY_ID-domain
references where the alias field is null. Reverted in error
during the score-4-to-14 rollback; restoring to match the
working baseline.
2026-05-19 14:35:19 +01:00
Enginex0 9a7e4d7251 fix(intercept): restore updateAad SSE injection
Reverts 180dc03. The updateAad SSE injection landed earlier as
the F1 quirk fix and was part of the score-4 baseline on mt6768.
It was reverted in error during the score-4-to-14 rollback; the
actual regression driver was the uncommitted DELETE_KEY absorber
which has already been dropped. Restoring to match the working
baseline.
2026-05-19 14:35:12 +01:00
Enginex0 180dc039cd fix(intercept): revert updateAad SSE injection
Reverts 5d33701. Unconditionally injecting SSE(INVALID_TAG) on
non-AEAD updateAad matched the AOSP TA spec but diverged from
real-device behavior on mt6768, which returns silently. A
behavior-fingerprint detector on the Xiaomi probe flagged the
divergence and the Tamper score climbed from 4 to 14, with a
second detector raising key-tamper. Roll back to investigate a
device-conformant approach.
2026-05-19 14:29:39 +01:00
Enginex0 eea60018ca fix(shim): revert nspace attestation key lookup
Reverts 17c6312. The KEY_ID-domain alias-null branch targeted
duck-detector's timing-side-channel WARN, but the WARN persisted
in subsequent testing and the combined fix attempts pushed the
Tamper score from 4 to 14 with a new key-tamper detection on a
second detector. Roll back to the 2e55d56 baseline to investigate
from a clean state.
2026-05-19 14:29:29 +01:00
Enginex0 17c63120f5 fix(shim): resolve attest key by nspace
The probe in duck-detector's TimingSideChannelProbe chain calls
generateSigningKey with a KEY_ID-domain attestation key reference
where alias is null. KMSLI.kt:498 fed that null alias into the
non-null String param of KeyIdentifier, triggering an NPE that
the outer runCatching wraps into a ServiceSpecificException(-49).
Any SSE crossing the binder boundary carries the Parcel.read/
createException(OrNull) stack frames, which the detector's
TeeReportReducer at line 2635-2641 matches verbatim to emit
"Captured private binder exception during timing skip".

Branch on the alias before constructing KeyIdentifier: when
alias is present, retain the existing isAttestationKey lookup;
when null (Domain::KEY_ID), scan attestationKeys for a matching
uid + nspace via generatedKeys. Mirrors AOSP keystore2's own
dispatch in database.rs (Domain::APP by alias, Domain::KEY_ID
by key id).
2026-05-19 13:32:54 +01:00
Enginex0 5d33701601 feat(intercept): inject SSE on non-AEAD updateAad
Real MediaTek mt6768 KeyMint silently returns OK on non-AEAD
updateAad, contradicting AOSP's mandate at
system/keymint/ta/src/operation.rs:430-446 to throw InvalidTag
when aad_allowed is false. Duck-detector flags this divergence
as "updateAad mismatch" in OperationErrorPathProbe.

Wire UPDATE_AAD into OperationInterceptor and inject
ServiceSpecificException(INVALID_TAG) when create params
indicate non-AEAD. AEAD (BlockMode.GCM) passes through to real
KeyMint untouched so AES-GCM round-trips remain valid.
2026-05-19 13:05:36 +01:00
Enginex0 2e55d56426 feat(spoof): add TEE op latency floor
Attested keystore operations finishing faster than non-attested
ones on the same device is a timing inversion that detectors
score against TEE coherence. Floor TEE op latency at 4ms to
preserve the natural ordering.
2026-05-19 08:04:05 +01:00
Enginex0 c69e2d47b8 feat(spoof): fill absent vbmeta complement props
invalidate_on_error, avb_version, hash_alg, and size are sibling
props of vbmeta.device_state. When device_state is present but
its complements are missing, the partial set is itself a
detection signal. Fill with safe defaults if absent; never
overwrite when present.
2026-05-19 08:03:04 +01:00
Enginex0 3b1e908670 feat(spoof): skip absent boot-lock props
Creating a vendor-specific boot prop on a device that never had
one is itself a detection signal. Existence-guard the four
boot-lock targets so absent props stay absent.
2026-05-19 08:01:10 +01:00
Enginex0 259d27c3c7 feat(spoof): include vbmeta.device_state
Live install on the user's Pixel surfaced a fourth bootloader-lock
prop the source plan never enumerated. The Chunqiu-style detector
card listed three indicators on its bootloader-unlock row and
flagged red because ro.boot.vbmeta.device_state remained unlocked
even after ro.boot.verifiedbootstate, ro.boot.flash.locked, and
ro.boot.veritymode were all spoofed.

Add ro.boot.vbmeta.device_state=locked to BootStateManager.targets.
Verified post-reboot on device: all four props now report the
spoofed values.
2026-05-19 06:06:58 +01:00
Enginex0 d15abe3c62 fix(spoof): emit validation_rejected status
The source plan's bulletin-history schema declared a four-value
status enum: success, network_error, parse_error,
validation_rejected. The poller only ever wrote the first three;
when PatchLevelManager.updateTo silently rejected a date for bad
format, floor violation, past/future bounds, or atomicWrite IO
error, the history still recorded status=success and applied=true
because applied was set from isNewer before updateTo ran.

Make updateTo return Boolean. Wire the result through fetchAndParse
so a rejected apply lands as status=validation_rejected with
applied=false and an error string identifying the date that failed.
Closes the only spec gap from fancy-humming-firefly.md uncovered
during the source-plan cross-audit.
2026-05-19 05:55:18 +01:00
Enginex0 99957e18c4 fix(spoof): validate currentPatch against date regex
currentPatch returned the raw system= value unchanged. A
malformed value such as system=tomorrow flowed into the
lexicographic comparison `date > current`, where any well-formed
YYYY-MM-DD from the bulletin sorts before lowercase letters, so
the poller permanently judged its date "not newer" and never
applied an update. Validate the read value against the date
pattern; on mismatch, log a warning and treat as passive.
2026-05-19 05:36:36 +01:00
Enginex0 393ae6073f docs(spoof): explain MAX_FUTURE_DAYS rationale
The 60-day window covers Pixel monthly bulletin cadence plus
pre-announcement slip but rejects far-future hostile inputs. The
prior bare constant left readers wondering whether the value was
arbitrary; the KDoc closes that loop.
2026-05-19 05:35:40 +01:00
Enginex0 fbe819d6d8 fix(spoof): serialize concurrent applyToProps calls
applyToProps reaches Runtime.exec("resetprop", name, value) twice
per call, once for system and once for vendor. Boot-time
initialize, BulletinPoller's handler thread, and the PifObserver
inotify thread can all reach applyToProps independently. Two
concurrent invocations for different dates could interleave such
that system and vendor end up with mismatched values. Synchronize
on the singleton so each apply runs to completion before the
next begins.
2026-05-19 05:35:28 +01:00
Enginex0 ad1a9b8c32 fix(spoof): propagate read errors out of mergedContents
A read failure (partial-write race during user edit, SELinux
denial, or any other IOException) used to fall through the
runCatching and rewrite the file with only the global block,
silently destroying every existing [pkg] override. Drop the
runCatching so the failure bubbles to atomicWrite, where the M3
guard in updateTo logs and returns without applyToProps, leaving
both file and props untouched.
2026-05-19 05:35:13 +01:00
Enginex0 d146ad8234 fix(spoof): require = in global key-assignment check
isGlobalKeyAssignment treated any bare line whose first token
matched system/boot/vendor/all as a global assignment and
stripped it. A user line of literally "all" or "system" written
without a value (malformed config but reachable) thus got eaten
on the next atomicWrite. Require '=' in the trimmed line before
treating it as a key=value assignment.
2026-05-19 05:34:59 +01:00
Enginex0 e737453e02 fix(util): skip day synthesis for YYYY-MM input
parsePatchLevelValue silently synthesized day=01 when input was
6 chars (YYYY-MM) and caller wanted 8-digit YYYYMMDD. If
ro.vendor.build.security_patch ever returns YYYY-MM on a target
device (older Samsung firmware does), the synthesized day
disagrees with the real bulletin day -- a detection fingerprint.
Return null so getRealDevicePatchLevelInt falls through to
Build.VERSION.SECURITY_PATCH, which is always YYYY-MM-DD.
2026-05-19 05:17:40 +01:00
Enginex0 429e033b7f fix(interception): emit KEY_SIZE for EC keys
Revert 59dfb2e. AOSP 15 KeyMint reference TA at
system/keymint/common/src/tag/info.rs:61-89 lists both Tag::EcCurve
and Tag::KeySize in KEYMINT_ENFORCED_CHARACTERISTICS, and
check_ec_params at common/src/tag.rs:632 says "Key size is not
needed, but if present should match the curve" -- the TA passes
through whatever the caller supplies and keystore2 supplies both
for EC keys per KeyMintBenchmark.cpp:234,259. Omitting KEY_SIZE
made the simulator's characteristics list shorter than real
hardware, a detection fingerprint.
2026-05-19 05:15:51 +01:00