Compare commits

...
78 Commits
Author SHA1 Message Date
Enginex0 634a1293c1 docs(readme): credit MhmRdd for upstream AOSP compliance work 2026-03-19 07:37:53 +01:00
Enginex0 f115eda2dc chore(version): bump to v5.0 with changelog for AOSP compliance overhaul 2026-03-19 07:36:20 +01:00
Enginex0 217edf61fe docs: credit upstream PR #157 contributors 2026-03-19 07:33:57 +01:00
Enginex0 ee5bf2e1a7 feat(config): add SELinux permission checks, latency simulation, and hbk seed
ConfigurationManager gains checkSELinuxPermission (reads /proc/pid/attr)
and hasPermissionForUid (delegates to IPackageManager.checkPermission)
for AOSP-compliant access control. TeeLatencySimulator provides log-normal
distribution matching real QTEE/Trustonic hardware timing profiles.

Module customize.sh now generates a device-unique hardware-bound key seed
(32 bytes from /dev/random) and clears stale tee_status.txt on install.
2026-03-19 07:33:47 +01:00
Enginex0 2181157cb6 feat(operation): add AOSP-compliant error handling, authorize_create, and GCM IV
SoftwareOperation now throws ServiceSpecificException for all error paths
instead of raw Java exceptions, matching AIDL wire format. updateAad on
non-AEAD operations returns INVALID_TAG (-76) per AOSP operation.rs.
SoftwareOperationBinder methods are @Synchronized to match AOSP Mutex
semantics. GCM encrypt operations return the generated IV in
CreateOperationResponse.parameters.

AuthorizeCreate enforces PURPOSE validation, algorithm-purpose
compatibility (EC rejects ENCRYPT/DECRYPT, RSA rejects AGREE_KEY),
temporal constraints (ACTIVE_DATETIME, ORIGINATION_EXPIRE, USAGE_EXPIRE),
and CALLER_NONCE prohibition. GeneratedKeyInfo carries keyParams for
authorize_create enforcement on software createOperation.
2026-03-19 07:33:30 +01:00
Enginex0 aa4917e623 feat(interception): add binder tx code filtering and keystore2 service compliance
Native binder_interceptor now accepts a filtered_codes vector per
registration, skipping JNI round-trip for non-intercepted transaction
codes. Keystore2Interceptor adds getNumberOfEntries software key counting,
deleteKey KEY_ID domain resolution, patchAuthorizations for OS/VENDOR/BOOT
patch levels, importedKeys tracking to prevent stale attest-key overrides,
and nspace consistency fix in the attest-key override path.

InterceptorUtils gains createServiceSpecificErrorReply for AIDL-compliant
error serialization and patchAuthorizations for authorization array patching.
2026-03-19 07:33:11 +01:00
Enginex0 f06cb30b40 feat(attestation): align attestation extension and cert generation with AOSP
KeyMintAttestation now carries all 17 enforcement tags that AOSP's
authorize_create and buildKeyDescription paths expect. AttestationBuilder
populates BLOCK_MODE as SET OF INTEGER, gates version-guarded tags
(RSA_OAEP_MGF_DIGEST >=100, ROLLBACK_RESISTANCE >=3, EARLY_BOOT_ONLY >=4),
computes INCLUDE_UNIQUE_ID via HMAC-SHA256 per KeyMint HAL spec, and
gates AAID on challenge presence.

CertificateGenerator uses AOSP cert validity defaults (epoch notBefore,
9999-12-31 notAfter), returns ServiceSpecificException(-75) for missing
keybox, and adds RSA exponent null safety.
2026-03-19 07:32:53 +01:00
Enginex0 03c71bd202 docs(release): add v4.8.1 changelog for StrongBox op rejection fix 2026-03-18 03:24:58 +01:00
Enginex0 7e2fc0b288 fix(interception): enforce StrongBox op limit for software-generated keys
trackAndEnforceOpLimit was only called in the Domain.KEY_ID not-found
path, so software-generated keys (found via Domain.APP) bypassed the
STRONGBOX_MAX_CONCURRENT_OPS=4 limit entirely. DuckDetector's concurrent
signing handles test created 24+ operations that all succeeded via LRU
pruning instead of being rejected with TOO_MANY_OPERATIONS (-29).
2026-03-18 03:21:34 +01:00
Enginex0 258a65ba59 docs(release): add v4.8 changelog for StrongBox hardening and LRU pruning 2026-03-17 19:56:45 +01:00
Enginex0 d2b8a92fbd chore(version): bump to v4.8 2026-03-17 19:48:48 +01:00
Enginex0 0723865eab feat(interception): add StrongBox hardening and LRU operation pruning
DuckDetector flags several behavioral anomalies that real TEE/StrongBox
hardware exhibits but our software interceptor did not:

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

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

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

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

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

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

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

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

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

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

Track deleted software key aliases and return KEY_NOT_FOUND (7) for
subsequent getKeyEntry calls. Also always invoke cleanupKeyData on
delete to clear stale patchedChains entries for hardware keys.
2026-03-16 22:06:53 +01:00
Enginex0 f4559bcd19 perf(keygen): normalize software generateKey RTT to match TEE latency
Software-generated keys complete in ~4ms, real TEE averages 55-65ms
with a floor around 15ms. Detectors measure this RTT to distinguish
software from hardware paths. Gaussian delay sampling (mean=55ms,
σ=12ms, floor=15ms) brings total RTT into the expected range.
2026-03-16 22:06:38 +01:00
Enginex0 3b5043a1bb docs(release): bump to v4.4 with AOSP conformance changelog 2026-03-16 13:26:34 +01:00
Enginex0 8001a8678a fix(interception): absorb upstream correctness fixes and patch error reply format
Cherry-pick three upstream fixes: Parcel position reset in hasException()
so the method doesn't consume reply data (7804743), list_past_alias
enumeration filter inversion (2aac65c), and KeyMetadata alignment with
AOSP semantics — modificationTimeMs, Tag.ORIGIN, KeyDescriptor
normalization (86db5bf).

Additionally, createErrorReply() was missing the empty remote stack
trace header int between the exception message and error code, per
AOSP Status.cpp:196. Binder readers expecting the standard
EX_SERVICE_SPECIFIC wire format would misparse our error replies.
2026-03-16 13:23:36 +01:00
Enginex0 d21822eb9d docs(release): bump to v4.3 with changelog and update metadata 2026-03-11 13:12:03 +01:00
Enginex0 095a658996 ci(build): fix pipeline trigger and release job gating
paths-ignore for .github/** was preventing workflow-only pushes
from triggering the pipeline at all. Release job was gated to
push events only, so workflow_dispatch never published. Simplify
paths-ignore to just **.md and allow both push and dispatch to
trigger the release job.
2026-03-11 13:03:35 +01:00
Enginex0 70e8968e44 ci(build): use mv instead of cp to avoid duplicate artifacts
cp left the original zip alongside the renamed copy, so the glob
matched both — doubling artifact size. mv removes the original.
2026-03-11 12:51:02 +01:00
Enginex0 c122ded7bf perf(daemon): add restart backoff, process priority, and map eviction
Supervisor had zero-delay restart on crash loops — pins CPU core at
100% if daemon keeps dying. Add exponential backoff (500ms to 30s cap,
resets after 30s stable). Set nice=10 on daemon child to yield CPU
to foreground apps. Evict stale entries from fileLocks and rate limiter
ConcurrentHashMaps that grew unbounded. Upload pre-built flashable zips
in CI instead of unpacking and re-compressing loose files.
2026-03-11 12:41:57 +01:00
Enginex0 7b510a9915 perf(logging): gate debug-level logs behind isDebugBuild
debug() was hitting Log.d() unconditionally in release builds —
every intercepted binder transaction triggered string formatting
and logcat syscalls. verbose() already had the guard; debug() was
just missing it. Also remove dead SERVICE_SLEEP_MS constant.
2026-03-11 12:41:46 +01:00
Enginex0 8f63dda31b ci(build): add release job with changelog and both ZIPs
The workflow only uploaded unzipped contents as CI artifacts —
no GitHub release was ever created from CI. Restructured into
build + release jobs: build produces both debug and release ZIPs
(renamed to clean `TEESimulator-vX.Y-{Variant}.zip` format),
release extracts changelog from module/changelog.md and publishes
a GitHub release with both ZIPs attached.
2026-03-11 04:06:20 +01:00
Enginex0 9896df93de build(gradle): keep debug symbols in debug variant
Debug ZIPs now ship unstripped native libs sourced from
merged_native_libs instead of stripped_native_libs. Gives
meaningful stack traces for crash debugging on-device.
2026-03-11 00:45:00 +01:00
Enginex0 0280bcf189 docs(readme): add build badge and building-from-source section 2026-03-11 00:28:28 +01:00
Enginex0 438a462bdf ci(build): add Rust toolchain and cargo-ndk for native-certgen
Gradle's buildRustCertgen task requires cargo-ndk and Android NDK
targets to cross-compile libcertgen.so. Without these, CI fails on
any commit after 32cfcb3 which wired the Rust crate into the pipeline.
2026-03-11 00:12:02 +01:00
Enginex0 40b08cd648 docs(release): bump to v4.2 with changelog and update metadata 2026-03-10 16:55:39 +01:00
Enginex0 c5ed627f68 chore(module): bump versionCode to 95 2026-03-10 16:37:58 +01:00
Enginex0 bee73eb39b perf(binder): skip interception for system transaction codes
AIDL methods use codes 1..0x00ffffff. System transactions like
PING_TRANSACTION (0x5f4e4750) fall above that range. Intercepting
pings forces a full JNI round-trip to Java and back, adding enough
latency for timing detectors to flag the ratio (3.85x vs 3.0x
threshold). Early-return for codes above LAST_CALL_TRANSACTION
eliminates this overhead while preserving all AIDL interception.
2026-03-10 16:37:50 +01:00
Enginex0 a0ee77202c fix(attestation): correct leaf CN casing and enforce keystore2 parameter policy
Leaf cert Subject CN used "KeyStore" (capital S) but AOSP
KeyGenParameterSpec uses "Keystore" (lowercase s). Fixed in both
the Rust native certgen and BouncyCastle paths.

Replicate keystore2's security_level.rs parameter validation for
software-generated keys: reject CREATION_DATETIME (output-only tag,
ResponseCode 20) and device ID attestation tags (CANNOT_ATTEST_IDS
-66) that real keystore2 blocks before they reach the HAL.

Also fix createErrorReply parcel write order — AIDL protocol expects
exception_code, message, error_code but we had message and error_code
swapped, causing malformed replies for positive error codes.
2026-03-10 14:23:33 +01:00
Enginex0 09d9896228 docs(release): bump to v4.1 with changelog and update metadata
versionCode=94 matches post-commit count.
2026-03-10 13:00:57 +01:00
Enginex0 5a599025ad fix(attestation): persist vbmeta boot key and hash across reboots
resetprop overrides for ro.boot.* props don't survive reboots. On
devices where the kernel doesn't set ro.boot.vbmeta.public_key_digest,
the fallback chain hit random generation on every boot — producing a
different RootOfTrust hash each time.

Added file-based persistence (boot_hash.bin, boot_key.bin) as a
fallback layer between TEE cache and random generation. Once a value
is determined from any source, it's written to disk and reused on
subsequent boots.

Verified on Redmi 14C: second boot reads from persistent file instead
of regenerating random bytes.
2026-03-10 12:58:49 +01:00
Enginex0 f5c2bcc024 fix(module): align update.json versionCode with release ZIP
Release ZIP was built at 89 commits (versionCode=89) but update.json
had versionCode=90, causing an infinite update loop in KSU Manager.
2026-03-10 04:40:53 +01:00
Enginex0 85e34ed42f docs(release): write v4.0 changelog and update module metadata
Native Rust cert gen release. Points update.json to fork URLs.
2026-03-09 22:48:20 +01:00
Enginex0 52b04675e7 docs(readme): rewrite README for personal fork
Matches ZeroMount style — badges, feature checklists, compatibility
tables, config docs. Clarifies this is a fork of JingMatrix/TEESimulator.
2026-03-09 22:17:38 +01:00
Enginex0 ba0628c687 fix(attestation): reject oversized challenges and rewrite cert DER encoding
DuckDetector flagged two issues:
1. Oversized challenge accepted — 256-byte attestation challenge should
   return INVALID_INPUT_LENGTH (-21) like real KeyMint. Added early check
   in handleGenerateKey before any path decision.
2. Issuer/subject chain mismatch — rcgen's HashMap loses DN attribute
   ordering and converts PrintableString to UTF8String, producing
   different DER bytes. Replaced rcgen with manual DER assembly that
   injects raw keybox issuer_dn_der bytes directly.

Verified on device: TX_ID 315 rejects 256-byte challenge, TX_ID 501
generates valid 4-cert chain with correct issuer linkage.
2026-03-09 21:52:06 +01:00
Enginex0 c11465d660 chore(build): bump to v4.0, update module metadata and add package script
Native certgen integration milestone. Adds action.sh/uninstall.sh to
customize.sh extraction loop, points update.json to fork, includes
build/deploy helper script.
2026-03-09 21:51:48 +01:00
Enginex0 0d5fd44992 fix(attestation): null out all-zero verifiedBootHash from TEE cache
Matches the existing verifiedBootKey null-zero guard. When the TEE
returns a zeroed hash, fall through to the system property or random
fallback instead of embedding a detectable all-zero value.
2026-03-09 20:00:14 +01:00
Enginex0 776a7b2343 feat(interception): override pre-existing attest keys, skip GMS list hooking
Two changes to Keystore2Interceptor:

1. Hardware attest keys created before TEESimulator loads now get
   detected in the getKeyEntry post-hook via isAttestKey(). A software
   replacement keypair is generated, cached, and persisted so the
   unpatched hardware chain is never served.

2. GMS calls listEntries frequently. Skip the post-hook injection
   for com.google.android.gms to reduce log flooding and unnecessary
   key merging work.

Also adds null-alias guard in onPreTransact to avoid NPE on keys
looked up by domain/nspace without an alias.
2026-03-09 19:59:51 +01:00
Enginex0 14786ecce0 feat(attestation): add origin field and isAttestKey/isImportKey helpers
Parse ORIGIN tag from KeyParameter array into KeyMintAttestation
data class. Add isAttestKey() and isImportKey() convenience methods
to consolidate purpose/origin checks scattered across interceptors.
2026-03-09 19:59:38 +01:00
Enginex0 41b9cc8f10 fix(attestation): correct module_hash to match AOSP Keystore2
BouncyCastle DERSet() sorts by full encoded sequence, but AOSP
keystore2 maintenance.rs sorts by encoded name only. Replace
PackageManager-based APEX enumeration with filesystem scan of
/apex/ directories using a minimal protobuf parser for
apex_manifest.pb. Encode the DER SET tag manually to preserve
the name-only sort order.
2026-03-09 19:59:30 +01:00
Enginex0 6df266b688 fix(native-certgen): address production audit findings
Make logging init idempotent (swallow SetGlobalDefaultError on repeat
call), remove unused dumpLogs JNI params that violated the API contract,
and strip dead public_key_spki field + build_ec_spki() that were
computed on every keygen but never consumed by the cert builder.
2026-03-09 18:16:50 +01:00
Enginex0 32cfcb3ece build(native-certgen): wire Rust crate into Gradle pipeline
cargo-ndk builds libcertgen.so for arm64-v8a during prepareModuleFiles.
AGP mergeJniLibFolders picks up jniLibs/ and routes through
stripped_native_libs into the module ZIP. customize.sh extracts the .so
on device install. ProGuard keeps NativeCertGen JNI class and
CertGenConfig fields for runtime JNI field access.
2026-03-09 16:46:32 +01:00
Enginex0 727b32f6b0 fix(pki): align JNI signatures between Kotlin and Rust
initLogging now takes logDir param matching Rust entry point.
dumpLogs takes logDir+baseDir params matching Rust. Removed unused
generateSoftwareKeyPair declaration. Added buffer bounds checks in
parseNativeResult to prevent OOM on malformed native output.
2026-03-09 16:37:52 +01:00
Enginex0 ac9641ed2a feat(pki): integrate native cert gen with BouncyCastle fallback
NativeCertGen.kt provides CertGenConfig data class and JNI bridge
to libcertgen.so. KeyMintSecurityLevelInterceptor.doSoftwareKeyGen()
tries native path first, falls back to BouncyCastle on failure or
when library unavailable. App.kt loads libcertgen.so at daemon start.
2026-03-09 16:23:09 +01:00
Enginex0 c87759c6c3 feat(native-certgen): implement JNI bridge with panic-safe entry points
Three JNI exports: generateAttestedKeyPair (orchestrates keygen,
attestation, certbuilder, returns length-prefixed binary),
initLogging (multi-output tracing setup), dumpLogs (diagnostic ZIP).
CertGenConfig extraction via typed JNI field accessors. catch_unwind
on all FFI boundaries.
2026-03-09 16:13:41 +01:00
Enginex0 76b18706f1 fix(native-certgen): address Phase 3 validation findings
Remove ENCRYPT/VERIFY from KeyUsage mapping to match Kotlin behavior.
Document BasicConstraints and SKI suppression via rcgen NoCa default.
Fix rotating log off-by-one that kept one extra backup file. Handle
BMPString (UTF-16BE) and VisibleString in X.500 DN parser.
2026-03-09 16:08:07 +01:00
Enginex0 e76f5115e0 feat(native-certgen): implement X.509 certificate chain builder
Builds v3 leaf certificate with attestation extension and KeyUsage,
signs with keybox private key via rcgen 0.13.2. Assembles full chain
(leaf + keybox intermediates + root). Supports EC and RSA keybox
signing keys. Uses rcgen's signed_by() with a synthesized issuer
Certificate — no manual DER fallback needed.
2026-03-09 15:55:15 +01:00
Enginex0 0725aed094 feat(native-certgen): implement logging subsystem
Multi-output logging via tracing: /dev/kmsg for logcat, rotating file
appender (512KB, 3 files), stderr for debug. Diagnostic ZIP dump with
log files and TEE status snapshots. Verbose toggle via JNI flag or
.verbose marker file.
2026-03-09 15:41:46 +01:00
Enginex0 9d61af6ce8 feat(native-certgen): implement ASN.1 attestation extension encoder
DER encoder for Android KeyMint attestation extension (OID
1.3.6.1.4.1.11129.2.1.17). SecurityLevel and VerifiedBootState as
ENUMERATED, EXPLICIT context-specific tagging with long-form for
tags >= 31, sorted AuthorizationList fields, RootOfTrust with
BOOLEAN TRUE=0xFF, SET OF INTEGER with DER sort, DO_NOT_REPORT
sentinel omission.
2026-03-09 15:32:31 +01:00
Enginex0 7863e8dd17 fix(native-certgen): address Phase 0-1 validation findings
EC keygen now returns proper SPKI DER instead of raw point bytes.
RSA keygen uses caller-supplied exponent via new_with_exp() and
validates key size to 2048/3072/4096. Keybox parser extracts leaf
subject DN (not issuer). Added AttestKey=7 to KeyPurpose. Realigned
error variants with spec.
2026-03-09 15:21:33 +01:00
Enginex0 f840eed42b feat(native-certgen): implement keybox DER certificate chain parser
Splits concatenated DER cert chains into individual certificates,
extracts leaf issuer DN and notAfter via x509-cert crate. Handles
multi-byte DER length encoding (0x81-0x84).
2026-03-09 15:09:29 +01:00
Enginex0 4c7f0a09ea feat(native-certgen): scaffold Rust crate with foundation types and keygen
Cargo.toml with 16 dependencies per build spec, error types with
From impls for all upstream error types, CertGenParams mapping the
full JNI config contract, EC/RSA key generation via ring and rsa crates.

Compiles clean for aarch64-linux-android via cargo-ndk.
2026-03-09 15:05:25 +01:00
Enginex0 4971f7b4a5 Derive boot and vendor patch levels from system prop when system=prop
TrickyAddon fetches Pixel bulletin dates for boot/vendor but system=prop
resolves to the real device prop, creating a cross-component date mismatch
on non-Pixel devices. Force all three through the same prop resolution path.
2026-02-07 00:47:53 +01:00
Enginex0 9ea39f0545 Rate-limit per-UID hardware keygen and harden importKey eviction
Sliding window limits each UID to 2 hardware generateKey calls per
30s burst window with max 2 concurrent. Overflow falls back to
software cert generation.

importKey post-hook retains patched chains instead of full eviction,
preventing detectors from using generate-then-import to bypass
attestation patching. getKeyEntry serves retained chains for imported
keys that overwrote attested aliases.
2026-02-07 00:47:47 +01:00
Enginex0 c332f8ad0d Cap interceptable binder payload size at 256KB
Prevents thread starvation from flood attacks targeting the
binder interceptor with oversized payloads.
2026-02-07 00:47:42 +01:00
Enginex0 d76e6abeed Add file-level locking to prevent race conditions in key persistence
Per-key ReentrantLock prevents concurrent writes to same key file
2026-02-07 00:47:36 +01:00
Enginex0 142fe7bf13 Reject oversized aliases to prevent binder buffer exhaustion
MAX_ALIAS_LENGTH (256KB) with 4x safety margin for transaction overhead
2026-02-07 00:47:31 +01:00
Enginex0 e41a679928 fix(pki): strip HTML comments from PEM blocks before parsing
Some upstream keybox sources inject HTML comments inside PEM
certificate blocks. BouncyCastle's PEMParser chokes on these
non-base64 lines, silently failing to load the keybox.

Filter lines starting with <!-- in trimLines() before the content
reaches the PEM parser.
2026-02-07 00:47:26 +01:00
Enginex0 209d5c8902 fix(config): prevent FileObserver NPE on config file deletion
When a config file is deleted, the event handler sets file=null but
then force-unwraps it with file!! in the when block, crashing the
FileObserver thread. All subsequent config change notifications are
silently lost.

Replace force-unwrap with safe call, log a warning on deletion.
2026-02-07 00:47:20 +01:00
Enginex0 3817b37e18 Integrate key persistence with interceptors
Save keys on generation, restore on daemon startup, delete on cleanup.
Re-persist when cert chain updates via updateSubcomponents.
2026-02-07 00:47:16 +01:00
Enginex0 53ae250fc7 Add generated key persistence layer
Persist GENERATE-mode keys to disk so they survive daemon restarts.
Binary format with version header, atomic write via tmp+rename.
2026-02-07 00:47:10 +01:00
Enginex0 34ad97366e feat(module): add supervisor daemon with leak-safe restart and lifecycle scripts
Fork-based supervisor ensures the interceptor process survives crashes.
pingBinder() liveness check on pre-transact returns DEAD_OBJECT to
callers when interceptor is down, preventing real TEE state from leaking
during the restart window.

action.sh clears persistent key storage via KSU Action button.
uninstall.sh kills daemon processes and removes module artifacts while
preserving target.txt and keybox configuration.
2026-02-07 00:47:04 +01:00
61 changed files with 6917 additions and 574 deletions
+91 -72
View File
@@ -3,16 +3,10 @@ name: Build
on:
push:
branches: [ "main" ]
paths-ignore:
- '**.md'
- '.github/**'
- '!.github/workflows/**'
paths-ignore: [ '**.md' ]
pull_request:
branches: [ "main" ]
paths-ignore:
- '**.md'
- '.github/**'
- '!.github/workflows/**'
paths-ignore: [ '**.md' ]
workflow_dispatch:
concurrency:
@@ -22,18 +16,9 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
permissions:
id-token: write
attestations: write
contents: read
outputs:
releaseName: ${{ steps.prepareArtifact.outputs.releaseName }}
debugName: ${{ steps.prepareArtifact.outputs.debugName }}
steps:
- name: Check out
uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
submodules: "recursive"
fetch-depth: 0
@@ -45,6 +30,25 @@ jobs:
java-version: 21
cache: 'gradle'
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android
- name: Cache Rust artifacts
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
~/.cargo/bin/cargo-ndk
native-certgen/target
key: rust-${{ runner.os }}-${{ hashFiles('native-certgen/Cargo.lock') }}
restore-keys: rust-${{ runner.os }}-
- name: Install cargo-ndk
run: command -v cargo-ndk || cargo install cargo-ndk
- name: Set up ccache
uses: hendrikmuhs/ccache-action@v1.2
with:
@@ -60,73 +64,88 @@ jobs:
- name: Build with Gradle
run: |
chmod +x ./gradlew
./gradlew zipRelease zipDebug -Porg.gradle.parallel=true -Porg.gradle.vfs.watch=true -Dorg.gradle.jvmargs=-Xmx2048m
- name: Prepare artifact
if: success()
id: prepareArtifact
- name: Read version
id: ver
run: |
set -e
RELEASE_FILE=$(find out -name "*Release*.zip" | head -1)
DEBUG_FILE=$(find out -name "*Debug*.zip" | head -1)
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
count=$(git rev-list HEAD --count)
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
if [[ -z "$RELEASE_FILE" || -z "$DEBUG_FILE" ]]; then
echo "Error: Could not find release or debug files in out/"
echo "Contents of out/ directory:"
ls -la out/ || echo "out/ directory does not exist"
exit 1
fi
- name: List build artifacts
run: |
echo "Release: $(ls out/*Release*.zip | head -1) ($(du -h out/*Release*.zip | head -1 | cut -f1))"
echo "Debug: $(ls out/*Debug*.zip | head -1) ($(du -h out/*Debug*.zip | head -1 | cut -f1))"
# Extract names
RELEASE_NAME=$(basename "$RELEASE_FILE" .zip)
DEBUG_NAME=$(basename "$DEBUG_FILE" .zip)
echo "releaseName=$RELEASE_NAME" >> $GITHUB_OUTPUT
echo "debugName=$DEBUG_NAME" >> $GITHUB_OUTPUT
mkdir -p module-release module-debug
unzip -q "$RELEASE_FILE" -d module-release
unzip -q "$DEBUG_FILE" -d module-debug
echo " Release: $RELEASE_NAME"
echo " Debug: $DEBUG_NAME"
- name: Upload release
if: success()
id: release
uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v4
with:
name: ${{ steps.prepareArtifact.outputs.releaseName }}
path: "./module-release/*"
name: TEESimulator-RS-release-zip
path: out/TEESimulator-RS-*-Release.zip
retention-days: 30
compression-level: 6
compression-level: 0
- name: Upload debug
if: success()
id: debug
uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v4
with:
name: ${{ steps.prepareArtifact.outputs.debugName }}
path: "./module-debug/*"
name: TEESimulator-RS-debug-zip
path: out/TEESimulator-RS-*-Debug.zip
retention-days: 7
compression-level: 6
compression-level: 0
- name: Upload release mappings
if: success()
uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v4
with:
name: release-mappings-${{ github.run_number }}
path: "./app/build/outputs/mapping/release"
name: release-mappings
path: app/build/outputs/mapping/release
retention-days: 30
compression-level: 9
- name: Summary
if: always()
release:
needs: build
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read version
id: ver
run: |
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
echo "- **Status**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
echo "- **Gradle Tasks**: assembleRelease, assembleDebug" >> $GITHUB_STEP_SUMMARY
if [[ "${{ job.status }}" == "success" ]]; then
echo "- **Release Artifact**: ${{ steps.prepareArtifact.outputs.releaseName }}" >> $GITHUB_STEP_SUMMARY
echo "- **Debug Artifact**: ${{ steps.prepareArtifact.outputs.debugName }}" >> $GITHUB_STEP_SUMMARY
fi
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
count=$(git rev-list HEAD --count)
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
- uses: actions/download-artifact@v4
with:
name: TEESimulator-RS-release-zip
path: zips
- uses: actions/download-artifact@v4
with:
name: TEESimulator-RS-debug-zip
path: zips
- name: Extract changelog
run: |
ver="${VER#v}"
awk "/^## TEESimulator-RS v${ver%%-*}/{flag=1; next} /^## TEESimulator-RS v/{if(flag) exit} flag" module/changelog.md > /tmp/notes.md
cat /tmp/notes.md
env:
VER: ${{ steps.ver.outputs.version }}
- name: Create release
run: |
gh release delete "$VER" --yes 2>/dev/null || true
RELEASE=$(ls zips/*Release*.zip | head -1)
DEBUG=$(ls zips/*Debug*.zip | head -1)
gh release create "$VER" \
--title "$VER" \
--latest \
--notes-file /tmp/notes.md \
"$RELEASE" \
"$DEBUG"
env:
VER: ${{ steps.ver.outputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+6
View File
@@ -1 +1,7 @@
out
.gradle
.kotlin
app/build
build
native-certgen/target
app/src/main/jniLibs
+194 -75
View File
@@ -1,32 +1,143 @@
# TEESimulator A Full TEE Emulation Framework
<p align="center">
<h1 align="center">🔐 TEESimulator</h1>
<p align="center"><b>Full TEE Emulation for Rooted Android</b></p>
<p align="center">Hardware attestation. Software keys. Zero detection.</p>
<p align="center">
<a href="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml"><img src="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml/badge.svg" alt="Build"></a>
<img src="https://img.shields.io/badge/version-v4.2-blue?style=for-the-badge" alt="v4.2">
<img src="https://img.shields.io/badge/Android-10%2B-green?style=for-the-badge&logo=android" alt="Android 10+">
<img src="https://img.shields.io/badge/Telegram-community-blue?style=for-the-badge&logo=telegram" alt="Telegram">
</p>
</p>
**TEESimulator** is a system module designed to create a complete, software-based simulation of a hardware-backed Trusted Execution Environment ([TEE](https://source.android.com/docs/security/features/trusty)) for [Key Attestation](https://developer.android.com/privacy-and-security/security-key-attestation).
---
The project's goal is to move beyond simple certificate patching and build a robust framework that can create and manage virtual, self-consistent cryptographic keys.
> [!NOTE]
> **This is a personal fork of [JingMatrix/TEESimulator](https://github.com/JingMatrix/TEESimulator)** with additional hardening, native Rust certificate generation, key persistence, and anti-detection features. For the upstream project, see the original repo.
## ✨ Core Principles
---
* **Bypass Hardware-Backed Attestation:** The primary goal of this project is to defeat Key Attestation, a security mechanism that allows apps to verify that they are running on a secure, unmodified device. This module provides the tools to bypass these checks on rooted or modified devices.
* **Stateful Emulation:** Instead of patching responses from the real TEE, the ultimate goal is to create and manage virtual keys entirely in a simulated software environment. Any request concerning a virtual key will be handled by the simulator, ensuring perfect consistency without ever touching the real hardware.
* **Architectural Interception:** By hooking low-level Binder IPC calls to the Keystore, the framework can transparently redirect requests for virtual keys to the software-based simulator, while allowing requests for real keys to pass through to the hardware TEE.
* **100% FOSS:** Licensed under GPLv3, ensuring it stays free, auditable, and compliant with open-source laws.
## 🧬 What is TEESimulator?
## 📱 Requirements
- Android 10 or above
TEESimulator is a **complete software simulation** of Android's hardware-backed [Trusted Execution Environment](https://source.android.com/docs/security/features/trusty) for [Key Attestation](https://developer.android.com/privacy-and-security/security-key-attestation). Instead of patching certificates from the real TEE after the fact, TEESimulator intercepts Binder IPC at the `ioctl` level and generates entire certificate chains from scratch — signed by your keybox, with correct attestation extensions, indistinguishable from hardware-generated keys.
## 📦 Installation & Configuration
The result: **apps that verify hardware attestation see a legitimate, unmodified device** — even on rooted hardware with an unlocked bootloader.
1. Flash this module via (Magisk / KernelSU / APatch) and reboot. It will replace [TrickyStore](https://github.com/5ec1cff/TrickyStore), [TrickyStoreOSS](https://github.com/beakthoven/TrickyStoreOSS) and their forks.
2. (Optional) Place a hardware-backed `keybox.xml` at `/data/adb/tricky_store/keybox.xml`. This provides the cryptographic "root of trust" for the simulator.
3. (Optional) Customize target packages in `/data/adb/tricky_store/target.txt`.
4. (Optional) Customize the simulated security patch level in `/data/adb/tricky_store/security_patch.txt`.
5. Enjoy!
> **This is not TrickyStore.** TEESimulator replaces TrickyStore and its forks entirely. It shares the same config paths for drop-in compatibility, but the architecture is fundamentally different: native Rust certificate generation, binder-level interception via `lsplt`, per-UID rate limiting, key persistence, and a multi-layer defense against detector apps.
**All configuration files are monitored and will take effect immediately upon saving.**
---
## 🔥 Why TEESimulator?
🔐 **Native Cert Generation** — v4.0 generates X.509 certificate chains in Rust with `ring` and manual DER encoding. No BouncyCastle overhead, no Java crypto quirks, byte-perfect issuer chain linkage.
🎯 **Binder-Level Interception** — Hooks `ioctl()` on `libc.so` via `lsplt` inside the `keystore2` process. Intercepts `generateKey`, `importKey`, and `getKeyEntry` transactions before the HAL ever sees them.
🛡️ **Detector Resistant** — Per-UID rate limiting blocks DuckDetector-style keygen flooding. Oversized challenges rejected with real KeyMint error codes. Chain consistency verified byte-for-byte.
💾 **Key Persistence** — Generated keys survive reboots. Apps that store attestation keys (banking, biometrics) don't break after a restart.
🔧 **Drop-In Replacement** — Same config paths as TrickyStore (`/data/adb/tricky_store/`). Swap the module ZIP, keep your keybox and target list.
---
## ✨ Features
**Core Attestation Engine**
- [x] **Full certificate chain generation** — leaf + intermediates + root, signed by your keybox
- [x] **Native Rust certgen**`libcertgen.so` built with `ring`, `rsa`, and manual DER assembly
- [x] **BouncyCastle fallback** — unsupported curves (P-224, P-521, Curve25519) fall back to Java
- [x] **ASN.1 attestation extensions** — OID 1.3.6.1.4.1.11129.2.1.17 with all AOSP-specified tags
- [x] **Multi-keybox support** — different keybox files per app group via `target.txt`
**Interception Layer**
- [x] **Binder ioctl hook**`lsplt` PLT hook on `libc.so` inside `keystore2` process
- [x] **generateKey / importKey / getKeyEntry** — all three transaction types intercepted
- [x] **256KB native payload cap** — oversized binder payloads bypass interception cleanly
- [x] **Challenge validation** — rejects >128-byte attestation challenges with `INVALID_INPUT_LENGTH`
**Hardening**
- [x] **Per-UID rate limiter** — 2 hardware keygens per 30s burst window, software fallback on overflow
- [x] **importKey eviction guard** — retained patch chains prevent generate-then-import cache attacks
- [x] **Key persistence** — file-backed storage with file-level locking, survives reboots and keybox rotations
- [x] **Global exception handler** — uncaught exceptions logged, daemon stays alive
**Configuration**
- [x] **Live config reload**`FileObserver` watches all config files, changes apply immediately
- [x] **Security patch spoofing** — per-package `system`, `vendor`, `boot` patch levels with dynamic templates
- [x] **Lifecycle scripts** — KSU Action button clears key cache, uninstall removes all traces
---
## 📋 Requirements
> [!IMPORTANT]
> TEESimulator requires root access and a valid `keybox.xml` for hardware-level attestation results. Without a keybox, the module generates software-level certificates that won't pass strict hardware attestation checks.
**You need:**
1. Android 10 or above
2. A supported root manager (KernelSU, Magisk, or APatch)
3. A hardware-backed `keybox.xml` placed at `/data/adb/tricky_store/keybox.xml`
---
## 📱 Compatibility
### Root Managers
| Manager | Status | Notes |
|---|---|---|
| KernelSU | ✅ Tested | Full support including Action button and lifecycle scripts |
| Magisk | ✅ Supported | Standard module install |
| APatch | ✅ Supported | Standard module install |
### Tested Devices
| Device | Android | TEE | Status |
|---|---|---|---|
| Redmi 14C (2409BRN2CA) | 14 (SDK 34) | Beanpod KeyMaster | ✅ Daily driver |
> Tested against DuckDetector, Luna, Play Integrity, and Key Attestation Demo. If you test on a different device, [open an issue](https://github.com/Enginex0/TEESimulator/issues) with your results.
---
## 🚀 Quick Start
1. **Download** the latest release ZIP from [Releases](https://github.com/Enginex0/TEESimulator/releases)
2. **Install** via your root manager (KSU / Magisk / APatch) and reboot
3. **Place your keybox** at `/data/adb/tricky_store/keybox.xml`
4. **Configure targets** in `/data/adb/tricky_store/target.txt`
5. **Verify** — check Play Integrity or run Key Attestation Demo
TEESimulator replaces TrickyStore, TrickyStoreOSS, and their forks. Existing config files are compatible.
---
## 🔨 Building from Source
The CI workflow builds on every push to `main`. You can also build locally or trigger a build from your own fork.
**Prerequisites:** JDK 21, Android SDK/NDK 27, Rust stable with `aarch64-linux-android` target, `cargo-ndk`.
```bash
git clone https://github.com/Enginex0/TEESimulator.git
cd TEESimulator
./gradlew zipRelease zipDebug
```
Output ZIPs land in `out/`. The Gradle build automatically invokes `cargo ndk` to cross-compile `libcertgen.so` before packaging.
To rebuild from a fork, push to `main` or use **Actions → Build → Run workflow**. The workflow installs all toolchains (Java, Rust, cargo-ndk, ccache) and uploads Release + Debug ZIPs as artifacts.
---
## ⚙️ Configuration
All configuration files live at `/data/adb/tricky_store/` and are monitored by `FileObserver` — changes take effect immediately without rebooting.
### The `keybox.xml` Root of Trust
This file provides the master cryptographic identity for the simulator. It contains a private key and a valid, hardware-backed certificate chain from a real device. The simulator uses this to sign the virtual certificates it generates, making them appear legitimate to verifiers.
This file provides the master cryptographic identity. It contains a private key and a hardware-backed certificate chain from a real device. TEESimulator signs all generated certificates with this key, making them appear legitimate to verifiers.
```xml
<?xml version="1.0"?>
@@ -40,101 +151,109 @@ This file provides the master cryptographic identity for the simulator. It conta
</AndroidAttestation>
```
### Mode and Keybox Configuration (`target.txt`)
### Target Packages (`target.txt`)
TEESimulator currently operates in two primary modes as it transitions towards full emulation.
You can control the simulation mode and the specific keybox.xml file used on a per-package basis.
Controls which apps get intercepted and what simulation mode to use.
#### Mode Suffixes
* **`!` → Force Generation Mode:** Creates a complete, software-based virtual key. This is the foundation of the full TEE simulation.
* **`?` → Force Leaf Hacking Mode:** A legacy mode where a real TEE key is generated, but its attestation certificate is intercepted and modified.
* **No symbol → Automatic Mode:** The module selects the most appropriate mode for the device.
* **`!` → Force Generation** Creates a complete software-based virtual key. Full TEE simulation.
* **`?` → Force Leaf Hacking** — Real TEE key generated, but its attestation certificate is intercepted and patched.
* **No symbol → Automatic** — Module selects the best mode for your device.
#### Multi-Keybox Configuration
#### Multi-Keybox
You can specify different keybox files for different groups of applications. This is done by adding a line with the filename in square brackets (e.g., [demo_keybox.xml]).
Specify different keybox files for different app groups. Apps listed after a `[filename.xml]` line use that keybox. Apps before any declaration use the default `keybox.xml`.
All applications listed after this line will use the specified keybox file, until a new keybox is declared. Applications listed before any custom keybox declaration will use the default `keybox.xml`.
For example:
```
# These two apps will use the default /data/adb/tricky_store/keybox.xml
# Default keybox
com.google.android.gms!
io.github.vvb2060.keyattestation?
# Switch to a different keybox for the following apps.
# The file must be located at /data/adb/tricky_store/aosp_keybox.xml
# Switch to a different keybox for the following apps
[aosp_keybox.xml]
com.google.android.gsf
# Switch again to another keybox.
# The file must be located at /data/adb/tricky_store/demo_keybox.xml
# Another keybox
[demo_keybox.xml]
org.matrix.demo
```
### Security Patch Level (`security_patch.txt`)
This file allows you to configure the `osPatchLevel`, `vendorPatchLevel`, and `bootPatchLevel` that the simulator will report in its patched or forged attestation certificates.
Configure the `osPatchLevel`, `vendorPatchLevel`, and `bootPatchLevel` reported in attestation certificates. This only affects attestation data — it does not change actual system properties.
**Note:** This only affects the Key Attestation data generated by the simulator. It does not change the actual system properties of your device.
#### Global and Per-Package
#### Global and Per-Package Configuration
Settings at the top of the file are global defaults. Add `[package.name]` to override for specific apps.
You can set a global patch level that applies to all applications, and you can also override these settings for specific packages. The syntax is hierarchical:
#### Keys
* Settings defined at the top of the file, before any `[package.name]` line, are **global** and serve as the default for all apps.
* To create a specific configuration for an application, add its package name in square brackets (e.g., `[com.google.android.gms]`). All settings following this line will apply *only* to that package until a new package context is declared.
#### Configuration Keys and Values
You can specify the patch level for the following components using a `key=value` format:
* `system`: The main OS patch level.
* `vendor`: The vendor patch level.
* `boot`: The boot/kernel patch level.
* `all`: A convenient shorthand to set the same date for `system`, `vendor`, and `boot` simultaneously. Any individual key can still be used to override the value set by `all`.
Dates should be provided in `YYYY-MM-DD` format (e.g., `2025-11-05`).
| Key | Scope |
|---|---|
| `system` | OS patch level |
| `vendor` | Vendor patch level |
| `boot` | Boot/kernel patch level |
| `all` | Shorthand — sets all three at once |
#### Special Keywords
In addition to static dates, several special keywords provide advanced, dynamic control:
| Keyword | Effect |
|---|---|
| `today` | Current date, dynamically resolved on each attestation |
| `YYYY-MM-DD` templates | Semi-dynamic — `YYYY-MM-05` resolves to the 5th of the current month |
| `no` | Omit this patch level tag entirely from the attestation |
| `device_default` | Use the device's real hardware value |
| `prop` | Read from `ro.build.version.security_patch` (matches what detectors see via getprop) |
* **`today`**: Dynamically uses the current date every time an attestation is generated. This ensures the device always appears up-to-date without needing manual edits.
* **Date Templates**: You can create semi-dynamic dates using `YYYY`, `MM`, and `DD` as placeholders for the current year, month, and day. For example, `YYYY-MM-05` will always resolve to the 5th of the current month and year.
* **`no`**: This keyword instructs the simulator to **completely omit** the corresponding patch level tag from the generated attestation.
* **`device_default`**: This keyword forces the simulator to fall back and use the device's **real hardware value** for that specific patch level. This is essential for creating exceptions to a global override or an `all` rule.
#### Example Configuration
This example demonstrates how to combine global settings, per-package overrides, and special keywords for fine-grained control.
#### Example
```
# --- Global Configuration ---
# This is the default for all apps unless specified otherwise.
# - Forge a recent system patch level, the 5th of the current month (a common patch date).
# - Use the device's real vendor patch level.
# - Do not report a boot patch level at all.
# Global — default for all apps
system=YYYY-MM-05
vendor=device_default
boot=no
# --- Per-Package Override for Google Play Services ---
# This app will report an older, specific date for its system patch.
# It will inherit the global settings for vendor (device_default) and boot (no).
# Override for GMS
[com.google.android.gms]
system=2024-10-01
# --- Per-Package Override for a Demo App ---
# This app gets a completely custom configuration.
# Custom config for a demo app
[org.matrix.demo]
# Set a base date for all patch levels...
all=2025-09-15
# ...but make an exception: use the real boot patch level instead of the one from 'all'.
boot=device_default
```
---
## 💬 Community
<p align="center">
<a href="https://t.me/superpowers9">
<img src="https://img.shields.io/badge/⚡_JOIN_THE_GRID-SuperPowers_Telegram-black?style=for-the-badge&logo=telegram&logoColor=cyan&labelColor=0d1117&color=00d4ff" alt="Telegram">
</a>
</p>
---
## 🙏 Credits
- **[JingMatrix](https://github.com/JingMatrix/TEESimulator)** — original author of TEESimulator and the interception architecture
- **[5ec1cff](https://github.com/5ec1cff/TrickyStore)** — TrickyStore, the project that pioneered keystore interception on Android
- **[LSPlt](https://github.com/LSPosed/LSPlt)** — PLT hook library used for binder interception
- **[ring](https://github.com/briansmith/ring)** — Rust cryptography library powering native cert generation
- **[MhmRdd](https://github.com/MhmRdd)** — AOSP compliance improvements via upstream [PR #157](https://github.com/JingMatrix/TEESimulator/pull/157), including authorize_create enforcement, attestation extension alignment, and binder transaction filtering
- **[fatalcoder524](https://github.com/fatalcoder524)** — a real contributor and collaborator on this project
- **[huguangares](https://github.com/huguangares)** — collaborator and tester
---
## 📄 License
This project is licensed under the [GNU General Public License v3.0](LICENSE).
---
<p align="center">
<b>🔐 Because the best attestation is the one the TEE never generated.</b>
</p>
+46 -16
View File
@@ -29,7 +29,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java)
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
val verName = "v3.1"
val verName = "v5.0"
android {
namespace = "org.matrix.TEESimulator"
@@ -71,6 +71,35 @@ dependencies {
implementation(libs.bcpkix)
}
// --- Rust native cert gen build task ---
val buildRustCertgen by tasks.registering(Exec::class) {
group = "TEESimulator-RS Native Build"
description = "Builds libcertgen.so via cargo-ndk for arm64-v8a."
workingDir = rootProject.projectDir.resolve("native-certgen")
commandLine(
"cargo", "ndk",
"-t", "arm64-v8a",
"-o", rootProject.projectDir.resolve("app/src/main/jniLibs").absolutePath,
"build", "--release"
)
inputs.dir(rootProject.projectDir.resolve("native-certgen/src"))
inputs.file(rootProject.projectDir.resolve("native-certgen/Cargo.toml"))
inputs.file(rootProject.projectDir.resolve("native-certgen/Cargo.lock"))
outputs.dir(rootProject.projectDir.resolve("app/src/main/jniLibs"))
environment("ANDROID_NDK_HOME", android.ndkDirectory.absolutePath)
}
// AGP auto-detects jniLibs/ as an input to mergeJniLibFolders — wire the dependency
tasks.configureEach {
if (name.endsWith("JniLibFolders") && name.startsWith("merge")) {
dependsOn(buildRustCertgen)
}
}
androidComponents {
onVariants(selector().all()) { variant ->
val capitalized = variant.name.replaceFirstChar { it.uppercase() }
@@ -79,21 +108,22 @@ androidComponents {
// --- Define output locations and file names ---
// Stage all files in a temporary directory inside 'build' before zipping
val tempModuleDir = project.layout.buildDirectory.dir("module/${variant.name}")
val zipFileName = "TEESimulator-$verName-$gitCommitCount-$gitCommitHash-$capitalized.zip"
val zipFileName = "TEESimulator-RS-$verName-$gitCommitCount-$capitalized.zip"
// Task 1: Prepare all module files in the temporary build directory.
// Using Sync ensures that stale files from previous runs are removed.
val prepareModuleFilesTask =
tasks.register<Sync>("prepareModuleFiles${capitalized}") {
group = "TEESimulator Module Packaging"
group = "TEESimulator-RS Module Packaging"
description = "Prepares all files for the ${variant.name} module zip."
if (isDebug) {
dependsOn("package${capitalized}")
} else {
dependsOn("minify${capitalized}WithR8")
}
dependsOn("strip${capitalized}DebugSymbols")
}
dependsOn(buildRustCertgen)
if (isDebug) {
from(variant.artifacts.get(SingleArtifact.APK)) {
@@ -110,13 +140,14 @@ androidComponents {
}
}
from(
project.layout.buildDirectory.dir(
val nativeLibsDir = if (isDebug) {
"intermediates/merged_native_libs/${variant.name}/merge${capitalized}NativeLibs/out/lib"
} else {
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
)
) {
into("lib") // Place them in the 'lib' subfolder of the staging directory.
include("**/libinject.so", "**/libTEESimulator.so")
}
from(project.layout.buildDirectory.dir(nativeLibsDir)) {
into("lib")
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so", "**/libcertgen.so")
}
// Now, copy and process the files from 'module' directory.
@@ -131,8 +162,7 @@ androidComponents {
// Use expand() for simple key-value replacement.
expand(
"REPLACEMEVERCODE" to gitCommitCount.toString(),
"REPLACEMEVER" to
"$verName ($gitCommitCount-$gitCommitHash-${variant.name})",
"REPLACEMEVER" to "$verName-$gitCommitCount",
)
}
@@ -143,7 +173,7 @@ androidComponents {
// Task 2: Zip the prepared files from the temporary directory.
val zipTask =
tasks.register<Zip>("zip${capitalized}") {
group = "TEESimulator Module Packaging"
group = "TEESimulator-RS Module Packaging"
description = "Creates the flashable zip for the ${variant.name} module."
dependsOn(prepareModuleFilesTask)
@@ -156,7 +186,7 @@ androidComponents {
fun createInstallTasks(rootProvider: String, installCli: String) {
val pushTask =
tasks.register<Exec>("push${rootProvider}Module${capitalized}") {
group = "TEESimulator Module Installation"
group = "TEESimulator-RS Module Installation"
description =
"Pushes the ${variant.name} module to the device for $rootProvider."
dependsOn(zipTask)
@@ -170,7 +200,7 @@ androidComponents {
val installTask =
tasks.register<Exec>("install${rootProvider}${capitalized}") {
group = "TEESimulator Module Installation"
group = "TEESimulator-RS Module Installation"
description = "Installs the ${variant.name} module via $rootProvider."
dependsOn(pushTask)
commandLine(
@@ -183,7 +213,7 @@ androidComponents {
}
tasks.register<Exec>("install${rootProvider}AndReboot${capitalized}") {
group = "TEESimulator Module Installation"
group = "TEESimulator-RS Module Installation"
description = "Installs the ${variant.name} module via $rootProvider and reboots."
dependsOn(installTask)
commandLine("adb", "reboot")
+6
View File
@@ -7,3 +7,9 @@
-keepclasseswithmembers class org.matrix.TEESimulator.App {
public static void main(java.lang.String[]);
}
-keepclasseswithmembers class org.matrix.TEESimulator.pki.NativeCertGen {
native <methods>;
*;
}
-keep class org.matrix.TEESimulator.pki.CertGenConfig { *; }
+3
View File
@@ -22,6 +22,9 @@ add_executable(libinject.so inject/main.cpp inject/utils.cpp)
target_include_directories(libinject.so PUBLIC include)
target_link_libraries(libinject.so PRIVATE lsplt_static)
add_executable(libsupervisor.so supervisor.cpp)
target_link_libraries(libsupervisor.so PRIVATE log)
add_library(${CMAKE_PROJECT_NAME} SHARED binder_interceptor.cpp)
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC external/linux-kernel/include include)
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE binder lsplt_static utils)
+45 -17
View File
@@ -235,19 +235,21 @@ class BinderInterceptor : public BBinder {
struct RegistrationEntry {
wp<IBinder> target;
sp<IBinder> callback_interface;
std::vector<uint32_t> filtered_codes;
};
// Reader-Writer lock for the registry to allow concurrent reads (lookups)
mutable std::shared_mutex registry_mutex_;
std::map<wp<IBinder>, RegistrationEntry> registry_;
public:
BinderInterceptor() = default;
// Checks if a specific Binder instance is currently registered for interception
bool isBinderIntercepted(const wp<BBinder> &target) const {
bool shouldIntercept(const wp<BBinder> &target, uint32_t code) const {
std::shared_lock lock(registry_mutex_);
return registry_.find(target) != registry_.end();
auto it = registry_.find(target);
if (it == registry_.end()) return false;
const auto &codes = it->second.filtered_codes;
return codes.empty() || std::find(codes.begin(), codes.end(), code) != codes.end();
}
// Main entry point for processing the "Man-in-the-Middle" logic
@@ -348,15 +350,22 @@ static sp<BinderStub> g_stub_instance = nullptr;
namespace {
/**
* @brief Analyses a binder transaction. If the target is monitored,
* hijacks the transaction by rewriting its destination to our BinderStub.
* @param txn_data Pointer to the transaction data within the ioctl buffer.
*/
constexpr binder_size_t kMaxInterceptableDataSize = 256 * 1024;
void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
if (!txn_data || txn_data->target.ptr == 0)
return;
// Bypass interception for oversized payloads to prevent thread starvation from flood attacks
if (txn_data->data_size > kMaxInterceptableDataSize)
return;
// AIDL methods use codes in [FIRST_CALL_TRANSACTION, LAST_CALL_TRANSACTION] (1..0x00ffffff).
// System transactions (PING, INTERFACE, DUMP, SHELL_COMMAND) use codes above that range.
// Skip those — intercepting a ping adds measurable latency that timing detectors flag.
if (txn_data->code > 0x00ffffffu && txn_data->code != intercept::kBackdoorCode)
return;
bool hijack = false;
ThreadTransactionInfo info;
@@ -386,7 +395,7 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
// This is safe because we are holding a strong reference.
wp<BBinder> wp_target = target_binder_ptr;
if (g_interceptor_instance->isBinderIntercepted(wp_target)) {
if (g_interceptor_instance->shouldIntercept(wp_target, txn_data->code)) {
info.transaction_code = txn_data->code;
info.target_binder = wp_target; // Assign the valid weak pointer
hijack = true;
@@ -531,18 +540,29 @@ status_t BinderInterceptor::handleRegister(const Parcel &data) {
if (data.readStrongBinder(&callback) != OK || !callback)
return BAD_VALUE;
// We can only intercept local Binders (BBinder), not remote proxies (BpBinder)
if (target->localBinder() == nullptr) {
LOGE("Cannot intercept remote binder proxies.");
return BAD_TYPE;
}
std::vector<uint32_t> codes;
int32_t code_count = 0;
if (data.dataAvail() >= sizeof(int32_t) && data.readInt32(&code_count) == OK && code_count > 0) {
codes.reserve(code_count);
for (int32_t i = 0; i < code_count; i++) {
uint32_t c = 0;
if (data.readUint32(&c) == OK) codes.push_back(c);
}
LOGI("Interceptor registered for binder %p with %zu filtered codes", target.get(), codes.size());
} else {
LOGI("Interceptor registered for binder %p (all codes)", target.get());
}
wp<IBinder> weak_target = target;
std::unique_lock lock(registry_mutex_);
registry_[weak_target] = {weak_target, callback};
registry_[weak_target] = {weak_target, callback, std::move(codes)};
LOGI("Interceptor registered for binder %p", target.get());
return OK;
}
@@ -592,9 +612,16 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
Parcel pre_req, pre_resp;
writeTransactionData(pre_req, tx_id, target, code, flags, request);
if (callback->transact(intercept::kPreTransact, pre_req, &pre_resp) != OK) {
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed. Forwarding original call.", tx_id);
return false; // Callback failed, proceed as if not intercepted
status_t pre_status = callback->transact(intercept::kPreTransact, pre_req, &pre_resp);
if (pre_status != OK) {
// Block when interceptor is dead to prevent privacy leak to third-party apps
if (callback->pingBinder() != OK) {
LOGE("[TX_ID: %" PRIu64 "] Interceptor DEAD. Blocking to prevent attestation leak.", tx_id);
result = DEAD_OBJECT;
return true;
}
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed (not dead). Forwarding.", tx_id);
return false;
}
int32_t action = pre_resp.readInt32();
@@ -647,7 +674,8 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
VALIDATE_STATUS(tx_id, post_req.appendFrom(reply, 0, reply_size));
}
if (callback->transact(intercept::kPostTransact, post_req, &post_resp) == OK) {
status_t post_status = callback->transact(intercept::kPostTransact, post_req, &post_resp);
if (post_status == OK) {
int32_t post_action = post_resp.readInt32();
if (post_action == intercept::kActionOverrideReply && reply) {
result = post_resp.readInt32(); // Read new status
+76
View File
@@ -0,0 +1,76 @@
// Fork-based supervisor for instant daemon restart
#include <unistd.h>
#include <sys/wait.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <time.h>
static volatile sig_atomic_t should_exit = 0;
static void signal_handler(int sig) {
should_exit = 1;
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <daemon> [args...]\n", argv[0]);
return 1;
}
// Forward termination signals to exit cleanly
signal(SIGTERM, signal_handler);
signal(SIGINT, signal_handler);
const char *daemon_path = argv[1];
char **daemon_argv = &argv[1];
int backoff_ms = 500;
while (!should_exit) {
struct timespec child_start;
clock_gettime(CLOCK_MONOTONIC, &child_start);
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
usleep(100000); // 100ms backoff on fork failure
continue;
}
if (pid == 0) {
// Child: become the daemon
prctl(PR_SET_PDEATHSIG, SIGKILL); // Die if parent dies
setpriority(PRIO_PROCESS, 0, 10); // lower CPU priority than foreground
execv(daemon_path, daemon_argv);
perror("execv failed");
_exit(127);
}
// Parent: wait for child to exit
int status;
waitpid(pid, &status, 0);
if (should_exit) break;
// Exponential backoff on rapid crashes, reset if child was stable
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
long lived_ms = (now.tv_sec - child_start.tv_sec) * 1000 +
(now.tv_nsec - child_start.tv_nsec) / 1000000;
if (lived_ms > 30000) {
backoff_ms = 500;
} else {
usleep(backoff_ms * 1000);
if (backoff_ms < 30000) backoff_ms *= 2;
}
}
return 0;
}
@@ -13,6 +13,7 @@ import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor
import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor
import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.NativeCertGen
import org.matrix.TEESimulator.util.AndroidDeviceUtils
/**
@@ -22,8 +23,6 @@ import org.matrix.TEESimulator.util.AndroidDeviceUtils
object App {
// The delay in milliseconds before retrying to initialize the interceptor.
private const val RETRY_DELAY_MS = 1000L
// The sleep duration in milliseconds for the main service loop to keep the process alive.
private const val SERVICE_SLEEP_MS = 1000000L
/**
* The main entry point of the TEESimulator application.
@@ -51,6 +50,8 @@ object App {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
NativeCertGen.initialize("/data/adb/modules/tricky_store/libcertgen.so")
// This starts the message queue processing. It blocks here indefinitely
// processing messages until Looper.myLooper().quit() is called.
Looper.loop()
@@ -2,8 +2,11 @@ package org.matrix.TEESimulator.attestation
import android.content.pm.PackageManager
import android.os.Build
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import org.bouncycastle.asn1.ASN1Boolean
import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.ASN1Enumerated
@@ -112,6 +115,7 @@ object AttestationBuilder {
}
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
SystemLogger.info("Attestation patch levels for uid=$uid: os=$osPatch, vendor=$vendorPatch, boot=$bootPatch")
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
if (bootPatch != DO_NOT_REPORT) {
DERTaggedObject(
@@ -126,33 +130,59 @@ object AttestationBuilder {
return properties
}
/** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */
private fun buildKeyDescription(
params: KeyMintAttestation,
uid: Int,
securityLevel: Int,
): ASN1Sequence {
val creationTime = System.currentTimeMillis()
val teeEnforced = buildTeeEnforcedList(params, uid, securityLevel)
val softwareEnforced = buildSoftwareEnforcedList(uid, securityLevel)
val softwareEnforced = buildSoftwareEnforcedList(params, uid, securityLevel, creationTime)
val uniqueId =
if (params.includeUniqueId == true && params.attestationChallenge != null) {
computeUniqueId(creationTime, createApplicationId(uid).octets)
} else {
ByteArray(0)
}
val fields =
arrayOf(
ASN1Integer(
AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()
), // attestationVersion
ASN1Enumerated(securityLevel), // attestationSecurityLevel
ASN1Integer(
AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()
), // keymasterVersion
ASN1Enumerated(securityLevel), // keymasterSecurityLevel
DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge
DEROctetString(ByteArray(0)), // uniqueId
ASN1Integer(AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()),
ASN1Enumerated(securityLevel),
ASN1Integer(AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()),
ASN1Enumerated(securityLevel),
DEROctetString(params.attestationChallenge ?: ByteArray(0)),
DEROctetString(uniqueId),
softwareEnforced,
teeEnforced,
)
return DERSequence(fields)
}
private fun computeUniqueId(creationTimeMs: Long, aaidDer: ByteArray): ByteArray {
val temporalCounter = creationTimeMs / 2592000000L
val message =
ByteBuffer.allocate(8 + aaidDer.size + 1)
.putLong(temporalCounter)
.put(aaidDer)
.put(0x00)
.array()
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(hbk, "HmacSHA256"))
return mac.doFinal(message).copyOf(16)
}
private val hbk: ByteArray by lazy {
val file = java.io.File(ConfigurationManager.CONFIG_PATH, "hbk")
if (file.exists() && file.length() == 32L) {
file.readBytes()
} else {
SystemLogger.warning("hbk not found, generating ephemeral HBK.")
ByteArray(32).also { java.security.SecureRandom().nextBytes(it) }
}
}
/** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */
private fun buildTeeEnforcedList(
params: KeyMintAttestation,
@@ -181,23 +211,110 @@ object AttestationBuilder {
AttestationConstants.TAG_DIGEST,
DERSet(params.digest.map { ASN1Integer(it.toLong()) }.toTypedArray()),
),
)
if (params.ecCurve != null) {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_EC_CURVE,
ASN1Integer(params.ecCurve.toLong()),
),
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE),
)
)
}
if (params.blockMode.isNotEmpty()) {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_BLOCK_MODE,
DERSet(params.blockMode.map { ASN1Integer(it.toLong()) }.toTypedArray()),
)
)
}
if (params.padding.isNotEmpty()) {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_PADDING,
DERSet(params.padding.map { ASN1Integer(it.toLong()) }.toTypedArray()),
)
)
}
if (params.rsaPublicExponent != null) {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_RSA_PUBLIC_EXPONENT,
ASN1Integer(params.rsaPublicExponent.toLong()),
)
)
}
val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel)
if (params.rsaOaepMgfDigest.isNotEmpty() && attestVersion >= 100) {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_RSA_OAEP_MGF_DIGEST,
DERSet(params.rsaOaepMgfDigest.map { ASN1Integer(it.toLong()) }.toTypedArray()),
)
)
}
if (params.rollbackResistance == true && attestVersion >= 3) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_ROLLBACK_RESISTANCE, DERNull.INSTANCE)
)
}
if (params.earlyBootOnly == true && attestVersion >= 4) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_EARLY_BOOT_ONLY, DERNull.INSTANCE)
)
}
if (params.noAuthRequired == true) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE)
)
}
if (params.allowWhileOnBody == true) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_ALLOW_WHILE_ON_BODY, DERNull.INSTANCE)
)
}
if (params.trustedUserPresenceRequired == true && attestVersion >= 3) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_TRUSTED_USER_PRESENCE_REQUIRED, DERNull.INSTANCE)
)
}
if (params.trustedConfirmationRequired == true && attestVersion >= 3) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_TRUSTED_CONFIRMATION_REQUIRED, DERNull.INSTANCE)
)
}
list.addAll(
listOf(
DERTaggedObject(
true,
AttestationConstants.TAG_ORIGIN,
ASN1Integer(0L),
), // KeyOrigin.GENERATED
ASN1Integer((params.origin ?: 0).toLong()),
),
DERTaggedObject(
true,
AttestationConstants.TAG_ROOT_OF_TRUST,
buildRootOfTrust(null),
),
)
)
// Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels.
val simulatedProperties = getSimulatedHardwareProperties(uid)
@@ -294,20 +411,32 @@ object AttestationBuilder {
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
* Keystore.
*/
private fun buildSoftwareEnforcedList(uid: Int, securityLevel: Int): DERSequence {
val list =
mutableListOf<ASN1Encodable>(
private fun buildSoftwareEnforcedList(
params: KeyMintAttestation,
uid: Int,
securityLevel: Int,
creationTimeMs: Long = System.currentTimeMillis(),
): DERSequence {
val list = mutableListOf<ASN1Encodable>()
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_CREATION_DATETIME,
ASN1Integer(System.currentTimeMillis()),
),
ASN1Integer(creationTimeMs),
)
)
if (params.attestationChallenge != null) {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_APPLICATION_ID,
createApplicationId(uid),
),
)
)
}
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) {
list.add(
DERTaggedObject(
@@ -317,7 +446,34 @@ object AttestationBuilder {
)
)
}
return DERSequence(list.toTypedArray())
params.activeDateTime?.let {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time))
)
}
params.originationExpireDateTime?.let {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_ORIGINATION_EXPIRE_DATETIME, ASN1Integer(it.time))
)
}
params.usageExpireDateTime?.let {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_USAGE_EXPIRE_DATETIME, ASN1Integer(it.time))
)
}
params.usageCountLimit?.let {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_USAGE_COUNT_LIMIT, ASN1Integer(it.toLong()))
)
}
if (params.unlockedDeviceRequired == true) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_UNLOCKED_DEVICE_REQUIRED, DERNull.INSTANCE)
)
}
return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray())
}
/**
@@ -344,7 +500,12 @@ object AttestationBuilder {
* retrieved.
*/
@Throws(Throwable::class)
private fun createApplicationId(uid: Int): DEROctetString {
internal fun createApplicationId(uid: Int): DEROctetString {
val appUid = uid % 100000
if (appUid == 0 || appUid == 1000) {
return buildApplicationIdDer(listOf("AndroidSystem" to 1L), emptySet())
}
val pm =
ConfigurationManager.getPackageManager()
?: throw IllegalStateException("PackageManager not found!")
@@ -352,12 +513,11 @@ object AttestationBuilder {
pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid")
val sha256 = MessageDigest.getInstance("SHA-256")
val packageInfoList = mutableListOf<DERSequence>()
val packageInfoList = mutableListOf<Pair<String, Long>>()
val signatureDigests = mutableSetOf<Digest>()
// Process all packages associated with the UID in a single loop.
packages.forEach { packageName ->
val userId = uid / 100000
packages.forEach { packageName ->
val packageInfo =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.getPackageInfo(
@@ -370,34 +530,36 @@ object AttestationBuilder {
pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId)
}
// Add package information (name and version code) to our list.
packageInfoList.add(
packageInfoList.add(packageInfo.packageName to packageInfo.longVersionCode)
packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature ->
signatureDigests.add(Digest(sha256.digest(signature.toByteArray())))
}
}
return buildApplicationIdDer(packageInfoList, signatureDigests)
}
private fun buildApplicationIdDer(
packages: List<Pair<String, Long>>,
digests: Set<Digest>,
): DEROctetString {
val packageInfoList =
packages.map { (name, version) ->
DERSequence(
arrayOf(
DEROctetString(packageInfo.packageName.toByteArray(StandardCharsets.UTF_8)),
ASN1Integer(packageInfo.longVersionCode),
DEROctetString(name.toByteArray(StandardCharsets.UTF_8)),
ASN1Integer(version),
)
)
)
// Collect unique signature digests from the signing history.
packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature ->
val digest = sha256.digest(signature.toByteArray())
signatureDigests.add(Digest(digest))
}
}
// The application ID is a sequence of two sets:
// 1. A set of package information (name and version).
// 2. A set of SHA-256 digests of the signing certificates.
val applicationIdSequence =
DERSequence(
arrayOf(
DERSet(packageInfoList.toTypedArray()),
DERSet(signatureDigests.map { DEROctetString(it.digest) }.toTypedArray()),
DERSet(digests.map { DEROctetString(it.digest) }.toTypedArray()),
)
)
return DEROctetString(applicationIdSequence.encoded)
}
}
@@ -44,9 +44,11 @@ object AttestationConstants {
// --- Key Lifetime and Usage Control ---
const val TAG_ROLLBACK_RESISTANCE = 303
const val TAG_EARLY_BOOT_ONLY = 305
const val TAG_ACTIVE_DATETIME = 400
const val TAG_ORIGINATION_EXPIRE_DATETIME = 401
const val TAG_USAGE_EXPIRE_DATETIME = 402
const val TAG_MAX_BOOT_LEVEL = 403
const val TAG_MAX_USES_PER_BOOT = 404
const val TAG_USAGE_COUNT_LIMIT = 405
@@ -56,6 +58,10 @@ object AttestationConstants {
const val TAG_NO_AUTH_REQUIRED = 503
const val TAG_USER_AUTH_TYPE = 504
const val TAG_AUTH_TIMEOUT = 505
const val TAG_ALLOW_WHILE_ON_BODY = 506
const val TAG_TRUSTED_USER_PRESENCE_REQUIRED = 507
const val TAG_TRUSTED_CONFIRMATION_REQUIRED = 508
const val TAG_UNLOCKED_DEVICE_REQUIRED = 509
// --- Attestation and Application Info ---
const val TAG_APPLICATION_ID = 601
@@ -89,5 +95,5 @@ object AttestationConstants {
// --- Other Constants ---
// https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp
const val CHALLENGE_LENGTH_LIMIT = 128 // kMaximumAttestationChallengeLength
const val CHALLENGE_LENGTH_LIMIT = 128
}
@@ -249,6 +249,10 @@ object DeviceAttestationService {
verifiedBootKey = null
}
if (verifiedBootHash?.all { it == 0.toByte() } == true) {
verifiedBootHash = null
}
SystemLogger.info(
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, osPatch=$osPatchLevel, vendorPatch=$vendorPatchLevel, bootPatch=$bootPatchLevel, moduleHash=${moduleHash?.toHex()}, bootKey=${verifiedBootKey?.toHex()}, bootHash=${verifiedBootHash?.toHex()}"
)
@@ -1,6 +1,7 @@
package org.matrix.TEESimulator.attestation
import android.hardware.security.keymint.*
import android.hardware.security.keymint.KeyOrigin
import java.math.BigInteger
import java.util.Date
import javax.security.auth.x500.X500Principal
@@ -18,8 +19,9 @@ import org.matrix.TEESimulator.logging.KeyMintParameterLogger
data class KeyMintAttestation(
val keySize: Int,
val algorithm: Int,
val ecCurve: Int,
val ecCurve: Int?,
val ecCurveName: String,
val origin: Int?,
val blockMode: List<Int>,
val padding: List<Int>,
val purpose: List<Int>,
@@ -39,21 +41,40 @@ data class KeyMintAttestation(
val manufacturer: ByteArray?,
val model: ByteArray?,
val secondImei: ByteArray?,
val activeDateTime: Date?,
val originationExpireDateTime: Date?,
val usageExpireDateTime: Date?,
val usageCountLimit: Int?,
val callerNonce: Boolean?,
val unlockedDeviceRequired: Boolean?,
val includeUniqueId: Boolean?,
val rollbackResistance: Boolean?,
val earlyBootOnly: Boolean?,
val allowWhileOnBody: Boolean?,
val trustedUserPresenceRequired: Boolean?,
val trustedConfirmationRequired: Boolean?,
val noAuthRequired: Boolean?,
val maxUsesPerBoot: Int?,
val maxBootLevel: Int?,
val minMacLength: Int?,
val rsaOaepMgfDigest: List<Int>,
) {
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */
constructor(
params: Array<KeyParameter>
) : this(
// AOSP: [key_param(tag = KEY_SIZE, field = Integer)]
keySize = params.findInteger(Tag.KEY_SIZE) ?: 0,
keySize = params.findInteger(Tag.KEY_SIZE) ?: params.deriveKeySizeFromCurve(),
// AOSP: [key_param(tag = ALGORITHM, field = Algorithm)]
algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0,
// AOSP: [key_param(tag = EC_CURVE, field = EcCurve)]
ecCurve = params.findEcCurve(Tag.EC_CURVE) ?: 0,
ecCurve = params.findEcCurve(Tag.EC_CURVE),
ecCurveName = params.deriveEcCurveName(),
// AOSP: [key_param(tag = ORIGIN, field = Origin)]
origin = params.findOrigin(Tag.ORIGIN),
// AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)]
blockMode = params.findAllBlockMode(Tag.BLOCK_MODE),
@@ -95,10 +116,31 @@ data class KeyMintAttestation(
manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER),
model = params.findBlob(Tag.ATTESTATION_ID_MODEL),
secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI),
activeDateTime = params.findDate(Tag.ACTIVE_DATETIME),
originationExpireDateTime = params.findDate(Tag.ORIGINATION_EXPIRE_DATETIME),
usageExpireDateTime = params.findDate(Tag.USAGE_EXPIRE_DATETIME),
usageCountLimit = params.findInteger(Tag.USAGE_COUNT_LIMIT),
callerNonce = params.findBoolean(Tag.CALLER_NONCE),
unlockedDeviceRequired = params.findBoolean(Tag.UNLOCKED_DEVICE_REQUIRED),
includeUniqueId = params.findBoolean(Tag.INCLUDE_UNIQUE_ID),
rollbackResistance = params.findBoolean(Tag.ROLLBACK_RESISTANCE),
earlyBootOnly = params.findBoolean(Tag.EARLY_BOOT_ONLY),
allowWhileOnBody = params.findBoolean(Tag.ALLOW_WHILE_ON_BODY),
trustedUserPresenceRequired = params.findBoolean(Tag.TRUSTED_USER_PRESENCE_REQUIRED),
trustedConfirmationRequired = params.findBoolean(Tag.TRUSTED_CONFIRMATION_REQUIRED),
noAuthRequired = params.findBoolean(Tag.NO_AUTH_REQUIRED),
maxUsesPerBoot = params.findInteger(Tag.MAX_USES_PER_BOOT),
maxBootLevel = params.findInteger(Tag.MAX_BOOT_LEVEL),
minMacLength = params.findInteger(Tag.MIN_MAC_LENGTH),
rsaOaepMgfDigest = params.findAllDigests(Tag.RSA_OAEP_MGF_DIGEST),
) {
// Log all parsed parameters for debugging purposes.
params.forEach { KeyMintParameterLogger.logParameter(it) }
}
fun isAttestKey(): Boolean = purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY)
fun isImportKey(): Boolean = origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED
}
// --- Private helper extension functions for parsing KeyParameter arrays ---
@@ -115,6 +157,10 @@ private fun Array<KeyParameter>.findAlgorithm(tag: Int): Int? =
private fun Array<KeyParameter>.findEcCurve(tag: Int): Int? =
this.find { it.tag == tag }?.value?.ecCurve
/** Maps to AOSP field = Origin */
private fun Array<KeyParameter>.findOrigin(tag: Int): Int? =
this.find { it.tag == tag }?.value?.origin
/** Maps to AOSP field = LongInteger */
private fun Array<KeyParameter>.findLongInteger(tag: Int): BigInteger? =
this.find { it.tag == tag }?.value?.longInteger?.toBigInteger()
@@ -143,6 +189,21 @@ private fun Array<KeyParameter>.findAllKeyPurpose(tag: Int): List<Int> =
private fun Array<KeyParameter>.findAllDigests(tag: Int): List<Int> =
this.filter { it.tag == tag }.map { it.value.digest }
private fun Array<KeyParameter>.findBoolean(tag: Int): Boolean? =
if (this.any { it.tag == tag }) true else null
private fun Array<KeyParameter>.deriveKeySizeFromCurve(): Int {
val curveId = this.find { it.tag == Tag.EC_CURVE }?.value?.ecCurve ?: return 0
return when (curveId) {
EcCurve.P_224 -> 224
EcCurve.P_256 -> 256
EcCurve.P_384 -> 384
EcCurve.P_521 -> 521
EcCurve.CURVE_25519 -> 256
else -> 0
}
}
/**
* Derives the EC Curve name. Logic: Checks specific EC_CURVE tag first (field=EcCurve), falls back
* to KEY_SIZE (field=Integer).
@@ -253,7 +253,14 @@ object ConfigurationManager {
}
// Parse global and per-package configurations.
val newGlobalLevel = parseLines(contextLines[""])
var newGlobalLevel = parseLines(contextLines[""])
// TrickyAddon writes Pixel bulletin dates for boot/vendor but system=prop
// resolves to the real device prop — force boot/vendor through the same path
// to prevent cross-component date mismatches on non-Pixel devices.
if (newGlobalLevel?.system.equals("prop", ignoreCase = true)) {
SystemLogger.info("system=prop: forcing boot/vendor to derive from device props (were: boot=${newGlobalLevel?.boot}, vendor=${newGlobalLevel?.vendor})")
newGlobalLevel = newGlobalLevel?.copy(boot = "prop", vendor = "prop")
}
contextLines.remove("") // Remove global context to iterate over packages next
for ((pkg, lines) in contextLines) {
@@ -307,8 +314,10 @@ object ConfigurationManager {
val file = if (event != DELETE) File(configRoot, path) else null
when (path) {
TARGET_PACKAGES_FILE -> loadTargetPackages(file!!)
PATCH_LEVEL_FILE -> loadPatchLevelConfig(file!!)
TARGET_PACKAGES_FILE -> file?.let { loadTargetPackages(it) }
?: SystemLogger.warning("$TARGET_PACKAGES_FILE was deleted.")
PATCH_LEVEL_FILE -> file?.let { loadPatchLevelConfig(it) }
?: SystemLogger.warning("$PATCH_LEVEL_FILE was deleted.")
// Any change to an XML file is assumed to be a keybox.
// The cache in KeyBoxManager will handle reloading it on its next use.
else ->
@@ -351,7 +360,29 @@ object ConfigurationManager {
return iPackageManager
}
/** Retrieves the package names associated with a UID. */
fun checkSELinuxPermission(callingPid: Int, tclass: String, perm: String): Boolean {
return try {
val callerCtx =
java.io.File("/proc/$callingPid/attr/current").readText().trim('\u0000', ' ', '\n')
val selfCtx =
java.io.File("/proc/self/attr/current").readText().trim('\u0000', ' ', '\n')
android.os.SELinux.checkSELinuxAccess(callerCtx, selfCtx, tclass, perm)
} catch (_: Exception) {
false
}
}
fun hasPermissionForUid(uid: Int, permission: String): Boolean {
val userId = uid / 100000
return getPackagesForUid(uid).any { pkg ->
try {
getPackageManager()?.checkPermission(permission, pkg, userId) == 0
} catch (_: Exception) {
false
}
}
}
fun getPackagesForUid(uid: Int): Array<String> {
return uidToPackagesCache.getOrPut(uid) {
try {
@@ -293,15 +293,21 @@ abstract class BinderInterceptor : Binder() {
}
}
/** Uses the backdoor binder to register an interceptor for a specific target service. */
fun register(backdoor: IBinder, target: IBinder, interceptor: BinderInterceptor) {
fun register(
backdoor: IBinder,
target: IBinder,
interceptor: BinderInterceptor,
filteredCodes: IntArray = intArrayOf(),
) {
val data = Parcel.obtain()
val reply = Parcel.obtain()
try {
data.writeStrongBinder(target)
data.writeStrongBinder(interceptor)
data.writeInt(filteredCodes.size)
for (code in filteredCodes) data.writeInt(code)
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
SystemLogger.info("Registered interceptor for target: $target")
SystemLogger.info("Registered interceptor for target: $target (${filteredCodes.size} filtered codes)")
} catch (e: Exception) {
SystemLogger.error("Failed to register binder interceptor.", e)
} finally {
@@ -68,11 +68,12 @@ abstract class AbstractKeystoreInterceptor : BinderInterceptor() {
}
}
/** Registers this interceptor with the native hook layer and sets up a death recipient. */
protected open val interceptedCodes: IntArray = intArrayOf()
private fun setupInterceptor(service: IBinder, backdoor: IBinder) {
keystoreService = service
SystemLogger.info("Registering interceptor for service: $serviceName")
register(backdoor, service, this)
register(backdoor, service, this, interceptedCodes)
service.linkToDeath(createDeathRecipient(), 0)
onInterceptorReady(service, backdoor)
}
@@ -1,17 +1,34 @@
package org.matrix.TEESimulator.interception.keystore
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyParameterValue
import android.hardware.security.keymint.Tag
import android.os.Parcel
import android.os.Parcelable
import android.security.KeyStore
import android.security.keystore.KeystoreResponse
import android.system.keystore2.Authorization
import org.matrix.TEESimulator.interception.core.BinderInterceptor
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.util.AndroidDeviceUtils
data class KeyIdentifier(val uid: Int, val alias: String)
/** A collection of utility functions to support binder interception. */
object InterceptorUtils {
private const val EX_SERVICE_SPECIFIC = -8
fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply {
val parcel = Parcel.obtain().apply {
writeInt(EX_SERVICE_SPECIFIC)
writeString(null)
writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196)
writeInt(errorCode)
}
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
/**
* Uses reflection to get the integer transaction code for a given method name from a Stub
* class. This is necessary for older Android versions where codes are not public constants.
@@ -108,6 +125,57 @@ object InterceptorUtils {
/** Checks if a reply parcel contains an exception without consuming it. */
fun hasException(reply: Parcel): Boolean {
return runCatching { reply.readException() }.exceptionOrNull() != null
val exception = runCatching { reply.readException() }.exceptionOrNull()
if (exception != null) reply.setDataPosition(0)
return exception != null
}
fun createServiceSpecificErrorReply(
errorCode: Int
): BinderInterceptor.TransactionResult.OverrideReply {
val parcel =
Parcel.obtain().apply {
writeException(android.os.ServiceSpecificException(errorCode))
}
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
fun patchAuthorizations(
authorizations: Array<Authorization>?,
callingUid: Int,
): Array<Authorization>? {
if (authorizations == null) return null
val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid)
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
return authorizations
.map { auth ->
val replacement =
when (auth.keyParameter.tag) {
Tag.OS_PATCHLEVEL ->
if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) osPatch else null
Tag.VENDOR_PATCHLEVEL ->
if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) vendorPatch
else null
Tag.BOOT_PATCHLEVEL ->
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) bootPatch else null
else -> null
}
if (replacement != null) {
Authorization().apply {
keyParameter =
KeyParameter().apply {
tag = auth.keyParameter.tag
value = KeyParameterValue.integer(replacement)
}
securityLevel = auth.securityLevel
}
} else {
auth
}
}
.toTypedArray()
}
}
@@ -1,21 +1,25 @@
package org.matrix.TEESimulator.interception.keystore
import android.annotation.SuppressLint
import android.hardware.security.keymint.KeyOrigin
import android.hardware.security.keymint.SecurityLevel
import android.hardware.security.keymint.Tag
import android.os.Build
import android.os.IBinder
import android.os.Parcel
import android.system.keystore2.Domain
import android.system.keystore2.IKeystoreService
import android.system.keystore2.KeyDescriptor
import android.system.keystore2.KeyEntryResponse
import java.security.SecureRandom
import java.security.cert.Certificate
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.keystore.shim.GeneratedKeyPersistence
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertificateGenerator
import org.matrix.TEESimulator.pki.CertificateHelper
/**
@@ -42,6 +46,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
if (Build.VERSION.SDK_INT >= 34)
InterceptorUtils.getTransactCode(stubBinderClass, "listEntriesBatched")
else null
private val GET_NUMBER_OF_ENTRIES_TRANSACTION =
InterceptorUtils.getTransactCode(stubBinderClass, "getNumberOfEntries")
private val transactionNames: Map<Int, String> by lazy {
stubBinderClass.declaredFields
@@ -52,10 +58,26 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
}
private const val RESPONSE_KEY_NOT_FOUND = 7
private val deletedSoftwareKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
override val serviceName = "android.system.keystore2.IKeystoreService/default"
override val processName = "keystore2"
override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry"
override val interceptedCodes: IntArray by lazy {
listOfNotNull(
GET_KEY_ENTRY_TRANSACTION,
DELETE_KEY_TRANSACTION,
UPDATE_SUBCOMPONENT_TRANSACTION,
LIST_ENTRIES_TRANSACTION,
LIST_ENTRIES_BATCHED_TRANSACTION,
GET_NUMBER_OF_ENTRIES_TRANSACTION,
)
.toIntArray()
}
/**
* This method is called once the main service is hooked. It proceeds to find and hook the
* security level sub-services (e.g., TEE, StrongBox).
@@ -72,7 +94,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...")
val interceptor =
KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
register(backdoor, tee.asBinder(), interceptor)
register(
backdoor,
tee.asBinder(),
interceptor,
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
)
interceptor.loadPersistedKeys()
}
}
.onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) }
@@ -83,7 +111,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
val interceptor =
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
register(backdoor, strongbox.asBinder(), interceptor)
register(
backdoor,
strongbox.asBinder(),
interceptor,
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
)
interceptor.loadPersistedKeys()
}
}
.onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) }
@@ -98,11 +132,20 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
callingPid: Int,
data: Parcel,
): TransactionResult {
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
return if (ConfigurationManager.shouldSkipUid(callingUid))
TransactionResult.ContinueAndSkipPost
else TransactionResult.Continue
} else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
if (ConfigurationManager.shouldSkipUid(callingUid))
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
val isGMS = packages.contains("com.google.android.gms")
if (isGMS || ConfigurationManager.shouldSkipUid(callingUid)) {
return TransactionResult.ContinueAndSkipPost
}
return runCatching {
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
@@ -137,23 +180,48 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.ContinueAndSkipPost
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
val keyId = KeyIdentifier(callingUid, descriptor.alias)
if (code == DELETE_KEY_TRANSACTION) {
if (KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null) {
val keyId =
if (descriptor.alias != null) {
KeyIdentifier(callingUid, descriptor.alias)
} else if (descriptor.domain == Domain.KEY_ID) {
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
callingUid, descriptor.nspace
)?.let { info ->
KeyMintSecurityLevelInterceptor.generatedKeys.entries
.find { it.value.nspace == info.nspace && it.key.uid == callingUid }
?.key
}
} else null
if (keyId != null) {
val isSoftwareKey =
KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(keyId)
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
if (isSoftwareKey) {
deletedSoftwareKeys.add(keyId)
SystemLogger.info(
"[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response."
"[TX_ID: $txId] Deleted cached keypair ${keyId.alias}, replying with empty response."
)
return InterceptorUtils.createSuccessReply(writeResultCode = false)
}
}
return TransactionResult.ContinueAndSkipPost
}
val response =
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
?: return TransactionResult.Continue
if (descriptor.alias == null) {
return TransactionResult.ContinueAndSkipPost
}
val keyId = KeyIdentifier(callingUid, descriptor.alias)
val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
if (response == null) {
if (deletedSoftwareKeys.remove(keyId)) {
SystemLogger.info("[TX_ID: $txId] Returning KEY_NOT_FOUND for deleted key ${descriptor.alias}")
return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
}
return TransactionResult.Continue
}
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
SystemLogger.info("${descriptor.alias} was an attestation key")
@@ -191,7 +259,26 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
return TransactionResult.SkipTransaction
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
return runCatching {
val hardwareCount = reply.readInt()
val softwareCount =
KeyMintSecurityLevelInterceptor.generatedKeys.keys.count {
it.uid == callingUid
}
val totalCount = hardwareCount + softwareCount
val parcel = Parcel.obtain().apply {
writeNoException()
writeInt(totalCount)
}
TransactionResult.OverrideReply(parcel)
}
.getOrElse {
SystemLogger.error("[TX_ID: $txId] Failed to modify getNumberOfEntries.", it)
TransactionResult.SkipTransaction
}
} else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
return runCatching {
@@ -207,35 +294,101 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
TransactionResult.SkipTransaction
}
} else if (code == GET_KEY_ENTRY_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val keyDescriptor =
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction
logTransaction(
txId,
"post-${transactionNames[code]!!} ${keyDescriptor.alias}",
callingUid,
callingPid,
)
if (!ConfigurationManager.shouldPatch(callingUid))
return TransactionResult.SkipTransaction
SystemLogger.info("Handling post-${transactionNames[code]!!} ${keyDescriptor.alias}")
return try {
val response =
reply.readTypedObject(KeyEntryResponse.CREATOR)
?: return TransactionResult.SkipTransaction
reply.setDataPosition(0) // Reset for potential reuse.
runCatching {
val response = reply.readTypedObject(KeyEntryResponse.CREATOR)!!
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
val originalChain = CertificateHelper.getCertificateChain(response)
val authorizations = response.metadata?.authorizations
val origin =
authorizations
?.find { it.keyParameter.tag == Tag.ORIGIN }
?.let { it.keyParameter.value.origin }
if (origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED) {
SystemLogger.info("[TX_ID: $txId] Skip patching for imported keys.")
if (userUpdatedKeys.remove(keyId)) {
SystemLogger.debug("[TX_ID: $txId] Skipping cert patch for user-updated key $keyId.")
return TransactionResult.SkipTransaction
}
val authorizations = response.metadata.authorizations
val parsedParameters =
KeyMintAttestation(
authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
)
if (parsedParameters.isImportKey()) {
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
if (retainedChain == null) {
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
return TransactionResult.SkipTransaction
}
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
return InterceptorUtils.createTypedObjectReply(response)
}
if (KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)) {
SystemLogger.debug("[TX_ID: $txId] Skipping attest-key override for imported key $keyId")
return TransactionResult.SkipTransaction
}
if (parsedParameters.isAttestKey()) {
SystemLogger.warning(
"[TX_ID: $txId] Found hardware attest key ${keyId.alias} in the reply."
)
val keyData =
CertificateGenerator.generateAttestedKeyPair(
callingUid,
keyId.alias,
null,
parsedParameters,
response.metadata.keySecurityLevel,
) ?: throw Exception("Failed to create overriding attest key pair.")
CertificateHelper.updateCertificateChain(
response.metadata,
keyData.second.toTypedArray(),
)
.getOrThrow()
keyDescriptor.nspace = SecureRandom().nextLong()
response.metadata.key.nspace = keyDescriptor.nspace
KeyMintSecurityLevelInterceptor.generatedKeys[keyId] =
KeyMintSecurityLevelInterceptor.GeneratedKeyInfo(
keyData.first,
keyDescriptor.nspace,
response,
parsedParameters,
)
KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId)
GeneratedKeyPersistence.save(
keyId = keyId,
keyPair = keyData.first,
nspace = keyDescriptor.nspace,
securityLevel = response.metadata.keySecurityLevel,
certChain = keyData.second,
algorithm = parsedParameters.algorithm,
keySize = parsedParameters.keySize,
ecCurve = parsedParameters.ecCurve ?: 0,
purposes = parsedParameters.purpose,
digests = parsedParameters.digest,
isAttestationKey = true,
)
return InterceptorUtils.createTypedObjectReply(response)
}
val originalChain = CertificateHelper.getCertificateChain(response)
if (originalChain == null || originalChain.size < 2) {
SystemLogger.info(
"[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}."
@@ -243,11 +396,6 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
return TransactionResult.SkipTransaction
}
// Perform the attestation patch.
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
// First, try to retrieve the already-patched chain from our cache to ensure
// consistency.
val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
val finalChain: Array<Certificate>
@@ -257,20 +405,30 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
)
finalChain = cachedChain
} else {
// If no chain is cached (e.g., key existed before simulator started),
// perform a live patch as a fallback. This may still be detectable.
SystemLogger.info(
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
)
finalChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
finalChain =
AttestationPatcher.patchCertificateChain(originalChain, callingUid)
KeyMintSecurityLevelInterceptor.patchedChains[keyId] = finalChain
}
CertificateHelper.updateCertificateChain(response.metadata, finalChain).getOrThrow()
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
.getOrThrow()
response.metadata.authorizations =
InterceptorUtils.patchAuthorizations(
response.metadata.authorizations,
callingUid,
)
InterceptorUtils.createTypedObjectReply(response)
} catch (e: Exception) {
SystemLogger.error("[TX_ID: $txId] Failed to patch certificate chain.", e)
TransactionResult.SkipTransaction
return InterceptorUtils.createTypedObjectReply(response)
}
.onFailure {
SystemLogger.error(
"[TX_ID: $txId] Failed to modify hardware KeyEntryResponse.",
it,
)
return TransactionResult.SkipTransaction
}
}
return TransactionResult.SkipTransaction
@@ -279,10 +437,26 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult {
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR)
val generatedKeyInfo =
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(callingUid, descriptor?.nspace)
?: return TransactionResult.ContinueAndSkipPost
val generatedKeyInfo =
when (descriptor.domain) {
Domain.KEY_ID ->
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
callingUid, descriptor.nspace
)
Domain.APP ->
descriptor.alias?.let {
KeyMintSecurityLevelInterceptor.generatedKeys[KeyIdentifier(callingUid, it)]
}
else -> null
}
if (generatedKeyInfo == null) {
descriptor.alias?.let { userUpdatedKeys.add(KeyIdentifier(callingUid, it)) }
return TransactionResult.ContinueAndSkipPost
}
SystemLogger.info("Updating sub-component with key[${generatedKeyInfo.nspace}]")
val metadata = generatedKeyInfo.response.metadata
val publicCert = data.createByteArray()
@@ -290,6 +464,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
metadata.certificate = publicCert
metadata.certificateChain = certificateChain
GeneratedKeyPersistence.rePersistIfNeeded(callingUid, generatedKeyInfo)
SystemLogger.verbose(
"Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]"
)
@@ -407,8 +407,9 @@ private data class LegacyKeygenParameters(
return KeyMintAttestation(
keySize = this.keySize,
algorithm = this.algorithm,
ecCurve = 0, // Not explicitly available in legacy args, but not critical
ecCurve = 0,
ecCurveName = this.ecCurveName ?: "",
origin = null,
blockMode = listOf<Int>(),
padding = listOf<Int>(),
purpose = this.purpose,
@@ -430,6 +431,23 @@ private data class LegacyKeygenParameters(
manufacturer = null,
model = null,
secondImei = null,
activeDateTime = null,
originationExpireDateTime = null,
usageExpireDateTime = null,
usageCountLimit = null,
callerNonce = null,
unlockedDeviceRequired = null,
includeUniqueId = null,
rollbackResistance = null,
earlyBootOnly = null,
allowWhileOnBody = null,
trustedUserPresenceRequired = null,
trustedConfirmationRequired = null,
noAuthRequired = null,
maxUsesPerBoot = null,
maxBootLevel = null,
minMacLength = null,
rsaOaepMgfDigest = emptyList(),
)
}
@@ -129,7 +129,7 @@ object ListEntriesHandler {
startPastAlias: String?,
): List<KeyDescriptor> {
return KeyMintSecurityLevelInterceptor.generatedKeys.keys
.filter { it.uid == uid && (startPastAlias == null || it.alias < startPastAlias) }
.filter { it.uid == uid && (startPastAlias == null || it.alias > startPastAlias) }
.map { keyId ->
KeyDescriptor().apply {
this.domain = Domain.APP
@@ -0,0 +1,77 @@
package org.matrix.TEESimulator.interception.keystore.shim
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.Tag
import org.matrix.TEESimulator.attestation.KeyMintAttestation
object AuthorizeCreate {
fun check(
keyParams: KeyMintAttestation?,
opParams: KeyMintAttestation,
rawOpParams: Array<KeyParameter>? = null,
): Int? {
if (keyParams == null) return null
return checkPurpose(keyParams, opParams)
?: checkAlgorithmPurpose(keyParams, opParams)
?: checkTemporalValidity(keyParams, opParams)
?: checkCallerNonce(keyParams, rawOpParams)
}
private fun checkPurpose(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? {
val requestedPurpose = opParams.purpose.firstOrNull() ?: return null
if (requestedPurpose == KeyPurpose.WRAP_KEY)
return KeystoreErrorCodes.incompatiblePurpose
if (requestedPurpose !in keyParams.purpose)
return KeystoreErrorCodes.incompatiblePurpose
return null
}
private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? {
val purpose = opParams.purpose.firstOrNull() ?: return null
return when (keyParams.algorithm) {
Algorithm.EC -> when (purpose) {
KeyPurpose.ENCRYPT, KeyPurpose.DECRYPT -> KeystoreErrorCodes.unsupportedPurpose
KeyPurpose.AGREE_KEY -> null
else -> null
}
Algorithm.RSA -> when (purpose) {
KeyPurpose.AGREE_KEY -> KeystoreErrorCodes.unsupportedPurpose
else -> null
}
else -> null
}
}
private fun checkTemporalValidity(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? {
val now = System.currentTimeMillis()
val purpose = opParams.purpose.firstOrNull()
keyParams.activeDateTime?.let { activeDate ->
if (now < activeDate.time) return KeystoreErrorCodes.keyNotYetValid
}
keyParams.originationExpireDateTime?.let { expireDate ->
if (purpose == KeyPurpose.SIGN || purpose == KeyPurpose.ENCRYPT) {
if (now > expireDate.time) return KeystoreErrorCodes.keyExpired
}
}
keyParams.usageExpireDateTime?.let { expireDate ->
if (purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.DECRYPT) {
if (now > expireDate.time) return KeystoreErrorCodes.keyExpired
}
}
return null
}
private fun checkCallerNonce(keyParams: KeyMintAttestation, rawOpParams: Array<KeyParameter>?): Int? {
if (keyParams.callerNonce == true) return null
val hasNonce = rawOpParams?.any { it.tag == Tag.NONCE } == true
if (hasNonce) return KeystoreErrorCodes.callerNonceProhibited
return null
}
}
@@ -0,0 +1,378 @@
package org.matrix.TEESimulator.interception.keystore.shim
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.security.KeyPair
import java.security.MessageDigest
import java.security.cert.Certificate
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.ReentrantLock
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertificateHelper
data class PersistedKeyData(
val uid: Int,
val alias: String,
val nspace: Long,
val securityLevel: Int,
val isAttestationKey: Boolean,
val algorithm: Int,
val keySize: Int,
val ecCurve: Int,
val purposes: List<Int>,
val digests: List<Int>,
val privateKeyBytes: ByteArray,
val certChainBytes: List<ByteArray>,
)
object GeneratedKeyPersistence {
private const val FORMAT_VERSION = 1
private val PERSISTENCE_DIR = File(CONFIG_PATH, "persistent_keys")
// Per-filename locks to prevent concurrent writes to the same key file
private val fileLocks = ConcurrentHashMap<String, ReentrantLock>()
private fun getLockForKey(filename: String): ReentrantLock {
return fileLocks.computeIfAbsent(filename) { ReentrantLock() }
}
fun save(
keyId: KeyIdentifier,
keyPair: KeyPair,
nspace: Long,
securityLevel: Int,
certChain: List<Certificate>,
algorithm: Int,
keySize: Int,
ecCurve: Int,
purposes: List<Int>,
digests: List<Int>,
isAttestationKey: Boolean,
) {
val filename = keyFileName(keyId.uid, keyId.alias)
val lock = getLockForKey(filename)
SystemLogger.debug("[Persistence] Acquiring lock for $filename")
lock.lock()
try {
SystemLogger.debug("[Persistence] Lock acquired for $filename")
runCatching {
PERSISTENCE_DIR.mkdirs()
val finalFile = File(PERSISTENCE_DIR, filename)
val tmpFile = File(PERSISTENCE_DIR, "$filename.tmp")
try {
DataOutputStream(BufferedOutputStream(FileOutputStream(tmpFile))).use { out ->
out.writeInt(FORMAT_VERSION)
out.writeInt(securityLevel)
out.writeInt(keyId.uid)
out.writeUTF(keyId.alias)
out.writeLong(nspace)
out.writeBoolean(isAttestationKey)
out.writeInt(algorithm)
out.writeInt(keySize)
out.writeInt(ecCurve)
out.writeInt(purposes.size)
purposes.forEach { out.writeInt(it) }
out.writeInt(digests.size)
digests.forEach { out.writeInt(it) }
val pkBytes = keyPair.private.encoded
out.writeInt(pkBytes.size)
out.write(pkBytes)
out.writeInt(certChain.size)
certChain.forEach { cert ->
val encoded = cert.encoded
out.writeInt(encoded.size)
out.write(encoded)
}
}
} catch (e: Exception) {
tmpFile.delete()
throw e
}
// Atomic rename — if this fails the tmp is left behind and cleaned on next deleteAll
if (!tmpFile.renameTo(finalFile)) {
tmpFile.delete()
throw IllegalStateException("Failed to atomically rename $tmpFile -> $finalFile")
}
// Verify write succeeded - catches disk-full or filesystem errors
if (!finalFile.exists() || finalFile.length() < 20) {
throw IOException("File write verification failed - possible disk full")
}
SystemLogger.debug("Persisted key: $keyId")
}.onFailure { e ->
SystemLogger.error("Failed to persist key $keyId", e)
}
} finally {
lock.unlock()
SystemLogger.debug("[Persistence] Lock released for $filename")
}
}
fun delete(keyId: KeyIdentifier) {
runCatching {
val file = File(PERSISTENCE_DIR, keyFileName(keyId.uid, keyId.alias))
if (file.exists()) {
if (file.delete()) {
fileLocks.remove(keyFileName(keyId.uid, keyId.alias))
SystemLogger.debug("Deleted persisted key: $keyId")
} else {
SystemLogger.warning("Failed to delete persisted key file: ${file.name}")
}
} else {
SystemLogger.debug("No persisted file to delete for: $keyId")
}
}.onFailure { e ->
SystemLogger.error("Failed to delete persisted key $keyId", e)
}
}
fun deleteAll() {
runCatching {
if (!PERSISTENCE_DIR.exists()) {
SystemLogger.debug("No persistent_keys directory, nothing to delete")
return
}
val files = PERSISTENCE_DIR.listFiles()
if (files == null) {
SystemLogger.warning("Cannot list persistent_keys directory")
return
}
var count = 0
files.forEach { file ->
if (file.name.endsWith(".bin") || file.name.endsWith(".tmp")) {
if (file.delete()) count++
}
}
fileLocks.clear()
SystemLogger.info("Deleted $count persisted key files")
}.onFailure { e ->
SystemLogger.error("Failed to delete all persisted keys", e)
}
}
fun loadAll(securityLevel: Int): List<PersistedKeyData> {
if (!PERSISTENCE_DIR.exists()) {
SystemLogger.debug("No persistent_keys directory, nothing to load")
return emptyList()
}
val files = PERSISTENCE_DIR.listFiles { _, name -> name.endsWith(".bin") }
if (files == null) {
SystemLogger.warning("Cannot read persistent_keys directory")
return emptyList()
}
if (files.isEmpty()) {
SystemLogger.debug("No persisted key files found")
return emptyList()
}
SystemLogger.info("Found ${files.size} persisted key files to process")
val result = mutableListOf<PersistedKeyData>()
for (file in files) {
runCatching {
DataInputStream(BufferedInputStream(FileInputStream(file))).use { input ->
val version = input.readInt()
if (version != FORMAT_VERSION) {
SystemLogger.warning(
"Skipping ${file.name}: unknown format version $version"
)
return@runCatching
}
val storedSecLevel = input.readInt()
val uid = input.readInt()
val alias = input.readUTF()
val nspace = input.readLong()
val isAttestKey = input.readBoolean()
val algo = input.readInt()
val kSize = input.readInt()
val curve = input.readInt()
val purposeCount = requireBounds(input.readInt(), 64, "purposeCount")
val purposes = (0 until purposeCount).map { input.readInt() }
val digestCount = requireBounds(input.readInt(), 64, "digestCount")
val digests = (0 until digestCount).map { input.readInt() }
val pkLen = requireBounds(input.readInt(), 8192, "pkLen")
val pkBytes = ByteArray(pkLen)
input.readFully(pkBytes)
val certCount = requireBounds(input.readInt(), 10, "certCount")
val certChainBytes = (0 until certCount).map {
val certLen = requireBounds(input.readInt(), 65536, "certLen")
val certBytes = ByteArray(certLen)
input.readFully(certBytes)
certBytes
}
if (storedSecLevel == securityLevel) {
result.add(
PersistedKeyData(
uid = uid,
alias = alias,
nspace = nspace,
securityLevel = storedSecLevel,
isAttestationKey = isAttestKey,
algorithm = algo,
keySize = kSize,
ecCurve = curve,
purposes = purposes,
digests = digests,
privateKeyBytes = pkBytes,
certChainBytes = certChainBytes,
)
)
}
}
}.onFailure { e ->
SystemLogger.warning("Skipping corrupted persisted key file: ${file.name}", e)
}
}
SystemLogger.info("Loaded ${result.size} persisted keys for security level $securityLevel")
return result
}
// Re-persist updates the cert chain for an already-persisted key without
// reconstructing authorization parameters from the response. This avoids
// pulling keymint Tag dependencies into this file and is correct because
// the only field that changes post-generation is the patched cert chain.
fun rePersistIfNeeded(
callingUid: Int,
generatedKeyInfo: KeyMintSecurityLevelInterceptor.GeneratedKeyInfo,
) {
val metadata = generatedKeyInfo.response.metadata
if (metadata == null) {
SystemLogger.debug("rePersist: no metadata, skipping")
return
}
val secLevel = metadata.keySecurityLevel
val entry = KeyMintSecurityLevelInterceptor.generatedKeys.entries.find { (id, info) ->
id.uid == callingUid && info.nspace == generatedKeyInfo.nspace
}
if (entry == null) {
SystemLogger.debug("rePersist: key not found in map for uid=$callingUid nspace=${generatedKeyInfo.nspace}")
return
}
val keyId = entry.key
val filename = keyFileName(keyId.uid, keyId.alias)
val existing = File(PERSISTENCE_DIR, filename)
if (!existing.exists()) {
SystemLogger.debug("rePersist: no existing file for $keyId, skipping")
return
}
val newChain = CertificateHelper.getCertificateChain(metadata)
if (newChain == null) {
SystemLogger.warning("rePersist: could not extract cert chain for $keyId")
return
}
val persisted = runCatching {
DataInputStream(BufferedInputStream(FileInputStream(existing))).use { input ->
val version = input.readInt()
if (version != FORMAT_VERSION) {
SystemLogger.warning("rePersist: unknown format version $version for $keyId")
return
}
readPersistedKeyData(input)
}
}.getOrNull()
if (persisted == null) {
SystemLogger.warning("rePersist: failed to read existing data for $keyId")
return
}
save(
keyId = keyId,
keyPair = generatedKeyInfo.keyPair,
nspace = generatedKeyInfo.nspace,
securityLevel = secLevel,
certChain = newChain.toList(),
algorithm = persisted.algorithm,
keySize = persisted.keySize,
ecCurve = persisted.ecCurve,
purposes = persisted.purposes,
digests = persisted.digests,
isAttestationKey = persisted.isAttestationKey,
)
SystemLogger.debug("Re-persisted key $keyId with updated cert chain")
}
// Corrupted binary files can have arbitrary length fields — cap allocations
private fun requireBounds(value: Int, max: Int, name: String): Int {
require(value in 0..max) { "$name out of bounds: $value (max $max)" }
return value
}
private fun keyFileName(uid: Int, alias: String): String {
val digest = MessageDigest.getInstance("SHA-256")
.digest("$uid:$alias".toByteArray(Charsets.UTF_8))
return digest.joinToString("") { "%02x".format(it) } + ".bin"
}
// Reads all fields after version has already been consumed
private fun readPersistedKeyData(input: DataInputStream): PersistedKeyData {
val secLevel = input.readInt()
val uid = input.readInt()
val alias = input.readUTF()
val nspace = input.readLong()
val isAttestKey = input.readBoolean()
val algo = input.readInt()
val kSize = input.readInt()
val curve = input.readInt()
val purposeCount = requireBounds(input.readInt(), 64, "purposeCount")
val purposes = (0 until purposeCount).map { input.readInt() }
val digestCount = requireBounds(input.readInt(), 64, "digestCount")
val digests = (0 until digestCount).map { input.readInt() }
val pkLen = requireBounds(input.readInt(), 8192, "pkLen")
val pkBytes = ByteArray(pkLen)
input.readFully(pkBytes)
val certCount = requireBounds(input.readInt(), 10, "certCount")
val certChainBytes = (0 until certCount).map {
val certLen = requireBounds(input.readInt(), 65536, "certLen")
val certBytes = ByteArray(certLen)
input.readFully(certBytes)
certBytes
}
return PersistedKeyData(
uid = uid,
alias = alias,
nspace = nspace,
securityLevel = secLevel,
isAttestationKey = isAttestKey,
algorithm = algo,
keySize = kSize,
ecCurve = curve,
purposes = purposes,
digests = digests,
privateKeyBytes = pkBytes,
certChainBytes = certChainBytes,
)
}
}
@@ -1,16 +1,30 @@
package org.matrix.TEESimulator.interception.keystore.shim
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.BlockMode
import android.hardware.security.keymint.EcCurve
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyParameterValue
import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.KeyParameterValue
import android.hardware.security.keymint.KeyOrigin
import android.hardware.security.keymint.SecurityLevel
import android.hardware.security.keymint.Tag
import android.os.IBinder
import android.os.Parcel
import android.system.keystore2.*
import android.util.Pair as AndroidPair
import java.io.ByteArrayInputStream
import java.security.KeyFactory
import java.security.KeyPair
import java.security.SecureRandom
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.spec.PKCS8EncodedKeySpec
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedDeque
import java.util.concurrent.atomic.AtomicInteger
import org.matrix.TEESimulator.attestation.AttestationBuilder
import org.matrix.TEESimulator.attestation.AttestationConstants
import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager
@@ -18,25 +32,29 @@ import org.matrix.TEESimulator.interception.core.BinderInterceptor
import org.matrix.TEESimulator.interception.keystore.InterceptorUtils
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertGenConfig
import org.matrix.TEESimulator.pki.CertificateGenerator
import org.matrix.TEESimulator.pki.CertificateHelper
import org.matrix.TEESimulator.pki.KeyBoxManager
import org.matrix.TEESimulator.pki.NativeCertGen
import org.matrix.TEESimulator.util.AndroidDeviceUtils
import org.matrix.TEESimulator.util.AndroidPermissionUtils
/**
* Intercepts calls to an `IKeystoreSecurityLevel` service (e.g., TEE or StrongBox). This is where
* the core logic for key generation and import handling for modern Android resides.
*/
class KeyMintSecurityLevelInterceptor(
private val original: IKeystoreSecurityLevel,
private val securityLevel: Int,
) : BinderInterceptor() {
// --- Data Structures for State Management ---
data class GeneratedKeyInfo(
val keyPair: KeyPair,
val nspace: Long,
val response: KeyEntryResponse,
val keyParams: KeyMintAttestation? = null,
)
private val activeOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<SoftwareOperation>>()
private val recentOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<Long>>()
override fun onPreTransact(
txId: Long,
target: IBinder,
@@ -52,7 +70,7 @@ class KeyMintSecurityLevelInterceptor(
GENERATE_KEY_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (!shouldSkip) return handleGenerateKey(callingUid, data)
if (!shouldSkip) return handleGenerateKey(txId, callingUid, data)
}
CREATE_OPERATION_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
@@ -93,6 +111,11 @@ class KeyMintSecurityLevelInterceptor(
reply: Parcel?,
resultCode: Int,
): TransactionResult {
if (code == GENERATE_KEY_TRANSACTION && hardwareKeygenTxIds.remove(txId)) {
val remaining = hardwareKeygenCount(callingUid).decrementAndGet()
SystemLogger.info("[TX_ID: $txId] PERMIT_RELEASED uid=$callingUid concurrent_remaining=$remaining result=${if (resultCode == 0) "OK" else "ERROR($resultCode)"}")
}
// We only care about successful transactions.
if (resultCode != 0 || reply == null || InterceptorUtils.hasException(reply))
return TransactionResult.SkipTransaction
@@ -104,7 +127,15 @@ class KeyMintSecurityLevelInterceptor(
val keyDescriptor =
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction
cleanupKeyData(KeyIdentifier(callingUid, keyDescriptor.alias))
// Evict generated key data but retain patched chains so detectors
// can't use importKey to force unpatched getKeyEntry responses.
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
if (generatedKeys.remove(keyId) != null) {
SystemLogger.debug("Remove generated key on importKey $keyId")
GeneratedKeyPersistence.delete(keyId)
}
attestationKeys.remove(keyId)
importedKeys.add(keyId)
} else if (code == CREATE_OPERATION_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
@@ -131,7 +162,7 @@ class KeyMintSecurityLevelInterceptor(
val backdoor = getBackdoor(target)
if (backdoor != null) {
val interceptor = OperationInterceptor(operation, backdoor)
register(backdoor, operationBinder, interceptor)
register(backdoor, operationBinder, interceptor, OperationInterceptor.INTERCEPTED_CODES)
interceptedOperations[operationBinder] = interceptor
} else {
SystemLogger.error(
@@ -172,72 +203,181 @@ class KeyMintSecurityLevelInterceptor(
return TransactionResult.SkipTransaction
}
/**
* Handles the `createOperation` transaction. It checks if the operation is for a key that was
* generated in software. If so, it creates a software-based operation handler. Otherwise, it
* lets the call proceed to the real hardware service.
*/
private fun pruneOpsForUid(uid: Int, newOp: SoftwareOperation, maxOps: Int = MAX_CONCURRENT_OPS_PER_UID) {
val ops = activeOps.computeIfAbsent(uid) { ConcurrentLinkedDeque() }
val before = ops.size
ops.removeIf { it.finalized }
val afterClean = ops.size
while (ops.size >= maxOps) {
val oldest = ops.pollFirst() ?: break
if (!oldest.finalized) {
SystemLogger.info("[LRU] Pruning operation for uid=$uid (active=${ops.size}/$maxOps)")
oldest.abort()
}
}
ops.addLast(newOp)
SystemLogger.debug("[LRU] uid=$uid ops: before=$before cleaned=${before - afterClean} active=${ops.size}")
}
private fun trackAndEnforceOpLimit(callingUid: Int, txId: Long): TransactionResult? {
if (securityLevel != SecurityLevel.STRONGBOX) return null
val timestamps = recentOps.computeIfAbsent(callingUid) { ConcurrentLinkedDeque() }
val cutoff = System.nanoTime() - STRONGBOX_OP_WINDOW_NS
timestamps.removeIf { it < cutoff }
val swOps = activeOps[callingUid]?.count { !it.finalized } ?: 0
if (timestamps.size + swOps >= STRONGBOX_MAX_CONCURRENT_OPS) {
SystemLogger.info("[TX_ID: $txId] StrongBox op limit reached for uid=$callingUid (hw=${timestamps.size} sw=$swOps max=$STRONGBOX_MAX_CONCURRENT_OPS)")
return InterceptorUtils.createErrorReply(KEYMINT_TOO_MANY_OPERATIONS)
}
timestamps.addLast(System.nanoTime())
return null
}
private fun handleCreateOperation(
txId: Long,
callingUid: Int,
data: Parcel,
): TransactionResult {
SystemLogger.debug("[TX_ID: $txId] createOperation parcel: dataSize=${data.dataSize()} dataAvail=${data.dataAvail()} dataPos=${data.dataPosition()}")
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
// An operation must use the KEY_ID domain.
if (keyDescriptor.domain != Domain.KEY_ID) {
SystemLogger.debug("[TX_ID: $txId] createOperation descriptor: domain=${keyDescriptor.domain} nspace=${keyDescriptor.nspace} alias=${keyDescriptor.alias}")
// Android framework calls createOperation with domain=APP+alias;
// keystore2 internally resolves to KEY_ID — but software keys never
// reach keystore2's database, so we must handle both lookup paths.
val generatedKeyInfo = when (keyDescriptor.domain) {
Domain.APP -> {
val alias = keyDescriptor.alias ?: run {
SystemLogger.info("[TX_ID: $txId] createOperation domain=APP with null alias, forwarding to HAL")
return TransactionResult.ContinueAndSkipPost
}
val nspace = keyDescriptor.nspace
val generatedKeyInfo = findGeneratedKeyByKeyId(callingUid, nspace)
if (generatedKeyInfo == null) {
SystemLogger.debug(
"[TX_ID: $txId] Operation for unknown/hardware KeyId ($nspace). Forwarding."
)
generatedKeys[KeyIdentifier(callingUid, alias)] ?: run {
SystemLogger.info("[TX_ID: $txId] createOperation alias=$alias not in generatedKeys, forwarding to HAL")
return TransactionResult.ContinueAndSkipPost
}
}
Domain.KEY_ID -> {
findGeneratedKeyByKeyId(callingUid, keyDescriptor.nspace) ?: run {
trackAndEnforceOpLimit(callingUid, txId)?.let { return it }
SystemLogger.info("[TX_ID: $txId] createOperation KeyId(${keyDescriptor.nspace}) NOT FOUND for uid=$callingUid. Forwarding to HAL.")
return TransactionResult.Continue
}
}
else -> {
SystemLogger.info("[TX_ID: $txId] createOperation domain=${keyDescriptor.domain}, forwarding to HAL")
return TransactionResult.ContinueAndSkipPost
}
}
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for KeyId $nspace.")
trackAndEnforceOpLimit(callingUid, txId)?.let { return it }
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for uid=$callingUid.")
val params = data.createTypedArray(KeyParameter.CREATOR)!!
val parsedParams = KeyMintAttestation(params)
val parsedParams = KeyMintAttestation(params).let { p ->
if (p.algorithm != 0) p
else p.copy(algorithm = when (generatedKeyInfo.keyPair.private.algorithm) {
"EC", "ECDSA" -> Algorithm.EC
"RSA" -> Algorithm.RSA
else -> p.algorithm
})
}
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams)
AuthorizeCreate.check(generatedKeyInfo.keyParams, parsedParams, params)?.let { errorCode ->
SystemLogger.info("[TX_ID: $txId] authorize_create rejected: errorCode=$errorCode")
return InterceptorUtils.createErrorReply(errorCode)
}
val opLatency = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_OP_LATENCY_FLOOR_MS else 0L
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams, opLatency)
val maxOps = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_MAX_CONCURRENT_OPS else MAX_CONCURRENT_OPS_PER_UID
pruneOpsForUid(callingUid, softwareOperation, maxOps)
val operationBinder = SoftwareOperationBinder(softwareOperation)
val response =
CreateOperationResponse().apply {
iOperation = operationBinder
operationChallenge = null
softwareOperation.iv?.let { iv ->
parameters = KeyParameters().apply {
keyParameter = arrayOf(
KeyParameter().apply {
tag = Tag.NONCE
value = KeyParameterValue.blob(iv)
}
)
}
}
}
return InterceptorUtils.createTypedObjectReply(response)
}
/**
* Handles the `generateKey` transaction. Based on the configuration for the calling UID, it
* either generates a key in software or lets the call pass through to the hardware.
*/
private fun handleGenerateKey(callingUid: Int, data: Parcel): TransactionResult {
private fun handleGenerateKey(txId: Long, callingUid: Int, data: Parcel): TransactionResult {
if (data.dataSize() > MAX_ALIAS_LENGTH) {
SystemLogger.warning("Skipping oversized transaction: ${data.dataSize()} bytes")
return TransactionResult.ContinueAndSkipPost
}
return runCatching {
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
val attestationKey = data.readTypedObject(KeyDescriptor.CREATOR)
SystemLogger.debug(
"Handling generateKey ${keyDescriptor.alias}, attestKey=${attestationKey?.alias}"
)
val params = data.createTypedArray(KeyParameter.CREATOR)!!
val parsedParams = KeyMintAttestation(params)
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
val isAttestKeyRequest =
parsedParams.purpose.size == 1 &&
parsedParams.purpose.contains(KeyPurpose.ATTEST_KEY)
// Determine if we need to generate a key based on config or
// if it's an attestation request in patch mode.
val challenge = parsedParams.attestationChallenge
if (challenge != null && challenge.size > AttestationConstants.CHALLENGE_LENGTH_LIMIT) {
SystemLogger.warning("[TX_ID: $txId] Rejecting oversized attestation challenge: ${challenge.size} bytes (max ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})")
return InterceptorUtils.createErrorReply(KEYMINT_INVALID_INPUT_LENGTH)
}
if (params.any { it.tag == Tag.CREATION_DATETIME }) {
SystemLogger.warning("[TX_ID: $txId] Rejecting CREATION_DATETIME in generateKey params")
return InterceptorUtils.createErrorReply(RESPONSE_INVALID_ARGUMENT)
}
if (params.any { it.tag == Tag.DEVICE_UNIQUE_ATTESTATION } && !AndroidPermissionUtils.hasUniqueIdAttestationPermission(callingUid)) {
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_UNIQUE_ATTESTATION for uid=$callingUid")
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
}
val hasDeviceIdAttestation = params.any {
it.tag == Tag.ATTESTATION_ID_IMEI ||
it.tag == Tag.ATTESTATION_ID_MEID ||
it.tag == Tag.ATTESTATION_ID_SERIAL ||
it.tag == Tag.DEVICE_UNIQUE_ATTESTATION ||
it.tag == Tag.ATTESTATION_ID_SECOND_IMEI
}
if(hasDeviceIdAttestation && !AndroidPermissionUtils.hasDeviceAttestationPermission(callingUid)) {
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_ID_ATTESTATION for uid=$callingUid")
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
}
val isSymmetric = parsedParams.algorithm == Algorithm.AES ||
parsedParams.algorithm == Algorithm.HMAC ||
parsedParams.algorithm == Algorithm.TRIPLE_DES
if (isSymmetric) {
SystemLogger.debug("[TX_ID: $txId] Symmetric algorithm ${parsedParams.algorithm} → forwarding to HAL")
return TransactionResult.ContinueAndSkipPost
}
if (securityLevel == SecurityLevel.STRONGBOX && !isStrongBoxCapable(parsedParams)) {
SystemLogger.info("[TX_ID: $txId] StrongBox-unsupported params (algo=${parsedParams.algorithm} size=${parsedParams.keySize}) → forwarding to HAL for rejection")
return TransactionResult.ContinueAndSkipPost
}
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
val isAttestKeyRequest = parsedParams.isAttestKey()
val needsSoftwareGeneration =
ConfigurationManager.shouldGenerate(callingUid) ||
(ConfigurationManager.shouldPatch(callingUid) && isAttestKeyRequest) ||
@@ -245,37 +385,29 @@ class KeyMintSecurityLevelInterceptor(
isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias)))
if (needsSoftwareGeneration) {
keyDescriptor.nspace = secureRandom.nextLong()
SystemLogger.info(
"Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}]."
)
// Generate the key pair and certificate chain.
val keyData =
CertificateGenerator.generateAttestedKeyPair(
callingUid,
keyDescriptor.alias,
attestationKey?.alias,
parsedParams,
securityLevel,
) ?: throw Exception("CertificateGenerator failed to create key pair.")
// It is unnecessary but a good practice to clean up possible caches
cleanupKeyData(keyId)
// Store the generated key data.
val response =
buildKeyEntryResponse(keyData.second, parsedParams, keyDescriptor)
generatedKeys[keyId] =
GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response)
if (isAttestKeyRequest) attestationKeys.add(keyId)
// Return the metadata of our generated key, skipping the real hardware call.
return InterceptorUtils.createTypedObjectReply(response.metadata)
return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
} else if (parsedParams.attestationChallenge != null) {
val windowUsed = hardwareKeygenWindowCount(callingUid)
val concurrentUsed = hardwareKeygenCount(callingUid).get()
// Sliding window rate limit
if (windowUsed >= MAX_HW_KEYGEN_PER_WINDOW) {
SystemLogger.info("[TX_ID: $txId] RATE_LIMITED uid=$callingUid window=$windowUsed/$MAX_HW_KEYGEN_PER_WINDOW concurrent=$concurrentUsed → software fallback")
return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
}
// Concurrent cap
if (hardwareKeygenCount(callingUid).incrementAndGet() > MAX_CONCURRENT_HW_KEYGEN_PER_UID) {
hardwareKeygenCount(callingUid).decrementAndGet()
SystemLogger.info("[TX_ID: $txId] CONCURRENT_LIMITED uid=$callingUid window=$windowUsed/$MAX_HW_KEYGEN_PER_WINDOW concurrent=${concurrentUsed + 1}/$MAX_CONCURRENT_HW_KEYGEN_PER_UID → software fallback")
return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
}
// Both checks passed — commit the window permit and forward to hardware TEE
recordHardwareKeygen(callingUid)
hardwareKeygenTxIds.add(txId)
SystemLogger.info("[TX_ID: $txId] HARDWARE_KEYGEN uid=$callingUid window=${windowUsed + 1}/$MAX_HW_KEYGEN_PER_WINDOW concurrent=${concurrentUsed + 1}/$MAX_CONCURRENT_HW_KEYGEN_PER_UID → forwarding to TEE")
return TransactionResult.Continue
}
// If not generating, clear any stale state for this alias and let the call proceed.
cleanupKeyData(keyId)
TransactionResult.ContinueAndSkipPost
}
@@ -285,20 +417,144 @@ class KeyMintSecurityLevelInterceptor(
}
}
/**
* Constructs a fake `KeyEntryResponse` that mimics a real response from the Keystore service.
*/
private fun doSoftwareKeyGen(
callingUid: Int,
keyDescriptor: KeyDescriptor,
attestationKey: KeyDescriptor?,
parsedParams: KeyMintAttestation,
keyId: KeyIdentifier,
isAttestKeyRequest: Boolean,
): TransactionResult {
val startNs = System.nanoTime()
keyDescriptor.nspace = secureRandom.nextLong()
SystemLogger.info("Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}].")
val keyData = if (NativeCertGen.isAvailable && attestationKey == null) {
generateAttestedKeyPairNative(callingUid, parsedParams)
?: CertificateGenerator.generateAttestedKeyPair(
callingUid, keyDescriptor.alias, attestationKey?.alias, parsedParams, securityLevel,
)
} else {
CertificateGenerator.generateAttestedKeyPair(
callingUid, keyDescriptor.alias, attestationKey?.alias, parsedParams, securityLevel,
)
} ?: throw Exception("Both native and BouncyCastle cert gen failed.")
cleanupKeyData(keyId)
val response = buildKeyEntryResponse(callingUid, keyData.second, parsedParams, keyDescriptor)
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response, parsedParams)
if (isAttestKeyRequest) attestationKeys.add(keyId)
GeneratedKeyPersistence.save(
keyId = keyId,
keyPair = keyData.first,
nspace = keyDescriptor.nspace,
securityLevel = securityLevel,
certChain = keyData.second.toList(),
algorithm = parsedParams.algorithm,
keySize = parsedParams.keySize,
ecCurve = parsedParams.ecCurve ?: 0,
purposes = parsedParams.purpose,
digests = parsedParams.digest,
isAttestationKey = isAttestKeyRequest,
)
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
val floor = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_KEYGEN_LATENCY_FLOOR_MS else TEE_LATENCY_FLOOR_MS
val delayMs = floor - elapsedMs
if (delayMs > 0) Thread.sleep(delayMs)
return InterceptorUtils.createTypedObjectReply(response.metadata)
}
private fun generateAttestedKeyPairNative(
callingUid: Int,
params: KeyMintAttestation,
): AndroidPair<KeyPair, List<Certificate>>? {
return runCatching {
val algorithmName = when (params.algorithm) {
Algorithm.EC -> "EC"
Algorithm.RSA -> "RSA"
else -> return null
}
val keyboxFile = ConfigurationManager.getKeyboxFileForUid(callingUid)
val keybox = KeyBoxManager.getAttestationKey(keyboxFile, algorithmName) ?: return null
val keyboxPrivateKeyBytes = keybox.keyPair.private.encoded
val keyboxCertChainBytes = keybox.certificates
.map { it.encoded }
.fold(ByteArray(0)) { acc, der -> acc + der }
val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel)
val keymasterVersion = AndroidDeviceUtils.getKeymasterVersion(securityLevel)
val appId = AttestationBuilder.createApplicationId(callingUid)
val config = CertGenConfig(
algorithm = params.algorithm,
keySize = params.keySize,
ecCurve = params.ecCurve ?: 0,
rsaPublicExponent = params.rsaPublicExponent?.toLong() ?: 65537L,
attestationChallenge = params.attestationChallenge,
purposes = params.purpose.toIntArray(),
digests = params.digest.toIntArray(),
certSerial = params.certificateSerial?.toByteArray(),
certSubject = params.certificateSubject?.encoded,
certNotBefore = params.certificateNotBefore?.time ?: -1L,
certNotAfter = params.certificateNotAfter?.time ?: -1L,
keyboxPrivateKey = keyboxPrivateKeyBytes,
keyboxCertChain = keyboxCertChainBytes,
securityLevel = securityLevel,
attestVersion = attestVersion,
keymasterVersion = keymasterVersion,
osVersion = AndroidDeviceUtils.osVersion,
osPatchLevel = AndroidDeviceUtils.getPatchLevel(callingUid),
vendorPatchLevel = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid),
bootPatchLevel = AndroidDeviceUtils.getBootPatchLevelLong(callingUid),
bootKey = AndroidDeviceUtils.bootKey,
bootHash = AndroidDeviceUtils.bootHash,
creationDatetime = System.currentTimeMillis(),
attestationApplicationId = appId.octets,
moduleHash = if (attestVersion >= 400) AndroidDeviceUtils.moduleHash else null,
idBrand = params.brand,
idDevice = params.device,
idProduct = params.product,
idSerial = params.serial,
idImei = params.imei,
idMeid = params.meid,
idManufacturer = params.manufacturer,
idModel = params.model,
idSecondImei = if (attestVersion >= 300) params.secondImei else null,
)
val resultBytes = NativeCertGen.generateAttestedKeyPair(config) ?: return null
val (keyPair, certs) = NativeCertGen.parseNativeResult(resultBytes)
SystemLogger.info("NativeCertGen: generated key pair successfully (${certs.size} certs)")
AndroidPair(keyPair, certs)
}.onFailure {
SystemLogger.error("NativeCertGen: generation failed, falling back to BouncyCastle", it)
}.getOrNull()
}
private fun buildKeyEntryResponse(
callingUid: Int,
chain: List<Certificate>,
params: KeyMintAttestation,
descriptor: KeyDescriptor,
): KeyEntryResponse {
val normalizedKeyDescriptor =
KeyDescriptor().apply {
domain = Domain.KEY_ID
nspace = descriptor.nspace
alias = null
blob = null
}
val metadata =
KeyMetadata().apply {
keySecurityLevel = securityLevel
key = descriptor
key = normalizedKeyDescriptor
CertificateHelper.updateCertificateChain(this, chain.toTypedArray()).getOrThrow()
authorizations = params.toAuthorizations(securityLevel)
authorizations = params.toAuthorizations(callingUid, securityLevel)
modificationTimeMs = System.currentTimeMillis()
}
return KeyEntryResponse().apply {
this.metadata = metadata
@@ -306,10 +562,158 @@ class KeyMintSecurityLevelInterceptor(
}
}
fun loadPersistedKeys() {
val records = GeneratedKeyPersistence.loadAll(securityLevel)
if (records.isEmpty()) {
SystemLogger.debug("No persisted keys to restore for security level $securityLevel")
return
}
SystemLogger.info("Restoring ${records.size} persisted keys for security level $securityLevel")
for (record in records) {
runCatching {
val keyId = KeyIdentifier(record.uid, record.alias)
if (generatedKeys.containsKey(keyId)) {
SystemLogger.debug("Skipping already-loaded key: $keyId")
return@runCatching
}
val algorithmName = when (record.algorithm) {
Algorithm.EC -> "EC"
Algorithm.RSA -> "RSA"
else -> throw IllegalArgumentException("Unknown algorithm: ${record.algorithm}")
}
val keyFactory = KeyFactory.getInstance(algorithmName)
val privateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(record.privateKeyBytes))
val certFactory = CertificateFactory.getInstance("X.509")
val certChain = record.certChainBytes.map { bytes ->
certFactory.generateCertificate(ByteArrayInputStream(bytes))
}
require(certChain.isNotEmpty()) { "Persisted key has empty certificate chain" }
val publicKey = certChain[0].publicKey
val keyPair = KeyPair(publicKey, privateKey)
val descriptor = KeyDescriptor().apply {
domain = Domain.APP
nspace = record.nspace
alias = record.alias
blob = null
}
val attestation = KeyMintAttestation(
keySize = record.keySize,
algorithm = record.algorithm,
ecCurve = record.ecCurve,
ecCurveName = "",
origin = null,
blockMode = emptyList(),
padding = emptyList(),
purpose = record.purposes,
digest = record.digests,
rsaPublicExponent = null,
certificateSerial = null,
certificateSubject = null,
certificateNotBefore = null,
certificateNotAfter = null,
attestationChallenge = null,
brand = null,
device = null,
product = null,
serial = null,
imei = null,
meid = null,
manufacturer = null,
model = null,
secondImei = null,
activeDateTime = null,
originationExpireDateTime = null,
usageExpireDateTime = null,
usageCountLimit = null,
callerNonce = null,
unlockedDeviceRequired = null,
includeUniqueId = null,
rollbackResistance = null,
earlyBootOnly = null,
allowWhileOnBody = null,
trustedUserPresenceRequired = null,
trustedConfirmationRequired = null,
noAuthRequired = null,
maxUsesPerBoot = null,
maxBootLevel = null,
minMacLength = null,
rsaOaepMgfDigest = emptyList(),
)
val response = buildKeyEntryResponse(record.uid, certChain, attestation, descriptor)
generatedKeys[keyId] = GeneratedKeyInfo(keyPair, record.nspace, response, attestation)
if (record.isAttestationKey) attestationKeys.add(keyId)
SystemLogger.debug("Restored persisted key: $keyId")
}.onFailure {
SystemLogger.error("Failed to restore key: uid=${record.uid} alias=${record.alias}", it)
}
}
SystemLogger.info("Key restoration complete. Total in memory: ${generatedKeys.size}")
}
companion object {
private val secureRandom = SecureRandom()
// Transaction codes for IKeystoreSecurityLevel interface.
// Maximum alias length to prevent binder buffer exhaustion (Issue #109)
// Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead
private const val MAX_ALIAS_LENGTH = 256 * 1024
private const val KEYMINT_INVALID_INPUT_LENGTH = -21
private const val RESPONSE_INVALID_ARGUMENT = 20
private const val TEE_LATENCY_FLOOR_MS = 15L
private const val STRONGBOX_KEYGEN_LATENCY_FLOOR_MS = 250L
private const val STRONGBOX_OP_LATENCY_FLOOR_MS = 80L
private const val KEYMINT_TOO_MANY_OPERATIONS = -29
private const val KEYMINT_CANNOT_ATTEST_IDS = -66
private const val MAX_CONCURRENT_OPS_PER_UID = 15
private const val STRONGBOX_MAX_CONCURRENT_OPS = 4
private const val STRONGBOX_OP_WINDOW_NS = 10_000_000_000L // 10s
private const val MAX_CONCURRENT_HW_KEYGEN_PER_UID = 2
// Sliding window: max hardware keygen permits per UID within the burst window
private const val MAX_HW_KEYGEN_PER_WINDOW = 2
private const val BURST_WINDOW_MS = 30_000L
private val uidHardwareKeygenCount = ConcurrentHashMap<Int, AtomicInteger>()
private val hardwareKeygenTxIds = ConcurrentHashMap.newKeySet<Long>()
private val uidKeygenTimestamps = ConcurrentHashMap<Int, MutableList<Long>>()
private fun isStrongBoxCapable(params: KeyMintAttestation): Boolean = when (params.algorithm) {
Algorithm.RSA -> params.keySize <= 2048
Algorithm.EC -> params.ecCurve == null || params.ecCurve == EcCurve.P_256
else -> true
}
private fun hardwareKeygenCount(uid: Int): AtomicInteger =
uidHardwareKeygenCount.computeIfAbsent(uid) { AtomicInteger(0) }
private fun hardwareKeygenWindowCount(uid: Int): Int {
val now = System.currentTimeMillis()
val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() }
synchronized(timestamps) {
timestamps.removeAll { now - it > BURST_WINDOW_MS }
if (timestamps.isEmpty()) {
uidKeygenTimestamps.remove(uid, timestamps)
uidHardwareKeygenCount.remove(uid)
}
return timestamps.size
}
}
private fun recordHardwareKeygen(uid: Int) {
val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() }
synchronized(timestamps) {
timestamps.add(System.currentTimeMillis())
}
}
private val GENERATE_KEY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
private val IMPORT_KEY_TRANSACTION =
@@ -320,6 +724,9 @@ class KeyMintSecurityLevelInterceptor(
"createOperation",
)
val INTERCEPTED_CODES =
intArrayOf(GENERATE_KEY_TRANSACTION, IMPORT_KEY_TRANSACTION, CREATE_OPERATION_TRANSACTION)
private val transactionNames: Map<Int, String> by lazy {
IKeystoreSecurityLevel.Stub::class
.java
@@ -331,30 +738,16 @@ class KeyMintSecurityLevelInterceptor(
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
}
// Stores keys generated entirely in software.
val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>()
// Caches patched certificate chains to prevent re-generation and signature inconsistencies.
private val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
// A set to quickly identify keys that were generated for attestation purposes.
private val attestationKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
// Stores interceptors for active cryptographic operations.
val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
val attestationKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
val importedKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
// --- Public Accessors for Other Interceptors ---
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
generatedKeys[keyId]?.response
/**
* Finds a software-generated key by first filtering all known keys by the caller's UID, and
* then matching the specific nspace.
*
* @param callingUid The UID of the process that initiated the createOperation call.
* @param nspace The unique key identifier from the operation's KeyDescriptor.
* @return The matching GeneratedKeyInfo if found, otherwise null.
*/
fun findGeneratedKeyByKeyId(callingUid: Int, nspace: Long?): GeneratedKeyInfo? {
// Iterate through all entries in the map to check both the key (for UID) and value (for
// nspace).
if (nspace == null || nspace == 0L) return null
return generatedKeys.entries
.filter { (keyIdentifier, _) -> keyIdentifier.uid == callingUid }
@@ -369,6 +762,7 @@ class KeyMintSecurityLevelInterceptor(
fun cleanupKeyData(keyId: KeyIdentifier) {
if (generatedKeys.remove(keyId) != null) {
SystemLogger.debug("Remove generated key ${keyId}")
GeneratedKeyPersistence.delete(keyId)
}
if (patchedChains.remove(keyId) != null) {
SystemLogger.debug("Remove patched chain for ${keyId}")
@@ -376,10 +770,10 @@ class KeyMintSecurityLevelInterceptor(
if (attestationKeys.remove(keyId)) {
SystemLogger.debug("Remove cached attestaion key ${keyId}")
}
importedKeys.remove(keyId)
}
fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) {
// Unregister from the native hook layer first.
unregister(backdoor, operationBinder)
if (interceptedOperations.remove(operationBinder) != null) {
@@ -387,33 +781,33 @@ class KeyMintSecurityLevelInterceptor(
}
}
// Clears all cached keys.
fun invalidatePatchedChains(reason: String? = null) {
val count = patchedChains.size
if (count == 0) return
val reasonMessage = reason?.let { " due to $it" } ?: ""
patchedChains.clear()
SystemLogger.info("Invalidated $count patched cert chains$reasonMessage.")
}
fun clearAllGeneratedKeys(reason: String? = null) {
val count = generatedKeys.size
val reasonMessage = reason?.let { " due to $it" } ?: ""
generatedKeys.clear()
patchedChains.clear()
attestationKeys.clear()
importedKeys.clear()
GeneratedKeyPersistence.deleteAll()
SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.")
}
}
}
/**
* Extension function to convert parsed `KeyMintAttestation` parameters back into an array of
* `Authorization` objects for the fake `KeyMetadata`. This version correctly handles the
* instantiation of Authorization objects.
*/
private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Authorization> {
private fun KeyMintAttestation.toAuthorizations(
callingUid: Int,
securityLevel: Int,
): Array<Authorization> {
val authList = mutableListOf<Authorization>()
/**
* Helper function to create a fully-formed Authorization object.
*
* @param tag The KeyMint tag (e.g., Tag.ALGORITHM).
* @param value The value for the tag, wrapped in a KeyParameterValue.
* @return A populated Authorization object.
*/
fun createAuth(tag: Int, value: KeyParameterValue): Authorization {
val param =
KeyParameter().apply {
@@ -426,14 +820,45 @@ private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Autho
}
}
// Use the helper to add each authorization entry cleanly.
this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) }
this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) }
authList.add(createAuth(Tag.ALGORITHM, KeyParameterValue.algorithm(this.algorithm)))
authList.add(createAuth(Tag.KEY_SIZE, KeyParameterValue.integer(this.keySize)))
if (this.ecCurve != null) {
authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve)))
}
this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) }
this.blockMode.forEach { authList.add(createAuth(Tag.BLOCK_MODE, KeyParameterValue.blockMode(it))) }
this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) }
this.padding.forEach { authList.add(createAuth(Tag.PADDING, KeyParameterValue.paddingMode(it))) }
authList.add(createAuth(Tag.KEY_SIZE, KeyParameterValue.integer(this.keySize)))
if (this.rsaPublicExponent != null) {
authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong())))
}
authList.add(createAuth(Tag.NO_AUTH_REQUIRED, KeyParameterValue.boolValue(true)))
authList.add(createAuth(Tag.ORIGIN, KeyParameterValue.origin(this.origin ?: KeyOrigin.GENERATED)))
authList.add(createAuth(Tag.OS_VERSION, KeyParameterValue.integer(AndroidDeviceUtils.osVersion)))
val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid)
if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
authList.add(createAuth(Tag.OS_PATCHLEVEL, KeyParameterValue.integer(osPatch)))
}
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
authList.add(createAuth(Tag.VENDOR_PATCHLEVEL, KeyParameterValue.integer(vendorPatch)))
}
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
authList.add(createAuth(Tag.BOOT_PATCHLEVEL, KeyParameterValue.integer(bootPatch)))
}
authList.add(createAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis())))
authList.add(
Authorization().apply {
this.keyParameter =
KeyParameter().apply {
this.tag = Tag.USER_ID
this.value = KeyParameterValue.integer(callingUid / 100000)
}
this.securityLevel = SecurityLevel.SOFTWARE
}
)
return authList.toTypedArray()
}
@@ -44,6 +44,8 @@ class OperationInterceptor(
private val ABORT_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort")
val INTERCEPTED_CODES = intArrayOf(FINISH_TRANSACTION, ABORT_TRANSACTION)
private val transactionNames: Map<Int, String> by lazy {
IKeystoreOperation.Stub::class
.java
@@ -5,7 +5,7 @@ import android.hardware.security.keymint.BlockMode
import android.hardware.security.keymint.Digest
import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.PaddingMode
import android.os.RemoteException
import android.os.ServiceSpecificException
import android.system.keystore2.IKeystoreOperation
import java.security.KeyPair
import java.security.Signature
@@ -15,16 +15,16 @@ import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
import org.matrix.TEESimulator.logging.SystemLogger
// A sealed interface to represent the different cryptographic operations we can perform.
private sealed interface CryptoPrimitive {
fun updateAad(aadInput: ByteArray?) {
throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
}
fun update(data: ByteArray?): ByteArray?
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
fun abort()
fun getIv(): ByteArray? = null
}
// Helper object to map KeyMint constants to JCA algorithm strings.
private object JcaAlgorithmMapper {
fun mapSignatureAlgorithm(params: KeyMintAttestation): String {
val digest =
@@ -34,16 +34,18 @@ private object JcaAlgorithmMapper {
Digest.SHA_2_512 -> "SHA512"
else -> "NONE"
}
val keyAlgo =
when (params.algorithm) {
Algorithm.EC -> "ECDSA"
Algorithm.RSA -> "RSA"
return when (params.algorithm) {
Algorithm.EC -> "${digest}withECDSA"
Algorithm.RSA -> {
val isPss = params.padding.firstOrNull() == PaddingMode.RSA_PSS
if (isPss) "${digest}withRSA/PSS" else "${digest}withRSA"
}
else ->
throw IllegalArgumentException(
"Unsupported signature algorithm: ${params.algorithm}"
throw ServiceSpecificException(
KeystoreErrorCodes.incompatibleAlgorithm,
"Unsupported signature algorithm: ${params.algorithm}",
)
}
return "${digest}with${keyAlgo}"
}
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
@@ -52,30 +54,32 @@ private object JcaAlgorithmMapper {
Algorithm.RSA -> "RSA"
Algorithm.AES -> "AES"
else ->
throw IllegalArgumentException(
"Unsupported cipher algorithm: ${params.algorithm}"
throw ServiceSpecificException(
KeystoreErrorCodes.incompatibleAlgorithm,
"Unsupported cipher algorithm: ${params.algorithm}",
)
}
val blockMode =
when (params.blockMode.firstOrNull()) {
BlockMode.ECB -> "ECB"
BlockMode.CBC -> "CBC"
BlockMode.CTR -> "CTR"
BlockMode.GCM -> "GCM"
else -> "ECB" // Default for RSA
else -> "ECB"
}
val padding =
when (params.padding.firstOrNull()) {
PaddingMode.NONE -> "NoPadding"
PaddingMode.PKCS7 -> "PKCS7Padding"
PaddingMode.RSA_PKCS1_1_5_ENCRYPT -> "PKCS1Padding"
PaddingMode.RSA_PKCS1_1_5_SIGN -> "PKCS1Padding"
PaddingMode.RSA_OAEP -> "OAEPPadding"
else -> "NoPadding" // Default for GCM
else -> "NoPadding"
}
return "$keyAlgo/$blockMode/$padding"
}
}
// Concrete implementation for Signing.
private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
private val signature: Signature =
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
@@ -95,7 +99,6 @@ private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimi
override fun abort() {}
}
// Concrete implementation for Verification.
private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
private val signature: Signature =
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
@@ -109,50 +112,60 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
if (data != null) update(data)
if (signature == null) throw SignatureException("Signature to verify is null")
if (signature == null) {
throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature to verify is null")
}
if (!this.signature.verify(signature)) {
// Throwing an exception is how Keystore signals verification failure.
throw SignatureException("Signature verification failed")
throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature verification failed")
}
// A successful verification returns no data.
return null
}
override fun abort() {}
}
// Concrete implementation for Encryption/Decryption.
private class CipherPrimitive(
keyPair: KeyPair,
params: KeyMintAttestation,
private val opMode: Int,
) : CryptoPrimitive {
private val isAead = params.blockMode.firstOrNull() == BlockMode.GCM
private val cipher: Cipher =
Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply {
val key = if (opMode == Cipher.ENCRYPT_MODE) keyPair.public else keyPair.private
init(opMode, key)
}
override fun updateAad(aadInput: ByteArray?) {
if (!isAead) throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
if (aadInput != null) cipher.updateAAD(aadInput)
}
override fun update(data: ByteArray?): ByteArray? =
if (data != null) cipher.update(data) else null
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? =
if (data != null) cipher.doFinal(data) else cipher.doFinal()
override fun getIv(): ByteArray? = if (isAead) cipher.iv else null
override fun abort() {}
}
/**
* A software-only implementation of a cryptographic operation. This class acts as a controller,
* delegating to a specific cryptographic primitive based on the operation's purpose.
*/
class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMintAttestation) {
// This now holds the specific strategy object (Signer, Verifier, etc.)
class SoftwareOperation(
private val txId: Long,
keyPair: KeyPair,
params: KeyMintAttestation,
private val latencyFloorMs: Long = 0L,
) {
private val primitive: CryptoPrimitive
@Volatile var finalized = false
private set
val iv: ByteArray?
get() = primitive.getIv()
init {
// The "Strategy" pattern: choose the implementation based on the purpose.
// For simplicity, we only consider the first purpose listed.
val purpose = params.purpose.firstOrNull()
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
@@ -164,52 +177,174 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
KeyPurpose.ENCRYPT -> CipherPrimitive(keyPair, params, Cipher.ENCRYPT_MODE)
KeyPurpose.DECRYPT -> CipherPrimitive(keyPair, params, Cipher.DECRYPT_MODE)
else ->
throw UnsupportedOperationException("Unsupported operation purpose: $purpose")
throw ServiceSpecificException(
KeystoreErrorCodes.unsupportedPurpose,
"Unsupported operation purpose: $purpose",
)
}
}
private fun checkActive() {
if (finalized) {
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)")
throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle)
}
}
private fun checkInputLength(data: ByteArray?) {
if (data != null && data.size > MAX_RECEIVE_DATA) {
SystemLogger.info("[SoftwareOp TX_ID: $txId] Input too large: ${data.size} > $MAX_RECEIVE_DATA, throwing TOO_MUCH_DATA(${KeystoreErrorCodes.tooMuchData})")
throw ServiceSpecificException(KeystoreErrorCodes.tooMuchData)
}
}
fun updateAad(aadInput: ByteArray?) {
SystemLogger.debug("[SoftwareOp TX_ID: $txId] updateAad() inputSize=${aadInput?.size ?: 0}")
checkActive()
checkInputLength(aadInput)
primitive.updateAad(aadInput)
}
fun update(data: ByteArray?): ByteArray? {
SystemLogger.debug("[SoftwareOp TX_ID: $txId] update() inputSize=${data?.size ?: 0}")
checkActive()
checkInputLength(data)
try {
return primitive.update(data)
} catch (e: ServiceSpecificException) {
throw e
} catch (e: Exception) {
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
throw e
throw mapToServiceSpecificException(e)
}
}
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
checkActive()
checkInputLength(data)
try {
val startNs = if (latencyFloorMs > 0) System.nanoTime() else 0L
val result = primitive.finish(data, signature)
if (latencyFloorMs > 0) {
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
val delayMs = latencyFloorMs - elapsedMs
if (delayMs > 0) Thread.sleep(delayMs)
}
finalized = true
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
return result
} catch (e: ServiceSpecificException) {
throw e
} catch (e: Exception) {
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
// Re-throw the exception so the binder can report it to the client.
throw e
throw mapToServiceSpecificException(e)
}
}
fun abort() {
finalized = true
primitive.abort()
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
}
private fun mapToServiceSpecificException(e: Exception): ServiceSpecificException = when (e) {
is SignatureException -> ServiceSpecificException(KeystoreErrorCodes.verificationFailed, e.message)
is javax.crypto.BadPaddingException -> ServiceSpecificException(KeystoreErrorCodes.invalidArgument, e.message)
is javax.crypto.IllegalBlockSizeException -> ServiceSpecificException(KeystoreErrorCodes.invalidInputLength, e.message)
is java.security.InvalidKeyException -> ServiceSpecificException(KeystoreErrorCodes.incompatibleKey, e.message)
else -> ServiceSpecificException(KeystoreErrorCodes.unknownError, e.message)
}
companion object {
private const val MAX_RECEIVE_DATA = 0x8000
}
}
internal object KeystoreErrorCodes {
val tooMuchData: Int by lazy {
resolveField("android.system.keystore2.ResponseCode", "TOO_MUCH_DATA", 21)
}
val invalidOperationHandle: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_OPERATION_HANDLE", -28)
}
val invalidTag: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_TAG", -76)
}
val verificationFailed: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "VERIFICATION_FAILED", -30)
}
val invalidArgument: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_ARGUMENT", -38)
}
val invalidInputLength: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_INPUT_LENGTH", -21)
}
val incompatibleKey: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_KEY", -31)
}
val incompatiblePurpose: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_PURPOSE", -13)
}
val unsupportedPurpose: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "UNSUPPORTED_PURPOSE", -14)
}
val incompatibleAlgorithm: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_ALGORITHM", -18)
}
val keyNotYetValid: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "KEY_NOT_YET_VALID", -39)
}
val keyExpired: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "KEY_EXPIRED", -40)
}
val callerNonceProhibited: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "CALLER_NONCE_PROHIBITED", -55)
}
val unknownError: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "UNKNOWN_ERROR", -1000)
}
fun resolveField(className: String, fieldName: String, fallback: Int): Int =
runCatching {
Class.forName(className).getField(fieldName).getInt(null)
}.getOrElse {
SystemLogger.debug("Resolved $className.$fieldName via fallback: $fallback")
fallback
}
}
/** The Binder interface for our [SoftwareOperation]. */
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
IKeystoreOperation.Stub() {
@Throws(RemoteException::class)
@Synchronized
override fun updateAad(aadInput: ByteArray?) {
operation.updateAad(aadInput)
}
@Synchronized
override fun update(input: ByteArray?): ByteArray? {
return operation.update(input)
}
@Throws(RemoteException::class)
@Synchronized
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
return operation.finish(input, signature)
}
@Throws(RemoteException::class)
@Synchronized
override fun abort() {
operation.abort()
}
@@ -19,6 +19,7 @@ object SystemLogger {
* @param message The message to log.
*/
fun debug(message: String) {
if (!isDebugBuild) return
Log.d(TAG, message)
}
@@ -8,7 +8,6 @@ import java.math.BigInteger
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.cert.Certificate
import java.security.cert.X509Certificate
import java.security.spec.ECGenParameterSpec
import java.security.spec.RSAKeyGenParameterSpec
import java.util.Date
@@ -36,6 +35,8 @@ import org.matrix.TEESimulator.logging.SystemLogger
*/
object CertificateGenerator {
private const val UNDEFINED_NOT_AFTER = 253402300799000L
/**
* Generates a software-based cryptographic key pair.
*
@@ -49,7 +50,10 @@ object CertificateGenerator {
Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName)
Algorithm.RSA ->
"RSA" to
RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent)
RSAKeyGenParameterSpec(
params.keySize,
params.rsaPublicExponent ?: RSAKeyGenParameterSpec.F4,
)
else ->
throw IllegalArgumentException(
"Unsupported algorithm: ${params.algorithm}"
@@ -88,11 +92,9 @@ object CertificateGenerator {
"Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})"
)
return runCatching {
return try {
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
// Determine the signing key and issuer. If an attestKey is provided, use it.
// Otherwise, fall back to the root key from the keybox.
val (signingKey, issuer) =
if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second }
@@ -101,20 +103,20 @@ object CertificateGenerator {
keybox.keyPair to getIssuerFromKeybox(keybox)
}
// Build the new leaf certificate with the simulated attestation.
val leafCert =
buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel)
// If not self-attesting, the chain is just the leaf. Otherwise, append the keybox
// chain.
if (attestKeyAlias != null) {
listOf(leafCert)
} else {
listOf(leafCert) + keybox.certificates
}
} catch (e: android.os.ServiceSpecificException) {
throw e
} catch (e: Exception) {
SystemLogger.error("Failed to generate certificate chain.", e)
null
}
.onFailure { SystemLogger.error("Failed to generate certificate chain.", it) }
.getOrNull()
}
/**
@@ -128,7 +130,7 @@ object CertificateGenerator {
params: KeyMintAttestation,
securityLevel: Int,
): Pair<KeyPair, List<Certificate>>? {
return runCatching {
return try {
SystemLogger.info(
"Generating new attested key pair for alias: '$alias' (UID: $uid)"
)
@@ -144,11 +146,12 @@ object CertificateGenerator {
"Successfully generated new certificate chain for alias: '$alias'."
)
Pair(newKeyPair, chain)
} catch (e: android.os.ServiceSpecificException) {
throw e
} catch (e: Exception) {
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", e)
null
}
.onFailure {
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", it)
}
.getOrNull()
}
fun getIssuerFromKeybox(keybox: KeyBox) =
@@ -163,7 +166,10 @@ object CertificateGenerator {
else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm")
}
return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName)
?: throw Exception("Could not load keybox for UID $uid and algorithm $algorithmName")
?: throw android.os.ServiceSpecificException(
-75, // ATTESTATION_KEYS_NOT_PROVISIONED
"No attestation key for algorithm $algorithmName in $keyboxFile",
)
}
/** Retrieves the key pair and issuer name for a given attestation key alias. */
@@ -213,17 +219,16 @@ object CertificateGenerator {
uid: Int,
securityLevel: Int,
): Certificate {
val subject = params.certificateSubject ?: X500Name("CN=Android KeyStore Key")
val leafNotAfter =
(signingKeyPair.public as? X509Certificate)?.notAfter
?: Date(System.currentTimeMillis() + 31536000000L)
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
val notBefore = params.certificateNotBefore ?: Date(0)
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
val builder =
JcaX509v3CertificateBuilder(
issuer,
params.certificateSerial ?: BigInteger.ONE,
params.certificateNotBefore ?: Date(),
params.certificateNotAfter ?: leafNotAfter,
notBefore,
notAfter,
subject,
subjectKeyPair.public,
)
@@ -239,10 +244,10 @@ object CertificateGenerator {
)
val signerAlgorithm =
when (params.algorithm) {
Algorithm.EC -> "SHA256withECDSA"
Algorithm.RSA -> "SHA256withRSA"
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}")
when (signingKeyPair.private.algorithm) {
"EC", "ECDSA" -> "SHA256withECDSA"
"RSA" -> "SHA256withRSA"
else -> throw IllegalArgumentException("Unsupported signing key: ${signingKeyPair.private.algorithm}")
}
val contentSigner =
JcaContentSignerBuilder(signerAlgorithm)
@@ -0,0 +1,117 @@
package org.matrix.TEESimulator.pki
import java.io.ByteArrayInputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.KeyFactory
import java.security.KeyPair
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.spec.PKCS8EncodedKeySpec
import org.matrix.TEESimulator.logging.SystemLogger
data class CertGenConfig(
val algorithm: Int,
val keySize: Int,
val ecCurve: Int,
val rsaPublicExponent: Long,
val attestationChallenge: ByteArray?,
val purposes: IntArray,
val digests: IntArray,
val certSerial: ByteArray?,
val certSubject: ByteArray?,
val certNotBefore: Long,
val certNotAfter: Long,
val keyboxPrivateKey: ByteArray,
val keyboxCertChain: ByteArray,
val securityLevel: Int,
val attestVersion: Int,
val keymasterVersion: Int,
val osVersion: Int,
val osPatchLevel: Int,
val vendorPatchLevel: Int,
val bootPatchLevel: Int,
val bootKey: ByteArray,
val bootHash: ByteArray,
val creationDatetime: Long,
val attestationApplicationId: ByteArray,
val moduleHash: ByteArray?,
val idBrand: ByteArray?,
val idDevice: ByteArray?,
val idProduct: ByteArray?,
val idSerial: ByteArray?,
val idImei: ByteArray?,
val idMeid: ByteArray?,
val idManufacturer: ByteArray?,
val idModel: ByteArray?,
val idSecondImei: ByteArray?,
)
object NativeCertGen {
private const val LOG_DIR = "/data/adb/tricky_store/logs"
@Volatile
var isAvailable: Boolean = false
private set
fun initialize(libraryPath: String) {
try {
System.load(libraryPath)
initLogging(false, LOG_DIR)
isAvailable = true
SystemLogger.info("NativeCertGen: loaded libcertgen.so successfully")
} catch (e: UnsatisfiedLinkError) {
SystemLogger.error("NativeCertGen: failed to load libcertgen.so, falling back to BouncyCastle", e)
}
}
external fun generateAttestedKeyPair(config: CertGenConfig): ByteArray?
private external fun initLogging(verbose: Boolean, logDir: String): Boolean
private external fun dumpLogs(): String?
fun dump(): String? = if (isAvailable) dumpLogs() else null
fun parseNativeResult(bytes: ByteArray): Pair<KeyPair, List<Certificate>> {
val buf = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN)
val pkLen = buf.getInt()
if (pkLen < 0 || pkLen > buf.remaining()) {
throw IllegalStateException("Invalid private key length: $pkLen")
}
val pkBytes = ByteArray(pkLen)
buf.get(pkBytes)
val numCerts = buf.getInt()
if (numCerts < 0 || numCerts > buf.remaining()) {
throw IllegalStateException("Invalid cert count: $numCerts")
}
val certs = mutableListOf<Certificate>()
val certFactory = CertificateFactory.getInstance("X.509")
repeat(numCerts) {
val certLen = buf.getInt()
if (certLen < 0 || certLen > buf.remaining()) {
throw IllegalStateException("Invalid cert length: $certLen")
}
val certBytes = ByteArray(certLen)
buf.get(certBytes)
certs.add(certFactory.generateCertificate(ByteArrayInputStream(certBytes)))
}
if (certs.isEmpty()) {
throw IllegalStateException("No certificates in native result")
}
val algorithmName = when (certs[0].publicKey.algorithm) {
"EC", "ECDSA" -> "EC"
"RSA" -> "RSA"
else -> certs[0].publicKey.algorithm
}
val keyFactory = KeyFactory.getInstance(algorithmName)
val privateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(pkBytes))
val publicKey = certs[0].publicKey
return Pair(KeyPair(publicKey, privateKey), certs)
}
}
@@ -1,9 +1,11 @@
package org.matrix.TEESimulator.util
import android.content.pm.PackageManager
import android.hardware.security.keymint.SecurityLevel
import android.os.Build
import android.os.SystemProperties
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.security.MessageDigest
import java.time.LocalDate
import java.util.concurrent.ThreadLocalRandom
@@ -11,7 +13,6 @@ import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.DERSet
import org.matrix.TEESimulator.attestation.DeviceAttestationService
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger
@@ -90,27 +91,33 @@ object AndroidDeviceUtils {
attestationValueProvider: () -> ByteArray?,
expectedSize: Int,
): ByteArray {
// 1. Attempt to get the value from the system property.
getProperty(propertyName, expectedSize)?.let {
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
persistToFile(propertyName, it)
return it
}
// 2. Fallback to the value from a cached TEE attestation.
try {
attestationValueProvider()?.let {
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
setProperty(propertyName, it) // Persist for consistency
setProperty(propertyName, it)
persistToFile(propertyName, it)
return it
}
} catch (e: Exception) {
SystemLogger.error("Failed to get $propertyName from attestation.", e)
}
// 3. As a final fallback, generate a random value.
readFromFile(propertyName, expectedSize)?.let {
SystemLogger.debug("Using $propertyName from persistent file: ${it.toHex()}")
setProperty(propertyName, it)
return it
}
return generateRandomBytes(expectedSize).also {
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
setProperty(propertyName, it)
persistToFile(propertyName, it)
}
}
@@ -157,10 +164,37 @@ object AndroidDeviceUtils {
}
}
/** Generates a cryptographically random byte array of a specified length. */
private fun generateRandomBytes(size: Int): ByteArray =
ByteArray(size).also { ThreadLocalRandom.current().nextBytes(it) }
private val PERSIST_DIR = File("/data/adb/tricky_store")
private fun fileForProperty(propertyName: String): File = when (propertyName) {
"ro.boot.vbmeta.digest" -> File(PERSIST_DIR, "boot_hash.bin")
"ro.boot.vbmeta.public_key_digest" -> File(PERSIST_DIR, "boot_key.bin")
else -> File(PERSIST_DIR, "${propertyName.replace('.', '_')}.bin")
}
private fun persistToFile(propertyName: String, bytes: ByteArray) {
try {
fileForProperty(propertyName).writeBytes(bytes)
} catch (e: Exception) {
SystemLogger.error("Failed to persist $propertyName to file.", e)
}
}
private fun readFromFile(propertyName: String, expectedSize: Int): ByteArray? {
return try {
val file = fileForProperty(propertyName)
if (!file.exists()) return null
val bytes = file.readBytes()
if (bytes.size == expectedSize) bytes else null
} catch (e: Exception) {
SystemLogger.error("Failed to read $propertyName from file.", e)
null
}
}
// --- Patch Level Properties ---
fun getPatchLevel(uid: Int): Int {
@@ -239,11 +273,12 @@ object AndroidDeviceUtils {
val resolvedValue = resolveDateKeywords(value)
return when {
// "device_default" indicates falling back to the system property.
resolvedValue.equals("device_default", ignoreCase = true) -> null
// "no" indicates this value should not be reported.
// Resolve from live system prop — matches what detectors see via getprop,
// even when PIF has spoofed ro.build.version.security_patch via resetprop
resolvedValue.equals("prop", ignoreCase = true) ->
parsePatchLevelValue(SystemProperties.get("ro.build.version.security_patch", ""), isLong)
resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
// Otherwise, parse the resolved date string.
else -> parsePatchLevelValue(resolvedValue, isLong)
}
}
@@ -370,52 +405,174 @@ object AndroidDeviceUtils {
// --- APEX and Module Hash Properties ---
private val apexInfos: List<Pair<String, Long>> by lazy {
runCatching {
val pm = ConfigurationManager.getPackageManager()
val packages =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm?.getInstalledPackages(PackageManager.MATCH_APEX.toLong(), 0)
// Minimal protobuf parser for apex_manifest.pb (field 1: name, field 2: version)
private class MinimalApexManifestParser(private val data: ByteArray) {
var pos = 0
fun parse(): Pair<String, Long>? {
var name: String? = null
var version: Long? = null
while (pos < data.size) {
val tag = readVarint()
val fieldNum = tag ushr 3
val wireType = (tag and 0x07).toInt()
when (fieldNum) {
1L -> {
val length = readVarint().toInt()
if (pos + length > data.size) return null
name = String(data, pos, length, Charsets.UTF_8)
pos += length
}
2L -> {
version = readVarint()
}
else -> skipField(wireType)
}
}
return if (name != null && version != null) {
name to version
} else {
@Suppress("DEPRECATION")
pm?.getInstalledPackages(PackageManager.MATCH_APEX, 0)
null
}
packages?.list.orEmpty().map { it.packageName to it.longVersionCode }
}
.getOrElse {
SystemLogger.error("Failed to get APEX package information.", it)
emptyList()
private fun readVarint(): Long {
var value = 0L
var shift = 0
while (pos < data.size) {
val b = data[pos++].toInt()
value = value or ((b and 0x7F).toLong() shl shift)
if ((b and 0x80) == 0) return value
shift += 7
}
return value
}
private fun skipField(wireType: Int) {
when (wireType) {
0 -> readVarint()
1 -> pos += 8
2 -> {
val len = readVarint().toInt()
pos += len
}
5 -> pos += 4
else -> throw IllegalStateException("Unknown wire type $wireType")
}
}
}
private val apexInfos: List<Pair<String, Long>> by lazy {
val results = mutableListOf<Pair<String, Long>>()
val apexRoot = File("/apex")
if (!apexRoot.exists() || !apexRoot.isDirectory) {
return@lazy emptyList()
}
apexRoot.listFiles()?.forEach { file ->
if (!file.isDirectory) return@forEach
val name = file.name
if (name.startsWith(".")) return@forEach
if (name.contains("@")) return@forEach
if (name == "sharedlibs") return@forEach
val manifestFile = File(file, "apex_manifest.pb")
if (manifestFile.exists()) {
runCatching {
val bytes = FileInputStream(manifestFile).use { it.readBytes() }
val parser = MinimalApexManifestParser(bytes)
parser.parse()?.let { (pkgName, version) -> results.add(pkgName to version) }
}
}
}
results.distinctBy { it.first }
}
val moduleHash: ByteArray by lazy {
DeviceAttestationService.CachedAttestationData?.moduleHash
?: runCatching {
// TODO: figure out the correct calculation
val moduleSequences = ASN1EncodableVector()
data class ModuleEntry(
val nameEncoded: ByteArray,
val fullEncoded: ByteArray,
)
// 1. Create a DERSequence for each module.
apexInfos.forEach { (packageName, versionCode) ->
val moduleVector = ASN1EncodableVector()
// Use explicit UTF-8 encoding for the package name.
moduleVector.add(DEROctetString(packageName.toByteArray(Charsets.UTF_8)))
moduleVector.add(ASN1Integer(versionCode))
moduleSequences.add(DERSequence(moduleVector))
val modules =
apexInfos.map { (packageName, versionCode) ->
val nameOctet = DEROctetString(packageName.toByteArray(Charsets.UTF_8))
val versionInt = ASN1Integer(versionCode)
val vec = ASN1EncodableVector()
vec.add(nameOctet)
vec.add(versionInt)
val sequence = DERSequence(vec)
// AOSP sorts by encoded name only, not full sequence
ModuleEntry(
nameEncoded = nameOctet.encoded,
fullEncoded = sequence.encoded,
)
}
// 2. Create a DERSet. Bouncy Castle will automatically handle
// the sorting based on the DER-encoded value of each sequence.
val modulesSet = DERSet(moduleSequences)
val sortedModules =
modules.sortedWith { m1, m2 ->
compareByteArrays(m1.nameEncoded, m2.nameEncoded)
}
// 3. Get the final DER-encoded byte array of the SET.
val encodedModules = modulesSet.encoded
val payloadStream = ByteArrayOutputStream()
sortedModules.forEach { payloadStream.write(it.fullEncoded) }
val payload = payloadStream.toByteArray()
// 4. Compute the SHA-256 hash.
MessageDigest.getInstance("SHA-256").digest(encodedModules)
// Wrap in DER SET tag manually — DERSet() re-sorts by full encoding
val finalDerSet = encodeAsDerSet(payload)
MessageDigest.getInstance("SHA-256").digest(finalDerSet)
}
.getOrElse {
SystemLogger.error("Failed to compute module hash.", it)
ByteArray(32) // Return empty hash on failure
ByteArray(32)
}
}
private fun compareByteArrays(a: ByteArray, b: ByteArray): Int {
val length = minOf(a.size, b.size)
for (i in 0 until length) {
val byteA = a[i].toInt() and 0xFF
val byteB = b[i].toInt() and 0xFF
if (byteA != byteB) {
return byteA - byteB
}
}
return a.size - b.size
}
private fun encodeAsDerSet(payload: ByteArray): ByteArray {
val out = ByteArrayOutputStream()
out.write(0x31)
writeDerLength(out, payload.size)
out.write(payload)
return out.toByteArray()
}
private fun writeDerLength(out: ByteArrayOutputStream, length: Int) {
if (length < 128) {
out.write(length)
} else {
var size = length
val bytes = ArrayList<Byte>()
while (size > 0) {
bytes.add((size and 0xFF).toByte())
size = size ushr 8
}
out.write(0x80 or bytes.size)
for (i in bytes.indices.reversed()) {
out.write(bytes[i].toInt())
}
}
}
}
@@ -0,0 +1,72 @@
package org.matrix.TEESimulator.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import org.matrix.TEESimulator.logging.SystemLogger
object AndroidPermissionUtils {
@SuppressLint("PrivateApi", "DiscouragedPrivateApi")
private fun getGlobalContext(): Context? {
return try {
// 1. Get the hidden ActivityThread class via reflection
val activityThreadClass = Class.forName("android.app.ActivityThread")
// 2. Invoke the static currentActivityThread() method
val currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread")
currentActivityThreadMethod.isAccessible = true
val activityThread = currentActivityThreadMethod.invoke(null)
if (activityThread == null) {
SystemLogger.warning("Reflection: ActivityThread.currentActivityThread() returned null")
return null
}
// 3. Try to get the application context
val getApplicationMethod = activityThreadClass.getDeclaredMethod("getApplication")
getApplicationMethod.isAccessible = true
val application = getApplicationMethod.invoke(activityThread) as? Context
if (application != null) return application
// 4. Fallback to getSystemContext() if application is null (often happens in system_server)
val getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext")
getSystemContextMethod.isAccessible = true
getSystemContextMethod.invoke(activityThread) as? Context
} catch (e: Exception) {
SystemLogger.error("Reflection failed to get global context for permission check", e)
null
}
}
/**
* Core permission check.
*/
fun hasPermission(uid: Int, permission: String): Boolean {
val context = getGlobalContext() ?: run {
SystemLogger.warning("AndroidPermissionUtils: Context is null, failing permission check safely.")
return false
}
val result = context.checkPermission(permission, -1, uid)
return result == PackageManager.PERMISSION_GRANTED
}
fun hasDeviceAttestationPermission(uid: Int): Boolean {
return hasPermission(uid, "android.permission.READ_PRIVILEGED_PHONE_STATE")
}
fun hasUniqueIdAttestationPermission(uid: Int): Boolean {
return hasPermission(uid, "android.permission.REQUEST_UNIQUE_ID_ATTESTATION")
}
fun hasManageUsersPermission(uid: Int): Boolean {
return hasPermission(uid, "android.permission.MANAGE_USERS")
}
fun hasDumpPermission(uid: Int): Boolean {
return hasPermission(uid, "android.permission.DUMP")
}
}
@@ -6,7 +6,11 @@ package org.matrix.TEESimulator.util
*
* @return A new string with each line individually trimmed.
*/
fun String.trimLines(): String = this.trim().lines().joinToString("\n") { it.trim() }
fun String.trimLines(): String =
this.trim()
.lines()
.filter { !it.trim().startsWith("<!--") }
.joinToString("\n") { it.trim() }
/**
* Converts a ByteArray to its hexadecimal string representation.
@@ -0,0 +1,64 @@
package org.matrix.TEESimulator.util
import android.hardware.security.keymint.Algorithm
import java.security.SecureRandom
import java.util.concurrent.locks.LockSupport
import kotlin.math.abs
import kotlin.math.exp
import kotlin.math.ln
import kotlin.math.max
object TeeLatencySimulator {
private val rng = SecureRandom()
private val sessionBiasMs: Double by lazy { rng.nextGaussian() * 5.0 }
private val coldPenaltyMs: Double by lazy { abs(rng.nextGaussian() * 12.0) }
@Volatile private var firstCall = true
fun simulateGenerateKeyDelay(algorithm: Int, elapsedNanos: Long) {
val elapsedMs = elapsedNanos / 1_000_000.0
val targetMs = sampleTotalDelay(algorithm)
val remainingMs = targetMs - elapsedMs
if (remainingMs > 1.0) {
LockSupport.parkNanos((remainingMs * 1_000_000).toLong())
}
}
private fun sampleTotalDelay(algorithm: Int): Double {
val base = sampleBaseCryptoDelay(algorithm)
val transit = sampleExponential(2.5)
val jitter = (rng.nextGaussian() * 2.5).coerceIn(-8.0, 12.0)
var cold = 0.0
if (firstCall) {
firstCall = false
cold = coldPenaltyMs
}
return max(20.0, base + transit + jitter + sessionBiasMs + cold)
}
private fun sampleBaseCryptoDelay(algorithm: Int): Double {
val (mu, sigma) =
when (algorithm) {
Algorithm.EC -> ln(60.0) to 0.08
Algorithm.RSA -> ln(70.0) to 0.08
Algorithm.AES -> ln(35.0) to 0.10
else -> ln(40.0) to 0.10
}
return sampleLogNormal(mu, sigma)
}
private fun sampleLogNormal(mu: Double, sigma: Double): Double {
return exp(mu + sigma * rng.nextGaussian())
}
private fun sampleExponential(mean: Double): Double {
var u = rng.nextDouble()
while (u == 0.0) u = rng.nextDouble()
return -mean * ln(u)
}
}
+11
View File
@@ -0,0 +1,11 @@
#!/system/bin/sh
MODDIR=${0%/*}
CONFIG_DIR=/data/adb/tricky_store
if [ -d "$CONFIG_DIR/persistent_keys" ]; then
rm -rf "$CONFIG_DIR/persistent_keys"
mkdir -p "$CONFIG_DIR/persistent_keys"
echo "Persistent key storage cleared"
else
echo "No persistent key storage found"
fi
+178 -17
View File
@@ -1,26 +1,187 @@
## 🎉 TEESimulator v3.1: Legacy Support & Resilience
## TEESimulator-RS v5.0: AOSP Compliance Overhaul
This release marks a significant step forward in our mission, focusing on breathing life into devices with **broken TEEs** and extending full support to older Android versions (**Android 1012**).
Major release integrating 30+ AOSP compliance improvements from upstream PR #157 analysis, layered on top of our StrongBox hardening and native cert gen architecture.
### 🛡️ Enhanced Keystore2 Emulation
We have implemented critical APIs to support devices where the hardware TEE is broken or for applications configured to use key generation mode. These improvements directly address detection vectors identified in v3.0:
### Attestation Extension Alignment
- 17 enforcement tags added to KeyMintAttestation (ACTIVE_DATETIME, ORIGINATION_EXPIRE, USAGE_EXPIRE, USAGE_COUNT_LIMIT, CALLER_NONCE, UNLOCKED_DEVICE_REQUIRED, INCLUDE_UNIQUE_ID, ROLLBACK_RESISTANCE, EARLY_BOOT_ONLY, ALLOW_WHILE_ON_BODY, TRUSTED_USER_PRESENCE_REQUIRED, TRUSTED_CONFIRMATION_REQUIRED, NO_AUTH_REQUIRED, MAX_USES_PER_BOOT, MAX_BOOT_LEVEL, MIN_MAC_LENGTH, RSA_OAEP_MGF_DIGEST)
- BLOCK_MODE encoded as SET OF INTEGER per AOSP attestation_record.h
- Version-guarded tags (RSA_OAEP_MGF_DIGEST >=100, ROLLBACK_RESISTANCE >=3, EARLY_BOOT_ONLY >=4)
- INCLUDE_UNIQUE_ID computed via HMAC-SHA256 per KeyMint HAL spec using device HBK
- AAID gated on attestation challenge presence
- Certificate validity defaults aligned with AOSP (epoch notBefore, 9999-12-31 notAfter)
* **✅ Full Crypto Operations (`createOperation`)**: The simulator now correctly handles `SIGN`, `VERIFY`, `ENCRYPT`, and `DECRYPT` purposes for software-generated keys.
* **🔗 Certificate Chain Updates (`updateSubcomponent`)**: Added support for applications updating the certificate chain of virtual keys (e.g., via `KeyStore.setKeyEntry`).
* **📋 Enumeration Support (`listEntries`)**: Generated keys are now properly visible in enumeration APIs like `KeyStore.aliases()`, thanks to the implementation of `listEntries` and `listEntriesBatched`.
### Binder Infrastructure
- Native transaction code filtering at C++ level, skipping JNI for non-intercepted codes
- getNumberOfEntries includes software-generated key count
- deleteKey resolves KEY_ID domain via generatedKeys lookup
- patchAuthorizations for OS/VENDOR/BOOT patch levels in authorization arrays
### 🔧 Compatibility & Stability
Weve ironed out crashes and architecture-specific bugs to ensure a smooth experience across more devices:
### Software Operation AOSP Conformance
- updateAad on non-AEAD operations returns INVALID_TAG (-76), matching AOSP operation.rs
- All crypto exceptions wrapped as ServiceSpecificException with correct KeyMint error codes
- GCM IV returned in CreateOperationResponse.parameters for encrypt operations
- SoftwareOperationBinder methods @Synchronized, matching AOSP Mutex per operation
- authorize_create enforcement: PURPOSE validation, algorithm-purpose compatibility, temporal constraints, CALLER_NONCE prohibition, WRAP_KEY rejection
* **Android 10**: Fixed a crash caused by the missing `waitForService` method.
* **Android 11**: Implemented environment initialization and daemon UID spoofing to successfully bypass keystore generation permission checks.
* **ARM 32-bit (Android 12)**: Resolved `ptrace` compatibility issues by falling back to `PTRACE_GETREGS` and `PTRACE_SETREGS`.
* **x86_64 Emulators**: Enforced respect for the stack pointer "red zone" and added a staging fallback mechanism for file descriptor transfering of `libTEESimulator.so`.
### Security and Configuration
- SELinux permission checks via /proc/pid/attr/current
- Per-UID permission verification through IPackageManager.checkPermission
- Imported key tracking prevents stale attest-key overrides in getKeyEntry
- nspace consistency fix in attest-key override path
- TeeLatencySimulator with log-normal distribution matching real hardware profiles
- Device-unique HBK seed generated on install (32 bytes from /dev/random)
### 🚀 The Road Ahead
### Preserved from v4.8
- StrongBox op limits (4 concurrent max, TOO_MANY_OPERATIONS rejection)
- LRU operation pruning per security level
- Hardware keygen rate limiting (2/30s sliding window, 2 concurrent cap)
- Native Rust cert generation with BouncyCastle fallback
- Key persistence across reboots
We are aware of the remaining detection vectors (see the issues list) and have clear solutions mapped out for the next release.
---
Google's aggressive push for **Remote Key Provisioning (RKP)** and the drying up of leaked keyboxes is **not** the end for TEESimulator. Our ultimate goal remains unchanged: defeating Keystore attestation **without relying on a valid keybox**.
## TEESimulator-RS v4.8.1: StrongBox Op Rejection Fix
We are inching closer to this milestone, but the fight for device freedom is complex and resource-intensive. Your patience and support (both time and financial) are vital as we conquer these new challenges.
- **StrongBox op limit gate fix**`trackAndEnforceOpLimit` was only called in the `Domain.KEY_ID` not-found path, so software-generated keys (found via `Domain.APP`) bypassed `STRONGBOX_MAX_CONCURRENT_OPS=4` entirely. DuckDetector's concurrent signing handles test created 24+ operations that all succeeded via LRU pruning instead of being rejected with `TOO_MANY_OPERATIONS (-29)`. Now enforced for all StrongBox createOperation paths.
---
## TEESimulator-RS v4.8: StrongBox Hardening & LRU Pruning
Tested against DuckDetector on OnePlus (Android 16, KSU). Tamper score dropped from 32 to 8.
- **LRU operation pruning** — Concurrent software operations capped at 15 per UID (TEE) and 4 per UID (StrongBox), with oldest-first eviction. Pruned operations return `INVALID_OPERATION_HANDLE (-28)`, matching AOSP keystore2 malus-based pruning.
- **StrongBox param guard** — Unsupported StrongBox params (RSA >2048-bit, non-P256 EC curves) forwarded to real HAL for proper rejection instead of generating in software.
- **StrongBox timing** — Key generation floors at 250ms, signing at 80ms on StrongBox security level to match real secure element latency.
- **StrongBox op limit** — Sliding-window enforcer caps concurrent StrongBox operations for both software and hardware key paths, returning `TOO_MANY_OPERATIONS (-29)` when exceeded.
- **ECDSA algorithm alias** — Accept "ECDSA" in addition to "EC" as JCA private key algorithm name. Fixes SIGSEGV crash on Android 10 devices where the provider reports EC keys as "ECDSA". Closes #4.
- **createOperation domain handling** — Software-generated keys now found via both `Domain.APP` (alias) and `Domain.KEY_ID` (nspace) lookup paths.
- **Permission guards** — Device ID attestation tags (IMEI, MEID, serial) require caller permission checks.
---
## TEESimulator-RS v4.7: Operation & Attestation Fixes
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) and [Key Attestation](https://github.com/nickel-lang/nickel) on OnePlus (Android 16) and Xiaomi Redmi 14C (Android 14).
- **PADDING encoding** — Fixed ASN.1 encoding of PADDING tag in attestation extension from individual `[6] INTEGER` entries to `[6] SET OF INTEGER`, matching AOSP `attestation_record.h` schema. Broke all RSA key attestation since v4.6.
- **Operation error-path conformance** — Software operations now track finalized state and return `INVALID_OPERATION_HANDLE (-28)` on post-abort calls. Input length guard (32KB) returns `TOO_MUCH_DATA` matching AOSP `operation.rs`. Passes KeyDetector's OperationErrorPathChecker.
- **updateAad support** — Added `updateAad` to `SoftwareOperationBinder`, fixing `AbstractMethodError` on Android 16 where the runtime Stub declares it abstract.
- **Algorithm inference**`createOperation` now infers algorithm from the stored key pair when operation params omit the ALGORITHM tag, matching AOSP behavior.
---
## TEESimulator-RS v4.6: Rebrand & Detection Fix
- **RTT normalization rework** — Replaced Gaussian sleep (mean=55ms) with a 15ms floor fence. The old approach triggered Chunqiu Native Check 2.8 timing analysis; the floor-only approach satisfies the minimum RTT threshold without creating a detectable delay pattern.
- **Cross-algorithm attestation** — Signing algorithm now derived from the attestation key's actual type, not the generated key's algorithm. Fixes BouncyCastle crash when signing RSA keys with EC attestation keys (Shizuku attestation flow).
- **Device ID attestation** — Serial/IMEI/MEID/secondImei tags now flow through to software cert gen instead of blanket rejection. Only DEVICE_UNIQUE_ATTESTATION is rejected, matching AOSP keystore2 policy.
- **Rebrand to TEESimulator-RS** — Distinguishes this fork from upstream. Version scheme simplified to v{major}.{minor}-{commitCount}.
- **CI streamlined** — Release pipeline uses Gradle-generated filenames directly, eliminating the rename step.
---
## TEESimulator v4.5: Detection Hardening
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) (23-check attestation validator). All keystore-level checks now pass.
- **Key deletion consistency** — After deleting a software-generated key, `getKeyEntry` now correctly returns `KEY_NOT_FOUND` instead of falling through to a stale live-patch fallback. Fixes binder consistency checks that detect ghost key responses.
- **generateKey timing normalization** — Software key generation RTT now matches real TEE latency profile (Gaussian distribution, mean=55ms, floor=15ms). Previously completed in ~4ms, which is an immediate timing side-channel.
- **Delete cleanup scope**`deleteKey` now clears all cached state (patched chains, attestation keys) regardless of whether the key was software or hardware-generated.
---
## TEESimulator v4.4: AOSP Conformance
- **Binder error reply format** — Aligned EX_SERVICE_SPECIFIC wire layout with AOSP Status.cpp, including the remote stack trace header field.
- **Key enumeration** — Corrected list_past_alias pagination order to match AOSP database.rs semantics.
- **KeyMetadata fields** — Generated key responses now include modificationTimeMs, Tag.ORIGIN, and normalized KeyDescriptor fields per AOSP Keystore2.
- **Parcel handling** — hasException() preserves reply position for downstream consumers.
---
## TEESimulator v4.3: Performance & Reliability
- **Debug log gating**`SystemLogger.debug()` now skipped entirely in release builds, eliminating unnecessary logcat syscalls on every intercepted transaction.
- **Supervisor backoff** — Exponential restart delay (500ms → 30s cap) prevents CPU spin if the daemon crashes repeatedly. Resets automatically once stable.
- **Process priority** — Daemon runs at nice=10, yielding CPU to foreground apps on constrained devices.
- **Map eviction** — Rate limiter and file lock maps now evict stale entries instead of growing unbounded.
- **CI pipeline** — Single-trigger build→release pipeline with proper changelog extraction and correctly sized artifacts.
---
## TEESimulator v4.2: Detection Evasion Hardening
Fixes 6 detection vectors flagged by attestation validator apps.
### Attestation Policy Enforcement
Replicate AOSP keystore2's `add_required_parameters()` validation that our software keygen path was bypassing:
- **CREATION_DATETIME** — Reject caller-provided input with `INVALID_ARGUMENT (20)`, matching `security_level.rs:424`. Our cert gen still adds its own timestamp, same as real keystore2.
- **Device ID attestation** — Reject ATTESTATION_ID_SERIAL, IMEI, MEID, SECOND_IMEI, and DEVICE_UNIQUE_ATTESTATION with `CANNOT_ATTEST_IDS (-66)`. No consumer app has READ_PRIVILEGED_PHONE_STATE.
- **Error reply format** — Fixed AIDL ServiceSpecificException parcel write order (was errorCode→message, now message→errorCode).
### Certificate Fix
Leaf certificate Subject CN corrected from "Android KeyStore Key" to "Android Keystore Key" (lowercase s), matching AOSP `KeyGenParameterSpec.java:282`. Both Kotlin and Rust paths.
### Binder Timing
Skip interception for system transaction codes (PING, INTERFACE, DUMP) above LAST_CALL_TRANSACTION. Eliminates the JNI round-trip that inflated binder ping ratio to 3.85x (detector threshold: 3.0x).
---
## TEESimulator v4.1: Boot Identity Persistence
Bugfix release. The vbmeta boot key digest was randomizing on every reboot, producing a different RootOfTrust in attestation certificates each boot.
On devices where the kernel doesn't set `ro.boot.vbmeta.public_key_digest`, the fallback chain hit random generation every boot because `resetprop` overrides for `ro.boot.*` props don't survive reboots. Added file-based persistence (`boot_hash.bin`, `boot_key.bin`) between the TEE cache and random fallback. Once determined, boot identity values persist across reboots.
Verified on Redmi 14C: second boot reads from persistent file instead of regenerating.
---
## TEESimulator v4.0: Native Rust Cert Generation
Major release. Certificate chain generation rebuilt from the ground up in Rust, replacing the BouncyCastle Java path for EC and RSA keys. Hardened against every known detector app.
### Native Cert Generation
The headline feature. `libcertgen.so` generates X.509 certificate chains using `ring` (EC-P256/P384) and `rsa` (RSA-2048/4096) with manual DER assembly. No more BouncyCastle quirks — issuer/subject DN bytes are injected directly from the keybox, ensuring byte-perfect chain linkage. BouncyCastle remains as fallback for unsupported curves (P-224, P-521, Curve25519).
### Anti-Detection Hardening
- **Challenge validation** — Oversized attestation challenges (>128 bytes) now return `INVALID_INPUT_LENGTH (-21)`, matching real KeyMint behavior. Previously accepted silently — DuckDetector exploited this.
- **Per-UID rate limiter** — 2 hardware keygens per 30s burst, 2 concurrent max. Overflow falls back to software certs. Blocks DuckDetector-style keygen flooding that starves GMS.
- **importKey eviction guard** — Retained patch chains prevent generate-then-import attacks that evict cached attestation data.
- **256KB native payload cap** — Oversized binder payloads bypass interception cleanly instead of stalling threads.
- **Alias size rejection** — Oversized key aliases rejected before they hit the binder buffer.
### Key Persistence
Generated keys now survive reboots. File-backed storage with file-level locking, preserved across keybox rotations. Banking and biometric apps that cache attestation keys no longer break after restart.
### Attestation Fixes
- Null out all-zero `verifiedBootHash` from TEE cache (fingerprinting vector)
- Correct `module_hash` field to match AOSP Keystore2 format
- Override pre-existing attest keys instead of skipping them
- Strip HTML comments from PEM blocks in keybox parsing
- Security patch consistency — `system=prop` forces boot/vendor to match
### Module Lifecycle
- Supervisor daemon keeps the interceptor alive
- KSU Action button clears persistent key cache
- Clean uninstall removes all traces (persistent keys, TEE status, daemon)
### Stability
- FileObserver NPE on config deletion fixed
- Global uncaught exception handler — daemon stays alive on unexpected errors
- PEM parsing hardened against malformed keybox files
### Tested Against
DuckDetector, Luna, Play Integrity, Key Attestation Demo — all passing on Redmi 14C (Android 14, Beanpod KeyMaster, KSU).
+13 -2
View File
@@ -15,7 +15,7 @@ fi
# --- Version Info ---
VERSION=$(grep_prop version "${TMPDIR}/module.prop")
ui_print "- Installing TEESimulator $VERSION"
ui_print "- Installing TEESimulator-RS $VERSION"
ui_print ""
# --- Architecture Handling ---
@@ -48,7 +48,7 @@ install_file() {
# --- Installation ---
ui_print "- Extracting module files"
for file in customize.sh module.prop service.sh sepolicy.rule daemon; do
for file in customize.sh module.prop service.sh sepolicy.rule daemon action.sh uninstall.sh; do
install_file "$file" "$MODPATH"
done
@@ -67,10 +67,14 @@ ui_print ""
ui_print "- Extracting $ARCH libraries"
install_file "lib/$ABI_DIR/libTEESimulator.so" "$MODPATH"
install_file "lib/$ABI_DIR/libinject.so" "$MODPATH"
install_file "lib/$ABI_DIR/libsupervisor.so" "$MODPATH"
install_file "lib/$ABI_DIR/libcertgen.so" "$MODPATH"
ui_print ""
mv "$MODPATH/libinject.so" "$MODPATH/inject"
mv "$MODPATH/libsupervisor.so" "$MODPATH/supervisor"
chmod 755 "$MODPATH/inject"
chmod 755 "$MODPATH/supervisor"
# --- Configuration Files ---
if [ ! -d "$CONFIG_DIR" ]; then
@@ -87,3 +91,10 @@ if [ ! -f "$CONFIG_DIR/target.txt" ]; then
ui_print "- Adding default target scope"
install_file "target.txt" "$CONFIG_DIR"
fi
rm -f "$CONFIG_DIR/tee_status.txt"
if [ ! -f "$CONFIG_DIR/hbk" ]; then
ui_print "- Generating device-unique hardware-bound key seed"
head -c 32 /dev/random > "$CONFIG_DIR/hbk"
fi
+3 -3
View File
@@ -1,7 +1,7 @@
id=tricky_store
name=TEESimulator
name=TEESimulator-RS
version=${REPLACEMEVER}
versionCode=${REPLACEMEVERCODE}
author=JingMatrix
author=JingMatrix, Enginex0
description=Software simulation for Android hardware-backed key pairs with key attestation
updateJson=https://raw.githubusercontent.com/JingMatrix/TEESimulator/main/module/update.json
updateJson=https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/update.json
+2 -8
View File
@@ -1,11 +1,5 @@
DEBUG=false
MODDIR=${0%/*}
cd $MODDIR
while true; do
./daemon "$MODDIR" || exit 1
# ensure keystore initialized
sleep 2
done &
# Fork-based supervisor for instant restart
./supervisor ./daemon "$MODDIR" &
+12
View File
@@ -0,0 +1,12 @@
#!/system/bin/sh
MODDIR=${0%/*}
CONFIG_DIR=/data/adb/tricky_store
# Kill daemon and supervisor
for pid in $(pidof TEESimulator) $(pidof supervisor) $(pidof daemon); do
kill -9 "$pid" 2>/dev/null
done
rm -rf "$CONFIG_DIR/persistent_keys"
rm -f "$CONFIG_DIR/tee_status.txt"
rm -f "$CONFIG_DIR/boot_hash.bin" "$CONFIG_DIR/boot_key.bin"
+4 -4
View File
@@ -1,6 +1,6 @@
{
"version": "v3.1",
"versionCode": 59,
"zipUrl": "https://github.com/JingMatrix/TEESimulator/releases/download/v3.1/TEESimulator-v3.1-59-Release.zip",
"changelog": "https://raw.githubusercontent.com/JingMatrix/TEESimulator/main/module/changelog.md"
"version": "v4.5",
"versionCode": 111,
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.5/TEESimulator-v4.5-Release.zip",
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md"
}
+11
View File
@@ -0,0 +1,11 @@
[target.aarch64-linux-android]
linker = "aarch64-linux-android29-clang"
[target.armv7-linux-androideabi]
linker = "armv7a-linux-androideabi29-clang"
[target.i686-linux-android]
linker = "i686-linux-android29-clang"
[target.x86_64-linux-android]
linker = "x86_64-linux-android29-clang"
+1166
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
[package]
name = "certgen"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
jni = { version = "0.21.1", default-features = false }
ring = "0.17.14"
rsa = { version = "0.9", features = ["sha2"] }
pkcs8 = { version = "0.10", features = ["alloc"] }
rand = "0.8"
der = { version = "0.7.10", features = ["alloc", "oid"] }
const-oid = "0.9.6"
x509-cert = { version = "0.2.5", features = ["pem"] }
time = { version = "0.3", features = ["std"] }
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
libc = "0.2"
zip = { version = "2.2", default-features = false, features = ["deflate"] }
serde_json = "1.0"
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
strip = "symbols"
panic = "abort"
+8
View File
@@ -0,0 +1,8 @@
[toolchain]
channel = "stable"
targets = [
"aarch64-linux-android",
"armv7-linux-androideabi",
"i686-linux-android",
"x86_64-linux-android",
]
+644
View File
@@ -0,0 +1,644 @@
use crate::error::Result;
use crate::types::CertGenParams;
const DO_NOT_REPORT: i32 = -1;
pub fn build_attestation_extension(params: &CertGenParams) -> Result<Vec<u8>> {
let sw = build_software_enforced(params)?;
let tee = build_tee_enforced(params)?;
let mut inner = Vec::new();
// attestationVersion — INTEGER
inner.extend_from_slice(&enc_integer(params.attest_version as i64));
// attestationSecurityLevel — ENUMERATED, not INTEGER
inner.extend_from_slice(&enc_enumerated(params.security_level));
// keymintVersion — INTEGER
inner.extend_from_slice(&enc_integer(params.keymaster_version as i64));
// keymintSecurityLevel — ENUMERATED, not INTEGER
inner.extend_from_slice(&enc_enumerated(params.security_level));
// attestationChallenge — OCTET STRING
inner.extend_from_slice(&enc_octet_string(
params.attestation_challenge.as_deref().unwrap_or(&[]),
));
// uniqueId — OCTET STRING (always empty)
inner.extend_from_slice(&enc_octet_string(&[]));
// softwareEnforced
inner.extend_from_slice(&sw);
// teeEnforced
inner.extend_from_slice(&tee);
Ok(enc_sequence(&inner))
}
fn build_software_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
let mut fields: Vec<(u32, Vec<u8>)> = Vec::new();
// Tag 701: CREATION_DATETIME — INTEGER (milliseconds)
fields.push((701, enc_integer(params.creation_datetime)));
// Tag 709: ATTESTATION_APPLICATION_ID — OCTET STRING
// The bytes are already the DER-encoded AttestationApplicationId wrapped in OCTET STRING
// by the Kotlin layer. We wrap them in an EXPLICIT tag.
if !params.attestation_application_id.is_empty() {
fields.push((709, enc_octet_string(&params.attestation_application_id)));
}
// Tag 724: MODULE_HASH — OCTET STRING (only if attestVersion >= 400)
if params.attest_version >= 400 {
if let Some(ref hash) = params.module_hash {
fields.push((724, enc_octet_string(hash)));
}
}
Ok(build_authorization_list(&mut fields))
}
fn build_tee_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
let mut fields: Vec<(u32, Vec<u8>)> = Vec::new();
// Tag 1: PURPOSE — SET OF INTEGER
if !params.purposes.is_empty() {
fields.push((1, build_set_of_integer(&params.purposes)));
}
// Tag 2: ALGORITHM — INTEGER
fields.push((2, enc_integer(params.algorithm as i32 as i64)));
// Tag 3: KEY_SIZE — INTEGER
fields.push((3, enc_integer(params.key_size as i64)));
// Tag 5: DIGEST — SET OF INTEGER
if !params.digests.is_empty() {
fields.push((5, build_set_of_integer(&params.digests)));
}
// Tag 10: EC_CURVE — INTEGER (only for EC keys)
if let Some(curve) = params.ec_curve {
fields.push((10, enc_integer(curve as i32 as i64)));
}
// Tag 503: NO_AUTH_REQUIRED — NULL (presence = true)
fields.push((503, enc_null()));
// Tag 702: ORIGIN — INTEGER 0 (GENERATED)
fields.push((702, enc_integer(0)));
// Tag 704: ROOT_OF_TRUST — SEQUENCE
fields.push((704, build_root_of_trust(params)));
// Tag 705: OS_VERSION — INTEGER
if params.os_version != DO_NOT_REPORT {
fields.push((705, enc_integer(params.os_version as i64)));
}
// Tag 706: OS_PATCHLEVEL — INTEGER
if params.os_patch_level != DO_NOT_REPORT {
fields.push((706, enc_integer(params.os_patch_level as i64)));
}
// Tags 710-717: ATTESTATION_ID_* — OCTET STRING (optional)
if let Some(ref v) = params.id_brand {
fields.push((710, enc_octet_string(v)));
}
if let Some(ref v) = params.id_device {
fields.push((711, enc_octet_string(v)));
}
if let Some(ref v) = params.id_product {
fields.push((712, enc_octet_string(v)));
}
if let Some(ref v) = params.id_serial {
fields.push((713, enc_octet_string(v)));
}
if let Some(ref v) = params.id_imei {
fields.push((714, enc_octet_string(v)));
}
if let Some(ref v) = params.id_meid {
fields.push((715, enc_octet_string(v)));
}
if let Some(ref v) = params.id_manufacturer {
fields.push((716, enc_octet_string(v)));
}
if let Some(ref v) = params.id_model {
fields.push((717, enc_octet_string(v)));
}
// Tag 718: VENDOR_PATCHLEVEL — INTEGER
if params.vendor_patch_level != DO_NOT_REPORT {
fields.push((718, enc_integer(params.vendor_patch_level as i64)));
}
// Tag 719: BOOT_PATCHLEVEL — INTEGER
if params.boot_patch_level != DO_NOT_REPORT {
fields.push((719, enc_integer(params.boot_patch_level as i64)));
}
// Tag 723: ATTESTATION_ID_SECOND_IMEI — OCTET STRING (only if attestVersion >= 300)
if params.attest_version >= 300 {
if let Some(ref v) = params.id_second_imei {
fields.push((723, enc_octet_string(v)));
}
}
Ok(build_authorization_list(&mut fields))
}
fn build_root_of_trust(params: &CertGenParams) -> Vec<u8> {
let mut inner = Vec::new();
// verifiedBootKey — OCTET STRING (32 bytes)
inner.extend_from_slice(&enc_octet_string(&params.boot_key));
// deviceLocked — BOOLEAN TRUE (0xFF, not 0x01)
inner.extend_from_slice(&enc_boolean(true));
// verifiedBootState — ENUMERATED 0 (Verified), not INTEGER
inner.extend_from_slice(&enc_enumerated(0));
// verifiedBootHash — OCTET STRING (32 bytes)
inner.extend_from_slice(&enc_octet_string(&params.boot_hash));
enc_sequence(&inner)
}
fn build_authorization_list(fields: &mut Vec<(u32, Vec<u8>)>) -> Vec<u8> {
fields.sort_by_key(|(tag, _)| *tag);
let mut inner = Vec::new();
for (tag, value) in fields.iter() {
inner.extend_from_slice(&enc_explicit_tag(*tag, value));
}
enc_sequence(&inner)
}
fn build_set_of_integer(values: &[i32]) -> Vec<u8> {
// DER SET OF: elements sorted by encoded byte value
let mut encoded: Vec<Vec<u8>> = values.iter().map(|v| enc_integer(*v as i64)).collect();
encoded.sort();
let mut inner = Vec::new();
for e in &encoded {
inner.extend_from_slice(e);
}
enc_set(&inner)
}
// --- DER primitives ---
fn enc_length(len: usize) -> Vec<u8> {
if len < 0x80 {
vec![len as u8]
} else if len <= 0xFF {
vec![0x81, len as u8]
} else if len <= 0xFFFF {
vec![0x82, (len >> 8) as u8, len as u8]
} else if len <= 0xFF_FFFF {
vec![0x83, (len >> 16) as u8, (len >> 8) as u8, len as u8]
} else {
vec![
0x84,
(len >> 24) as u8,
(len >> 16) as u8,
(len >> 8) as u8,
len as u8,
]
}
}
fn enc_integer(value: i64) -> Vec<u8> {
// DER INTEGER: tag 0x02, minimal two's complement big-endian
let bytes = integer_bytes(value);
let mut out = vec![0x02];
out.extend_from_slice(&enc_length(bytes.len()));
out.extend_from_slice(&bytes);
out
}
fn integer_bytes(value: i64) -> Vec<u8> {
if value == 0 {
return vec![0x00];
}
let raw = value.to_be_bytes();
// Find first significant byte
let mut start = 0;
if value > 0 {
while start < 7 && raw[start] == 0x00 {
start += 1;
}
// If high bit set, need leading 0x00 to keep positive
if raw[start] & 0x80 != 0 {
let mut out = vec![0x00];
out.extend_from_slice(&raw[start..]);
return out;
}
} else {
while start < 7 && raw[start] == 0xFF {
start += 1;
}
// If high bit clear, need leading 0xFF to keep negative
if raw[start] & 0x80 == 0 {
let mut out = vec![0xFF];
out.extend_from_slice(&raw[start..]);
return out;
}
}
raw[start..].to_vec()
}
fn enc_enumerated(value: i32) -> Vec<u8> {
// DER ENUMERATED: tag 0x0A, same value encoding as INTEGER
let bytes = integer_bytes(value as i64);
let mut out = vec![0x0A];
out.extend_from_slice(&enc_length(bytes.len()));
out.extend_from_slice(&bytes);
out
}
fn enc_octet_string(data: &[u8]) -> Vec<u8> {
let mut out = vec![0x04];
out.extend_from_slice(&enc_length(data.len()));
out.extend_from_slice(data);
out
}
fn enc_null() -> Vec<u8> {
vec![0x05, 0x00]
}
fn enc_boolean(value: bool) -> Vec<u8> {
// DER BOOLEAN: TRUE = 0xFF, FALSE = 0x00
vec![0x01, 0x01, if value { 0xFF } else { 0x00 }]
}
fn enc_sequence(contents: &[u8]) -> Vec<u8> {
let mut out = vec![0x30];
out.extend_from_slice(&enc_length(contents.len()));
out.extend_from_slice(contents);
out
}
fn enc_set(contents: &[u8]) -> Vec<u8> {
let mut out = vec![0x31];
out.extend_from_slice(&enc_length(contents.len()));
out.extend_from_slice(contents);
out
}
fn enc_explicit_tag(tag_number: u32, inner: &[u8]) -> Vec<u8> {
// EXPLICIT context-specific constructed tag
let mut out = Vec::new();
if tag_number < 31 {
// Short form: single byte 0xA0 | tag_number
out.push(0xA0 | tag_number as u8);
} else {
// Long form: 0xBF followed by base-128 encoding of tag number
out.push(0xBF);
enc_base128_tag(&mut out, tag_number);
}
out.extend_from_slice(&enc_length(inner.len()));
out.extend_from_slice(inner);
out
}
fn enc_base128_tag(out: &mut Vec<u8>, tag: u32) {
// Base-128 with continuation bits: MSB first, bit 7 set on all but last byte
let mut digits = Vec::new();
let mut val = tag;
digits.push((val & 0x7F) as u8);
val >>= 7;
while val > 0 {
digits.push((val & 0x7F) as u8 | 0x80);
val >>= 7;
}
// Written MSB first
for b in digits.iter().rev() {
out.push(*b);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Algorithm, EcCurve};
#[test]
fn test_enc_integer_zero() {
assert_eq!(enc_integer(0), vec![0x02, 0x01, 0x00]);
}
#[test]
fn test_enc_integer_small_positive() {
assert_eq!(enc_integer(3), vec![0x02, 0x01, 0x03]);
assert_eq!(enc_integer(127), vec![0x02, 0x01, 0x7F]);
}
#[test]
fn test_enc_integer_needs_leading_zero() {
// 128 = 0x80, high bit set so needs 0x00 prefix
assert_eq!(enc_integer(128), vec![0x02, 0x02, 0x00, 0x80]);
assert_eq!(enc_integer(256), vec![0x02, 0x02, 0x01, 0x00]);
}
#[test]
fn test_enc_integer_multi_byte() {
// 140000 = 0x02_22_E0
assert_eq!(enc_integer(140000), vec![0x02, 0x03, 0x02, 0x22, 0xE0]);
}
#[test]
fn test_enc_integer_large() {
// 20250301 = 0x01_34_FE_BD
assert_eq!(
enc_integer(20250301),
vec![0x02, 0x04, 0x01, 0x34, 0xFE, 0xBD]
);
}
#[test]
fn test_enc_enumerated() {
// SecurityLevel TEE = 1
assert_eq!(enc_enumerated(1), vec![0x0A, 0x01, 0x01]);
// VerifiedBootState Verified = 0
assert_eq!(enc_enumerated(0), vec![0x0A, 0x01, 0x00]);
}
#[test]
fn test_enc_boolean_true() {
// DER: TRUE = 0xFF
assert_eq!(enc_boolean(true), vec![0x01, 0x01, 0xFF]);
}
#[test]
fn test_enc_null() {
assert_eq!(enc_null(), vec![0x05, 0x00]);
}
#[test]
fn test_enc_octet_string_empty() {
assert_eq!(enc_octet_string(&[]), vec![0x04, 0x00]);
}
#[test]
fn test_enc_explicit_tag_short() {
// Tag 1 wrapping INTEGER 2: A1 03 02 01 02
let inner = enc_integer(2);
let tagged = enc_explicit_tag(1, &inner);
assert_eq!(tagged, vec![0xA1, 0x03, 0x02, 0x01, 0x02]);
}
#[test]
fn test_enc_explicit_tag_10() {
// Tag 10: 0xAA
let inner = enc_integer(1);
let tagged = enc_explicit_tag(10, &inner);
assert_eq!(tagged[0], 0xAA);
}
#[test]
fn test_enc_explicit_tag_503() {
// Tag 503: 0xBF 0x83 0x77
// 503 = 3*128 + 119 => 0x83 0x77
let inner = enc_null();
let tagged = enc_explicit_tag(503, &inner);
assert_eq!(&tagged[..3], &[0xBF, 0x83, 0x77]);
}
#[test]
fn test_enc_explicit_tag_704() {
// Tag 704: 0xBF 0x85 0x40
// 704 = 5*128 + 64 => 0x85 0x40
let inner = enc_sequence(&[]);
let tagged = enc_explicit_tag(704, &inner);
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x40]);
}
#[test]
fn test_enc_explicit_tag_718() {
// Tag 718: 0xBF 0x85 0x4E
let inner = enc_integer(20250301);
let tagged = enc_explicit_tag(718, &inner);
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x4E]);
}
#[test]
fn test_enc_explicit_tag_719() {
// Tag 719: 0xBF 0x85 0x4F
let inner = enc_integer(20250301);
let tagged = enc_explicit_tag(719, &inner);
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x4F]);
}
#[test]
fn test_enc_explicit_tag_701() {
// Tag 701: 0xBF 0x85 0x3D
let inner = enc_integer(1000);
let tagged = enc_explicit_tag(701, &inner);
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x3D]);
}
#[test]
fn test_enc_explicit_tag_709() {
// Tag 709: 0xBF 0x85 0x45
let inner = enc_octet_string(&[0x01]);
let tagged = enc_explicit_tag(709, &inner);
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x45]);
}
#[test]
fn test_build_set_of_integer_sorted() {
// SET OF INTEGER must sort by encoded bytes
let result = build_set_of_integer(&[3, 2]);
// Expect sorted: INTEGER 2 before INTEGER 3
let expected = enc_set(&[0x02, 0x01, 0x02, 0x02, 0x01, 0x03]);
assert_eq!(result, expected);
}
#[test]
fn test_root_of_trust_structure() {
let params = make_test_params();
let rot = build_root_of_trust(&params);
// Should be a SEQUENCE (0x30)
assert_eq!(rot[0], 0x30);
// Find BOOLEAN TRUE inside
let rot_inner = &rot[2..]; // skip tag+length
// First: OCTET STRING (32 bytes boot key)
assert_eq!(rot_inner[0], 0x04);
assert_eq!(rot_inner[1], 0x20); // 32 bytes
// After boot key (34 bytes): BOOLEAN TRUE
assert_eq!(rot_inner[34], 0x01); // BOOLEAN tag
assert_eq!(rot_inner[35], 0x01); // length 1
assert_eq!(rot_inner[36], 0xFF); // TRUE = 0xFF
// Then ENUMERATED 0 (verifiedBootState)
assert_eq!(rot_inner[37], 0x0A); // ENUMERATED tag, not 0x02
assert_eq!(rot_inner[38], 0x01);
assert_eq!(rot_inner[39], 0x00);
}
#[test]
fn test_do_not_report_omits_fields() {
let mut params = make_test_params();
params.os_patch_level = DO_NOT_REPORT;
params.vendor_patch_level = DO_NOT_REPORT;
params.boot_patch_level = DO_NOT_REPORT;
let tee = build_tee_enforced(&params).unwrap();
let hex = hex_string(&tee);
// Tags 706, 718, 719 should not appear
// Tag 706 = BF 85 42, 718 = BF 85 4E, 719 = BF 85 4F
assert!(!hex.contains("bf8542"), "os_patch_level should be omitted");
assert!(
!hex.contains("bf854e"),
"vendor_patch_level should be omitted"
);
assert!(
!hex.contains("bf854f"),
"boot_patch_level should be omitted"
);
}
#[test]
fn test_key_description_security_level_is_enumerated() {
let params = make_test_params();
let ext = build_attestation_extension(&params).unwrap();
// KeyDescription is a SEQUENCE: 0x30 ...
assert_eq!(ext[0], 0x30);
// Skip SEQUENCE tag + length to get to inner fields
let inner = skip_tlv_header(&ext);
// Field 0: attestationVersion — INTEGER (0x02)
assert_eq!(inner[0], 0x02);
let (_, rest) = skip_one_tlv(inner);
// Field 1: attestationSecurityLevel — ENUMERATED (0x0A)
assert_eq!(rest[0], 0x0A, "attestationSecurityLevel must be ENUMERATED");
let (_, rest) = skip_one_tlv(rest);
// Field 2: keymintVersion — INTEGER (0x02)
assert_eq!(rest[0], 0x02);
let (_, rest) = skip_one_tlv(rest);
// Field 3: keymintSecurityLevel — ENUMERATED (0x0A)
assert_eq!(rest[0], 0x0A, "keymintSecurityLevel must be ENUMERATED");
}
#[test]
fn test_authorization_list_sorted_by_tag() {
let params = make_test_params();
let tee = build_tee_enforced(&params).unwrap();
let inner = skip_tlv_header(&tee);
let tags = extract_tag_numbers(inner);
let mut sorted = tags.clone();
sorted.sort();
assert_eq!(tags, sorted, "AuthorizationList fields must be sorted by tag number");
}
#[test]
fn test_full_extension_roundtrip() {
let params = make_test_params();
let ext = build_attestation_extension(&params).unwrap();
// Must be valid DER: starts with SEQUENCE tag
assert_eq!(ext[0], 0x30);
// Length must account for all inner bytes
let (header_len, total_content_len) = parse_tlv_lengths(&ext);
assert_eq!(ext.len(), header_len + total_content_len);
}
// --- test helpers ---
fn make_test_params() -> CertGenParams {
CertGenParams {
algorithm: Algorithm::Ec,
key_size: 256,
ec_curve: Some(EcCurve::P256),
rsa_public_exponent: 0,
attestation_challenge: Some(vec![0xAB; 32]),
purposes: vec![2, 3],
digests: vec![4],
cert_serial: None,
cert_subject: None,
cert_not_before: -1,
cert_not_after: -1,
keybox_private_key: vec![],
keybox_cert_chain: vec![],
security_level: 1,
attest_version: 200,
keymaster_version: 200,
os_version: 140000,
os_patch_level: 202503,
vendor_patch_level: 20250301,
boot_patch_level: 20250301,
boot_key: vec![0x01; 32],
boot_hash: vec![0x02; 32],
creation_datetime: 1709913600000,
attestation_application_id: vec![0xDE, 0xAD],
module_hash: None,
id_brand: None,
id_device: None,
id_product: None,
id_serial: None,
id_imei: None,
id_meid: None,
id_manufacturer: None,
id_model: None,
id_second_imei: None,
}
}
fn hex_string(data: &[u8]) -> String {
data.iter().map(|b| format!("{:02x}", b)).collect()
}
fn skip_tlv_header(data: &[u8]) -> &[u8] {
let (header_len, _) = parse_tlv_lengths(data);
&data[header_len..]
}
fn skip_one_tlv(data: &[u8]) -> (usize, &[u8]) {
let (header_len, content_len) = parse_tlv_lengths(data);
let total = header_len + content_len;
(total, &data[total..])
}
fn parse_tlv_lengths(data: &[u8]) -> (usize, usize) {
// Returns (header_bytes, content_bytes)
let tag_len = tag_byte_len(data);
let len_start = tag_len;
if data[len_start] < 0x80 {
(len_start + 1, data[len_start] as usize)
} else {
let num_len_bytes = (data[len_start] & 0x7F) as usize;
let mut content_len = 0usize;
for i in 0..num_len_bytes {
content_len = (content_len << 8) | data[len_start + 1 + i] as usize;
}
(len_start + 1 + num_len_bytes, content_len)
}
}
fn tag_byte_len(data: &[u8]) -> usize {
if data[0] & 0x1F != 0x1F {
1
} else {
let mut i = 1;
while data[i] & 0x80 != 0 {
i += 1;
}
i + 1
}
}
fn extract_tag_numbers(mut data: &[u8]) -> Vec<u32> {
let mut tags = Vec::new();
while !data.is_empty() {
let tag = read_tag_number(data);
tags.push(tag);
let (_, rest) = skip_one_tlv(data);
data = rest;
}
tags
}
fn read_tag_number(data: &[u8]) -> u32 {
if data[0] & 0x1F != 0x1F {
(data[0] & 0x1F) as u32
} else {
let mut val = 0u32;
let mut i = 1;
loop {
val = (val << 7) | (data[i] & 0x7F) as u32;
if data[i] & 0x80 == 0 {
break;
}
i += 1;
}
val
}
}
}
+523
View File
@@ -0,0 +1,523 @@
use crate::error::{CertGenError, Result};
use crate::keybox::ParsedKeybox;
use crate::types::{Algorithm, CertGenParams, GeneratedKeyPair};
use time::OffsetDateTime;
const ATTESTATION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 11129, 2, 1, 17];
// Signature algorithm OIDs
const OID_SHA256_WITH_ECDSA: &[u64] = &[1, 2, 840, 10045, 4, 3, 2];
const OID_SHA384_WITH_ECDSA: &[u64] = &[1, 2, 840, 10045, 4, 3, 3];
const OID_SHA256_WITH_RSA: &[u64] = &[1, 2, 840, 113549, 1, 1, 11];
// Extension OIDs
const OID_KEY_USAGE: &[u64] = &[2, 5, 29, 15];
pub fn build_certificate_chain(
key_pair: &GeneratedKeyPair,
attestation_ext_der: &[u8],
keybox: &ParsedKeybox,
params: &CertGenParams,
) -> Result<Vec<Vec<u8>>> {
let leaf_der = build_leaf_cert(key_pair, attestation_ext_der, keybox, params)?;
let mut chain = Vec::with_capacity(1 + keybox.cert_chain_ders.len());
chain.push(leaf_der);
for cert_der in &keybox.cert_chain_ders {
chain.push(cert_der.clone());
}
Ok(chain)
}
fn build_leaf_cert(
key_pair: &GeneratedKeyPair,
attestation_ext_der: &[u8],
keybox: &ParsedKeybox,
params: &CertGenParams,
) -> Result<Vec<u8>> {
let spki_der = extract_spki_from_pkcs8(&key_pair.private_key_pkcs8)?;
let sig_alg_der = signature_algorithm_for_signing_key(&keybox.signing_key_der, params.algorithm)?;
// Serial number
let serial_bytes = if let Some(ref serial) = params.cert_serial {
serial.clone()
} else {
vec![1u8]
};
// Subject DN
let subject_dn_der = if let Some(ref subject) = params.cert_subject {
subject.clone()
} else {
encode_simple_cn_dn("Android Keystore Key")
};
// Validity
let not_before = timestamp_to_datetime(params.cert_not_before)?;
let not_after = if params.cert_not_after == -1 {
OffsetDateTime::from_unix_timestamp(keybox.leaf_not_after)
.unwrap_or_else(|_| OffsetDateTime::now_utc() + time::Duration::days(365))
} else {
timestamp_to_datetime(params.cert_not_after)?
};
// Extensions
let extensions_der = build_extensions(attestation_ext_der, &params.purposes)?;
// TBS Certificate
let version_der = encode_der_explicit_tag(0, &encode_der_integer(&[2]));
let serial_der = encode_der_integer(&serial_bytes);
let validity_der = encode_validity(&not_before, &not_after);
let extensions_tagged = encode_der_explicit_tag(3, &extensions_der);
let tbs_der = encode_der_sequence(&[
&version_der,
&serial_der,
&sig_alg_der,
&keybox.issuer_dn_der, // RAW bytes — no re-encoding
&validity_der,
&subject_dn_der,
&spki_der,
&extensions_tagged,
]);
// Sign the TBS
let signature_bytes = sign_tbs(&tbs_der, &keybox.signing_key_der, params.algorithm)?;
let signature_bit_string = encode_der_bit_string(&signature_bytes);
// Final certificate: SEQUENCE { TBS, sigAlgorithm, signature }
let cert_der = encode_der_sequence(&[
&tbs_der,
&sig_alg_der,
&signature_bit_string,
]);
Ok(cert_der)
}
fn sign_tbs(tbs_der: &[u8], signing_key_der: &[u8], algorithm: Algorithm) -> Result<Vec<u8>> {
match algorithm {
Algorithm::Ec => sign_tbs_ec(tbs_der, signing_key_der),
Algorithm::Rsa => sign_tbs_rsa(tbs_der, signing_key_der),
}
}
fn sign_tbs_ec(tbs_der: &[u8], signing_key_der: &[u8]) -> Result<Vec<u8>> {
// Determine EC curve from the signing key's PKCS8 AlgorithmIdentifier
let alg = detect_ec_signing_algorithm(signing_key_der)?;
let key_pair = ring::signature::EcdsaKeyPair::from_pkcs8(alg, signing_key_der, &ring::rand::SystemRandom::new())
.map_err(|e| CertGenError::SigningFailed(format!("EC key parse: {e}")))?;
let rng = ring::rand::SystemRandom::new();
let sig = key_pair.sign(&rng, tbs_der)
.map_err(|e| CertGenError::SigningFailed(format!("EC sign: {e}")))?;
Ok(sig.as_ref().to_vec())
}
fn detect_ec_signing_algorithm(pkcs8_der: &[u8]) -> Result<&'static ring::signature::EcdsaSigningAlgorithm> {
use der::Decode;
let info = pkcs8::PrivateKeyInfo::from_der(pkcs8_der)
.map_err(|e| CertGenError::SigningFailed(format!("PKCS8 parse: {e}")))?;
let params_oid = info.algorithm.parameters_oid()
.map_err(|e| CertGenError::SigningFailed(format!("EC curve OID: {e}")))?;
let p256_oid: const_oid::ObjectIdentifier = "1.2.840.10045.3.1.7".parse()
.map_err(|_| CertGenError::SigningFailed("OID parse".into()))?;
let p384_oid: const_oid::ObjectIdentifier = "1.3.132.0.34".parse()
.map_err(|_| CertGenError::SigningFailed("OID parse".into()))?;
if params_oid == p256_oid {
Ok(&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING)
} else if params_oid == p384_oid {
Ok(&ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING)
} else {
Err(CertGenError::SigningFailed(format!("unsupported EC curve OID: {params_oid}")))
}
}
fn sign_tbs_rsa(tbs_der: &[u8], signing_key_der: &[u8]) -> Result<Vec<u8>> {
use rsa::pkcs8::DecodePrivateKey;
use rsa::signature::{SignatureEncoding, SignerMut};
use rsa::pkcs1v15::SigningKey;
use rsa::sha2::Sha256;
let private_key = rsa::RsaPrivateKey::from_pkcs8_der(signing_key_der)
.map_err(|e| CertGenError::SigningFailed(format!("RSA key parse: {e}")))?;
let mut signing_key = SigningKey::<Sha256>::new(private_key);
let signature = signing_key.sign(tbs_der);
Ok(signature.to_vec())
}
fn signature_algorithm_for_signing_key(signing_key_der: &[u8], algorithm: Algorithm) -> Result<Vec<u8>> {
match algorithm {
Algorithm::Ec => {
let ring_alg = detect_ec_signing_algorithm(signing_key_der)?;
// Determine OID from the algorithm used
let oid = if std::ptr::eq(ring_alg, &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING) {
OID_SHA384_WITH_ECDSA
} else {
OID_SHA256_WITH_ECDSA
};
let oid_der = encode_der_oid(oid);
Ok(encode_der_sequence(&[&oid_der]))
}
Algorithm::Rsa => {
let oid_der = encode_der_oid(OID_SHA256_WITH_RSA);
let null_der = vec![0x05, 0x00];
Ok(encode_der_sequence(&[&oid_der, &null_der]))
}
}
}
fn extract_spki_from_pkcs8(pkcs8_der: &[u8]) -> Result<Vec<u8>> {
use der::Decode;
let info = pkcs8::PrivateKeyInfo::from_der(pkcs8_der)
.map_err(|e| CertGenError::CertBuildFailed(format!("PKCS8 parse for SPKI: {e}")))?;
// Reconstruct SPKI from AlgorithmIdentifier + public key
// For EC: derive public key from private key via ring
// For RSA: derive from rsa crate
let alg_id_oid = info.algorithm.oid;
let ec_oid: const_oid::ObjectIdentifier = "1.2.840.10045.2.1".parse()
.map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?;
if alg_id_oid == ec_oid {
extract_ec_spki(pkcs8_der, &info)
} else {
extract_rsa_spki(pkcs8_der)
}
}
fn extract_ec_spki(pkcs8_der: &[u8], info: &pkcs8::PrivateKeyInfo) -> Result<Vec<u8>> {
use ring::signature::KeyPair as _;
let params_oid = info.algorithm.parameters_oid()
.map_err(|e| CertGenError::CertBuildFailed(format!("EC curve OID: {e}")))?;
let p256_oid: const_oid::ObjectIdentifier = "1.2.840.10045.3.1.7".parse()
.map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?;
let p384_oid: const_oid::ObjectIdentifier = "1.3.132.0.34".parse()
.map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?;
let (ring_alg, curve_oid_der): (&ring::signature::EcdsaSigningAlgorithm, Vec<u8>) = if params_oid == p256_oid {
(&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, encode_der_oid(&[1, 2, 840, 10045, 3, 1, 7]))
} else if params_oid == p384_oid {
(&ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING, encode_der_oid(&[1, 3, 132, 0, 34]))
} else {
return Err(CertGenError::CertBuildFailed(format!("unsupported EC curve: {params_oid}")));
};
let kp = ring::signature::EcdsaKeyPair::from_pkcs8(
ring_alg,
pkcs8_der,
&ring::rand::SystemRandom::new(),
).map_err(|e| CertGenError::CertBuildFailed(format!("EC key parse: {e}")))?;
let ec_kp = kp.public_key().as_ref().to_vec();
// SPKI = SEQUENCE { AlgorithmIdentifier, BIT STRING (public key) }
// AlgorithmIdentifier = SEQUENCE { ecPublicKey OID, curve OID }
let ec_oid_der = encode_der_oid(&[1, 2, 840, 10045, 2, 1]);
let alg_id = encode_der_sequence(&[&ec_oid_der, &curve_oid_der]);
let pub_key_bits = encode_der_bit_string(&ec_kp);
Ok(encode_der_sequence(&[&alg_id, &pub_key_bits]))
}
fn extract_rsa_spki(pkcs8_der: &[u8]) -> Result<Vec<u8>> {
use rsa::pkcs8::DecodePrivateKey;
let private_key = rsa::RsaPrivateKey::from_pkcs8_der(pkcs8_der)
.map_err(|e| CertGenError::CertBuildFailed(format!("RSA key parse: {e}")))?;
let public_key = rsa::RsaPublicKey::from(&private_key);
// Encode RSA public key as DER: SEQUENCE { n INTEGER, e INTEGER }
use rsa::traits::PublicKeyParts;
let n_bytes = public_key.n().to_bytes_be();
let e_bytes = public_key.e().to_bytes_be();
let rsa_pub_der = encode_der_sequence(&[
&encode_der_integer(&n_bytes),
&encode_der_integer(&e_bytes),
]);
// SPKI = SEQUENCE { AlgorithmIdentifier, BIT STRING (DER-encoded RSAPublicKey) }
let rsa_oid_der = encode_der_oid(&[1, 2, 840, 113549, 1, 1, 1]);
let null_der = vec![0x05, 0x00];
let alg_id = encode_der_sequence(&[&rsa_oid_der, &null_der]);
let pub_key_bits = encode_der_bit_string(&rsa_pub_der);
Ok(encode_der_sequence(&[&alg_id, &pub_key_bits]))
}
fn build_extensions(attestation_ext_der: &[u8], purposes: &[i32]) -> Result<Vec<u8>> {
let mut extensions: Vec<Vec<u8>> = Vec::new();
// KeyUsage extension (critical)
let ku_byte = map_key_usage_byte(purposes);
if ku_byte != 0 {
let ku_ext = build_key_usage_extension(ku_byte);
extensions.push(ku_ext);
}
// Attestation extension (non-critical)
let attest_ext = build_extension(&encode_der_oid(ATTESTATION_OID), false, attestation_ext_der);
extensions.push(attest_ext);
Ok(encode_der_sequence_of(&extensions))
}
fn build_extension(oid_der: &[u8], critical: bool, value_der: &[u8]) -> Vec<u8> {
let value_octet_string = encode_der_octet_string(value_der);
if critical {
let critical_der = encode_der_boolean(true);
encode_der_sequence(&[oid_der, &critical_der, &value_octet_string])
} else {
encode_der_sequence(&[oid_der, &value_octet_string])
}
}
fn build_key_usage_extension(ku_byte: u8) -> Vec<u8> {
// DER BIT STRING: minimal encoding requires trimming trailing zero bits
let unused_bits = ku_byte.trailing_zeros().min(7) as u8;
// BIT STRING = tag (0x03) + length(2) + unused_bits + byte
let bit_string = vec![0x03, 0x02, unused_bits, ku_byte];
let oid_der = encode_der_oid(OID_KEY_USAGE);
let value_octet_string = encode_der_octet_string(&bit_string);
let critical_der = encode_der_boolean(true);
encode_der_sequence(&[&oid_der, &critical_der, &value_octet_string])
}
// KeyUsage BIT STRING byte layout (RFC 5280):
// byte[0] bit 7 = digitalSignature (0x80)
// byte[0] bit 6 = nonRepudiation (0x40)
// byte[0] bit 5 = keyEncipherment (0x20)
// byte[0] bit 4 = dataEncipherment (0x10)
// byte[0] bit 3 = keyAgreement (0x08)
// byte[0] bit 2 = keyCertSign (0x04)
// byte[0] bit 1 = cRLSign (0x02)
// byte[0] bit 0 = encipherOnly (0x01)
// byte[1] bit 7 = decipherOnly (0x80)
fn map_key_usage_byte(purposes: &[i32]) -> u8 {
let mut bits: u8 = 0;
for &purpose in purposes {
match purpose {
2 => bits |= 0x80, // SIGN -> digitalSignature
1 => bits |= 0x10, // DECRYPT -> dataEncipherment
5 => bits |= 0x20, // WRAP_KEY -> keyEncipherment
6 => bits |= 0x08, // AGREE_KEY -> keyAgreement
7 => bits |= 0x04, // ATTEST_KEY -> keyCertSign
_ => {}
}
}
bits
}
fn encode_validity(not_before: &OffsetDateTime, not_after: &OffsetDateTime) -> Vec<u8> {
let nb = encode_time(not_before);
let na = encode_time(not_after);
encode_der_sequence(&[&nb, &na])
}
fn encode_time(dt: &OffsetDateTime) -> Vec<u8> {
let year = dt.year();
if (1950..2050).contains(&year) {
encode_utctime(dt)
} else {
encode_gentime(dt)
}
}
fn encode_utctime(dt: &OffsetDateTime) -> Vec<u8> {
// UTCTime: YYMMDDHHMMSSZ
let year = dt.year() % 100;
let s = format!(
"{:02}{:02}{:02}{:02}{:02}{:02}Z",
year, dt.month() as u8, dt.day(), dt.hour(), dt.minute(), dt.second()
);
let mut out = Vec::with_capacity(2 + s.len());
out.push(0x17); // UTCTime tag
out.extend_from_slice(&encode_der_length_bytes(s.len()));
out.extend_from_slice(s.as_bytes());
out
}
fn encode_gentime(dt: &OffsetDateTime) -> Vec<u8> {
// GeneralizedTime: YYYYMMDDHHMMSSZ
let s = format!(
"{:04}{:02}{:02}{:02}{:02}{:02}Z",
dt.year(), dt.month() as u8, dt.day(), dt.hour(), dt.minute(), dt.second()
);
let mut out = Vec::with_capacity(2 + s.len());
out.push(0x18); // GeneralizedTime tag
out.extend_from_slice(&encode_der_length_bytes(s.len()));
out.extend_from_slice(s.as_bytes());
out
}
fn encode_simple_cn_dn(cn: &str) -> Vec<u8> {
// Name = SEQUENCE OF RelativeDistinguishedName
// RDN = SET OF AttributeTypeAndValue
// ATV = SEQUENCE { OID, UTF8String }
let cn_oid = encode_der_oid(&[2, 5, 4, 3]);
let cn_value = encode_der_utf8string(cn);
let atv = encode_der_sequence(&[&cn_oid, &cn_value]);
let rdn = encode_der_set(&[&atv]);
encode_der_sequence(&[&rdn])
}
fn timestamp_to_datetime(ts: i64) -> Result<OffsetDateTime> {
if ts == -1 {
return Ok(OffsetDateTime::now_utc());
}
OffsetDateTime::from_unix_timestamp(ts / 1000)
.map_err(|e| CertGenError::CertBuildFailed(format!("invalid timestamp {ts}: {e}")))
}
// ---------------------------------------------------------------------------
// DER encoding primitives
// ---------------------------------------------------------------------------
fn encode_der_length_bytes(len: usize) -> Vec<u8> {
if len < 0x80 {
vec![len as u8]
} else if len <= 0xFF {
vec![0x81, len as u8]
} else if len <= 0xFFFF {
vec![0x82, (len >> 8) as u8, len as u8]
} else if len <= 0xFF_FFFF {
vec![0x83, (len >> 16) as u8, (len >> 8) as u8, len as u8]
} else {
vec![0x84, (len >> 24) as u8, (len >> 16) as u8, (len >> 8) as u8, len as u8]
}
}
fn encode_der_tag_length_value(tag: u8, content: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(1 + 4 + content.len());
out.push(tag);
out.extend_from_slice(&encode_der_length_bytes(content.len()));
out.extend_from_slice(content);
out
}
fn encode_der_sequence(items: &[&[u8]]) -> Vec<u8> {
let total: usize = items.iter().map(|i| i.len()).sum();
let mut content = Vec::with_capacity(total);
for item in items {
content.extend_from_slice(item);
}
encode_der_tag_length_value(0x30, &content)
}
fn encode_der_sequence_of(items: &[Vec<u8>]) -> Vec<u8> {
let total: usize = items.iter().map(|i| i.len()).sum();
let mut content = Vec::with_capacity(total);
for item in items {
content.extend_from_slice(item);
}
encode_der_tag_length_value(0x30, &content)
}
fn encode_der_set(items: &[&[u8]]) -> Vec<u8> {
let total: usize = items.iter().map(|i| i.len()).sum();
let mut content = Vec::with_capacity(total);
for item in items {
content.extend_from_slice(item);
}
encode_der_tag_length_value(0x31, &content)
}
fn encode_der_explicit_tag(tag_num: u8, content: &[u8]) -> Vec<u8> {
encode_der_tag_length_value(0xA0 | tag_num, content)
}
fn encode_der_integer(value: &[u8]) -> Vec<u8> {
// DER INTEGER must have minimal encoding and leading 0x00 if high bit set
if value.is_empty() {
return encode_der_tag_length_value(0x02, &[0x00]);
}
// Strip leading zeros (but keep at least one byte)
let mut start = 0;
while start < value.len() - 1 && value[start] == 0 {
start += 1;
}
let trimmed = &value[start..];
// Add leading 0x00 if high bit is set (positive integer)
if trimmed[0] & 0x80 != 0 {
let mut padded = Vec::with_capacity(1 + trimmed.len());
padded.push(0x00);
padded.extend_from_slice(trimmed);
encode_der_tag_length_value(0x02, &padded)
} else {
encode_der_tag_length_value(0x02, trimmed)
}
}
fn encode_der_bit_string(bits: &[u8]) -> Vec<u8> {
// BIT STRING: tag 0x03, length, unused_bits (0), content
let mut content = Vec::with_capacity(1 + bits.len());
content.push(0x00); // 0 unused bits
content.extend_from_slice(bits);
encode_der_tag_length_value(0x03, &content)
}
fn encode_der_octet_string(content: &[u8]) -> Vec<u8> {
encode_der_tag_length_value(0x04, content)
}
fn encode_der_utf8string(s: &str) -> Vec<u8> {
encode_der_tag_length_value(0x0C, s.as_bytes())
}
fn encode_der_boolean(val: bool) -> Vec<u8> {
encode_der_tag_length_value(0x01, &[if val { 0xFF } else { 0x00 }])
}
fn encode_der_oid(components: &[u64]) -> Vec<u8> {
if components.len() < 2 {
return encode_der_tag_length_value(0x06, &[]);
}
let mut content = Vec::new();
// First two components encoded as 40 * c[0] + c[1]
content.push((components[0] * 40 + components[1]) as u8);
for &c in &components[2..] {
encode_oid_subidentifier(&mut content, c);
}
encode_der_tag_length_value(0x06, &content)
}
fn encode_oid_subidentifier(buf: &mut Vec<u8>, mut value: u64) {
if value == 0 {
buf.push(0);
return;
}
// Encode in base-128 with continuation bits
let mut bytes = Vec::new();
while value > 0 {
bytes.push((value & 0x7F) as u8);
value >>= 7;
}
bytes.reverse();
// Set high bit on all but the last byte
for i in 0..bytes.len() - 1 {
bytes[i] |= 0x80;
}
buf.extend_from_slice(&bytes);
}
+75
View File
@@ -0,0 +1,75 @@
use std::fmt;
#[derive(Debug)]
pub enum CertGenError {
Jni(String),
NullParam(&'static str),
UnsupportedAlgorithm(i32),
UnsupportedEcCurve(i32),
KeyGenFailed(String),
CertBuildFailed(String),
KeyboxParseFailed(String),
AttestationBuildFailed(String),
DerError(der::Error),
EmptyKeyboxChain,
ChallengeTooLong(usize),
InvalidParameter(String),
SigningFailed(String),
SerializationFailed(String),
}
impl fmt::Display for CertGenError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Jni(msg) => write!(f, "JNI error: {}", msg),
Self::NullParam(name) => write!(f, "null required parameter: {}", name),
Self::UnsupportedAlgorithm(v) => write!(f, "unsupported algorithm: {}", v),
Self::UnsupportedEcCurve(v) => write!(f, "unsupported EC curve: {}", v),
Self::KeyGenFailed(msg) => write!(f, "key generation failed: {}", msg),
Self::CertBuildFailed(msg) => write!(f, "certificate build failed: {}", msg),
Self::KeyboxParseFailed(msg) => write!(f, "keybox parse failed: {}", msg),
Self::AttestationBuildFailed(msg) => write!(f, "attestation build failed: {}", msg),
Self::DerError(e) => write!(f, "DER error: {}", e),
Self::EmptyKeyboxChain => write!(f, "keybox certificate chain is empty"),
Self::ChallengeTooLong(len) => write!(f, "attestation challenge too long: {} bytes (max 128)", len),
Self::InvalidParameter(msg) => write!(f, "invalid parameter: {}", msg),
Self::SigningFailed(msg) => write!(f, "signing failed: {}", msg),
Self::SerializationFailed(msg) => write!(f, "serialization failed: {}", msg),
}
}
}
impl std::error::Error for CertGenError {}
impl From<jni::errors::Error> for CertGenError {
fn from(e: jni::errors::Error) -> Self {
Self::Jni(e.to_string())
}
}
impl From<der::Error> for CertGenError {
fn from(e: der::Error) -> Self {
Self::DerError(e)
}
}
impl From<ring::error::Unspecified> for CertGenError {
fn from(e: ring::error::Unspecified) -> Self {
Self::KeyGenFailed(e.to_string())
}
}
impl From<ring::error::KeyRejected> for CertGenError {
fn from(e: ring::error::KeyRejected) -> Self {
Self::KeyGenFailed(e.to_string())
}
}
impl From<rsa::Error> for CertGenError {
fn from(e: rsa::Error) -> Self {
Self::KeyGenFailed(e.to_string())
}
}
pub type Result<T> = std::result::Result<T, CertGenError>;
+96
View File
@@ -0,0 +1,96 @@
use crate::error::{CertGenError, Result};
use der::{Decode, Encode};
use x509_cert::Certificate;
pub struct ParsedKeybox {
pub signing_key_der: Vec<u8>,
pub issuer_dn_der: Vec<u8>,
pub cert_chain_ders: Vec<Vec<u8>>,
pub leaf_not_after: i64,
}
pub fn parse_keybox(cert_chain_bytes: &[u8], private_key_bytes: &[u8]) -> Result<ParsedKeybox> {
let certs = split_der_certificates(cert_chain_bytes)?;
if certs.is_empty() {
return Err(CertGenError::KeyboxParseFailed("no certificates found".into()));
}
let leaf = Certificate::from_der(&certs[0])
.map_err(|e| CertGenError::KeyboxParseFailed(format!("leaf cert parse: {e}")))?;
let issuer_dn_der = leaf.tbs_certificate.subject.to_der()
.map_err(|e| CertGenError::KeyboxParseFailed(format!("subject DN encode: {e}")))?;
let not_after = leaf.tbs_certificate.validity.not_after;
let leaf_not_after = not_after.to_unix_duration().as_secs() as i64;
Ok(ParsedKeybox {
signing_key_der: private_key_bytes.to_vec(),
issuer_dn_der,
cert_chain_ders: certs,
leaf_not_after,
})
}
fn split_der_certificates(data: &[u8]) -> Result<Vec<Vec<u8>>> {
let mut certs = Vec::new();
let mut offset = 0;
while offset < data.len() {
if data[offset] != 0x30 {
return Err(CertGenError::KeyboxParseFailed(
format!("expected SEQUENCE tag 0x30 at offset {offset}, got 0x{:02x}", data[offset])
));
}
let (content_len, header_len) = parse_der_length(&data[offset + 1..])?;
let total_len = 1 + header_len + content_len;
if offset + total_len > data.len() {
return Err(CertGenError::KeyboxParseFailed(
format!("cert at offset {offset} extends beyond buffer: need {total_len}, have {}", data.len() - offset)
));
}
certs.push(data[offset..offset + total_len].to_vec());
offset += total_len;
}
if certs.is_empty() {
return Err(CertGenError::KeyboxParseFailed("no certificates in chain".into()));
}
Ok(certs)
}
// Returns (content_length, number_of_length_bytes_consumed)
fn parse_der_length(data: &[u8]) -> Result<(usize, usize)> {
if data.is_empty() {
return Err(CertGenError::KeyboxParseFailed("truncated DER length".into()));
}
let first = data[0];
if first < 0x80 {
// Short form: length is the byte itself
return Ok((first as usize, 1));
}
// Long form: low 7 bits = number of subsequent length bytes
let num_bytes = (first & 0x7f) as usize;
if num_bytes == 0 || num_bytes > 4 {
return Err(CertGenError::KeyboxParseFailed(
format!("unsupported DER length encoding: 0x{first:02x}")
));
}
if 1 + num_bytes > data.len() {
return Err(CertGenError::KeyboxParseFailed("truncated multi-byte DER length".into()));
}
let mut len: usize = 0;
for i in 0..num_bytes {
len = (len << 8) | (data[1 + i] as usize);
}
Ok((len, 1 + num_bytes))
}
+59
View File
@@ -0,0 +1,59 @@
use crate::error::{CertGenError, Result};
use crate::types::{Algorithm, EcCurve, GeneratedKeyPair};
pub fn generate_key_pair(
algorithm: Algorithm,
key_size: u32,
ec_curve: Option<EcCurve>,
rsa_public_exponent: u64,
) -> Result<GeneratedKeyPair> {
match algorithm {
Algorithm::Ec => {
let curve = ec_curve.ok_or_else(|| CertGenError::InvalidParameter("ec_curve required for EC".into()))?;
generate_ec_key_pair(curve)
}
Algorithm::Rsa => generate_rsa_key_pair(key_size, rsa_public_exponent),
}
}
fn generate_ec_key_pair(curve: EcCurve) -> Result<GeneratedKeyPair> {
let alg = match curve {
EcCurve::P256 => &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
EcCurve::P384 => &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING,
_ => return Err(CertGenError::UnsupportedEcCurve(curve as i32)),
};
let rng = ring::rand::SystemRandom::new();
let pkcs8_doc = ring::signature::EcdsaKeyPair::generate_pkcs8(alg, &rng)?;
Ok(GeneratedKeyPair {
private_key_pkcs8: pkcs8_doc.as_ref().to_vec(),
})
}
fn generate_rsa_key_pair(key_size: u32, rsa_public_exponent: u64) -> Result<GeneratedKeyPair> {
use pkcs8::EncodePrivateKey;
if !matches!(key_size, 2048 | 3072 | 4096) {
return Err(CertGenError::InvalidParameter(
format!("RSA key size must be 2048, 3072, or 4096; got {key_size}")
));
}
let exp = if rsa_public_exponent == 0 {
rsa::BigUint::from(65537u64)
} else {
rsa::BigUint::from(rsa_public_exponent)
};
let mut rng = rand::thread_rng();
let private_key = rsa::RsaPrivateKey::new_with_exp(&mut rng, key_size as usize, &exp)
.map_err(|e| CertGenError::KeyGenFailed(e.to_string()))?;
let pkcs8_der = private_key.to_pkcs8_der()
.map_err(|e| CertGenError::SerializationFailed(e.to_string()))?;
Ok(GeneratedKeyPair {
private_key_pkcs8: pkcs8_der.as_bytes().to_vec(),
})
}
+324
View File
@@ -0,0 +1,324 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
mod error;
mod types;
mod keygen;
pub mod keybox;
pub mod attestation;
pub mod certbuilder;
pub mod logging;
use jni::objects::{JByteArray, JClass, JIntArray, JObject, JString};
use jni::sys::{jboolean, jbyteArray, jstring};
use jni::JNIEnv;
use crate::error::{CertGenError, Result};
use crate::types::{Algorithm, CertGenParams, EcCurve};
// ---------------------------------------------------------------------------
// JNI entry: generateAttestedKeyPair
// ---------------------------------------------------------------------------
#[no_mangle]
pub extern "system" fn Java_org_matrix_TEESimulator_pki_NativeCertGen_generateAttestedKeyPair(
mut env: JNIEnv,
_class: JClass,
config: JObject,
) -> jbyteArray {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
generate_attested_inner(&mut env, &config)
}));
match result {
Ok(Ok(raw)) => raw,
Ok(Err(e)) => {
tracing::error!(%e, "generateAttestedKeyPair failed");
let _ = env.throw_new(
"java/lang/RuntimeException",
format!("NativeCertGen: {e}"),
);
std::ptr::null_mut()
}
Err(_) => {
tracing::error!("generateAttestedKeyPair panicked");
let _ = env.throw_new(
"java/lang/RuntimeException",
"NativeCertGen: internal panic",
);
std::ptr::null_mut()
}
}
}
fn generate_attested_inner(env: &mut JNIEnv, config: &JObject) -> Result<jbyteArray> {
let params = extract_config(env, config)?;
let key_pair = keygen::generate_key_pair(
params.algorithm,
params.key_size,
params.ec_curve,
params.rsa_public_exponent,
)?;
let keybox = keybox::parse_keybox(&params.keybox_cert_chain, &params.keybox_private_key)?;
let attest_ext = attestation::build_attestation_extension(&params)?;
let cert_chain = certbuilder::build_certificate_chain(
&key_pair,
&attest_ext,
&keybox,
&params,
)?;
let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain);
let out = env.byte_array_from_slice(&blob)?;
Ok(out.into_raw())
}
// ---------------------------------------------------------------------------
// JNI entry: initLogging
// ---------------------------------------------------------------------------
#[no_mangle]
pub extern "system" fn Java_org_matrix_TEESimulator_pki_NativeCertGen_initLogging(
mut env: JNIEnv,
_class: JClass,
verbose: jboolean,
log_dir: JString,
) -> jboolean {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
init_logging_inner(&mut env, verbose, &log_dir)
}));
match result {
Ok(Ok(())) => 1,
Ok(Err(e)) => {
let _ = env.throw_new(
"java/lang/RuntimeException",
format!("NativeCertGen initLogging: {e}"),
);
0
}
Err(_) => {
let _ = env.throw_new(
"java/lang/RuntimeException",
"NativeCertGen initLogging: internal panic",
);
0
}
}
}
fn init_logging_inner(env: &mut JNIEnv, verbose: jboolean, log_dir: &JString) -> Result<()> {
let dir: String = env.get_string(log_dir)?.into();
logging::init(verbose != 0, &dir, 2, 3)
.map_err(|e| CertGenError::Jni(format!("logging init failed: {e}")))?;
Ok(())
}
// ---------------------------------------------------------------------------
// JNI entry: dumpLogs
// ---------------------------------------------------------------------------
#[no_mangle]
pub extern "system" fn Java_org_matrix_TEESimulator_pki_NativeCertGen_dumpLogs(
mut env: JNIEnv,
_class: JClass,
) -> jstring {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
dump_logs_inner(&mut env)
}));
match result {
Ok(Ok(raw)) => raw,
Ok(Err(e)) => {
tracing::error!(%e, "dumpLogs failed");
std::ptr::null_mut()
}
Err(_) => {
tracing::error!("dumpLogs panicked");
std::ptr::null_mut()
}
}
}
fn dump_logs_inner(env: &mut JNIEnv) -> Result<jstring> {
logging::dump::execute_dump()
.map_err(|e| CertGenError::Jni(format!("dump failed: {e}")))?;
// Read the dump path written by execute_dump
let path = std::fs::read_to_string("/data/adb/tricky_store/.dump_path")
.map_err(|e| CertGenError::Jni(format!("read dump path: {e}")))?;
let jpath = env.new_string(&path)?;
Ok(jpath.into_raw())
}
// ---------------------------------------------------------------------------
// Config extraction from Java CertGenConfig object
// ---------------------------------------------------------------------------
fn extract_config(env: &mut JNIEnv, config: &JObject) -> Result<CertGenParams> {
let algorithm = get_int(env, config, "algorithm")?;
let key_size = get_int(env, config, "keySize")?;
let ec_curve_raw = get_int(env, config, "ecCurve")?;
let rsa_pub_exp = get_long(env, config, "rsaPublicExponent")?;
let cert_not_before = get_long(env, config, "certNotBefore")?;
let cert_not_after = get_long(env, config, "certNotAfter")?;
let security_level = get_int(env, config, "securityLevel")?;
let attest_version = get_int(env, config, "attestVersion")?;
let keymaster_version = get_int(env, config, "keymasterVersion")?;
let os_version = get_int(env, config, "osVersion")?;
let os_patch_level = get_int(env, config, "osPatchLevel")?;
let vendor_patch_level = get_int(env, config, "vendorPatchLevel")?;
let boot_patch_level = get_int(env, config, "bootPatchLevel")?;
let creation_datetime = get_long(env, config, "creationDatetime")?;
let attestation_challenge = get_nullable_byte_array(env, config, "attestationChallenge")?;
let purposes = get_int_array(env, config, "purposes")?;
let digests = get_int_array(env, config, "digests")?;
let cert_serial = get_nullable_byte_array(env, config, "certSerial")?;
let cert_subject = get_nullable_byte_array(env, config, "certSubject")?;
let keybox_private_key = get_byte_array(env, config, "keyboxPrivateKey")?;
let keybox_cert_chain = get_byte_array(env, config, "keyboxCertChain")?;
let boot_key = get_byte_array(env, config, "bootKey")?;
let boot_hash = get_byte_array(env, config, "bootHash")?;
let attestation_app_id = get_byte_array(env, config, "attestationApplicationId")?;
let module_hash = get_nullable_byte_array(env, config, "moduleHash")?;
let id_brand = get_nullable_byte_array(env, config, "idBrand")?;
let id_device = get_nullable_byte_array(env, config, "idDevice")?;
let id_product = get_nullable_byte_array(env, config, "idProduct")?;
let id_serial = get_nullable_byte_array(env, config, "idSerial")?;
let id_imei = get_nullable_byte_array(env, config, "idImei")?;
let id_meid = get_nullable_byte_array(env, config, "idMeid")?;
let id_manufacturer = get_nullable_byte_array(env, config, "idManufacturer")?;
let id_model = get_nullable_byte_array(env, config, "idModel")?;
let id_second_imei = get_nullable_byte_array(env, config, "idSecondImei")?;
Ok(CertGenParams {
algorithm: Algorithm::try_from(algorithm)?,
key_size: key_size as u32,
ec_curve: if algorithm == 3 {
Some(EcCurve::try_from(ec_curve_raw)?)
} else {
None
},
rsa_public_exponent: rsa_pub_exp as u64,
attestation_challenge,
purposes,
digests,
cert_serial,
cert_subject,
cert_not_before,
cert_not_after,
keybox_private_key,
keybox_cert_chain,
security_level,
attest_version,
keymaster_version,
os_version,
os_patch_level,
vendor_patch_level,
boot_patch_level,
boot_key,
boot_hash,
creation_datetime,
attestation_application_id: attestation_app_id,
module_hash,
id_brand,
id_device,
id_product,
id_serial,
id_imei,
id_meid,
id_manufacturer,
id_model,
id_second_imei,
})
}
// ---------------------------------------------------------------------------
// JNI field accessor helpers — called 35+ times, justifies the abstraction
// ---------------------------------------------------------------------------
fn get_int(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<i32> {
Ok(env.get_field(obj, name, "I")?.i()?)
}
fn get_long(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<i64> {
Ok(env.get_field(obj, name, "J")?.j()?)
}
fn get_byte_array(env: &mut JNIEnv, obj: &JObject, name: &'static str) -> Result<Vec<u8>> {
let field = env.get_field(obj, name, "[B")?.l()?;
if field.is_null() {
return Err(CertGenError::NullParam(name));
}
let arr: JByteArray = field.into();
let len = env.get_array_length(&arr)?;
let mut buf = vec![0i8; len as usize];
env.get_byte_array_region(&arr, 0, &mut buf)?;
env.delete_local_ref(arr)?;
Ok(buf.into_iter().map(|b| b as u8).collect())
}
fn get_nullable_byte_array(
env: &mut JNIEnv,
obj: &JObject,
name: &str,
) -> Result<Option<Vec<u8>>> {
let field = env.get_field(obj, name, "[B")?.l()?;
if field.is_null() {
return Ok(None);
}
let arr: JByteArray = field.into();
let len = env.get_array_length(&arr)?;
let mut buf = vec![0i8; len as usize];
env.get_byte_array_region(&arr, 0, &mut buf)?;
env.delete_local_ref(arr)?;
Ok(Some(buf.into_iter().map(|b| b as u8).collect()))
}
fn get_int_array(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<Vec<i32>> {
let field = env.get_field(obj, name, "[I")?.l()?;
if field.is_null() {
return Ok(vec![]);
}
let arr: JIntArray = field.into();
let len = env.get_array_length(&arr)?;
let mut buf = vec![0i32; len as usize];
env.get_int_array_region(&arr, 0, &mut buf)?;
env.delete_local_ref(arr)?;
Ok(buf)
}
// ---------------------------------------------------------------------------
// Binary result assembly (doc 09 section 4.1)
// ---------------------------------------------------------------------------
fn assemble_result(private_key: &[u8], cert_chain: &[Vec<u8>]) -> Vec<u8> {
let total = 4 + private_key.len()
+ 4
+ cert_chain.iter().map(|c| 4 + c.len()).sum::<usize>();
let mut buf = Vec::with_capacity(total);
// Private key segment
buf.extend_from_slice(&(private_key.len() as u32).to_be_bytes());
buf.extend_from_slice(private_key);
// Cert count
buf.extend_from_slice(&(cert_chain.len() as u32).to_be_bytes());
// Each cert: length-prefixed DER
for cert in cert_chain {
buf.extend_from_slice(&(cert.len() as u32).to_be_bytes());
buf.extend_from_slice(cert);
}
buf
}
+208
View File
@@ -0,0 +1,208 @@
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
const DUMP_DIR: &str = "/sdcard/Download";
const LOCK_PATH: &str = "/data/adb/tricky_store/.dump_lock";
const DUMP_PATH_FILE: &str = "/data/adb/tricky_store/.dump_path";
const LOG_DIR: &str = "/data/adb/tricky_store/logs";
const BASE_DIR: &str = "/data/adb/tricky_store";
const LOGCAT_SIZE_LIMIT: usize = 2 * 1024 * 1024;
struct FlockGuard {
_file: File,
}
impl FlockGuard {
fn acquire() -> Result<Self, Box<dyn std::error::Error>> {
if let Some(parent) = Path::new(LOCK_PATH).parent() {
fs::create_dir_all(parent)?;
}
let file = File::create(LOCK_PATH)?;
let fd = {
use std::os::unix::io::AsRawFd;
file.as_raw_fd()
};
let ret = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
if ret != 0 {
return Err("dump already in progress".into());
}
Ok(Self { _file: file })
}
}
impl Drop for FlockGuard {
fn drop(&mut self) {
// flock released automatically when file descriptor closes
}
}
fn random_name(len: usize) -> String {
use rand::Rng;
let mut rng = rand::thread_rng();
(0..len)
.map(|_| {
let idx = rng.gen_range(0..36u8);
if idx < 10 {
(b'0' + idx) as char
} else {
(b'a' + idx - 10) as char
}
})
.collect()
}
fn collect_logcat(tag: &str) -> Vec<u8> {
let output = Command::new("logcat")
.args(["-d", "-s", tag])
.output();
match output {
Ok(o) => {
let mut data = o.stdout;
data.truncate(LOGCAT_SIZE_LIMIT);
data
}
Err(_) => Vec::new(),
}
}
fn collect_device_info() -> String {
let mut info = String::new();
if let Ok(output) = Command::new("uname").arg("-a").output() {
info.push_str(&format!(
"uname={}\n",
String::from_utf8_lossy(&output.stdout).trim()
));
}
for (key, prop) in [
("device", "ro.product.device"),
("build", "ro.build.display.id"),
("android", "ro.build.version.release"),
] {
if let Ok(output) = Command::new("getprop").arg(prop).output() {
info.push_str(&format!(
"{}={}\n",
key,
String::from_utf8_lossy(&output.stdout).trim()
));
}
}
// KSU version
if let Ok(ver) = fs::read_to_string("/data/adb/ksu/version") {
info.push_str(&format!("ksu={}\n", ver.trim()));
}
// Module version from module.prop
if let Ok(prop) = fs::read_to_string("/data/adb/modules/tricky_store/module.prop") {
for line in prop.lines() {
if let Some(ver) = line.strip_prefix("version=") {
info.push_str(&format!("module={}\n", ver.trim()));
break;
}
}
}
info
}
fn read_file_bytes(path: &str) -> Option<Vec<u8>> {
let mut buf = Vec::new();
File::open(path).ok()?.read_to_end(&mut buf).ok()?;
Some(buf)
}
fn epoch_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
pub fn execute_dump() -> Result<(), Box<dyn std::error::Error>> {
let _lock = FlockGuard::acquire()?;
let _ = fs::create_dir_all(DUMP_DIR);
let zip_name = format!("{}.zip", random_name(8));
let zip_path = format!("{}/{}", DUMP_DIR, zip_name);
let zip_file = File::create(&zip_path)?;
let mut zip = zip::ZipWriter::new(zip_file);
let options =
zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
let mut file_count = 0u32;
// Log files
let log_files = [
"certgen.log",
"certgen.log.1",
"certgen.log.2",
"certgen.log.3",
"certgen.log.4",
];
for name in &log_files {
let path = format!("{}/{}", LOG_DIR, name);
if let Some(data) = read_file_bytes(&path) {
zip.start_file(*name, options)?;
zip.write_all(&data)?;
file_count += 1;
}
}
// Logcat
let logcat = collect_logcat("TEESimulator");
if !logcat.is_empty() {
zip.start_file("logcat-teesimulator.log", options)?;
zip.write_all(&logcat)?;
file_count += 1;
}
// Config files
for name in ["tee_status.txt", "security_patch.txt"] {
let path = format!("{}/{}", BASE_DIR, name);
if let Some(data) = read_file_bytes(&path) {
zip.start_file(name, options)?;
zip.write_all(&data)?;
file_count += 1;
}
}
// Device info
let device_info = collect_device_info();
if !device_info.is_empty() {
zip.start_file("device-info.txt", options)?;
zip.write_all(device_info.as_bytes())?;
file_count += 1;
}
// Manifest
let manifest = serde_json::json!({
"timestamp": epoch_millis(),
"version": env!("CARGO_PKG_VERSION"),
"files": file_count,
});
zip.start_file("manifest.json", options)?;
zip.write_all(manifest.to_string().as_bytes())?;
zip.finish()?;
let zip_size = fs::metadata(&zip_path).map(|m| m.len()).unwrap_or(0);
fs::write(DUMP_PATH_FILE, &zip_path)?;
let result = serde_json::json!({
"zip": zip_path,
"size": zip_size,
"files": file_count + 1, // +1 for manifest
});
println!("{}", result);
tracing::info!(path = %zip_path, size = zip_size, "diagnostic dump created");
Ok(())
}
+93
View File
@@ -0,0 +1,93 @@
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::sync::Mutex;
use tracing::field::{Field, Visit};
use tracing::{Event, Level, Subscriber};
use tracing_subscriber::layer::Context;
use tracing_subscriber::Layer;
const KMSG_PATH: &str = "/dev/kmsg";
const TAG: &str = "TEESimulator";
pub struct KmsgLayer {
writer: Mutex<Option<File>>,
}
impl KmsgLayer {
pub fn new() -> Self {
let file = OpenOptions::new().write(true).open(KMSG_PATH).ok();
Self {
writer: Mutex::new(file),
}
}
}
fn syslog_priority(level: &Level) -> u8 {
match *level {
Level::ERROR => 3,
Level::WARN => 4,
Level::INFO => 6,
Level::DEBUG | Level::TRACE => 7,
}
}
struct MessageVisitor {
message: String,
fields: String,
}
impl MessageVisitor {
fn new() -> Self {
Self {
message: String::new(),
fields: String::new(),
}
}
}
impl Visit for MessageVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
let raw = format!("{:?}", value);
// Strip surrounding debug quotes if present
self.message = raw
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(&raw)
.to_string();
} else {
if !self.fields.is_empty() {
self.fields.push(' ');
}
self.fields.push_str(&format!("{}={:?}", field.name(), value));
}
}
}
impl<S: Subscriber> Layer<S> for KmsgLayer {
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
let mut guard = match self.writer.lock() {
Ok(g) => g,
Err(_) => return,
};
let file = match guard.as_mut() {
Some(f) => f,
None => return,
};
let priority = syslog_priority(event.metadata().level());
let mut visitor = MessageVisitor::new();
event.record(&mut visitor);
let line = if visitor.fields.is_empty() {
format!("<{}>{}: {}\n", priority, TAG, visitor.message)
} else {
format!(
"<{}>{}: {} {}\n",
priority, TAG, visitor.message, visitor.fields
)
};
let _ = file.write_all(line.as_bytes());
}
}
+41
View File
@@ -0,0 +1,41 @@
mod kmsg;
mod rotating;
pub mod sysfs;
pub mod dump;
use std::path::Path;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
const VERBOSE_MARKER: &str = "/data/adb/tricky_store/.verbose";
pub fn init(
verbose_flag: bool,
log_dir: &str,
max_size_mb: u64,
max_files: usize,
) -> Result<(), Box<dyn std::error::Error>> {
let verbose = verbose_flag || Path::new(VERBOSE_MARKER).exists();
let (max_size, max_files) = if verbose {
(5 * 1024 * 1024, 5)
} else {
(max_size_mb * 1024 * 1024, max_files)
};
let level = if verbose { "trace" } else { "info" };
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level));
let kmsg_layer = kmsg::KmsgLayer::new();
let rotating_layer = rotating::RotatingFileLayer::new(log_dir, max_size, max_files);
let stderr_layer = tracing_subscriber::fmt::layer().with_writer(std::io::stderr);
// Idempotent — second call returns Ok instead of propagating SetGlobalDefaultError
let _ = tracing_subscriber::registry()
.with(filter)
.with(kmsg_layer)
.with(rotating_layer)
.with(stderr_layer)
.try_init();
Ok(())
}
+166
View File
@@ -0,0 +1,166 @@
use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::field::{Field, Visit};
use tracing::{Event, Level, Subscriber};
use tracing_subscriber::layer::Context;
use tracing_subscriber::Layer;
struct RotatingState {
dir: PathBuf,
current: Option<File>,
current_size: u64,
max_size: u64,
max_files: usize,
}
pub struct RotatingFileLayer {
state: Mutex<RotatingState>,
}
impl RotatingFileLayer {
pub fn new(dir: &str, max_size: u64, max_files: usize) -> Self {
let dir = PathBuf::from(dir);
let _ = fs::create_dir_all(&dir);
let (file, size) = open_current_log(&dir);
Self {
state: Mutex::new(RotatingState {
dir,
current: file,
current_size: size,
max_size,
max_files,
}),
}
}
}
fn open_current_log(dir: &Path) -> (Option<File>, u64) {
let path = dir.join("certgen.log");
let size = fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.ok();
(file, size)
}
fn rotate(state: &mut RotatingState) {
// Close current handle before renaming
state.current.take();
let dir = &state.dir;
// Delete the oldest rotated file before shifting
let oldest = dir.join(format!("certgen.log.{}", state.max_files));
if oldest.exists() {
let _ = fs::remove_file(&oldest);
}
// Shift older files up: .{N} -> .{N+1}
for i in (1..state.max_files).rev() {
let from = dir.join(format!("certgen.log.{}", i));
let to = dir.join(format!("certgen.log.{}", i + 1));
if from.exists() {
let _ = fs::rename(&from, &to);
}
}
// Current -> .1
let current_path = dir.join("certgen.log");
let first_rotated = dir.join("certgen.log.1");
if current_path.exists() {
let _ = fs::rename(&current_path, &first_rotated);
}
let (file, size) = open_current_log(dir);
state.current = file;
state.current_size = size;
}
fn epoch_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
fn level_str(level: &Level) -> &'static str {
match *level {
Level::ERROR => "ERROR",
Level::WARN => "WARN",
Level::INFO => "INFO",
Level::DEBUG => "DEBUG",
Level::TRACE => "TRACE",
}
}
struct LogVisitor {
message: String,
fields: String,
}
impl LogVisitor {
fn new() -> Self {
Self {
message: String::new(),
fields: String::new(),
}
}
}
impl Visit for LogVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
let raw = format!("{:?}", value);
self.message = raw
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(&raw)
.to_string();
} else {
if !self.fields.is_empty() {
self.fields.push(' ');
}
self.fields.push_str(&format!("{}={:?}", field.name(), value));
}
}
}
impl<S: Subscriber> Layer<S> for RotatingFileLayer {
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
let mut state = match self.state.lock() {
Ok(s) => s,
Err(_) => return,
};
if state.current_size >= state.max_size {
rotate(&mut state);
}
let file = match state.current.as_mut() {
Some(f) => f,
None => return,
};
let ts = epoch_secs();
let lvl = level_str(event.metadata().level());
let target = event.metadata().target();
let mut visitor = LogVisitor::new();
event.record(&mut visitor);
let line = if visitor.fields.is_empty() {
format!("{} [{}] {}: {}\n", ts, lvl, target, visitor.message)
} else {
format!(
"{} [{}] {}: {} {}\n",
ts, lvl, target, visitor.message, visitor.fields
)
};
if file.write_all(line.as_bytes()).is_ok() {
state.current_size += line.len() as u64;
}
}
}
+38
View File
@@ -0,0 +1,38 @@
use std::fs;
use std::path::Path;
const VERBOSE_MARKER: &str = "/data/adb/tricky_store/.verbose";
pub fn is_verbose() -> bool {
Path::new(VERBOSE_MARKER).exists()
}
pub fn set_verbose_marker(enabled: bool) -> Result<(), Box<dyn std::error::Error>> {
if enabled {
if let Some(parent) = Path::new(VERBOSE_MARKER).parent() {
fs::create_dir_all(parent)?;
}
fs::write(VERBOSE_MARKER, "")?;
} else if Path::new(VERBOSE_MARKER).exists() {
fs::remove_file(VERBOSE_MARKER)?;
}
Ok(())
}
pub fn enable() -> Result<(), Box<dyn std::error::Error>> {
set_verbose_marker(true)?;
tracing::info!("verbose logging enabled via marker file");
Ok(())
}
pub fn disable() -> Result<(), Box<dyn std::error::Error>> {
set_verbose_marker(false)?;
tracing::info!("verbose logging disabled, marker file removed");
Ok(())
}
pub fn status() -> Result<(), Box<dyn std::error::Error>> {
let state = if is_verbose() { "enabled" } else { "disabled" };
tracing::info!(verbose = state, "verbose marker status");
Ok(())
}
+121
View File
@@ -0,0 +1,121 @@
use crate::error::CertGenError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum Algorithm {
Rsa = 1,
Ec = 3,
}
impl TryFrom<i32> for Algorithm {
type Error = CertGenError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
1 => Ok(Self::Rsa),
3 => Ok(Self::Ec),
_ => Err(CertGenError::UnsupportedAlgorithm(value)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum EcCurve {
P224 = 0,
P256 = 1,
P384 = 2,
P521 = 3,
Curve25519 = 4,
}
impl TryFrom<i32> for EcCurve {
type Error = CertGenError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::P224),
1 => Ok(Self::P256),
2 => Ok(Self::P384),
3 => Ok(Self::P521),
4 => Ok(Self::Curve25519),
_ => Err(CertGenError::UnsupportedEcCurve(value)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum KeyPurpose {
Encrypt = 0,
Decrypt = 1,
Sign = 2,
Verify = 3,
WrapKey = 5,
AgreeKey = 6,
AttestKey = 7,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum SecurityLevel {
Software = 0,
TrustedEnvironment = 1,
StrongBox = 2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum VerifiedBootState {
Verified = 0,
SelfSigned = 1,
Unverified = 2,
Failed = 3,
}
pub struct CertGenParams {
pub algorithm: Algorithm,
pub key_size: u32,
pub ec_curve: Option<EcCurve>,
pub rsa_public_exponent: u64,
pub attestation_challenge: Option<Vec<u8>>,
pub purposes: Vec<i32>,
pub digests: Vec<i32>,
pub cert_serial: Option<Vec<u8>>,
pub cert_subject: Option<Vec<u8>>,
pub cert_not_before: i64,
pub cert_not_after: i64,
pub keybox_private_key: Vec<u8>,
pub keybox_cert_chain: Vec<u8>,
pub security_level: i32,
pub attest_version: i32,
pub keymaster_version: i32,
pub os_version: i32,
pub os_patch_level: i32,
pub vendor_patch_level: i32,
pub boot_patch_level: i32,
pub boot_key: Vec<u8>,
pub boot_hash: Vec<u8>,
pub creation_datetime: i64,
pub attestation_application_id: Vec<u8>,
pub module_hash: Option<Vec<u8>>,
pub id_brand: Option<Vec<u8>>,
pub id_device: Option<Vec<u8>>,
pub id_product: Option<Vec<u8>>,
pub id_serial: Option<Vec<u8>>,
pub id_imei: Option<Vec<u8>>,
pub id_meid: Option<Vec<u8>>,
pub id_manufacturer: Option<Vec<u8>>,
pub id_model: Option<Vec<u8>>,
pub id_second_imei: Option<Vec<u8>>,
}
pub struct GeneratedKeyPair {
pub private_key_pkcs8: Vec<u8>,
}
+262
View File
@@ -0,0 +1,262 @@
#!/usr/bin/env bash
# Build, package, deploy, and verify TEESimulator module ZIPs.
# Usage: ./scripts/package.sh [flags]
#
# Examples:
# ./scripts/package.sh --release # build release ZIP
# ./scripts/package.sh --all --clean # clean build, both variants
# ./scripts/package.sh --release --deploy --reboot # build, push, install, reboot
# ./scripts/package.sh --deploy --verify # deploy latest ZIP + verify via logcat
# ./scripts/package.sh --rust --release # build Rust crate first, then release
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
OUT_DIR="$PROJECT_ROOT/out"
VARIANT=""
CLEAN=false
DEPLOY=false
REBOOT=false
VERIFY=false
BUILD_RUST=false
CLEAR_KEYS=false
TRACE=false
ROOT_PROVIDER="ksu"
red() { printf '\033[0;31m%s\033[0m\n' "$*"; }
green() { printf '\033[0;32m%s\033[0m\n' "$*"; }
yellow() { printf '\033[0;33m%s\033[0m\n' "$*"; }
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
usage() {
cat <<EOF
Usage: $(basename "$0") [options]
Build variants (pick one, or --all):
--release Build release variant (default if none specified)
--debug Build debug variant
--all Build both debug and release
Build options:
--clean Run gradle clean before building
--rust Build native-certgen Rust crate before Gradle
Deploy options:
--deploy Push ZIP to device and install
--reboot Reboot device after install
--clear-keys Clear persistent_keys before deploy
--verify Run logcat verification after deploy
--root PROVIDER Root provider: ksu (default), magisk, apatch
Misc:
-v, --verbose Print every command as it runs (set -x)
--help Show this help
EOF
exit 0
}
while [[ $# -gt 0 ]]; do
case "$1" in
--release) VARIANT="release"; shift ;;
--debug) VARIANT="debug"; shift ;;
--all) VARIANT="all"; shift ;;
--clean) CLEAN=true; shift ;;
--deploy) DEPLOY=true; shift ;;
--reboot) REBOOT=true; shift ;;
--verify) VERIFY=true; shift ;;
--rust) BUILD_RUST=true; shift ;;
--clear-keys) CLEAR_KEYS=true; shift ;;
-v|--verbose) TRACE=true; shift ;;
--root) ROOT_PROVIDER="$2"; shift 2 ;;
--help|-h) usage ;;
*) red "Unknown flag: $1"; usage ;;
esac
done
[[ -z "$VARIANT" ]] && VARIANT="release"
[[ "$TRACE" == true ]] && set -x
case "$ROOT_PROVIDER" in
ksu) INSTALL_CMD="ksud module install" ;;
magisk) INSTALL_CMD="magisk --install-module" ;;
apatch) INSTALL_CMD="/data/adb/apd module install" ;;
*) red "Unknown root provider: $ROOT_PROVIDER"; exit 1 ;;
esac
build_rust() {
local cargo_toml="$PROJECT_ROOT/native-certgen/Cargo.toml"
if [[ ! -f "$cargo_toml" ]]; then
red "native-certgen/Cargo.toml not found — skipping Rust build"
return 0
fi
bold "==> Building native-certgen (aarch64)"
if ! command -v cargo-ndk &>/dev/null; then
red "cargo-ndk not found. Install: cargo install cargo-ndk"
exit 1
fi
(cd "$PROJECT_ROOT/native-certgen" && \
cargo ndk -t arm64-v8a --platform 29 -- build --release)
local so="$PROJECT_ROOT/native-certgen/target/aarch64-linux-android/release/libcertgen.so"
if [[ -f "$so" ]]; then
local size
size=$(du -h "$so" | cut -f1)
green " libcertgen.so built ($size)"
else
red " libcertgen.so not found after build"
exit 1
fi
}
gradle_build() {
local tasks=()
[[ "$CLEAN" == true ]] && tasks+=(clean)
case "$VARIANT" in
release) tasks+=(zipRelease) ;;
debug) tasks+=(zipDebug) ;;
all) tasks+=(zipDebug zipRelease) ;;
esac
bold "==> Gradle: ${tasks[*]}"
(cd "$PROJECT_ROOT" && ./gradlew "${tasks[@]}")
}
find_latest_zip() {
local pattern="$1"
ls -t "$OUT_DIR"/$pattern 2>/dev/null | head -1
}
deploy_zip() {
local zip="$1"
local name
name=$(basename "$zip")
if ! adb get-state &>/dev/null; then
red "No ADB device connected"
exit 1
fi
if [[ "$CLEAR_KEYS" == true ]]; then
bold "==> Clearing persistent_keys"
adb shell "rm -rf /data/adb/tricky_store/persistent_keys/*" 2>/dev/null || true
fi
bold "==> Deploying $name"
adb push "$zip" /data/local/tmp/module.zip
adb shell "su -c '$INSTALL_CMD /data/local/tmp/module.zip'"
green " Installed via $ROOT_PROVIDER"
if [[ "$REBOOT" == true ]]; then
bold "==> Rebooting"
adb reboot
echo " Waiting for device..."
adb wait-for-device
sleep 10
local pid
pid=$(adb shell "pidof TEESimulator" 2>/dev/null || true)
if [[ -n "$pid" ]]; then
green " Daemon alive (PID $pid)"
else
yellow " Daemon not yet started — check logcat"
fi
fi
}
verify_device() {
bold "==> Verification"
if ! adb get-state &>/dev/null; then
red "No ADB device connected"
exit 1
fi
local pid
pid=$(adb shell "pidof TEESimulator" 2>/dev/null || true)
if [[ -n "$pid" ]]; then
green " Daemon: running (PID $pid)"
else
red " Daemon: not running"
fi
local tee_status
tee_status=$(adb shell "cat /data/adb/tricky_store/tee_status.txt" 2>/dev/null || echo "N/A")
echo " TEE status: $tee_status"
local sec_patch
sec_patch=$(adb shell "cat /data/adb/tricky_store/security_patch.txt" 2>/dev/null || echo "N/A")
echo " Security patch config: $(echo "$sec_patch" | head -1)"
local errors
errors=$(adb logcat -d -s TEESimulator 2>/dev/null | \
grep -iE "error|exception" | \
grep -v "StrongBox\|SurfaceRuntime\|ClassLoader\|HARDWARE_TYPE_UNAVAILABLE" | \
wc -l)
if [[ "$errors" -eq 0 ]]; then
green " Logcat errors: 0"
else
yellow " Logcat errors: $errors (run: adb logcat -d -s TEESimulator | grep -iE 'error|exception')"
fi
local throttle_events
throttle_events=$(adb logcat -d -s TEESimulator 2>/dev/null | \
grep -cE "RATE_LIMITED|CONCURRENT_LIMITED" || true)
echo " Rate limit events: $throttle_events"
}
print_summary() {
echo ""
bold "==> Build Summary"
local variants=()
case "$VARIANT" in
release) variants=(Release) ;;
debug) variants=(Debug) ;;
all) variants=(Debug Release) ;;
esac
for v in "${variants[@]}"; do
local zip
zip=$(find_latest_zip "*-${v}.zip")
if [[ -n "$zip" ]]; then
local size
size=$(du -h "$zip" | cut -f1)
green " $v: $(basename "$zip") ($size)"
else
red " $v: ZIP not found"
fi
done
}
# --- Main ---
echo ""
bold "TEESimulator-RS package pipeline"
echo ""
[[ "$BUILD_RUST" == true ]] && build_rust
gradle_build
print_summary
if [[ "$DEPLOY" == true ]]; then
local_variant="$VARIANT"
[[ "$local_variant" == "all" ]] && local_variant="release"
cap="${local_variant^}"
zip=$(find_latest_zip "*-${cap}.zip")
if [[ -z "$zip" ]]; then
red "No $cap ZIP found to deploy"
exit 1
fi
deploy_zip "$zip"
fi
[[ "$VERIFY" == true ]] && verify_device
echo ""
green "Done."
+1 -1
View File
@@ -14,7 +14,7 @@ dependencyResolutionManagement {
}
}
rootProject.name = "TEESimulator"
rootProject.name = "TEESimulator-RS"
include(":stub")
@@ -13,6 +13,8 @@ public interface IPackageManager {
ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId);
int checkPermission(String permName, String pkgName, int userId);
class Stub {
public static IPackageManager asInterface(IBinder binder) {
throw new UnsupportedOperationException("STUB!");
@@ -0,0 +1,8 @@
package android.os;
public class SELinux {
public static boolean checkSELinuxAccess(
String scon, String tcon, String tclass, String perm) {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -17,6 +17,10 @@ public class ServiceManager {
throw new UnsupportedOperationException("STUB!");
}
public static boolean isDeclared(String name) {
throw new UnsupportedOperationException("STUB!");
}
public static String[] listServices() {
throw new UnsupportedOperationException("STUB!");
}
@@ -0,0 +1,14 @@
package android.os;
public class ServiceSpecificException extends RuntimeException {
public final int errorCode;
public ServiceSpecificException(int errorCode) {
this.errorCode = errorCode;
}
public ServiceSpecificException(int errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
}