Commit Graph
260 Commits
Author SHA1 Message Date
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
Enginex0 f706ffdf64 feat(spoof): hot-reload PIF via FileObserver
PatchLevelManager.initialize ran once at boot; PIF edits required
a reboot to take effect. Add a FileObserver on
/data/adb/modules/playintegrityfix for CLOSE_WRITE, MOVED_TO, and
DELETE on the four known PIF filenames, re-resolving and applying
the new date when any of them changes. Skip the watch when the
PIF dir is absent so the daemon does not start a stale inotify
node before the module is even installed.
2026-05-19 05:14:32 +01:00
Enginex0 bbeab27f11 fix(spoof): skip empty PIF source files
resolvePifPatch selected the last existing file regardless of
size. A 0-byte file picked up by lastOrNull caused JSONObject("")
to throw, the catch silently fell back to SystemProperties, and a
preceding non-empty PIF was ignored. Filter out zero-length files
so the lookup walks past them to the next valid candidate.
2026-05-19 05:10:59 +01:00
Enginex0 864c8841c1 fix(spoof): guard atomicWrite errors in updateTo
writeText and Files.move can throw IOException, SecurityException,
or AtomicMoveNotSupportedException. The exception previously
propagated through updateTo into BulletinPoller.fetchAndParse's
broad catch, which mislabelled it as "network_error" in the
history. Wrap atomicWrite, log the real failure, and return
before resetprop so the on-disk file and live props stay
consistent on failure.
2026-05-19 05:10:34 +01:00
Enginex0 4e55ba4e77 fix(spoof): preserve [pkg] sections in atomicWrite
atomicWrite previously overwrote the entire security_patch.txt
with only the three global lines, destroying the per-package
[pkg] overrides supported by ConfigurationManager. Read the
existing file, strip only global system/boot/vendor/all key
assignments, prepend the refreshed global block, and append
everything else (comments, blanks, all [pkg] sections) verbatim.
2026-05-19 05:09:39 +01:00
Enginex0 c511cc48e9 fix(spoof): respect system=prop passive default
PatchLevelManager.initialize previously called updateTo, which
overwrote security_patch.txt with explicit dates and destroyed
the Phase 1 default of system=prop. Split prop application into
a new private applyToProps so initialize only resetprops; never
writes the file. BulletinPoller now treats currentPatch() == null
(the signal for system=prop or missing/blank) as passive and
skips updateTo. The file becomes user-owned config; props track
PIF or the device default.
2026-05-19 05:06:59 +01:00
Enginex0 37179bbbfc fix(spoof): order spoofers before keystore hook
BootStateManager.apply and PatchLevelManager.initialize ran after
initializeInterceptors, so keystore2 cached ro.boot.* and
ro.build.version.security_patch from the un-spoofed values during
hook init. Move both before the interceptor so the hook sees the
spoofed snapshot. ConfigurationManager stays between them since
it only loads files and is independent of prop state.
2026-05-19 05:04:36 +01:00
Enginex0 a0e7fcf400 fix(spoof): isolate BulletinPoller.start failure
BulletinPoller.start ran inside App.main's outer try{...} catch
that rethrows, so any HandlerThread or Looper init failure killed
the daemon including keystore interception. Wrap the start call in
its own try so a poller failure logs and falls through, leaving
the rest of the pipeline alive.
2026-05-19 05:03:27 +01:00
Enginex0 c4e0a6ee48 fix(spoof): wrap pollOnce in umbrella try/catch
fetchAndParse and appendHistory each catch their own exceptions,
but scheduleNext can throw IllegalStateException if the Looper is
torn down or any helper raises an unanticipated error. Without an
outer catch, the reschedule chain broke and the poller stayed dead
until reboot. Wrap the entire body so a thrown exception still
attempts to schedule the next poll.
2026-05-19 05:03:19 +01:00
Enginex0 a79d7e3637 fix(spoof): allow UDP egress for DNS resolution
HttpsURLConnection resolves bulletin.source via getaddrinfo, which
uses UDP/53 first. Without UDP socket rules the resolver fails
before TCP even attempts, killing BulletinPoller silently on
enforcing SELinux kernels. Mirror the existing TCP rules onto UDP
for ksu and magisk.
2026-05-19 05:03:12 +01:00
Enginex0 8cb8616068 fix(spoof): bound future patch dates in updateTo
PatchLevelManager only rejected dates more than ~1 year in the
past. A MITM serving <td>2099-12-31</td> from a spoofed bulletin
response slipped through validation and got written to
security_patch.txt plus resetprop'd. Add a 60-day upper bound past
today using LocalDate.plusDays so month boundaries are handled
correctly. The existing past bound stays.
2026-05-19 05:01:51 +01:00
Enginex0 4b4b7ec626 chore(scripts): make package.sh find user-local cargo
Gradle's buildRustCertgen task uses commandLine("cargo", ...), which
ProcessBuilder resolves against the daemon's inherited PATH rather
than the env injected via Exec.environment(). Non-login shells (CI,
IDE-spawned terminals, fresh tmux panes) don't source the profile.d
hook that prepends ~/.cargo/bin, so the daemon dies with
"A problem occurred starting process 'command 'cargo''" even when
rustup is installed. Prepending ~/.cargo/bin at script entry makes
the script self-contained regardless of how the shell was launched.
2026-05-19 04:11:46 +01:00
Enginex0 439a9d8254 feat(spoof): periodic bulletin refresh via BulletinPoller
BulletinPoller fetches the Pixel security bulletin index page on
its own HandlerThread with 5s/30s/2m/10m/30m bootstrap backoff,
then 24h steady cadence. The first <td>YYYY-MM-DD</td> match is
the latest published patch; newer-than-current dates flow through
PatchLevelManager.updateTo for validation + atomic write + resetprop.

Persists the last 10 attempts to last_bulletin_fetch.json (atomic
rename) with status, http_code, parsed_date, applied, and error
fields so operators can audit history without logcat.

Sepolicy rule appends TCP-socket allow rules for both ksu and
magisk source domains so HttpsURLConnection survives SELinux
enforcement on either root provider. Uninstall.sh cleans the
three new artifacts.
2026-05-19 04:01:30 +01:00
Enginex0 128783dfd4 feat(spoof): PatchLevelManager with PIF resolution
PatchLevelManager resolves the active security patch from
PlayIntegrityFix via the same six-path override chain as
Tricky-Addon's get_extra.sh (pif.json/pif.prop/custom.pif.*,
later entries override earlier ones). Falls back to live
ro.build.version.security_patch when no PIF source is present.

updateTo() validates YYYY-MM-DD format, rejects dates below
2020-01-01 or more than one year older than today, then atomically
stages security_patch.txt with explicit system/boot/vendor dates
and resetprops ro.build.version.security_patch plus
ro.vendor.build.security_patch. Cert tags 706/718/719 then encode
consistent dates via AndroidDeviceUtils.parsePatchLevelValue
(YYYYMM for OS, YYYYMMDD for VENDOR/BOOT per AOSP Tag.aidl).

Wired from App.main after BootStateManager.apply().
2026-05-19 03:59:28 +01:00
Enginex0 da9ade723d feat(spoof): resetprop bootloader lock at boot
BootStateManager.apply() runs from App.main after ConfigurationManager
init and sets ro.boot.verifiedbootstate=green, ro.boot.flash.locked=1,
ro.boot.veritymode=enforcing via resetprop so the attestation
extension's hardcoded verifiedBootState=Verified agrees with what
detectors observe via getprop.

Adds an internal AndroidDeviceUtils.setProperty(name, value: String)
overload so the existing private ByteArray variant stays exclusive
to vbmeta digest persistence while config-package callers can set
plain string props without hex encoding.

Closes documented vulnerability D44 (countermeasure-matrix.md).
2026-05-19 03:57:23 +01:00
Enginex0 5a6d336fa8 feat(install): drop default security_patch.txt at install
Out-of-box install seeds /data/adb/tricky_store/security_patch.txt
with system=prop so TEESimulator passively mirrors live device props.
ConfigurationManager auto-forces boot=prop+vendor=prop when system=prop
(ConfigurationManager.kt:253-256), giving full coverage with one line.

Eliminates Chunqiu code 26 (Tampered Attestation Key) on out-of-box
installs without requiring the Tricky-Addon-Update-Target-List
companion module.
2026-05-19 03:53:53 +01:00
Enginex0 813ff814ea build(gradle): auto-rewrite update.json on packaging
Previously module/update.json had to be hand-bumped to keep
versionCode and zipUrl in lockstep with module.prop's expanded
$gitCommitCount. Wire a refreshUpdateJson task to the
prepareModuleFiles${variant} pipeline so every zipDebug/zipRelease
regenerates the file from current verName and gitCommitCount.
2026-05-19 03:25:58 +01:00
Enginex0 ee6770bcc7 build(gradle): expose cargo bin path to rust task
Gradle's exec environment does not inherit the user's interactive
shell PATH, so cargo-ndk could not find cargo even when it lived in
~/.cargo/bin. Prepend ~/.cargo/bin to PATH for buildRustCertgen so
the Rust toolchain resolves reliably from any shell.
2026-05-19 03:24:11 +01:00
Enginex0 bf29946fdc build(gradle): set kotlin jvmTarget to JVM_21
Java sourceCompatibility/targetCompatibility were already 21, but
the Kotlin compiler defaulted to JVM 17 bytecode, producing a
toolchain skew warning on every build. Align the Kotlin target to
match the Java target.
2026-05-19 03:23:55 +01:00
Enginex0 59dfb2eb85 fix(interception): omit KEY_SIZE for EC keys with ecCurve
AOSP keystore2 attestation lists KEY_SIZE only when there is no
authoritative key-shape tag. For EC keys the curve already pins the
key size, so emitting both KEY_SIZE and EC_CURVE is a forgery
fingerprint. Guard the createAuth call accordingly.
2026-05-19 03:23:10 +01:00
Enginex0 ee7e5ba698 fix(interception): drop delete marker on key regen
Regenerated keys were being filtered as deleted because the
deletion marker in Keystore2Interceptor.deletedSoftwareKeys
survived past the regen call. Clear the marker at both software
and TEE generation paths so the next getKeyEntry returns the
fresh key instead of NOT_FOUND.
2026-05-19 03:22:42 +01:00
Enginex0 04c310e8d3 wip(keystore): add F1 Phase A diagnostic logs in updateAad path
Instrumentation-only. Logs entry (primitive class, callingUid, input size)
and throwable propagation (class, SSE error code, message, stack top) in
both SoftwareOperation.updateAad and SoftwareOperationBinder.updateAad.
Intended to distinguish F1 hypotheses H1 (binder swallows SSE) vs H3
(AIDL signature drift) once duck's probe key is routed through our
simulator instead of the real TEE.
2026-05-19 00:27:38 +01:00
Yunzhe LiaoandGitHub f4d72a641e Merge branch 'Enginex0:main' into fix/persistence-and-keystore-issues 2026-05-18 15:01:17 +02:00
Andrea-lyz 5803039309 review: clean error codes, defensive symmetric fallback, v3 doc
Address Copilot/CodeRabbit review feedback on the persistence PR.

1. SoftwareOperation: replace requireNotNull(keyPair) in SIGN/VERIFY/AGREE_KEY
   branches with ServiceSpecificException(invalidArgument). The original
   requireNotNull throws IllegalArgumentException, which the binder layer
   wraps as KEYMINT_UNKNOWN_ERROR, defeating the goal of surfacing a
   clean keystore-style error. Aligns with how ENCRYPT/DECRYPT already
   handle missing key material in the same when block.

2. loadPersistedKeys: when a symmetric record has empty metadataBytes (e.g.
   a save where Parcel.marshall() was empty for any reason), rebuild a
   minimal KeyMetadata from PersistedKeyData primitive fields instead of
   skipping the record. Skipping silently dropped the AES key, which is
   the same 'logged out after reboot' behavior the PR is trying to fix.
   The rebuilt metadata is structurally minimal but preserves the secret
   material, which is the dominant correctness concern.

3. Comment fix: rebuildResponseFromRecord docs referred to 'v2 metadata
   snapshot', the format in this PR is v3.
2026-05-16 19:10:55 +02:00
Andrea-lyz 8b0acb649d fix: persist symmetric keys + byte-identical metadata; stop wiping keys on keybox edits
Five issues that together caused keystore-pinned apps to be silently
logged out across reboots and config changes. All flow from the same
root cause: GeneratedKeyPersistence loses information on save -> reload.

1. Symmetric keys (AES, HMAC, 3DES) were never persisted at all
   - GeneratedKeyPersistence.save only accepted KeyPair, ignoring SecretKey
   - AndroidX security MasterKey (AES-GCM-256) regenerated on every
     reboot, making EncryptedSharedPreferences undecryptable
   - Apps that wrap session tokens in EncryptedSharedPreferences
     interpret this as session expiry and force a relogin

2. Restored KeyMetadata authorizations differed from generation-time bytes
   - loadPersistedKeys rebuilt KeyMintAttestation with mostly null/empty
     fields, so toAuthorizations emitted a different tag set after
     reboot vs. at generateKey time
   - Apps that fingerprint metadata across keystore calls saw a
     "changed key"

3. certificate / certificateChain split could shift after restore
   - buildKeyEntryResponse called updateCertificateChain on the rebuilt
     metadata, which is allowed to repartition leaf vs. chain bytes
   - Apps with strict leaf fingerprint checks saw a "changed cert"

4. Touching ANY .xml under /data/adb/tricky_store wiped every cached key
   - ConfigObserver called clearAllGeneratedKeys() which also calls
     GeneratedKeyPersistence.deleteAll()
   - Editing keybox.xml (or any unrelated .xml) thus deleted every
     persisted key on disk
   - Even the keybox-cache argument does not justify wiping per-app keys:
     patched chains alone are stale, raw keypairs are not

5. SoftwareOperation NPE when restored keyParams missed PURPOSE tag
   - Init dereferenced keyPair!! before checking purpose, so a
     half-restored record crashed instead of producing a clean error

Single on-disk format (FORMAT_VERSION = 3) covers everything: PKCS8
private key bytes for asymmetric, raw secret bytes for symmetric, plus
the byte-identical KeyMetadata parcel snapshot so authorizations
restore exactly. Earlier dev-only formats are silently skipped by the
loader; the next generateKey for those aliases re-creates them in v3.

ConfigObserver now calls invalidatePatchedChains() instead of
clearAllGeneratedKeys() on .xml edits - only the chain cache is
stale, not the underlying keypairs.

Tested on OnePlus 13 (Android 16, KSU 3.2.4):
- Apps survive force-stop + cold reboot without losing keystore state
- Apps survive keybox.xml edits / replacements (touch, sed, cp -mv)
- Tamper score still 4 (CONSISTENT) on Duck Detector
- KeyAttestation chain output unchanged
2026-05-16 18:43:09 +02:00
Enginex0andGitHub 15a1c0ca40 Merge pull request #21 from Andrea-lyz/fix/duck-detector-generate-fingerprint
fix: defeat Duck Detector "generate-mode fingerprint" probe
2026-05-15 16:47:44 +01:00
Andrea-lyz 4ecbfc7259 fix: use SecurityLevel.KEYSTORE in createSwAuth to match real hardware
Duck Detector's 'TEE Simulator generate-mode fingerprint' probe scans the
generateKey reply parcel for a 16-byte marker where the securityLevel byte
is 0x00 (SOFTWARE). Real KeyMint HAL uses 0x64 (KEYSTORE=100) for
keystore-enforced metadata (creation time, user ID, etc.).

This single-line change aligns with real hardware behavior and defeats
the probe. Tested on OnePlus 13 (Android 16, KSU 3.2.4):
- Before: 'TEE Simulator generate-mode fingerprint: Matched' (score 50)
- After:  'No TEE Simulator generate-mode fingerprint observed' (score 4)

Reference: https://github.com/eltavine/Duck-Detector-Refactoring/commit/e368038
2026-05-15 16:23:05 +02:00
github-actions[bot] db882ec326 chore(release): bump update.json to v6.0.0-162 [skip ci] 2026-03-31 18:47:49 +00:00
Enginex0 37454e6262 ci(release): upload versioned assets only, auto-update zipUrl 2026-03-31 19:41:12 +01:00
github-actions[bot] feee04b95d chore(release): bump versionCode to 160 [skip ci] 2026-03-31 18:32:18 +00:00