Compare commits

..
55 Commits
Author SHA1 Message Date
Enginex0 4e83a846c9 docs(release): add v5.1 changelog for interception architecture rewrite 2026-03-21 15:06:52 +01:00
Enginex0 3aff09e7dd feat(interception): dynamically register SecurityLevel binder interceptors
keystore2 may return a different BBinder for each getSecurityLevel call,
so the initial registration during setup might not cover all binder
instances that client apps receive. Intercept getSecurityLevel replies
to register our hook on every new BBinder, deduplicated by identity hash.
2026-03-21 06:09:00 +01:00
Enginex0 c7b0af2d29 fix(interception): restore G2 binder overhead mitigations from pre-PR157
Commit b6f9d7b introduced G2-specific fixes (ratio dropped from 5.00x
to 2.10x) that were lost when resetting to upstream PR #157 at 94c8e5b.

Restores: try-catch safety in BinderInterceptor.onTransact, shouldPatch
early-exit in getKeyEntry post-transact, safe parcel reads (!! to ?:)
at 6 sites, teeResponses cache population in generateKey/importKey
post-transact, uncaught exception handler in App.kt, and removes the
pingBinder liveness check that added ~1.8x overhead per pre-transact.
2026-03-21 05:41:56 +01:00
Enginex0 13a1dd7887 feat(interception): add hardening fixes from pre-PR157 backup
Ported challenge length validation, forced op rejection, system
transaction skip, dead interceptor blocking, and createErrorReply.
Removed size guards that broke G10. Fixed F14 error code to KeyMint
space (-38).
2026-03-20 08:16:12 +01:00
Enginex0 21a3cb1ec0 fix(build): restore customize.sh and build.gradle.kts with all native libs
PR 157 wholesale replacement dropped libsupervisor.so and
libcertgen.so from both the ZIP include filter and the
customize.sh extraction. service.sh calls ./supervisor and
App.kt loads libcertgen.so, so the module would fail on install.

Restores our versions with the Rust build task, 4-lib include
filter, and supervisor/certgen extraction. Adopts PR 157's
nativeLibsDir simplification (always use stripped libs).
2026-03-20 06:58:10 +01:00
Enginex0 2b31d4ef47 refactor(persistence): remove dead rePersistIfNeeded and helper
Software keys receive their final cert chain at generation time
and never go through onPostTransact patching, so re-persisting
after patching is unreachable in PR 157's architecture.
2026-03-20 06:26:08 +01:00
Enginex0 38e9b547a5 feat(certgen): wire NativeCertGen and key persistence onto PR 157 base
NativeCertGen serves as a fast path before BouncyCastle in
doSoftwareGeneration, falling back gracefully when libcertgen.so
is absent. GeneratedKeyPersistence saves software keys to disk
asynchronously and restores them on daemon restart.

Bump version to v5.1, restore TEESimulator-RS naming convention.
2026-03-20 06:23:05 +01:00
Enginex0 94c8e5b182 refactor(interception): reset Kotlin base to upstream PR #157
Our reimplementation of PR 157's logic had a silent divergence causing
G10 to still fail under binder stress. Instead of hunting line-by-line,
replace all shared Kotlin/Java/C++ files with PR 157's exact proven
versions that pass all 63 conformance tests. Our Rust-exclusive files
(NativeCertGen, GeneratedKeyPersistence, native-certgen crate) remain
in the repo but are dormant until re-wired in a follow-up commit.
2026-03-20 05:41:15 +01:00
Enginex0 8fdc59a142 feat(interception): add AUTO mode TEE race for G10 attestation consistency
AUTO mode now races TEE hardware against software generation via
CompletableFuture. If TEE succeeds, the cert chain is patched and
cached in teeResponses before returning, making attestation
stress-resilient. If TEE fails, software fallback is used.

ConfigurationManager no longer resolves AUTO at config time; it
passes Mode.AUTO through to KeyMintSecurityLevelInterceptor for
runtime dispatch. shouldPatch() returns true for both PATCH and
AUTO modes. TEE status file persistence removed entirely.

Aligns handleGenerateKey with upstream PR #157 three-way dispatch:
forceGenerate, raceTeePatch, or hardware forwarding with post-patch.

Hardware keygen rate limiting removed (replaced by raceTeePatch for
AUTO, plain Continue for PATCH). Attest key override in
Keystore2Interceptor now patches authorizations and uses null-safe
nspace assignment.
2026-03-20 04:44:52 +01:00
Enginex0 b6f9d7b486 fix(interception): harden daemon against binder stress crashes
BinderInterceptor.onTransact now catches Throwable, preventing any
exception on a binder thread from killing the daemon. Adds a global
uncaught exception handler as defense in depth.

Replace Thread.sleep with TeeLatencySimulator (LockSupport.parkNanos +
statistical delay model) for keygen latency, reducing binder thread
blocking. Move GeneratedKeyPersistence.save to a background executor
to avoid disk I/O on binder threads.

Convert force-unwrap parcel reads to safe calls with early returns in
onPreTransact/onPostTransact hot paths. Add -DNDEBUG to native release
builds to compile out verbose logging from the ioctl hook.

Targets G2 (ping overhead) and G10 (stress attestation consistency).
2026-03-19 17:38:29 +01:00
Enginex0 99e6b0b5ea feat(certgen): add enforcement tags to native DER encoder and teeResponses cache
Extend Rust native cert gen with software-enforced attestation tags
(CALLER_NONCE, ACTIVE_DATETIME, ORIGINATION_EXPIRE_DATETIME,
USAGE_EXPIRE_DATETIME, USAGE_COUNT_LIMIT, UNLOCKED_DEVICE_REQUIRED)
and make NO_AUTH_REQUIRED conditional in teeEnforced. Fixes F5/F6
test failures where these tags were missing from NativeCertGen path.

Add teeResponses cache so PATCH mode keys patched in onPostTransact
return consistent attestation via getKeyEntry. Without this, getKeyEntry
fell through to real keystore2, returning unpatched metadata.

Remove dead Rust enums (KeyPurpose, SecurityLevel, VerifiedBootState)
that were never referenced by the DER encoder.
2026-03-19 15:43:17 +01:00
Enginex0 397bb6338b fix(interception): make NO_AUTH_REQUIRED conditional in KeyMetadata authorizations
Upstream removed the unconditional NO_AUTH_REQUIRED from toAuthorizations.
A key generated with auth requirements would incorrectly report
NO_AUTH_REQUIRED in metadata, creating a detectable inconsistency
with the attestation extension.
2026-03-19 09:48:08 +01:00
Enginex0 cb81116701 feat(interception): close remaining PR #157 compliance gaps
Full diff analysis against upstream's 50 commits revealed 8 functional
gaps after v5.0. These are detectable by conformance tests or detector
apps inspecting KeyMetadata authorizations and operation semantics.

KeyMetadata authorizations:
- Add 9 TEE-enforced tags (CALLER_NONCE, MIN_MAC_LENGTH, ROLLBACK_RESISTANCE,
  EARLY_BOOT_ONLY, ALLOW_WHILE_ON_BODY, TRUSTED_USER_PRESENCE_REQUIRED,
  TRUSTED_CONFIRMATION_REQUIRED, MAX_USES_PER_BOOT, MAX_BOOT_LEVEL)
- Fix CREATION_DATETIME to SOFTWARE security level via createSwAuth
- Add SOFTWARE-enforced date enforcement, USAGE_COUNT_LIMIT, UNLOCKED_DEVICE_REQUIRED

Symmetric key support:
- Generate AES/HMAC keys in software via javax.crypto.KeyGenerator
- GeneratedKeyInfo expanded with nullable keyPair + secretKey fields
- CipherPrimitive accepts java.security.Key for symmetric operations
- SoftwareOperation routes ENCRYPT/DECRYPT to secretKey when available

Operation compliance:
- beginParameters property replaces manual IV wrapping for GCM
- KeyAgreementPrimitive for ECDH AGREE_KEY operations
- handleCreateOperation wrapped in runCatching (crash prevention)
- SECURE_HW_COMMUNICATION_FAILED on software gen failure

Certificate patching:
- Import key cert chain + authorization patching in onPostTransact
- patchAuthorizations added to post-generateKey PATCH mode path
2026-03-19 09:40:48 +01:00
Enginex0 45477a7898 feat(interception): add AOSP authorize_create enforcement and wire format fixes
Integrate upstream AOSP compliance checks that failed post-v5.0 testing:

- INCLUDE_UNIQUE_ID: SELinux gen_unique_id + Android permission gate
- Forced operation rejection with PERMISSION_DENIED
- Null purpose guard returning INVALID_ARGUMENT
- Wire format: use createServiceSpecificErrorReply for authorize_create
- USAGE_COUNT_LIMIT with AtomicInteger counters and onFinishCallback
- effectiveParams merging key digest with operation purpose
- AuthorizeCreate rewrite: algorithm-purpose before purpose-list (AOSP HAL order)
- CALLER_NONCE in attestation teeEnforced list

Based on upstream commits e55d16d, 3078ea9, 2bc46be, 07c98bc, 41abe77.
2026-03-19 09:21:52 +01:00
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
44 changed files with 2458 additions and 863 deletions
+4
View File
@@ -0,0 +1,4 @@
# Ensure shell scripts always have LF line endings, even on Windows.
# These get packaged into flashable zips and run on Android devices.
*.sh text eol=lf
module/daemon text eol=lf
+92 -73
View File
@@ -3,16 +3,10 @@ name: Build
on: on:
push: push:
branches: [ "main" ] branches: [ "main" ]
paths-ignore: paths-ignore: [ '**.md' ]
- '**.md'
- '.github/**'
- '!.github/workflows/**'
pull_request: pull_request:
branches: [ "main" ] branches: [ "main" ]
paths-ignore: paths-ignore: [ '**.md' ]
- '**.md'
- '.github/**'
- '!.github/workflows/**'
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
@@ -22,18 +16,9 @@ concurrency:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
id-token: write
attestations: write
contents: read
outputs:
releaseName: ${{ steps.prepareArtifact.outputs.releaseName }}
debugName: ${{ steps.prepareArtifact.outputs.debugName }}
steps: steps:
- name: Check out - uses: actions/checkout@v4
uses: actions/checkout@v4
with: with:
submodules: "recursive" submodules: "recursive"
fetch-depth: 0 fetch-depth: 0
@@ -45,6 +30,25 @@ jobs:
java-version: 21 java-version: 21
cache: 'gradle' 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 - name: Set up ccache
uses: hendrikmuhs/ccache-action@v1.2 uses: hendrikmuhs/ccache-action@v1.2
with: with:
@@ -60,73 +64,88 @@ jobs:
- name: Build with Gradle - name: Build with Gradle
run: | run: |
chmod +x ./gradlew chmod +x ./gradlew
./gradlew zipRelease zipDebug -Porg.gradle.parallel=true -Porg.gradle.vfs.watch=true -Dorg.gradle.jvmargs=-Xmx2048m ./gradlew zipRelease zipDebug -Porg.gradle.parallel=true -Porg.gradle.vfs.watch=true -Dorg.gradle.jvmargs=-Xmx2048m
- name: Prepare artifact - name: Read version
if: success() id: ver
id: prepareArtifact
run: | run: |
set -e ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
RELEASE_FILE=$(find out -name "*Release*.zip" | head -1) count=$(git rev-list HEAD --count)
DEBUG_FILE=$(find out -name "*Debug*.zip" | head -1) 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
# 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 - name: List build artifacts
unzip -q "$RELEASE_FILE" -d module-release run: |
unzip -q "$DEBUG_FILE" -d module-debug echo "Release: $(ls out/*Release*.zip | head -1) ($(du -h out/*Release*.zip | head -1 | cut -f1))"
echo " Release: $RELEASE_NAME" echo "Debug: $(ls out/*Debug*.zip | head -1) ($(du -h out/*Debug*.zip | head -1 | cut -f1))"
echo " Debug: $DEBUG_NAME"
- name: Upload release - uses: actions/upload-artifact@v4
if: success()
id: release
uses: actions/upload-artifact@v4
with: with:
name: ${{ steps.prepareArtifact.outputs.releaseName }} name: TEESimulator-RS-release-zip
path: "./module-release/*" path: out/TEESimulator-RS-*-Release.zip
retention-days: 30 retention-days: 30
compression-level: 6 compression-level: 0
- name: Upload debug - uses: actions/upload-artifact@v4
if: success()
id: debug
uses: actions/upload-artifact@v4
with: with:
name: ${{ steps.prepareArtifact.outputs.debugName }} name: TEESimulator-RS-debug-zip
path: "./module-debug/*" path: out/TEESimulator-RS-*-Debug.zip
retention-days: 7 retention-days: 7
compression-level: 6 compression-level: 0
- name: Upload release mappings - uses: actions/upload-artifact@v4
if: success()
uses: actions/upload-artifact@v4
with: with:
name: release-mappings-${{ github.run_number }} name: release-mappings
path: "./app/build/outputs/mapping/release" path: app/build/outputs/mapping/release
retention-days: 30 retention-days: 30
compression-level: 9 compression-level: 9
- name: Summary release:
if: always() 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: | run: |
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
echo "- **Status**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY count=$(git rev-list HEAD --count)
echo "- **Gradle Tasks**: assembleRelease, assembleDebug" >> $GITHUB_STEP_SUMMARY echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
if [[ "${{ job.status }}" == "success" ]]; then
echo "- **Release Artifact**: ${{ steps.prepareArtifact.outputs.releaseName }}" >> $GITHUB_STEP_SUMMARY - uses: actions/download-artifact@v4
echo "- **Debug Artifact**: ${{ steps.prepareArtifact.outputs.debugName }}" >> $GITHUB_STEP_SUMMARY with:
fi 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 out
.gradle
.kotlin
app/build
build
native-certgen/target
app/src/main/jniLibs
+23 -1
View File
@@ -3,7 +3,8 @@
<p align="center"><b>Full TEE Emulation for Rooted Android</b></p> <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">Hardware attestation. Software keys. Zero detection.</p>
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/version-v4.0-blue?style=for-the-badge" alt="v4.0"> <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/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"> <img src="https://img.shields.io/badge/Telegram-community-blue?style=for-the-badge&logo=telegram" alt="Telegram">
</p> </p>
@@ -112,6 +113,24 @@ TEESimulator replaces TrickyStore, TrickyStoreOSS, and their forks. Existing con
--- ---
## 🔨 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 ## ⚙️ Configuration
All configuration files live at `/data/adb/tricky_store/` and are monitored by `FileObserver` — changes take effect immediately without rebooting. All configuration files live at `/data/adb/tricky_store/` and are monitored by `FileObserver` — changes take effect immediately without rebooting.
@@ -223,6 +242,9 @@ boot=device_default
- **[5ec1cff](https://github.com/5ec1cff/TrickyStore)** — TrickyStore, the project that pioneered keystore interception on Android - **[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 - **[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 - **[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
--- ---
+10 -11
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 gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir) val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
val verName = "v4.2" val verName = "v5.1"
android { android {
namespace = "org.matrix.TEESimulator" namespace = "org.matrix.TEESimulator"
@@ -73,7 +73,7 @@ dependencies {
// --- Rust native cert gen build task --- // --- Rust native cert gen build task ---
val buildRustCertgen by tasks.registering(Exec::class) { val buildRustCertgen by tasks.registering(Exec::class) {
group = "TEESimulator Native Build" group = "TEESimulator-RS Native Build"
description = "Builds libcertgen.so via cargo-ndk for arm64-v8a." description = "Builds libcertgen.so via cargo-ndk for arm64-v8a."
workingDir = rootProject.projectDir.resolve("native-certgen") workingDir = rootProject.projectDir.resolve("native-certgen")
@@ -108,13 +108,13 @@ androidComponents {
// --- Define output locations and file names --- // --- Define output locations and file names ---
// Stage all files in a temporary directory inside 'build' before zipping // Stage all files in a temporary directory inside 'build' before zipping
val tempModuleDir = project.layout.buildDirectory.dir("module/${variant.name}") 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. // Task 1: Prepare all module files in the temporary build directory.
// Using Sync ensures that stale files from previous runs are removed. // Using Sync ensures that stale files from previous runs are removed.
val prepareModuleFilesTask = val prepareModuleFilesTask =
tasks.register<Sync>("prepareModuleFiles${capitalized}") { tasks.register<Sync>("prepareModuleFiles${capitalized}") {
group = "TEESimulator Module Packaging" group = "TEESimulator-RS Module Packaging"
description = "Prepares all files for the ${variant.name} module zip." description = "Prepares all files for the ${variant.name} module zip."
if (isDebug) { if (isDebug) {
@@ -145,7 +145,7 @@ androidComponents {
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib" "intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
) )
) { ) {
into("lib") // Place them in the 'lib' subfolder of the staging directory. into("lib")
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so", "**/libcertgen.so") include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so", "**/libcertgen.so")
} }
@@ -161,8 +161,7 @@ androidComponents {
// Use expand() for simple key-value replacement. // Use expand() for simple key-value replacement.
expand( expand(
"REPLACEMEVERCODE" to gitCommitCount.toString(), "REPLACEMEVERCODE" to gitCommitCount.toString(),
"REPLACEMEVER" to "REPLACEMEVER" to "$verName-$gitCommitCount",
"$verName ($gitCommitCount-$gitCommitHash-${variant.name})",
) )
} }
@@ -173,7 +172,7 @@ androidComponents {
// Task 2: Zip the prepared files from the temporary directory. // Task 2: Zip the prepared files from the temporary directory.
val zipTask = val zipTask =
tasks.register<Zip>("zip${capitalized}") { tasks.register<Zip>("zip${capitalized}") {
group = "TEESimulator Module Packaging" group = "TEESimulator-RS Module Packaging"
description = "Creates the flashable zip for the ${variant.name} module." description = "Creates the flashable zip for the ${variant.name} module."
dependsOn(prepareModuleFilesTask) dependsOn(prepareModuleFilesTask)
@@ -186,7 +185,7 @@ androidComponents {
fun createInstallTasks(rootProvider: String, installCli: String) { fun createInstallTasks(rootProvider: String, installCli: String) {
val pushTask = val pushTask =
tasks.register<Exec>("push${rootProvider}Module${capitalized}") { tasks.register<Exec>("push${rootProvider}Module${capitalized}") {
group = "TEESimulator Module Installation" group = "TEESimulator-RS Module Installation"
description = description =
"Pushes the ${variant.name} module to the device for $rootProvider." "Pushes the ${variant.name} module to the device for $rootProvider."
dependsOn(zipTask) dependsOn(zipTask)
@@ -200,7 +199,7 @@ androidComponents {
val installTask = val installTask =
tasks.register<Exec>("install${rootProvider}${capitalized}") { tasks.register<Exec>("install${rootProvider}${capitalized}") {
group = "TEESimulator Module Installation" group = "TEESimulator-RS Module Installation"
description = "Installs the ${variant.name} module via $rootProvider." description = "Installs the ${variant.name} module via $rootProvider."
dependsOn(pushTask) dependsOn(pushTask)
commandLine( commandLine(
@@ -213,7 +212,7 @@ androidComponents {
} }
tasks.register<Exec>("install${rootProvider}AndReboot${capitalized}") { 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." description = "Installs the ${variant.name} module via $rootProvider and reboots."
dependsOn(installTask) dependsOn(installTask)
commandLine("adb", "reboot") commandLine("adb", "reboot")
+1
View File
@@ -5,6 +5,7 @@ set(CMAKE_CXX_STANDARD 23)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -DNDEBUG")
# LSPlt configuration # LSPlt configuration
OPTION(LSPLT_BUILD_SHARED OFF) OPTION(LSPLT_BUILD_SHARED OFF)
+31 -26
View File
@@ -235,6 +235,8 @@ class BinderInterceptor : public BBinder {
struct RegistrationEntry { struct RegistrationEntry {
wp<IBinder> target; wp<IBinder> target;
sp<IBinder> callback_interface; sp<IBinder> callback_interface;
// Transaction codes to intercept. Empty = intercept all (legacy behavior).
std::vector<uint32_t> filtered_codes;
}; };
// Reader-Writer lock for the registry to allow concurrent reads (lookups) // Reader-Writer lock for the registry to allow concurrent reads (lookups)
@@ -244,10 +246,15 @@ class BinderInterceptor : public BBinder {
public: public:
BinderInterceptor() = default; BinderInterceptor() = default;
// Checks if a specific Binder instance is currently registered for interception // Checks if a specific Binder+code combination should be intercepted.
bool isBinderIntercepted(const wp<BBinder> &target) const { // Returns true if the binder is registered AND the code is in its filter
// (or the filter is empty, meaning intercept everything).
bool shouldIntercept(const wp<BBinder> &target, uint32_t code) const {
std::shared_lock lock(registry_mutex_); 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 // Main entry point for processing the "Man-in-the-Middle" logic
@@ -348,19 +355,11 @@ static sp<BinderStub> g_stub_instance = nullptr;
namespace { namespace {
constexpr binder_size_t kMaxInterceptableDataSize = 256 * 1024;
void inspectAndRewriteTransaction(binder_transaction_data *txn_data) { void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
if (!txn_data || txn_data->target.ptr == 0) if (!txn_data || txn_data->target.ptr == 0)
return; return;
// Bypass interception for oversized payloads to prevent thread starvation from flood attacks // Skip system transactions (PING, INTERFACE, DUMP) to avoid latency detectors
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) if (txn_data->code > 0x00ffffffu && txn_data->code != intercept::kBackdoorCode)
return; return;
@@ -393,7 +392,7 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
// This is safe because we are holding a strong reference. // This is safe because we are holding a strong reference.
wp<BBinder> wp_target = target_binder_ptr; 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.transaction_code = txn_data->code;
info.target_binder = wp_target; // Assign the valid weak pointer info.target_binder = wp_target; // Assign the valid weak pointer
hijack = true; hijack = true;
@@ -544,12 +543,26 @@ status_t BinderInterceptor::handleRegister(const Parcel &data) {
return BAD_TYPE; return BAD_TYPE;
} }
// Read optional transaction code filter. If present: int32 count + count * uint32 codes.
// If absent or count <= 0: intercept all transaction codes (legacy behavior).
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; wp<IBinder> weak_target = target;
std::unique_lock lock(registry_mutex_); 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; return OK;
} }
@@ -599,15 +612,8 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
Parcel pre_req, pre_resp; Parcel pre_req, pre_resp;
writeTransactionData(pre_req, tx_id, target, code, flags, request); writeTransactionData(pre_req, tx_id, target, code, flags, request);
status_t pre_status = callback->transact(intercept::kPreTransact, pre_req, &pre_resp); if (callback->transact(intercept::kPreTransact, pre_req, &pre_resp) != OK) {
if (pre_status != OK) { LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed. Forwarding original call.", tx_id);
// 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; return false;
} }
@@ -661,8 +667,7 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
VALIDATE_STATUS(tx_id, post_req.appendFrom(reply, 0, reply_size)); VALIDATE_STATUS(tx_id, post_req.appendFrom(reply, 0, reply_size));
} }
status_t post_status = callback->transact(intercept::kPostTransact, post_req, &post_resp); if (callback->transact(intercept::kPostTransact, post_req, &post_resp) == OK) {
if (post_status == OK) {
int32_t post_action = post_resp.readInt32(); int32_t post_action = post_resp.readInt32();
if (post_action == intercept::kActionOverrideReply && reply) { if (post_action == intercept::kActionOverrideReply && reply) {
result = post_resp.readInt32(); // Read new status result = post_resp.readInt32(); // Read new status
+20 -1
View File
@@ -2,11 +2,13 @@
#include <unistd.h> #include <unistd.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <sys/prctl.h> #include <sys/prctl.h>
#include <sys/resource.h>
#include <signal.h> #include <signal.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <errno.h> #include <errno.h>
#include <time.h>
static volatile sig_atomic_t should_exit = 0; static volatile sig_atomic_t should_exit = 0;
@@ -27,7 +29,12 @@ int main(int argc, char *argv[]) {
const char *daemon_path = argv[1]; const char *daemon_path = argv[1];
char **daemon_argv = &argv[1]; char **daemon_argv = &argv[1];
int backoff_ms = 500;
while (!should_exit) { while (!should_exit) {
struct timespec child_start;
clock_gettime(CLOCK_MONOTONIC, &child_start);
pid_t pid = fork(); pid_t pid = fork();
if (pid < 0) { if (pid < 0) {
@@ -39,6 +46,7 @@ int main(int argc, char *argv[]) {
if (pid == 0) { if (pid == 0) {
// Child: become the daemon // Child: become the daemon
prctl(PR_SET_PDEATHSIG, SIGKILL); // Die if parent dies prctl(PR_SET_PDEATHSIG, SIGKILL); // Die if parent dies
setpriority(PRIO_PROCESS, 0, 10); // lower CPU priority than foreground
execv(daemon_path, daemon_argv); execv(daemon_path, daemon_argv);
perror("execv failed"); perror("execv failed");
_exit(127); _exit(127);
@@ -50,7 +58,18 @@ int main(int argc, char *argv[]) {
if (should_exit) break; if (should_exit) break;
// Instant restart - no delay // 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; return 0;
@@ -35,15 +35,16 @@ object App {
fun main(args: Array<String>) { fun main(args: Array<String>) {
SystemLogger.info("Welcome to TEESimulator!") SystemLogger.info("Welcome to TEESimulator!")
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
SystemLogger.error("Uncaught exception on ${thread.name}", throwable)
}
try { try {
// Initialize the Android framework environment
prepareEnvironment() prepareEnvironment()
// Initialize and start the appropriate keystore interceptors. // Initialize and start the appropriate keystore interceptors.
initializeInterceptors() initializeInterceptors()
// Load the package configuration.
ConfigurationManager.initialize() ConfigurationManager.initialize()
// Set up the device's boot key and hash, which are crucial for attestation.
AndroidDeviceUtils.setupBootKeyAndHash() AndroidDeviceUtils.setupBootKeyAndHash()
// Android ships with a stripped-down Bouncy Castle provider under the name "BC". // Android ships with a stripped-down Bouncy Castle provider under the name "BC".
@@ -2,8 +2,11 @@ package org.matrix.TEESimulator.attestation
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.security.MessageDigest import java.security.MessageDigest
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import org.bouncycastle.asn1.ASN1Boolean import org.bouncycastle.asn1.ASN1Boolean
import org.bouncycastle.asn1.ASN1Encodable import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.ASN1Enumerated import org.bouncycastle.asn1.ASN1Enumerated
@@ -112,7 +115,6 @@ object AttestationBuilder {
} }
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid) val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
SystemLogger.info("Attestation patch levels for uid=$uid: os=$osPatch, vendor=$vendorPatch, boot=$bootPatch")
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] = properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
if (bootPatch != DO_NOT_REPORT) { if (bootPatch != DO_NOT_REPORT) {
DERTaggedObject( DERTaggedObject(
@@ -133,8 +135,16 @@ object AttestationBuilder {
uid: Int, uid: Int,
securityLevel: Int, securityLevel: Int,
): ASN1Sequence { ): ASN1Sequence {
val creationTime = System.currentTimeMillis()
val teeEnforced = buildTeeEnforcedList(params, uid, securityLevel) 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 = val fields =
arrayOf( arrayOf(
@@ -147,13 +157,49 @@ object AttestationBuilder {
), // keymasterVersion ), // keymasterVersion
ASN1Enumerated(securityLevel), // keymasterSecurityLevel ASN1Enumerated(securityLevel), // keymasterSecurityLevel
DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge
DEROctetString(ByteArray(0)), // uniqueId DEROctetString(uniqueId),
softwareEnforced, softwareEnforced,
teeEnforced, teeEnforced,
) )
return DERSequence(fields) return DERSequence(fields)
} }
/**
* Computes the unique ID per the KeyMint HAL spec:
* HMAC-SHA256(T || C || R, HBK) truncated to 128 bits.
*
* T = temporal counter (creationTime / 2592000000, i.e. 30-day periods since epoch)
* C = DER-encoded ATTESTATION_APPLICATION_ID
* R = 0x00 (no factory reset since ID rotation)
* HBK = device-unique secret generated once during module installation
*/
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) // RESET_SINCE_ID_ROTATION = false
.array()
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(hbk, "HmacSHA256"))
return mac.doFinal(message).copyOf(16)
}
/** Device-unique key seed, generated once at module installation. */
private val hbk: ByteArray by lazy {
val file = java.io.File(ConfigurationManager.CONFIG_PATH, "hbk")
if (file.exists() && file.length() == 32L) {
file.readBytes()
} else {
// Fallback: generate in-memory (won't persist across reboots)
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". */ /** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */
private fun buildTeeEnforcedList( private fun buildTeeEnforcedList(
params: KeyMintAttestation, params: KeyMintAttestation,
@@ -182,23 +228,128 @@ object AttestationBuilder {
AttestationConstants.TAG_DIGEST, AttestationConstants.TAG_DIGEST,
DERSet(params.digest.map { ASN1Integer(it.toLong()) }.toTypedArray()), DERSet(params.digest.map { ASN1Integer(it.toLong()) }.toTypedArray()),
), ),
)
if (params.ecCurve != null) {
list.add(
DERTaggedObject( DERTaggedObject(
true, true,
AttestationConstants.TAG_EC_CURVE, AttestationConstants.TAG_EC_CURVE,
ASN1Integer(params.ecCurve.toLong()), 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( DERTaggedObject(
true, true,
AttestationConstants.TAG_ORIGIN, AttestationConstants.TAG_ORIGIN,
ASN1Integer(0L), ASN1Integer((params.origin ?: 0).toLong()),
), // KeyOrigin.GENERATED ),
DERTaggedObject( DERTaggedObject(
true, true,
AttestationConstants.TAG_ROOT_OF_TRUST, AttestationConstants.TAG_ROOT_OF_TRUST,
buildRootOfTrust(null), buildRootOfTrust(null),
), ),
) )
)
// Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels. // Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels.
val simulatedProperties = getSimulatedHardwareProperties(uid) val simulatedProperties = getSimulatedHardwareProperties(uid)
@@ -295,20 +446,32 @@ object AttestationBuilder {
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by * Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
* Keystore. * Keystore.
*/ */
private fun buildSoftwareEnforcedList(uid: Int, securityLevel: Int): DERSequence { private fun buildSoftwareEnforcedList(
val list = params: KeyMintAttestation,
mutableListOf<ASN1Encodable>( uid: Int,
DERTaggedObject( securityLevel: Int,
true, creationTimeMs: Long = System.currentTimeMillis(),
AttestationConstants.TAG_CREATION_DATETIME, ): DERSequence {
ASN1Integer(System.currentTimeMillis()), val list = mutableListOf<ASN1Encodable>()
),
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_CREATION_DATETIME,
ASN1Integer(creationTimeMs),
)
)
// ATTESTATION_APPLICATION_ID is only included when an attestation challenge is present.
if (params.attestationChallenge != null) {
list.add(
DERTaggedObject( DERTaggedObject(
true, true,
AttestationConstants.TAG_ATTESTATION_APPLICATION_ID, AttestationConstants.TAG_ATTESTATION_APPLICATION_ID,
createApplicationId(uid), createApplicationId(uid),
), )
) )
}
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) { if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) {
list.add( list.add(
DERTaggedObject( DERTaggedObject(
@@ -318,7 +481,52 @@ object AttestationBuilder {
) )
) )
} }
return DERSequence(list.toTypedArray())
// Keystore2-enforced tags belong in softwareEnforced, not teeEnforced.
// The HAL does not enforce these; keystore2's authorize_create handles them.
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())
} }
/** /**
@@ -346,6 +554,17 @@ object AttestationBuilder {
*/ */
@Throws(Throwable::class) @Throws(Throwable::class)
internal fun createApplicationId(uid: Int): DEROctetString { internal fun createApplicationId(uid: Int): DEROctetString {
// AOSP keystore_attestation_id.cpp: gather_attestation_application_id()
// uses a hardcoded identity for AID_SYSTEM (1000) and AID_ROOT (0):
// packageName = "AndroidSystem", versionCode = 1, no signing digests.
val appUid = uid % 100000
if (appUid == 0 || appUid == 1000) {
return buildApplicationIdDer(
listOf("AndroidSystem" to 1L),
emptySet(),
)
}
val pm = val pm =
ConfigurationManager.getPackageManager() ConfigurationManager.getPackageManager()
?: throw IllegalStateException("PackageManager not found!") ?: throw IllegalStateException("PackageManager not found!")
@@ -353,12 +572,11 @@ object AttestationBuilder {
pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid") pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid")
val sha256 = MessageDigest.getInstance("SHA-256") val sha256 = MessageDigest.getInstance("SHA-256")
val packageInfoList = mutableListOf<DERSequence>() val packageInfoList = mutableListOf<Pair<String, Long>>()
val signatureDigests = mutableSetOf<Digest>() val signatureDigests = mutableSetOf<Digest>()
// Process all packages associated with the UID in a single loop. val userId = uid / 100000
packages.forEach { packageName -> packages.forEach { packageName ->
val userId = uid / 100000
val packageInfo = val packageInfo =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.getPackageInfo( pm.getPackageInfo(
@@ -371,34 +589,36 @@ object AttestationBuilder {
pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId) pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId)
} }
// Add package information (name and version code) to our list. packageInfoList.add(packageInfo.packageName to packageInfo.longVersionCode)
packageInfoList.add(
DERSequence(
arrayOf(
DEROctetString(packageInfo.packageName.toByteArray(StandardCharsets.UTF_8)),
ASN1Integer(packageInfo.longVersionCode),
)
)
)
// Collect unique signature digests from the signing history.
packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature -> packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature ->
val digest = sha256.digest(signature.toByteArray()) signatureDigests.add(Digest(sha256.digest(signature.toByteArray())))
signatureDigests.add(Digest(digest))
} }
} }
// The application ID is a sequence of two sets: return buildApplicationIdDer(packageInfoList, signatureDigests)
// 1. A set of package information (name and version). }
// 2. A set of SHA-256 digests of the signing certificates.
private fun buildApplicationIdDer(
packages: List<Pair<String, Long>>,
digests: Set<Digest>,
): DEROctetString {
val packageInfoList =
packages.map { (name, version) ->
DERSequence(
arrayOf(
DEROctetString(name.toByteArray(StandardCharsets.UTF_8)),
ASN1Integer(version),
)
)
}
val applicationIdSequence = val applicationIdSequence =
DERSequence( DERSequence(
arrayOf( arrayOf(
DERSet(packageInfoList.toTypedArray()), DERSet(packageInfoList.toTypedArray()),
DERSet(signatureDigests.map { DEROctetString(it.digest) }.toTypedArray()), DERSet(digests.map { DEROctetString(it.digest) }.toTypedArray()),
) )
) )
return DEROctetString(applicationIdSequence.encoded) return DEROctetString(applicationIdSequence.encoded)
} }
} }
@@ -44,9 +44,11 @@ object AttestationConstants {
// --- Key Lifetime and Usage Control --- // --- Key Lifetime and Usage Control ---
const val TAG_ROLLBACK_RESISTANCE = 303 const val TAG_ROLLBACK_RESISTANCE = 303
const val TAG_EARLY_BOOT_ONLY = 305
const val TAG_ACTIVE_DATETIME = 400 const val TAG_ACTIVE_DATETIME = 400
const val TAG_ORIGINATION_EXPIRE_DATETIME = 401 const val TAG_ORIGINATION_EXPIRE_DATETIME = 401
const val TAG_USAGE_EXPIRE_DATETIME = 402 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_MAX_USES_PER_BOOT = 404
const val TAG_USAGE_COUNT_LIMIT = 405 const val TAG_USAGE_COUNT_LIMIT = 405
@@ -56,6 +58,10 @@ object AttestationConstants {
const val TAG_NO_AUTH_REQUIRED = 503 const val TAG_NO_AUTH_REQUIRED = 503
const val TAG_USER_AUTH_TYPE = 504 const val TAG_USER_AUTH_TYPE = 504
const val TAG_AUTH_TIMEOUT = 505 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 --- // --- Attestation and Application Info ---
const val TAG_APPLICATION_ID = 601 const val TAG_APPLICATION_ID = 601
@@ -1,13 +1,8 @@
package org.matrix.TEESimulator.attestation package org.matrix.TEESimulator.attestation
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyPairGenerator
import java.security.KeyStore import java.security.KeyStore
import java.security.SecureRandom
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.security.spec.ECGenParameterSpec
import org.bouncycastle.asn1.ASN1Integer import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.ASN1OctetString import org.bouncycastle.asn1.ASN1OctetString
@@ -57,55 +52,14 @@ object DeviceAttestationService {
val bootPatchLevel: Int?, val bootPatchLevel: Int?,
) )
// A unique alias for the key used to perform the TEE functionality check.
private const val TEE_CHECK_KEY_ALIAS = "TEESimulator_AttestationCheck" private const val TEE_CHECK_KEY_ALIAS = "TEESimulator_AttestationCheck"
/**
* Lazily determines if the device's TEE is functional by attempting to generate an
* attestation-backed key pair. The result is cached.
*/
val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() }
/** /**
* Lazily fetches and parses attestation data from a genuinely generated certificate. The result * Lazily fetches and parses attestation data from a genuinely generated certificate. The result
* is cached. Returns null if the TEE is not functional or parsing fails. * is cached. Returns null if the TEE is not functional or parsing fails.
*/ */
val CachedAttestationData: AttestationData? by lazy { fetchAttestationData() } val CachedAttestationData: AttestationData? by lazy { fetchAttestationData() }
/**
* Checks if the TEE is working correctly by generating a key in the Android Keystore with an
* attestation challenge.
*
* @return `true` if a key with attestation was generated successfully, `false` otherwise.
*/
private fun checkTeeFunctionality(): Boolean {
SystemLogger.info("Performing TEE functionality check...")
return try {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val keyPairGenerator =
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
// A random challenge is required for attestation.
val challenge = ByteArray(16).apply { SecureRandom().nextBytes(this) }
val spec =
KeyGenParameterSpec.Builder(TEE_CHECK_KEY_ALIAS, KeyProperties.PURPOSE_SIGN)
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256)
.setAttestationChallenge(challenge)
.build()
keyPairGenerator.initialize(spec)
keyPairGenerator.generateKeyPair()
SystemLogger.info("TEE functionality check successful.")
true
} catch (e: Exception) {
SystemLogger.warning("TEE functionality check failed.", e)
false
}
}
/** /**
* Retrieves the attestation certificate generated during the TEE check. The key entry is * Retrieves the attestation certificate generated during the TEE check. The key entry is
* deleted after retrieval to clean up. * deleted after retrieval to clean up.
@@ -113,8 +67,6 @@ object DeviceAttestationService {
* @return The leaf `X509Certificate` containing the attestation, or `null` if unavailable. * @return The leaf `X509Certificate` containing the attestation, or `null` if unavailable.
*/ */
private fun getAttestationCertificate(): X509Certificate? { private fun getAttestationCertificate(): X509Certificate? {
if (!isTeeFunctional) return null
return try { return try {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val certChain = keyStore.getCertificateChain(TEE_CHECK_KEY_ALIAS) val certChain = keyStore.getCertificateChain(TEE_CHECK_KEY_ALIAS)
@@ -1,7 +1,6 @@
package org.matrix.TEESimulator.attestation package org.matrix.TEESimulator.attestation
import android.hardware.security.keymint.* import android.hardware.security.keymint.*
import android.hardware.security.keymint.KeyOrigin
import java.math.BigInteger import java.math.BigInteger
import java.util.Date import java.util.Date
import javax.security.auth.x500.X500Principal import javax.security.auth.x500.X500Principal
@@ -17,11 +16,12 @@ import org.matrix.TEESimulator.logging.KeyMintParameterLogger
// Reference: // Reference:
// https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/key_parameter.rs // https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/key_parameter.rs
data class KeyMintAttestation( data class KeyMintAttestation(
val keySize: Int,
val algorithm: Int, val algorithm: Int,
val ecCurve: Int, val ecCurve: Int?,
val ecCurveName: String, val ecCurveName: String,
val keySize: Int,
val origin: Int?, val origin: Int?,
val noAuthRequired: Boolean?,
val blockMode: List<Int>, val blockMode: List<Int>,
val padding: List<Int>, val padding: List<Int>,
val purpose: List<Int>, val purpose: List<Int>,
@@ -41,24 +41,45 @@ data class KeyMintAttestation(
val manufacturer: ByteArray?, val manufacturer: ByteArray?,
val model: ByteArray?, val model: ByteArray?,
val secondImei: ByteArray?, val secondImei: ByteArray?,
// Enforcement tags
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 maxUsesPerBoot: Int?,
val maxBootLevel: Int?,
val minMacLength: Int?,
val rsaOaepMgfDigest: List<Int>,
) { ) {
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */ /** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */
constructor( constructor(
params: Array<KeyParameter> params: Array<KeyParameter>
) : this( ) : this(
// AOSP: [key_param(tag = KEY_SIZE, field = Integer)]
keySize = params.findInteger(Tag.KEY_SIZE) ?: 0,
// AOSP: [key_param(tag = ALGORITHM, field = Algorithm)] // AOSP: [key_param(tag = ALGORITHM, field = Algorithm)]
algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0, algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0,
// AOSP: [key_param(tag = KEY_SIZE, field = Integer)]
// For EC keys, derive keySize from EC_CURVE when KEY_SIZE is absent.
keySize = params.findInteger(Tag.KEY_SIZE) ?: params.deriveKeySizeFromCurve(),
// AOSP: [key_param(tag = EC_CURVE, field = EcCurve)] // AOSP: [key_param(tag = EC_CURVE, field = EcCurve)]
ecCurve = params.findEcCurve(Tag.EC_CURVE) ?: 0, ecCurve = params.findEcCurve(Tag.EC_CURVE),
ecCurveName = params.deriveEcCurveName(), ecCurveName = params.deriveEcCurveName(),
// AOSP: [key_param(tag = ORIGIN, field = Origin)] // AOSP: [key_param(tag = ORIGIN, field = Origin)]
origin = params.findOrigin(Tag.ORIGIN), origin = params.findOrigin(Tag.ORIGIN),
// AOSP: [key_param(tag = NO_AUTH_REQUIRED, field = BoolValue)]
noAuthRequired = params.findBoolean(Tag.NO_AUTH_REQUIRED),
// AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)] // AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)]
blockMode = params.findAllBlockMode(Tag.BLOCK_MODE), blockMode = params.findAllBlockMode(Tag.BLOCK_MODE),
@@ -100,18 +121,44 @@ data class KeyMintAttestation(
manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER), manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER),
model = params.findBlob(Tag.ATTESTATION_ID_MODEL), model = params.findBlob(Tag.ATTESTATION_ID_MODEL),
secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI), secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI),
// Enforcement tags
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),
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. // Log all parsed parameters for debugging purposes.
params.forEach { KeyMintParameterLogger.logParameter(it) } params.forEach { KeyMintParameterLogger.logParameter(it) }
} }
fun isAttestKey(): Boolean = purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY) fun isAttestKey(): Boolean {
return purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY)
}
fun isImportKey(): Boolean = origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED fun isImportKey(): Boolean {
return origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED
}
} }
// --- Private helper extension functions for parsing KeyParameter arrays --- // --- Private helper extension functions for parsing KeyParameter arrays ---
/** Maps to AOSP field = Integer */
private fun Array<KeyParameter>.findBoolean(tag: Int): Boolean? =
this.find { it.tag == tag }?.value?.boolValue
/** Maps to AOSP field = Integer */ /** Maps to AOSP field = Integer */
private fun Array<KeyParameter>.findInteger(tag: Int): Int? = private fun Array<KeyParameter>.findInteger(tag: Int): Int? =
this.find { it.tag == tag }?.value?.integer this.find { it.tag == tag }?.value?.integer
@@ -144,7 +191,7 @@ private fun Array<KeyParameter>.findBlob(tag: Int): ByteArray? =
private fun Array<KeyParameter>.findAllBlockMode(tag: Int): List<Int> = private fun Array<KeyParameter>.findAllBlockMode(tag: Int): List<Int> =
this.filter { it.tag == tag }.map { it.value.blockMode } this.filter { it.tag == tag }.map { it.value.blockMode }
/** Maps to AOSP field = BlockMode (Repeated) */ /** Maps to AOSP field = PaddingMode (Repeated) */
private fun Array<KeyParameter>.findAllPaddingMode(tag: Int): List<Int> = private fun Array<KeyParameter>.findAllPaddingMode(tag: Int): List<Int> =
this.filter { it.tag == tag }.map { it.value.paddingMode } this.filter { it.tag == tag }.map { it.value.paddingMode }
@@ -156,6 +203,19 @@ private fun Array<KeyParameter>.findAllKeyPurpose(tag: Int): List<Int> =
private fun Array<KeyParameter>.findAllDigests(tag: Int): List<Int> = private fun Array<KeyParameter>.findAllDigests(tag: Int): List<Int> =
this.filter { it.tag == tag }.map { it.value.digest } this.filter { it.tag == tag }.map { it.value.digest }
/** Derives keySize from EC_CURVE tag when KEY_SIZE is not explicitly provided. */
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 * Derives the EC Curve name. Logic: Checks specific EC_CURVE tag first (field=EcCurve), falls back
* to KEY_SIZE (field=Integer). * to KEY_SIZE (field=Integer).
@@ -7,7 +7,6 @@ import android.os.IBinder
import android.os.ServiceManager import android.os.ServiceManager
import java.io.File import java.io.File
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.attestation.DeviceAttestationService
import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.KeyBoxManager import org.matrix.TEESimulator.pki.KeyBoxManager
@@ -31,7 +30,6 @@ object ConfigurationManager {
// --- Configuration Paths --- // --- Configuration Paths ---
const val CONFIG_PATH = "/data/adb/tricky_store" const val CONFIG_PATH = "/data/adb/tricky_store"
private const val TARGET_PACKAGES_FILE = "target.txt" private const val TARGET_PACKAGES_FILE = "target.txt"
private const val TEE_STATUS_FILE = "tee_status.txt"
private const val PATCH_LEVEL_FILE = "security_patch.txt" private const val PATCH_LEVEL_FILE = "security_patch.txt"
private const val DEFAULT_KEYBOX_FILE = "keybox.xml" private const val DEFAULT_KEYBOX_FILE = "keybox.xml"
private val configRoot = File(CONFIG_PATH) private val configRoot = File(CONFIG_PATH)
@@ -39,7 +37,6 @@ object ConfigurationManager {
// --- In-Memory Configuration State --- // --- In-Memory Configuration State ---
@Volatile private var packageModes = mapOf<String, Mode>() @Volatile private var packageModes = mapOf<String, Mode>()
@Volatile private var packageKeyboxes = mapOf<String, String>() @Volatile private var packageKeyboxes = mapOf<String, String>()
@Volatile private var isTeeBroken: Boolean? = null
@Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null @Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null
@Volatile private var packagePatchLevels = mapOf<String, CustomPatchLevel>() @Volatile private var packagePatchLevels = mapOf<String, CustomPatchLevel>()
@@ -68,7 +65,6 @@ object ConfigurationManager {
// Initial load of all configuration files. // Initial load of all configuration files.
loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE)) loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE))
loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE)) loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE))
storeTeeStatus() // Check and store the current TEE status.
// Start watching for any subsequent file changes. // Start watching for any subsequent file changes.
ConfigObserver.startWatching() ConfigObserver.startWatching()
@@ -88,7 +84,10 @@ object ConfigurationManager {
} }
/** Determines if the certificate for a given UID needs to be patched. */ /** Determines if the certificate for a given UID needs to be patched. */
fun shouldPatch(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.PATCH fun shouldPatch(uid: Int): Boolean {
val mode = getPackageModeForUid(uid)
return mode == Mode.PATCH || mode == Mode.AUTO
}
/** Determines if a new certificate needs to be generated for a given UID. */ /** Determines if a new certificate needs to be generated for a given UID. */
fun shouldGenerate(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.GENERATE fun shouldGenerate(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.GENERATE
@@ -96,24 +95,23 @@ object ConfigurationManager {
/** Determines if no operation is needed for a given UID. */ /** Determines if no operation is needed for a given UID. */
fun shouldSkipUid(uid: Int): Boolean = getPackageModeForUid(uid) == null fun shouldSkipUid(uid: Int): Boolean = getPackageModeForUid(uid) == null
/** Determines if the UID is in AUTO mode (no explicit ! or ? suffix). */
fun isAutoMode(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.AUTO
/** Resolves the operating mode for a given UID based on its packages and the TEE status. */ /** Resolves the operating mode for a given UID based on its packages and the TEE status. */
private fun getPackageModeForUid(uid: Int): Mode? { private fun getPackageModeForUid(uid: Int): Mode? {
val packages = getPackagesForUid(uid) val packages = getPackagesForUid(uid)
if (packages.isEmpty()) return null if (packages.isEmpty()) return null
// Lazily load TEE status if it hasn't been checked yet.
if (isTeeBroken == null) loadTeeStatus()
// Find the first configured mode for any of the UID's packages.
for (pkg in packages) { for (pkg in packages) {
when (packageModes[pkg]) { when (packageModes[pkg]) {
Mode.GENERATE -> return Mode.GENERATE Mode.GENERATE -> return Mode.GENERATE
Mode.PATCH -> return Mode.PATCH Mode.PATCH -> return Mode.PATCH
Mode.AUTO -> return if (isTeeBroken == true) Mode.GENERATE else Mode.PATCH Mode.AUTO -> return Mode.AUTO
null -> continue // No config for this package, check the next one. null -> continue
} }
} }
return null // No configuration found for this UID. return null
} }
/** /**
@@ -158,25 +156,25 @@ object ConfigurationManager {
return@forEach return@forEach
} }
val mode: Mode
val rawPkg: String
when { when {
// Suffix '!' means force GENERATE mode.
trimmedLine.endsWith("!") -> { trimmedLine.endsWith("!") -> {
val pkg = trimmedLine.removeSuffix("!").trim() mode = Mode.GENERATE
newModes[pkg] = Mode.GENERATE rawPkg = trimmedLine.removeSuffix("!").trim()
newKeyboxes[pkg] = currentKeybox
} }
// Suffix '?' means force PATCH mode.
trimmedLine.endsWith("?") -> { trimmedLine.endsWith("?") -> {
val pkg = trimmedLine.removeSuffix("?").trim() mode = Mode.PATCH
newModes[pkg] = Mode.PATCH rawPkg = trimmedLine.removeSuffix("?").trim()
newKeyboxes[pkg] = currentKeybox
} }
// No suffix means AUTO mode.
else -> { else -> {
newModes[trimmedLine] = Mode.AUTO mode = Mode.AUTO
newKeyboxes[trimmedLine] = currentKeybox rawPkg = trimmedLine
} }
} }
newModes[rawPkg] = mode
newKeyboxes[rawPkg] = currentKeybox
} }
// Atomically update the configuration maps. // Atomically update the configuration maps.
@@ -253,14 +251,7 @@ object ConfigurationManager {
} }
// Parse global and per-package configurations. // Parse global and per-package configurations.
var newGlobalLevel = parseLines(contextLines[""]) val 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 contextLines.remove("") // Remove global context to iterate over packages next
for ((pkg, lines) in contextLines) { for ((pkg, lines) in contextLines) {
@@ -280,29 +271,6 @@ object ConfigurationManager {
} }
} }
/** Checks the device's TEE status and writes the result to a file for persistence. */
private fun storeTeeStatus() {
val statusFile = File(configRoot, TEE_STATUS_FILE)
isTeeBroken = !DeviceAttestationService.isTeeFunctional
try {
statusFile.writeText("tee_broken=$isTeeBroken")
SystemLogger.info("TEE status stored: isTeeBroken=$isTeeBroken")
} catch (e: Exception) {
SystemLogger.error("Failed to write TEE status to file.", e)
}
}
/** Loads the TEE status from the file. */
private fun loadTeeStatus() {
val statusFile = File(configRoot, TEE_STATUS_FILE)
isTeeBroken =
if (statusFile.exists()) {
statusFile.readText().trim() == "tee_broken=true"
} else {
null // Status is unknown.
}
}
/** /**
* A FileObserver that monitors the configuration directory for changes and triggers reloads of * A FileObserver that monitors the configuration directory for changes and triggers reloads of
* the relevant settings. * the relevant settings.
@@ -314,10 +282,8 @@ object ConfigurationManager {
val file = if (event != DELETE) File(configRoot, path) else null val file = if (event != DELETE) File(configRoot, path) else null
when (path) { when (path) {
TARGET_PACKAGES_FILE -> file?.let { loadTargetPackages(it) } TARGET_PACKAGES_FILE -> loadTargetPackages(file!!)
?: SystemLogger.warning("$TARGET_PACKAGES_FILE was deleted.") PATCH_LEVEL_FILE -> loadPatchLevelConfig(file!!)
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. // Any change to an XML file is assumed to be a keybox.
// The cache in KeyBoxManager will handle reloading it on its next use. // The cache in KeyBoxManager will handle reloading it on its next use.
else -> else ->
@@ -360,6 +326,32 @@ object ConfigurationManager {
return iPackageManager return iPackageManager
} }
/** Checks if any package belonging to the UID holds the given permission. */
/** Checks a SELinux permission for a caller identified by PID against the keystore context. */
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
}
}
/** Checks if any package belonging to the UID holds the given permission. */
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
}
}
}
/** Retrieves the package names associated with a UID. */ /** Retrieves the package names associated with a UID. */
fun getPackagesForUid(uid: Int): Array<String> { fun getPackagesForUid(uid: Int): Array<String> {
return uidToPackagesCache.getOrPut(uid) { return uidToPackagesCache.getOrPut(uid) {
@@ -109,17 +109,17 @@ abstract class BinderInterceptor : Binder() {
* `handlePostTransact`). * `handlePostTransact`).
*/ */
final override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { final override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
// The native hook prepends a transaction ID to the data parcel.
val txId = data.readLong() val txId = data.readLong()
val result = val result = try {
when (code) { when (code) {
// These codes are defined in the native layer to distinguish hook types.
PRE_TRANSACT_CODE -> handlePreTransact(txId, data) PRE_TRANSACT_CODE -> handlePreTransact(txId, data)
POST_TRANSACT_CODE -> handlePostTransact(txId, data) POST_TRANSACT_CODE -> handlePostTransact(txId, data)
else -> return super.onTransact(code, data, reply, flags) else -> return super.onTransact(code, data, reply, flags)
} }
} catch (e: Throwable) {
// The reply parcel is guaranteed to be non-null for our custom transactions. SystemLogger.error("[TX_ID: $txId] Interceptor exception, falling through to HAL", e)
TransactionResult.ContinueAndSkipPost
}
writeResultToReply(result, reply!!) writeResultToReply(result, reply!!)
return true return true
} }
@@ -293,15 +293,27 @@ 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) { * Uses the backdoor binder to register an interceptor for a specific target service.
*
* @param filteredCodes If non-empty, only these transaction codes will be intercepted at
* the native level. All other codes pass through without the round-trip to Java.
*/
fun register(
backdoor: IBinder,
target: IBinder,
interceptor: BinderInterceptor,
filteredCodes: IntArray = intArrayOf(),
) {
val data = Parcel.obtain() val data = Parcel.obtain()
val reply = Parcel.obtain() val reply = Parcel.obtain()
try { try {
data.writeStrongBinder(target) data.writeStrongBinder(target)
data.writeStrongBinder(interceptor) data.writeStrongBinder(interceptor)
data.writeInt(filteredCodes.size)
for (code in filteredCodes) data.writeInt(code)
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0) 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) { } catch (e: Exception) {
SystemLogger.error("Failed to register binder interceptor.", e) SystemLogger.error("Failed to register binder interceptor.", e)
} finally { } finally {
@@ -68,11 +68,17 @@ abstract class AbstractKeystoreInterceptor : BinderInterceptor() {
} }
} }
/**
* Transaction codes this interceptor needs to handle at the native level. Override in
* subclasses to filter; empty means intercept everything (legacy behavior).
*/
protected open val interceptedCodes: IntArray = intArrayOf()
/** Registers this interceptor with the native hook layer and sets up a death recipient. */ /** Registers this interceptor with the native hook layer and sets up a death recipient. */
private fun setupInterceptor(service: IBinder, backdoor: IBinder) { private fun setupInterceptor(service: IBinder, backdoor: IBinder) {
keystoreService = service keystoreService = service
SystemLogger.info("Registering interceptor for service: $serviceName") SystemLogger.info("Registering interceptor for service: $serviceName")
register(backdoor, service, this) register(backdoor, service, this, interceptedCodes)
service.linkToDeath(createDeathRecipient(), 0) service.linkToDeath(createDeathRecipient(), 0)
onInterceptorReady(service, backdoor) onInterceptorReady(service, backdoor)
} }
@@ -1,11 +1,16 @@
package org.matrix.TEESimulator.interception.keystore 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.Parcel
import android.os.Parcelable import android.os.Parcelable
import android.security.KeyStore import android.security.KeyStore
import android.security.keystore.KeystoreResponse import android.security.keystore.KeystoreResponse
import android.system.keystore2.Authorization
import org.matrix.TEESimulator.interception.core.BinderInterceptor import org.matrix.TEESimulator.interception.core.BinderInterceptor
import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.util.AndroidDeviceUtils
data class KeyIdentifier(val uid: Int, val alias: String) data class KeyIdentifier(val uid: Int, val alias: String)
@@ -18,6 +23,7 @@ object InterceptorUtils {
val parcel = Parcel.obtain().apply { val parcel = Parcel.obtain().apply {
writeInt(EX_SERVICE_SPECIFIC) writeInt(EX_SERVICE_SPECIFIC)
writeString(null) writeString(null)
writeInt(0)
writeInt(errorCode) writeInt(errorCode)
} }
return BinderInterceptor.TransactionResult.OverrideReply(parcel) return BinderInterceptor.TransactionResult.OverrideReply(parcel)
@@ -119,6 +125,69 @@ object InterceptorUtils {
/** Checks if a reply parcel contains an exception without consuming it. */ /** Checks if a reply parcel contains an exception without consuming it. */
fun hasException(reply: Parcel): Boolean { 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
}
/**
* Creates an `OverrideReply` that writes a `ServiceSpecificException` with the given error
* code via EX_SERVICE_SPECIFIC.
*/
fun createServiceSpecificErrorReply(
errorCode: Int
): BinderInterceptor.TransactionResult.OverrideReply {
val parcel =
Parcel.obtain().apply {
writeException(android.os.ServiceSpecificException(errorCode))
}
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
/**
* Patches the system-level authorization values (OS_PATCHLEVEL, VENDOR_PATCHLEVEL,
* BOOT_PATCHLEVEL) in an authorization array to match the configured patch levels for the
* given calling UID. Each authorization's original [Authorization.securityLevel] is preserved.
*
* When a patch level is configured as "no" ([AndroidDeviceUtils.DO_NOT_REPORT]), the original
* hardware value is kept as-is.
*/
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()
} }
} }
@@ -5,15 +5,18 @@ import android.hardware.security.keymint.SecurityLevel
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.Parcel import android.os.Parcel
import android.system.keystore2.Domain
import android.system.keystore2.IKeystoreSecurityLevel
import android.system.keystore2.IKeystoreService import android.system.keystore2.IKeystoreService
import android.system.keystore2.KeyDescriptor import android.system.keystore2.KeyDescriptor
import android.system.keystore2.KeyEntryResponse import android.system.keystore2.KeyEntryResponse
import java.security.SecureRandom import java.security.SecureRandom
import java.security.cert.Certificate import java.security.cert.Certificate
import java.util.Collections
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.attestation.AttestationPatcher import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.attestation.KeyMintAttestation import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager 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.interception.keystore.shim.KeyMintSecurityLevelInterceptor
import org.matrix.TEESimulator.logging.KeyMintParameterLogger import org.matrix.TEESimulator.logging.KeyMintParameterLogger
import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.logging.SystemLogger
@@ -44,6 +47,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
if (Build.VERSION.SDK_INT >= 34) if (Build.VERSION.SDK_INT >= 34)
InterceptorUtils.getTransactCode(stubBinderClass, "listEntriesBatched") InterceptorUtils.getTransactCode(stubBinderClass, "listEntriesBatched")
else null else null
private val GET_NUMBER_OF_ENTRIES_TRANSACTION =
InterceptorUtils.getTransactCode(stubBinderClass, "getNumberOfEntries")
private val GET_SECURITY_LEVEL_TRANSACTION =
InterceptorUtils.getTransactCode(stubBinderClass, "getSecurityLevel")
private val transactionNames: Map<Int, String> by lazy { private val transactionNames: Map<Int, String> by lazy {
stubBinderClass.declaredFields stubBinderClass.declaredFields
@@ -54,15 +61,43 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] } .associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
} }
// Keys whose certs were updated via updateSubcomponent; skip re-patching on getKeyEntry.
private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
// Backdoor binder for registering new interceptors at runtime.
private var backdoorBinder: IBinder? = null
// Per-security-level interceptor instances, keyed by SecurityLevel constant.
private val securityLevelInterceptors = ConcurrentHashMap<Int, KeyMintSecurityLevelInterceptor>()
// Identity set of SecurityLevel binders already registered with the native hook,
// tracked by System.identityHashCode to avoid re-registering the same BBinder.
private val registeredSecurityLevelBinders: MutableSet<Int> =
Collections.newSetFromMap(ConcurrentHashMap())
override val serviceName = "android.system.keystore2.IKeystoreService/default" override val serviceName = "android.system.keystore2.IKeystoreService/default"
override val processName = "keystore2" override val processName = "keystore2"
override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry" 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,
GET_SECURITY_LEVEL_TRANSACTION,
)
.toIntArray()
}
/** /**
* This method is called once the main service is hooked. It proceeds to find and hook the * 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). * security level sub-services (e.g., TEE, StrongBox).
*/ */
override fun onInterceptorReady(service: IBinder, backdoor: IBinder) { override fun onInterceptorReady(service: IBinder, backdoor: IBinder) {
backdoorBinder = backdoor
val keystoreInterface = IKeystoreService.Stub.asInterface(service) val keystoreInterface = IKeystoreService.Stub.asInterface(service)
setupSecurityLevelInterceptors(keystoreInterface, backdoor) setupSecurityLevelInterceptors(keystoreInterface, backdoor)
} }
@@ -74,7 +109,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...") SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...")
val interceptor = val interceptor =
KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT) KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
register(backdoor, tee.asBinder(), interceptor) securityLevelInterceptors[SecurityLevel.TRUSTED_ENVIRONMENT] = interceptor
registerSecurityLevelBinder(
backdoor,
tee.asBinder(),
interceptor,
)
interceptor.loadPersistedKeys() interceptor.loadPersistedKeys()
} }
} }
@@ -86,13 +126,42 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...") SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
val interceptor = val interceptor =
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX) KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
register(backdoor, strongbox.asBinder(), interceptor) securityLevelInterceptors[SecurityLevel.STRONGBOX] = interceptor
registerSecurityLevelBinder(
backdoor,
strongbox.asBinder(),
interceptor,
)
interceptor.loadPersistedKeys() interceptor.loadPersistedKeys()
} }
} }
.onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) } .onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) }
} }
/**
* Registers an interceptor for a SecurityLevel binder, tracking the binder identity
* to avoid duplicate registrations when keystore2 returns the same BBinder.
*/
private fun registerSecurityLevelBinder(
backdoor: IBinder,
binder: IBinder,
interceptor: KeyMintSecurityLevelInterceptor,
) {
val identity = System.identityHashCode(binder)
if (registeredSecurityLevelBinders.add(identity)) {
register(
backdoor,
binder,
interceptor,
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
)
} else {
SystemLogger.debug(
"SecurityLevel binder $binder (identity=$identity) already registered, skipping."
)
}
}
override fun onPreTransact( override fun onPreTransact(
txId: Long, txId: Long,
target: IBinder, target: IBinder,
@@ -102,7 +171,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
callingPid: Int, callingPid: Int,
data: Parcel, data: Parcel,
): TransactionResult { ): TransactionResult {
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) { 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) logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString() val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
@@ -110,23 +184,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
if (isGMS || ConfigurationManager.shouldSkipUid(callingUid)) { if (isGMS || ConfigurationManager.shouldSkipUid(callingUid)) {
return TransactionResult.ContinueAndSkipPost return TransactionResult.ContinueAndSkipPost
} else {
return TransactionResult.Continue
} }
return runCatching {
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
if (ListEntriesHandler.cacheParameters(txId, data, isBatchMode)) {
TransactionResult.Continue
} else {
TransactionResult.ContinueAndSkipPost
}
}
.getOrElse {
SystemLogger.error(
"[TX_ID: $txId] Failed to parse parameters for ${transactionNames[code]!!}",
it,
)
TransactionResult.ContinueAndSkipPost
}
} else if ( } else if (
code == GET_KEY_ENTRY_TRANSACTION || code == GET_KEY_ENTRY_TRANSACTION ||
code == DELETE_KEY_TRANSACTION || code == DELETE_KEY_TRANSACTION ||
@@ -145,27 +205,40 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
data.readTypedObject(KeyDescriptor.CREATOR) data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.ContinueAndSkipPost ?: return TransactionResult.ContinueAndSkipPost
if (descriptor.alias != null) {
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
} else {
SystemLogger.info(
"Skip ${transactionNames[code]!!} for key [alias, blob, domain, nspace]: [${descriptor.alias}, ${descriptor.blob}, ${descriptor.domain}, ${descriptor.nspace}]"
)
return TransactionResult.ContinueAndSkipPost
}
val keyId = KeyIdentifier(callingUid, descriptor.alias)
if (code == DELETE_KEY_TRANSACTION) { if (code == DELETE_KEY_TRANSACTION) {
if (KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null) { // Handle delete by alias (APP domain) or nspace (KEY_ID domain).
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) KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
SystemLogger.info( if (isSoftwareKey) {
"[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response." SystemLogger.info(
) "[TX_ID: $txId] Deleted cached keypair ${keyId.alias}, replying with empty response."
return InterceptorUtils.createSuccessReply(writeResultCode = false) )
return InterceptorUtils.createSuccessReply(writeResultCode = false)
}
} }
return TransactionResult.ContinueAndSkipPost return TransactionResult.ContinueAndSkipPost
} }
if (descriptor.alias == null) {
return TransactionResult.ContinueAndSkipPost
}
val keyId = KeyIdentifier(callingUid, descriptor.alias)
val response = val response =
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
?: return TransactionResult.Continue ?: return TransactionResult.Continue
@@ -178,6 +251,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
KeyMintParameterLogger.logParameter(it.keyParameter) KeyMintParameterLogger.logParameter(it.keyParameter)
} }
return InterceptorUtils.createTypedObjectReply(response) return InterceptorUtils.createTypedObjectReply(response)
} else if (code == GET_SECURITY_LEVEL_TRANSACTION) {
// Pass through to post-hook so we can register interceptors for newly-created
// SecurityLevel binders. keystore2 may create a new BBinder per call, so the
// initial registration in setupSecurityLevelInterceptors might not cover all
// binder instances that clients receive.
logTransaction(txId, "getSecurityLevel", callingUid, callingPid)
return TransactionResult.Continue
} else { } else {
logTransaction( logTransaction(
txId, txId,
@@ -206,12 +286,39 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply)) if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) { if (code == GET_SECURITY_LEVEL_TRANSACTION) {
return handlePostGetSecurityLevel(txId, data, reply)
}
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) logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
return runCatching { return runCatching {
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
val params =
ListEntriesHandler.cacheParameters(txId, data, isBatchMode)
?: throw Exception("Abort updating entries for invalid parameters.")
val updatedKeyDescriptors = val updatedKeyDescriptors =
ListEntriesHandler.injectGeneratedKeys(txId, callingUid, reply) ListEntriesHandler.injectGeneratedKeys(txId, callingUid, params, reply)
InterceptorUtils.createTypedArrayReply(updatedKeyDescriptors) InterceptorUtils.createTypedArrayReply(updatedKeyDescriptors)
} }
.getOrElse { .getOrElse {
@@ -241,27 +348,25 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
val response = reply.readTypedObject(KeyEntryResponse.CREATOR)!! val response = reply.readTypedObject(KeyEntryResponse.CREATOR)!!
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
// Skip patching for keys whose certs were explicitly set via updateSubcomponent.
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 authorizations = response.metadata.authorizations
val parsedParameters = val parsedParameters =
KeyMintAttestation( KeyMintAttestation(
authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray() authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
) )
if (parsedParameters.isImportKey()) { if (parsedParameters.isAttestKey() &&
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) !KeyMintSecurityLevelInterceptor.importedKeys.contains(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 (parsedParameters.isAttestKey()) {
SystemLogger.warning( SystemLogger.warning(
"[TX_ID: $txId] Found hardware attest key ${keyId.alias} in the reply." "[TX_ID: $txId] Found hardware attest key ${keyId.alias} in the reply."
) )
// Attest keys that are not under our control should be overriden.
val keyData = val keyData =
CertificateGenerator.generateAttestedKeyPair( CertificateGenerator.generateAttestedKeyPair(
callingUid, callingUid,
@@ -276,35 +381,29 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
keyData.second.toTypedArray(), keyData.second.toTypedArray(),
) )
.getOrThrow() .getOrThrow()
response.metadata.authorizations =
InterceptorUtils.patchAuthorizations(
response.metadata.authorizations,
callingUid,
)
keyDescriptor.nspace = SecureRandom().nextLong() val newNspace = SecureRandom().nextLong()
response.metadata.key?.let { it.nspace = newNspace }
KeyMintSecurityLevelInterceptor.generatedKeys[keyId] = KeyMintSecurityLevelInterceptor.generatedKeys[keyId] =
KeyMintSecurityLevelInterceptor.GeneratedKeyInfo( KeyMintSecurityLevelInterceptor.GeneratedKeyInfo(
keyData.first, keyData.first,
keyDescriptor.nspace, null,
newNspace,
response, response,
parsedParameters,
) )
KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId) 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,
purposes = parsedParameters.purpose,
digests = parsedParameters.digest,
isAttestationKey = true,
)
return InterceptorUtils.createTypedObjectReply(response) return InterceptorUtils.createTypedObjectReply(response)
} }
val originalChain = CertificateHelper.getCertificateChain(response) val originalChain = CertificateHelper.getCertificateChain(response)
// Check if we should perform attestation patch.
if (originalChain == null || originalChain.size < 2) { if (originalChain == null || originalChain.size < 2) {
SystemLogger.info( SystemLogger.info(
"[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}." "[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}."
@@ -312,6 +411,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
} }
// First, try to retrieve the already-patched chain from our cache to ensure
// consistency.
val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
val finalChain: Array<Certificate> val finalChain: Array<Certificate>
@@ -321,15 +422,25 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
) )
finalChain = cachedChain finalChain = cachedChain
} else { } 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( SystemLogger.info(
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback." "[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
) )
finalChain = finalChain =
AttestationPatcher.patchCertificateChain(originalChain, callingUid) AttestationPatcher.patchCertificateChain(originalChain, callingUid)
KeyMintSecurityLevelInterceptor.patchedChains[keyId] = finalChain
SystemLogger.debug("Cached patched certificate chain for $keyId.")
} }
CertificateHelper.updateCertificateChain(response.metadata, finalChain) CertificateHelper.updateCertificateChain(response.metadata, finalChain)
.getOrThrow() .getOrThrow()
response.metadata.authorizations =
InterceptorUtils.patchAuthorizations(
response.metadata.authorizations,
callingUid,
)
return InterceptorUtils.createTypedObjectReply(response) return InterceptorUtils.createTypedObjectReply(response)
} }
@@ -347,9 +458,28 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult { private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult {
data.enforceInterface(IKeystoreService.DESCRIPTOR) data.enforceInterface(IKeystoreService.DESCRIPTOR)
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR) val descriptor = data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.ContinueAndSkipPost
// Resolve by nspace (KEY_ID) or alias (APP), same as createOperation.
val generatedKeyInfo = val generatedKeyInfo =
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(callingUid, descriptor?.nspace) when (descriptor.domain) {
?: return TransactionResult.ContinueAndSkipPost Domain.KEY_ID ->
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
callingUid,
descriptor.nspace,
)
Domain.APP ->
descriptor.alias?.let {
KeyMintSecurityLevelInterceptor.generatedKeys[KeyIdentifier(callingUid, it)]
}
else -> null
}
if (generatedKeyInfo == null) {
// Hardware key: mark so getKeyEntry skips cert re-patching.
descriptor.alias?.let { userUpdatedKeys.add(KeyIdentifier(callingUid, it)) }
return TransactionResult.ContinueAndSkipPost
}
SystemLogger.info("Updating sub-component with key[${generatedKeyInfo.nspace}]") SystemLogger.info("Updating sub-component with key[${generatedKeyInfo.nspace}]")
val metadata = generatedKeyInfo.response.metadata val metadata = generatedKeyInfo.response.metadata
@@ -358,13 +488,73 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
metadata.certificate = publicCert metadata.certificate = publicCert
metadata.certificateChain = certificateChain metadata.certificateChain = certificateChain
GeneratedKeyPersistence.rePersistIfNeeded(callingUid, generatedKeyInfo)
SystemLogger.verbose( SystemLogger.verbose(
"Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]" "Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]"
) )
return InterceptorUtils.createSuccessReply(writeResultCode = false) return InterceptorUtils.createSuccessReply(writeResultCode = false)
} }
/**
* Intercepts the reply from getSecurityLevel to dynamically register our interceptor
* for the returned IKeystoreSecurityLevel binder.
*
* keystore2 may create a new BBinder for each getSecurityLevel call, so the binder
* registered during initial setup (in setupSecurityLevelInterceptors) might not be the
* same one that client apps receive. By intercepting every getSecurityLevel reply, we
* ensure that all SecurityLevel binders are covered.
*/
private fun handlePostGetSecurityLevel(
txId: Long,
data: Parcel,
reply: Parcel,
): TransactionResult {
val backdoor = backdoorBinder
if (backdoor == null) {
SystemLogger.warning("[TX_ID: $txId] post-getSecurityLevel: backdoor not available")
return TransactionResult.SkipTransaction
}
return runCatching {
// Read the security level argument from the original request.
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val requestedLevel = data.readInt()
// hasException already consumed the exception header from the reply.
// Next item is the IKeystoreSecurityLevel binder.
val secLevelBinder = reply.readStrongBinder()
if (secLevelBinder == null) {
SystemLogger.verbose(
"[TX_ID: $txId] getSecurityLevel($requestedLevel) returned null binder"
)
return@runCatching TransactionResult.SkipTransaction
}
// Only intercept TEE and StrongBox security levels.
if (requestedLevel != SecurityLevel.TRUSTED_ENVIRONMENT &&
requestedLevel != SecurityLevel.STRONGBOX
) {
return@runCatching TransactionResult.SkipTransaction
}
// Get or create the interceptor for this security level. The interceptor may not
// exist yet if the initial setupSecurityLevelInterceptors call failed for this level.
val interceptor = securityLevelInterceptors.getOrPut(requestedLevel) {
val secLevelInterface =
IKeystoreSecurityLevel.Stub.asInterface(secLevelBinder)
SystemLogger.info(
"[TX_ID: $txId] Late-creating interceptor for security level $requestedLevel"
)
KeyMintSecurityLevelInterceptor(secLevelInterface, requestedLevel).also {
it.loadPersistedKeys()
}
}
registerSecurityLevelBinder(backdoor, secLevelBinder, interceptor)
TransactionResult.SkipTransaction
}.getOrElse {
SystemLogger.error("[TX_ID: $txId] Failed to process post-getSecurityLevel.", it)
TransactionResult.SkipTransaction
}
}
} }
@@ -399,17 +399,18 @@ private data class LegacyKeygenParameters(
/** /**
* Converts the legacy parameters into the modern [KeyMintAttestation] data structure, which is * Converts the legacy parameters into the modern [KeyMintAttestation] data structure, which is
* required by the refactored [AttestationBuilder] and [CertificateGenerator]. * required by [AttestationBuilder] and [CertificateGenerator].
*/ */
fun toKeyMintAttestation(): KeyMintAttestation { fun toKeyMintAttestation(): KeyMintAttestation {
// This conversion acts as a bridge, allowing our new generic components // This conversion acts as a bridge, allowing our new generic components
// to be used by the legacy interceptor. // to be used by the legacy interceptor.
return KeyMintAttestation( return KeyMintAttestation(
keySize = this.keySize,
algorithm = this.algorithm, algorithm = this.algorithm,
ecCurve = 0, ecCurve = 0, // Not explicitly available in legacy args, but not critical
ecCurveName = this.ecCurveName ?: "", ecCurveName = this.ecCurveName ?: "",
origin = null, keySize = this.keySize,
origin = null, // Not needed to build attestaion
noAuthRequired = null,
blockMode = listOf<Int>(), blockMode = listOf<Int>(),
padding = listOf<Int>(), padding = listOf<Int>(),
purpose = this.purpose, purpose = this.purpose,
@@ -431,6 +432,22 @@ private data class LegacyKeygenParameters(
manufacturer = null, manufacturer = null,
model = null, model = null,
secondImei = 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,
maxUsesPerBoot = null,
maxBootLevel = null,
minMacLength = null,
rsaOaepMgfDigest = emptyList(),
) )
} }
@@ -5,7 +5,6 @@ import android.system.keystore2.Domain
import android.system.keystore2.IKeystoreService import android.system.keystore2.IKeystoreService
import android.system.keystore2.KeyDescriptor import android.system.keystore2.KeyDescriptor
import java.util.TreeMap import java.util.TreeMap
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.logging.SystemLogger
@@ -22,15 +21,6 @@ object ListEntriesHandler {
// Estimate for maximum size of a Binder response in bytes. // Estimate for maximum size of a Binder response in bytes.
private const val RESPONSE_SIZE_LIMIT = 358400 private const val RESPONSE_SIZE_LIMIT = 358400
// Parameters of AOSP function `list_key_entries` in utils.rs.
private data class ListEntriesParams(
val domain: Int,
val namespace: Long,
val startPastAlias: String?,
)
private val pendingParams = ConcurrentHashMap<Long, ListEntriesParams>()
// Based on AOSP function `estimate_safe_amount_to_return` in utils.rs. // Based on AOSP function `estimate_safe_amount_to_return` in utils.rs.
private fun estimateSafeAmountToReturn( private fun estimateSafeAmountToReturn(
keyDescriptors: Array<KeyDescriptor>, keyDescriptors: Array<KeyDescriptor>,
@@ -60,7 +50,7 @@ object ListEntriesHandler {
} }
// Parse and store parameters for later use (in post-transaction). // Parse and store parameters for later use (in post-transaction).
fun cacheParameters(txId: Long, data: Parcel, isBatchMode: Boolean): Boolean { fun cacheParameters(txId: Long, data: Parcel, isBatchMode: Boolean): ListEntriesParams? {
data.enforceInterface(IKeystoreService.DESCRIPTOR) data.enforceInterface(IKeystoreService.DESCRIPTOR)
val domain = data.readInt() val domain = data.readInt()
@@ -71,20 +61,21 @@ object ListEntriesHandler {
// See AOSP function `get_key_descriptor_for_lookup` in service.rs. // See AOSP function `get_key_descriptor_for_lookup` in service.rs.
// Note that all generated keys belong to Domain::APP. // Note that all generated keys belong to Domain::APP.
if (domain == Domain.APP) { if (domain == Domain.APP) {
pendingParams[txId] = ListEntriesParams(domain, namespace, startPastAlias) val params = ListEntriesParams(domain, namespace, startPastAlias)
SystemLogger.debug("[TX_ID: $txId] Cached ${pendingParams[txId]}.") SystemLogger.debug("[TX_ID: $txId] Cached $params.")
return true return params
} }
return false return null
} }
// Merge software-backed keys with hardware-backed keys in the reply parcel. // Merge software-backed keys with hardware-backed keys in the reply parcel.
fun injectGeneratedKeys(txId: Long, callingUid: Int, reply: Parcel): Array<KeyDescriptor> { fun injectGeneratedKeys(
val params = txId: Long,
pendingParams.remove(txId) callingUid: Int,
?: throw IllegalStateException("No params found for listing entries") params: ListEntriesParams,
reply: Parcel,
): Array<KeyDescriptor> {
// By default we use the calling uid as namespace if domain is Domain::APP. // By default we use the calling uid as namespace if domain is Domain::APP.
// The namespace parameter is thus ignored for non-privileged applications. // The namespace parameter is thus ignored for non-privileged applications.
// See AOSP function `get_key_descriptor_for_lookup` in service.rs. // See AOSP function `get_key_descriptor_for_lookup` in service.rs.
@@ -129,7 +120,7 @@ object ListEntriesHandler {
startPastAlias: String?, startPastAlias: String?,
): List<KeyDescriptor> { ): List<KeyDescriptor> {
return KeyMintSecurityLevelInterceptor.generatedKeys.keys return KeyMintSecurityLevelInterceptor.generatedKeys.keys
.filter { it.uid == uid && (startPastAlias == null || it.alias < startPastAlias) } .filter { it.uid == uid && (startPastAlias == null || it.alias > startPastAlias) }
.map { keyId -> .map { keyId ->
KeyDescriptor().apply { KeyDescriptor().apply {
this.domain = Domain.APP this.domain = Domain.APP
@@ -140,3 +131,6 @@ object ListEntriesHandler {
} }
} }
} }
// Parameters of AOSP function `list_key_entries` in utils.rs.
data class ListEntriesParams(val domain: Int, val namespace: Long, val startPastAlias: String?)
@@ -16,7 +16,7 @@ import java.util.concurrent.locks.ReentrantLock
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertificateHelper
data class PersistedKeyData( data class PersistedKeyData(
val uid: Int, val uid: Int,
@@ -129,6 +129,7 @@ object GeneratedKeyPersistence {
val file = File(PERSISTENCE_DIR, keyFileName(keyId.uid, keyId.alias)) val file = File(PERSISTENCE_DIR, keyFileName(keyId.uid, keyId.alias))
if (file.exists()) { if (file.exists()) {
if (file.delete()) { if (file.delete()) {
fileLocks.remove(keyFileName(keyId.uid, keyId.alias))
SystemLogger.debug("Deleted persisted key: $keyId") SystemLogger.debug("Deleted persisted key: $keyId")
} else { } else {
SystemLogger.warning("Failed to delete persisted key file: ${file.name}") SystemLogger.warning("Failed to delete persisted key file: ${file.name}")
@@ -158,6 +159,7 @@ object GeneratedKeyPersistence {
if (file.delete()) count++ if (file.delete()) count++
} }
} }
fileLocks.clear()
SystemLogger.info("Deleted $count persisted key files") SystemLogger.info("Deleted $count persisted key files")
}.onFailure { e -> }.onFailure { e ->
SystemLogger.error("Failed to delete all persisted keys", e) SystemLogger.error("Failed to delete all persisted keys", e)
@@ -248,76 +250,6 @@ object GeneratedKeyPersistence {
return result 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 { private fun requireBounds(value: Int, max: Int, name: String): Int {
require(value in 0..max) { "$name out of bounds: $value (max $max)" } require(value in 0..max) { "$name out of bounds: $value (max $max)" }
return value return value
@@ -328,49 +260,4 @@ object GeneratedKeyPersistence {
.digest("$uid:$alias".toByteArray(Charsets.UTF_8)) .digest("$uid:$alias".toByteArray(Charsets.UTF_8))
return digest.joinToString("") { "%02x".format(it) } + ".bin" 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,
)
}
} }
@@ -44,6 +44,9 @@ class OperationInterceptor(
private val ABORT_TRANSACTION = private val ABORT_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort") InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort")
/** Only intercept finish/abort for cleanup. Other ops pass through without round-trip. */
val INTERCEPTED_CODES = intArrayOf(FINISH_TRANSACTION, ABORT_TRANSACTION)
private val transactionNames: Map<Int, String> by lazy { private val transactionNames: Map<Int, String> by lazy {
IKeystoreOperation.Stub::class IKeystoreOperation.Stub::class
.java .java
@@ -3,10 +3,15 @@ package org.matrix.TEESimulator.interception.keystore.shim
import android.hardware.security.keymint.Algorithm import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.BlockMode import android.hardware.security.keymint.BlockMode
import android.hardware.security.keymint.Digest import android.hardware.security.keymint.Digest
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyParameterValue
import android.hardware.security.keymint.KeyPurpose import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.PaddingMode import android.hardware.security.keymint.PaddingMode
import android.hardware.security.keymint.Tag
import android.os.RemoteException import android.os.RemoteException
import android.os.ServiceSpecificException
import android.system.keystore2.IKeystoreOperation import android.system.keystore2.IKeystoreOperation
import android.system.keystore2.KeyParameters
import java.security.KeyPair import java.security.KeyPair
import java.security.Signature import java.security.Signature
import java.security.SignatureException import java.security.SignatureException
@@ -15,13 +20,45 @@ import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.logging.KeyMintParameterLogger import org.matrix.TEESimulator.logging.KeyMintParameterLogger
import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.logging.SystemLogger
/** Keystore2 error codes for ServiceSpecificException. Negative = KeyMint, positive = Keystore. */
internal object KeystoreErrorCode {
const val INVALID_OPERATION_HANDLE = -28
const val VERIFICATION_FAILED = -30
const val UNSUPPORTED_PURPOSE = -2
const val INCOMPATIBLE_PURPOSE = -3
const val SYSTEM_ERROR = 4
const val TOO_MUCH_DATA = 21
const val KEY_EXPIRED = -25
const val KEY_NOT_YET_VALID = -24
/** KeyMint ErrorCode::CALLER_NONCE_PROHIBITED */
const val CALLER_NONCE_PROHIBITED = -55
/** KeyMint ErrorCode::INVALID_ARGUMENT */
const val INVALID_ARGUMENT = -38
/** KeyMint ErrorCode::INVALID_TAG */
const val INVALID_TAG = -40
/** Keystore2 ResponseCode::PERMISSION_DENIED */
const val PERMISSION_DENIED = 6
/** Keystore2 ResponseCode::KEY_NOT_FOUND */
const val KEY_NOT_FOUND = 7
}
// A sealed interface to represent the different cryptographic operations we can perform. // A sealed interface to represent the different cryptographic operations we can perform.
private sealed interface CryptoPrimitive { private sealed interface CryptoPrimitive {
fun updateAad(data: ByteArray?)
fun update(data: ByteArray?): ByteArray? fun update(data: ByteArray?): ByteArray?
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
fun abort() fun abort()
/** Returns parameters from the begin phase (e.g. GCM nonce), or null if none. */
fun getBeginParameters(): Array<KeyParameter>? = null
} }
// Helper object to map KeyMint constants to JCA algorithm strings. // Helper object to map KeyMint constants to JCA algorithm strings.
@@ -39,8 +76,9 @@ private object JcaAlgorithmMapper {
Algorithm.EC -> "ECDSA" Algorithm.EC -> "ECDSA"
Algorithm.RSA -> "RSA" Algorithm.RSA -> "RSA"
else -> else ->
throw IllegalArgumentException( throw ServiceSpecificException(
"Unsupported signature algorithm: ${params.algorithm}" KeystoreErrorCode.SYSTEM_ERROR,
"Unsupported signature algorithm: ${params.algorithm}",
) )
} }
return "${digest}with${keyAlgo}" return "${digest}with${keyAlgo}"
@@ -52,8 +90,9 @@ private object JcaAlgorithmMapper {
Algorithm.RSA -> "RSA" Algorithm.RSA -> "RSA"
Algorithm.AES -> "AES" Algorithm.AES -> "AES"
else -> else ->
throw IllegalArgumentException( throw ServiceSpecificException(
"Unsupported cipher algorithm: ${params.algorithm}" KeystoreErrorCode.SYSTEM_ERROR,
"Unsupported cipher algorithm: ${params.algorithm}",
) )
} }
val blockMode = val blockMode =
@@ -82,6 +121,10 @@ private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimi
initSign(keyPair.private) initSign(keyPair.private)
} }
override fun updateAad(data: ByteArray?) {
throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG)
}
override fun update(data: ByteArray?): ByteArray? { override fun update(data: ByteArray?): ByteArray? {
if (data != null) signature.update(data) if (data != null) signature.update(data)
return null return null
@@ -102,6 +145,10 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri
initVerify(keyPair.public) initVerify(keyPair.public)
} }
override fun updateAad(data: ByteArray?) {
throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG)
}
override fun update(data: ByteArray?): ByteArray? { override fun update(data: ByteArray?): ByteArray? {
if (data != null) signature.update(data) if (data != null) signature.update(data)
return null return null
@@ -109,12 +156,17 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? { override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
if (data != null) update(data) if (data != null) update(data)
if (signature == null) throw SignatureException("Signature to verify is null") if (signature == null)
throw ServiceSpecificException(
KeystoreErrorCode.VERIFICATION_FAILED,
"Signature to verify is null",
)
if (!this.signature.verify(signature)) { if (!this.signature.verify(signature)) {
// Throwing an exception is how Keystore signals verification failure. throw ServiceSpecificException(
throw SignatureException("Signature verification failed") KeystoreErrorCode.VERIFICATION_FAILED,
"Signature/MAC verification failed",
)
} }
// A successful verification returns no data.
return null return null
} }
@@ -123,16 +175,19 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri
// Concrete implementation for Encryption/Decryption. // Concrete implementation for Encryption/Decryption.
private class CipherPrimitive( private class CipherPrimitive(
keyPair: KeyPair, cryptoKey: java.security.Key,
params: KeyMintAttestation, params: KeyMintAttestation,
private val opMode: Int, private val opMode: Int,
) : CryptoPrimitive { ) : CryptoPrimitive {
private val cipher: Cipher = private val cipher: Cipher =
Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply { Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply {
val key = if (opMode == Cipher.ENCRYPT_MODE) keyPair.public else keyPair.private init(opMode, cryptoKey)
init(opMode, key)
} }
override fun updateAad(data: ByteArray?) {
if (data != null) cipher.updateAAD(data)
}
override fun update(data: ByteArray?): ByteArray? = override fun update(data: ByteArray?): ByteArray? =
if (data != null) cipher.update(data) else null if (data != null) cipher.update(data) else null
@@ -140,77 +195,199 @@ private class CipherPrimitive(
if (data != null) cipher.doFinal(data) else cipher.doFinal() if (data != null) cipher.doFinal(data) else cipher.doFinal()
override fun abort() {} override fun abort() {}
/** Returns the cipher IV as a NONCE parameter for GCM operations. */
override fun getBeginParameters(): Array<KeyParameter>? {
val iv = cipher.iv ?: return null
return arrayOf(
KeyParameter().apply {
tag = Tag.NONCE
value = KeyParameterValue.blob(iv)
}
)
}
}
// Concrete implementation for ECDH Key Agreement.
private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive {
private val agreement: javax.crypto.KeyAgreement =
javax.crypto.KeyAgreement.getInstance("ECDH").apply { init(keyPair.private) }
override fun updateAad(data: ByteArray?) {
throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG)
}
override fun update(data: ByteArray?): ByteArray? = null
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
if (data == null)
throw ServiceSpecificException(
KeystoreErrorCode.INVALID_ARGUMENT,
"Peer public key required for key agreement",
)
val peerKey =
java.security.KeyFactory.getInstance("EC")
.generatePublic(java.security.spec.X509EncodedKeySpec(data))
agreement.doPhase(peerKey, true)
return agreement.generateSecret()
}
override fun abort() {}
} }
/** /**
* A software-only implementation of a cryptographic operation. This class acts as a controller, * 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. * delegating to a specific cryptographic primitive based on the operation's purpose.
*
* Tracks operation lifecycle: once [finish] or [abort] is called, subsequent calls throw
* [ServiceSpecificException] with [KeystoreErrorCode.INVALID_OPERATION_HANDLE].
*/ */
class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMintAttestation) { class SoftwareOperation(
// This now holds the specific strategy object (Signer, Verifier, etc.) private val txId: Long,
keyPair: KeyPair?,
secretKey: javax.crypto.SecretKey?,
params: KeyMintAttestation,
var onFinishCallback: (() -> Unit)? = null,
) {
private val primitive: CryptoPrimitive private val primitive: CryptoPrimitive
@Volatile private var finalized = false
init { 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 purpose = params.purpose.firstOrNull()
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN" val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.") SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
primitive = primitive =
when (purpose) { when (purpose) {
KeyPurpose.SIGN -> Signer(keyPair, params) KeyPurpose.SIGN -> Signer(keyPair!!, params)
KeyPurpose.VERIFY -> Verifier(keyPair, params) KeyPurpose.VERIFY -> Verifier(keyPair!!, params)
KeyPurpose.ENCRYPT -> CipherPrimitive(keyPair, params, Cipher.ENCRYPT_MODE) KeyPurpose.ENCRYPT -> {
KeyPurpose.DECRYPT -> CipherPrimitive(keyPair, params, Cipher.DECRYPT_MODE) val key: java.security.Key = secretKey ?: keyPair!!.public
CipherPrimitive(key, params, Cipher.ENCRYPT_MODE)
}
KeyPurpose.DECRYPT -> {
val key: java.security.Key = secretKey ?: keyPair!!.private
CipherPrimitive(key, params, Cipher.DECRYPT_MODE)
}
KeyPurpose.AGREE_KEY -> KeyAgreementPrimitive(keyPair!!)
else -> else ->
throw UnsupportedOperationException("Unsupported operation purpose: $purpose") throw ServiceSpecificException(
KeystoreErrorCode.UNSUPPORTED_PURPOSE,
"Unsupported operation purpose: $purpose",
)
} }
} }
/** Parameters produced during begin (e.g. GCM nonce), to populate CreateOperationResponse. */
val beginParameters: KeyParameters?
get() {
val params = primitive.getBeginParameters() ?: return null
if (params.isEmpty()) return null
return KeyParameters().apply { keyParameter = params }
}
private fun checkActive() {
if (finalized)
throw ServiceSpecificException(
KeystoreErrorCode.INVALID_OPERATION_HANDLE,
"Operation already finalized.",
)
}
fun updateAad(data: ByteArray?) {
checkActive()
try {
primitive.updateAad(data)
} catch (e: ServiceSpecificException) {
finalized = true
throw e
} catch (e: Exception) {
finalized = true
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to updateAad.", e)
throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message)
}
}
fun update(data: ByteArray?): ByteArray? { fun update(data: ByteArray?): ByteArray? {
checkActive()
try { try {
return primitive.update(data) return primitive.update(data)
} catch (e: Exception) { } catch (e: ServiceSpecificException) {
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e) finalized = true
throw e throw e
} catch (e: Exception) {
finalized = true
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message)
} }
} }
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? { fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
checkActive()
try { try {
val result = primitive.finish(data, signature) val result = primitive.finish(data, signature)
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.") SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
onFinishCallback?.invoke()
return result return result
} catch (e: ServiceSpecificException) {
throw e
} catch (e: Exception) { } catch (e: Exception) {
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e) 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 ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message)
throw e } finally {
finalized = true
} }
} }
fun abort() { fun abort() {
checkActive()
finalized = true
primitive.abort() primitive.abort()
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.") SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
} }
} }
/** The Binder interface for our [SoftwareOperation]. */ /** Binder interface for [SoftwareOperation]. Synchronized and input-length validated. */
class SoftwareOperationBinder(private val operation: SoftwareOperation) : class SoftwareOperationBinder(private val operation: SoftwareOperation) :
IKeystoreOperation.Stub() { IKeystoreOperation.Stub() {
private fun checkInputLength(data: ByteArray?) {
if (data != null && data.size > MAX_RECEIVE_DATA)
throw ServiceSpecificException(KeystoreErrorCode.TOO_MUCH_DATA)
}
@Throws(RemoteException::class)
override fun updateAad(aadInput: ByteArray?) {
synchronized(this) {
checkInputLength(aadInput)
operation.updateAad(aadInput)
}
}
@Throws(RemoteException::class) @Throws(RemoteException::class)
override fun update(input: ByteArray?): ByteArray? { override fun update(input: ByteArray?): ByteArray? {
return operation.update(input) synchronized(this) {
checkInputLength(input)
return operation.update(input)
}
} }
@Throws(RemoteException::class) @Throws(RemoteException::class)
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? { override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
return operation.finish(input, signature) synchronized(this) {
checkInputLength(input)
checkInputLength(signature)
return operation.finish(input, signature)
}
} }
@Throws(RemoteException::class) @Throws(RemoteException::class)
override fun abort() { override fun abort() {
operation.abort() synchronized(this) { operation.abort() }
}
companion object {
private const val MAX_RECEIVE_DATA = 0x8000
} }
} }
@@ -37,6 +37,22 @@ object KeyMintParameterLogger {
.associate { field -> (field.get(null) as Int) to field.name } .associate { field -> (field.get(null) as Int) to field.name }
} }
val hardwareAuthenticatorTypeNames: Map<Int, String> by lazy {
HardwareAuthenticatorType::class
.java
.fields
.filter { it.type == Int::class.java }
.associate { field -> (field.get(null) as Int) to field.name }
}
val keyOriginNames: Map<Int, String> by lazy {
KeyOrigin::class
.java
.fields
.filter { it.type == Int::class.java }
.associate { field -> (field.get(null) as Int) to field.name }
}
val paddingNames: Map<Int, String> by lazy { val paddingNames: Map<Int, String> by lazy {
PaddingMode::class PaddingMode::class
.java .java
@@ -81,22 +97,33 @@ object KeyMintParameterLogger {
when (param.tag) { when (param.tag) {
Tag.ALGORITHM -> algorithmNames[value.algorithm] Tag.ALGORITHM -> algorithmNames[value.algorithm]
Tag.BLOCK_MODE -> blockModeNames[value.blockMode] Tag.BLOCK_MODE -> blockModeNames[value.blockMode]
Tag.DIGEST -> digestNames[value.digest]
Tag.EC_CURVE -> ecCurveNames[value.ecCurve] Tag.EC_CURVE -> ecCurveNames[value.ecCurve]
Tag.ORIGIN -> keyOriginNames[value.origin]
Tag.PADDING -> paddingNames[value.paddingMode] Tag.PADDING -> paddingNames[value.paddingMode]
Tag.PURPOSE -> purposeNames[value.keyPurpose] Tag.PURPOSE -> purposeNames[value.keyPurpose]
Tag.DIGEST -> digestNames[value.digest] Tag.USER_AUTH_TYPE ->
hardwareAuthenticatorTypeNames[value.hardwareAuthenticatorType]
Tag.AUTH_TIMEOUT, Tag.AUTH_TIMEOUT,
Tag.BOOT_PATCHLEVEL,
Tag.KEY_SIZE, Tag.KEY_SIZE,
Tag.MIN_MAC_LENGTH -> value.integer.toString() Tag.MAC_LENGTH,
Tag.MIN_MAC_LENGTH,
Tag.OS_VERSION,
Tag.OS_PATCHLEVEL,
Tag.USER_ID,
Tag.VENDOR_PATCHLEVEL -> value.integer.toString()
Tag.CERTIFICATE_SERIAL -> BigInteger(value.blob).toString() Tag.CERTIFICATE_SERIAL -> BigInteger(value.blob).toString()
Tag.ACTIVE_DATETIME, Tag.ACTIVE_DATETIME,
Tag.CERTIFICATE_NOT_AFTER, Tag.CERTIFICATE_NOT_AFTER,
Tag.CERTIFICATE_NOT_BEFORE, Tag.CERTIFICATE_NOT_BEFORE,
Tag.CREATION_DATETIME,
Tag.ORIGINATION_EXPIRE_DATETIME, Tag.ORIGINATION_EXPIRE_DATETIME,
Tag.USAGE_EXPIRE_DATETIME -> Date(value.dateTime).toString() Tag.USAGE_EXPIRE_DATETIME -> Date(value.dateTime).toString()
Tag.CERTIFICATE_SUBJECT -> X500Name(X500Principal(value.blob).name).toString() Tag.CERTIFICATE_SUBJECT -> X500Name(X500Principal(value.blob).name).toString()
Tag.USER_SECURE_ID,
Tag.RSA_PUBLIC_EXPONENT -> value.longInteger.toString() Tag.RSA_PUBLIC_EXPONENT -> value.longInteger.toString()
Tag.NO_AUTH_REQUIRED -> "true" Tag.NO_AUTH_REQUIRED -> value.boolValue.toString()
Tag.ATTESTATION_CHALLENGE, Tag.ATTESTATION_CHALLENGE,
Tag.ATTESTATION_ID_BRAND, Tag.ATTESTATION_ID_BRAND,
Tag.ATTESTATION_ID_DEVICE, Tag.ATTESTATION_ID_DEVICE,
@@ -19,6 +19,7 @@ object SystemLogger {
* @param message The message to log. * @param message The message to log.
*/ */
fun debug(message: String) { fun debug(message: String) {
if (!isDebugBuild) return
Log.d(TAG, message) Log.d(TAG, message)
} }
@@ -8,7 +8,6 @@ import java.math.BigInteger
import java.security.KeyPair import java.security.KeyPair
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.cert.Certificate import java.security.cert.Certificate
import java.security.cert.X509Certificate
import java.security.spec.ECGenParameterSpec import java.security.spec.ECGenParameterSpec
import java.security.spec.RSAKeyGenParameterSpec import java.security.spec.RSAKeyGenParameterSpec
import java.util.Date import java.util.Date
@@ -36,6 +35,9 @@ import org.matrix.TEESimulator.logging.SystemLogger
*/ */
object CertificateGenerator { object CertificateGenerator {
// RFC 5280 GeneralizedTime maximum: 9999-12-31T23:59:59 UTC (millis since epoch).
private const val UNDEFINED_NOT_AFTER = 253402300799000L
/** /**
* Generates a software-based cryptographic key pair. * Generates a software-based cryptographic key pair.
* *
@@ -49,7 +51,10 @@ object CertificateGenerator {
Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName) Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName)
Algorithm.RSA -> Algorithm.RSA ->
"RSA" to "RSA" to
RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent) RSAKeyGenParameterSpec(
params.keySize,
params.rsaPublicExponent ?: RSAKeyGenParameterSpec.F4,
)
else -> else ->
throw IllegalArgumentException( throw IllegalArgumentException(
"Unsupported algorithm: ${params.algorithm}" "Unsupported algorithm: ${params.algorithm}"
@@ -88,11 +93,9 @@ object CertificateGenerator {
"Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})" "Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})"
) )
return runCatching { return try {
val keybox = getKeyboxForAlgorithm(uid, params.algorithm) 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) = val (signingKey, issuer) =
if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second } getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second }
@@ -101,20 +104,20 @@ object CertificateGenerator {
keybox.keyPair to getIssuerFromKeybox(keybox) keybox.keyPair to getIssuerFromKeybox(keybox)
} }
// Build the new leaf certificate with the simulated attestation.
val leafCert = val leafCert =
buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel) 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) { if (attestKeyAlias != null) {
listOf(leafCert) listOf(leafCert)
} else { } else {
listOf(leafCert) + keybox.certificates 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 +131,7 @@ object CertificateGenerator {
params: KeyMintAttestation, params: KeyMintAttestation,
securityLevel: Int, securityLevel: Int,
): Pair<KeyPair, List<Certificate>>? { ): Pair<KeyPair, List<Certificate>>? {
return runCatching { return try {
SystemLogger.info( SystemLogger.info(
"Generating new attested key pair for alias: '$alias' (UID: $uid)" "Generating new attested key pair for alias: '$alias' (UID: $uid)"
) )
@@ -144,11 +147,12 @@ object CertificateGenerator {
"Successfully generated new certificate chain for alias: '$alias'." "Successfully generated new certificate chain for alias: '$alias'."
) )
Pair(newKeyPair, chain) 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) = fun getIssuerFromKeybox(keybox: KeyBox) =
@@ -163,7 +167,10 @@ object CertificateGenerator {
else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm") else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm")
} }
return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName) 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. */ /** Retrieves the key pair and issuer name for a given attestation key alias. */
@@ -192,14 +199,16 @@ object CertificateGenerator {
private fun buildKeyUsageFromPurposes(purposes: List<Int>): Int { private fun buildKeyUsageFromPurposes(purposes: List<Int>): Int {
var bits = 0 var bits = 0
for (purpose in purposes) { for (purpose in purposes) {
bits = bits or when (purpose) { bits =
KeyPurpose.SIGN -> KeyUsage.digitalSignature bits or
KeyPurpose.DECRYPT -> KeyUsage.dataEncipherment when (purpose) {
KeyPurpose.WRAP_KEY -> KeyUsage.keyEncipherment KeyPurpose.SIGN -> KeyUsage.digitalSignature
KeyPurpose.AGREE_KEY -> KeyUsage.keyAgreement KeyPurpose.DECRYPT -> KeyUsage.dataEncipherment
KeyPurpose.ATTEST_KEY -> KeyUsage.keyCertSign KeyPurpose.WRAP_KEY -> KeyUsage.keyEncipherment
else -> 0 KeyPurpose.AGREE_KEY -> KeyUsage.keyAgreement
} KeyPurpose.ATTEST_KEY -> KeyUsage.keyCertSign
else -> 0
}
} }
return bits return bits
} }
@@ -214,16 +223,17 @@ object CertificateGenerator {
securityLevel: Int, securityLevel: Int,
): Certificate { ): Certificate {
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key") val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
val leafNotAfter =
(signingKeyPair.public as? X509Certificate)?.notAfter // Default validity: epoch to 9999-12-31T23:59:59 UTC (matches add_required_parameters).
?: Date(System.currentTimeMillis() + 31536000000L) val notBefore = params.certificateNotBefore ?: Date(0)
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
val builder = val builder =
JcaX509v3CertificateBuilder( JcaX509v3CertificateBuilder(
issuer, issuer,
params.certificateSerial ?: BigInteger.ONE, params.certificateSerial ?: BigInteger.ONE,
params.certificateNotBefore ?: Date(), notBefore,
params.certificateNotAfter ?: leafNotAfter, notAfter,
subject, subject,
subjectKeyPair.public, subjectKeyPair.public,
) )
@@ -238,11 +248,16 @@ object CertificateGenerator {
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel) AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
) )
// The signature algorithm must match the SIGNING key, not the subject key.
// An EC attestation key may sign an RSA subject key's certificate (or vice versa).
val signerAlgorithm = val signerAlgorithm =
when (params.algorithm) { when (signingKeyPair.private) {
Algorithm.EC -> "SHA256withECDSA" is java.security.interfaces.ECKey -> "SHA256withECDSA"
Algorithm.RSA -> "SHA256withRSA" is java.security.interfaces.RSAKey -> "SHA256withRSA"
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}") else ->
throw IllegalArgumentException(
"Unsupported signing key type: ${signingKeyPair.private.javaClass}"
)
} }
val contentSigner = val contentSigner =
JcaContentSignerBuilder(signerAlgorithm) JcaContentSignerBuilder(signerAlgorithm)
@@ -45,6 +45,13 @@ data class CertGenConfig(
val idManufacturer: ByteArray?, val idManufacturer: ByteArray?,
val idModel: ByteArray?, val idModel: ByteArray?,
val idSecondImei: ByteArray?, val idSecondImei: ByteArray?,
val activeDatetime: Long = -1L,
val originationExpireDatetime: Long = -1L,
val usageExpireDatetime: Long = -1L,
val usageCountLimit: Int = -1,
val callerNonce: Boolean = false,
val unlockedDeviceRequired: Boolean = false,
val noAuthRequired: Boolean = true,
) )
object NativeCertGen { object NativeCertGen {
@@ -105,7 +112,7 @@ object NativeCertGen {
} }
val algorithmName = when (certs[0].publicKey.algorithm) { val algorithmName = when (certs[0].publicKey.algorithm) {
"EC" -> "EC" "EC", "ECDSA" -> "EC"
"RSA" -> "RSA" "RSA" -> "RSA"
else -> certs[0].publicKey.algorithm else -> certs[0].publicKey.algorithm
} }
@@ -91,33 +91,27 @@ object AndroidDeviceUtils {
attestationValueProvider: () -> ByteArray?, attestationValueProvider: () -> ByteArray?,
expectedSize: Int, expectedSize: Int,
): ByteArray { ): ByteArray {
// 1. Attempt to get the value from the system property.
getProperty(propertyName, expectedSize)?.let { getProperty(propertyName, expectedSize)?.let {
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}") SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
persistToFile(propertyName, it)
return it return it
} }
// 2. Fallback to the value from a cached TEE attestation.
try { try {
attestationValueProvider()?.let { attestationValueProvider()?.let {
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}") SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
setProperty(propertyName, it) setProperty(propertyName, it) // Persist for consistency
persistToFile(propertyName, it)
return it return it
} }
} catch (e: Exception) { } catch (e: Exception) {
SystemLogger.error("Failed to get $propertyName from attestation.", e) SystemLogger.error("Failed to get $propertyName from attestation.", e)
} }
readFromFile(propertyName, expectedSize)?.let { // 3. As a final fallback, generate a random value.
SystemLogger.debug("Using $propertyName from persistent file: ${it.toHex()}")
setProperty(propertyName, it)
return it
}
return generateRandomBytes(expectedSize).also { return generateRandomBytes(expectedSize).also {
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}") SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
setProperty(propertyName, it) setProperty(propertyName, it)
persistToFile(propertyName, it)
} }
} }
@@ -164,37 +158,10 @@ object AndroidDeviceUtils {
} }
} }
/** Generates a cryptographically random byte array of a specified length. */
private fun generateRandomBytes(size: Int): ByteArray = private fun generateRandomBytes(size: Int): ByteArray =
ByteArray(size).also { ThreadLocalRandom.current().nextBytes(it) } 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 --- // --- Patch Level Properties ---
fun getPatchLevel(uid: Int): Int { fun getPatchLevel(uid: Int): Int {
@@ -273,12 +240,11 @@ object AndroidDeviceUtils {
val resolvedValue = resolveDateKeywords(value) val resolvedValue = resolveDateKeywords(value)
return when { return when {
// "device_default" indicates falling back to the system property.
resolvedValue.equals("device_default", ignoreCase = true) -> null resolvedValue.equals("device_default", ignoreCase = true) -> null
// Resolve from live system prop — matches what detectors see via getprop, // "no" indicates this value should not be reported.
// 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 resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
// Otherwise, parse the resolved date string.
else -> parsePatchLevelValue(resolvedValue, isLong) else -> parsePatchLevelValue(resolvedValue, isLong)
} }
} }
@@ -405,7 +371,10 @@ object AndroidDeviceUtils {
// --- APEX and Module Hash Properties --- // --- APEX and Module Hash Properties ---
// Minimal protobuf parser for apex_manifest.pb (field 1: name, field 2: version) // https://cs.android.com/android/platform/superproject/+/android-latest-release:system/apex/proto/apex_manifest.proto
// --- Minimal Protobuf Parser for ApexManifest ---
// Field 1: name (string)
// Field 2: version (int64)
private class MinimalApexManifestParser(private val data: ByteArray) { private class MinimalApexManifestParser(private val data: ByteArray) {
var pos = 0 var pos = 0
@@ -419,13 +388,13 @@ object AndroidDeviceUtils {
val wireType = (tag and 0x07).toInt() val wireType = (tag and 0x07).toInt()
when (fieldNum) { when (fieldNum) {
1L -> { 1L -> { // name
val length = readVarint().toInt() val length = readVarint().toInt()
if (pos + length > data.size) return null if (pos + length > data.size) return null
name = String(data, pos, length, Charsets.UTF_8) name = String(data, pos, length, Charsets.UTF_8)
pos += length pos += length
} }
2L -> { 2L -> { // version
version = readVarint() version = readVarint()
} }
else -> skipField(wireType) else -> skipField(wireType)
@@ -453,18 +422,19 @@ object AndroidDeviceUtils {
private fun skipField(wireType: Int) { private fun skipField(wireType: Int) {
when (wireType) { when (wireType) {
0 -> readVarint() 0 -> readVarint() // Varint
1 -> pos += 8 1 -> pos += 8 // 64-bit
2 -> { 2 -> { // Length-delimited
val len = readVarint().toInt() val len = readVarint().toInt()
pos += len pos += len
} }
5 -> pos += 4 5 -> pos += 4 // 32-bit
else -> throw IllegalStateException("Unknown wire type $wireType") else -> throw IllegalStateException("Unknown wire type $wireType")
} }
} }
} }
// https://cs.android.com/android/platform/superproject/main/+/main:system/apex/libs/libapexutil/apexutil.cpp
private val apexInfos: List<Pair<String, Long>> by lazy { private val apexInfos: List<Pair<String, Long>> by lazy {
val results = mutableListOf<Pair<String, Long>>() val results = mutableListOf<Pair<String, Long>>()
val apexRoot = File("/apex") val apexRoot = File("/apex")
@@ -473,14 +443,22 @@ object AndroidDeviceUtils {
return@lazy emptyList() return@lazy emptyList()
} }
// Logic from: GetActivePackages in apexutil.cpp
apexRoot.listFiles()?.forEach { file -> apexRoot.listFiles()?.forEach { file ->
if (!file.isDirectory) return@forEach if (!file.isDirectory) return@forEach
val name = file.name val name = file.name
// 1. Ignore "." (and implicitly "..")
if (name.startsWith(".")) return@forEach if (name.startsWith(".")) return@forEach
// 2. Ignore directories containing '@' (active mounts usually don't have version in
// path)
if (name.contains("@")) return@forEach if (name.contains("@")) return@forEach
// 3. Ignore "sharedlibs"
if (name == "sharedlibs") return@forEach if (name == "sharedlibs") return@forEach
// 4. Parse apex_manifest.pb
val manifestFile = File(file, "apex_manifest.pb") val manifestFile = File(file, "apex_manifest.pb")
if (manifestFile.exists()) { if (manifestFile.exists()) {
runCatching { runCatching {
@@ -491,46 +469,59 @@ object AndroidDeviceUtils {
} }
} }
// Ensure uniqueness (though filesystem scan usually prevents exact dupes,
// strictly speaking we want to behave like a Map keyed by package name)
results.distinctBy { it.first } results.distinctBy { it.first }
} }
// https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/maintenance.rs
val moduleHash: ByteArray by lazy { val moduleHash: ByteArray by lazy {
DeviceAttestationService.CachedAttestationData?.moduleHash DeviceAttestationService.CachedAttestationData?.moduleHash
?: runCatching { ?: runCatching {
// 1. Create a container to hold the sort key (name encoded) and the full data
// (sequence encoded)
data class ModuleEntry( data class ModuleEntry(
val nameEncoded: ByteArray, val nameEncoded: ByteArray, // The sort key
val fullEncoded: ByteArray, val fullEncoded: ByteArray, // The data to hash
) )
val modules = val modules =
apexInfos.map { (packageName, versionCode) -> apexInfos.map { (packageName, versionCode) ->
// Create the components
val nameOctet = DEROctetString(packageName.toByteArray(Charsets.UTF_8)) val nameOctet = DEROctetString(packageName.toByteArray(Charsets.UTF_8))
val versionInt = ASN1Integer(versionCode) val versionInt = ASN1Integer(versionCode)
// Create the Sequence: SEQUENCE { packageName, version }
val vec = ASN1EncodableVector() val vec = ASN1EncodableVector()
vec.add(nameOctet) vec.add(nameOctet)
vec.add(versionInt) vec.add(versionInt)
val sequence = DERSequence(vec) val sequence = DERSequence(vec)
// AOSP sorts by encoded name only, not full sequence // We store the encoded name separately because Rust sorts ONLY by this
ModuleEntry( ModuleEntry(
nameEncoded = nameOctet.encoded, nameEncoded = nameOctet.encoded,
fullEncoded = sequence.encoded, fullEncoded = sequence.encoded,
) )
} }
// 2. Sort manually based on the encoded Package Name (lexicographically)
// This mimics the Rust 'impl DerOrd for ModuleInfo' which delegates to
// 'self.name'
val sortedModules = val sortedModules =
modules.sortedWith { m1, m2 -> modules.sortedWith { m1, m2 ->
compareByteArrays(m1.nameEncoded, m2.nameEncoded) compareByteArrays(m1.nameEncoded, m2.nameEncoded)
} }
// 3. Concatenate the full sequences in the specific sorted order
val payloadStream = ByteArrayOutputStream() val payloadStream = ByteArrayOutputStream()
sortedModules.forEach { payloadStream.write(it.fullEncoded) } sortedModules.forEach { payloadStream.write(it.fullEncoded) }
val payload = payloadStream.toByteArray() val payload = payloadStream.toByteArray()
// Wrap in DER SET tag manually — DERSet() re-sorts by full encoding // 4. Wrap manually in a DER SET tag (0x31)
// We cannot use DERSet(vector) because it would re-sort incorrectly.
val finalDerSet = encodeAsDerSet(payload) val finalDerSet = encodeAsDerSet(payload)
// 5. Compute SHA-256
MessageDigest.getInstance("SHA-256").digest(finalDerSet) MessageDigest.getInstance("SHA-256").digest(finalDerSet)
} }
.getOrElse { .getOrElse {
@@ -539,6 +530,7 @@ object AndroidDeviceUtils {
} }
} }
/** Compares two byte arrays lexicographically (unsigned). */
private fun compareByteArrays(a: ByteArray, b: ByteArray): Int { private fun compareByteArrays(a: ByteArray, b: ByteArray): Int {
val length = minOf(a.size, b.size) val length = minOf(a.size, b.size)
for (i in 0 until length) { for (i in 0 until length) {
@@ -551,25 +543,31 @@ object AndroidDeviceUtils {
return a.size - b.size return a.size - b.size
} }
/** Manually wraps the payload in an ASN.1 SET (0x31) tag with correct length encoding. */
private fun encodeAsDerSet(payload: ByteArray): ByteArray { private fun encodeAsDerSet(payload: ByteArray): ByteArray {
val out = ByteArrayOutputStream() val out = ByteArrayOutputStream()
out.write(0x31) out.write(0x31) // ASN.1 Tag for SET
writeDerLength(out, payload.size) writeDerLength(out, payload.size)
out.write(payload) out.write(payload)
return out.toByteArray() return out.toByteArray()
} }
/** Writes the ASN.1 length field to the stream. */
private fun writeDerLength(out: ByteArrayOutputStream, length: Int) { private fun writeDerLength(out: ByteArrayOutputStream, length: Int) {
if (length < 128) { if (length < 128) {
// Short form
out.write(length) out.write(length)
} else { } else {
// Long form
var size = length var size = length
val bytes = ArrayList<Byte>() val bytes = ArrayList<Byte>()
while (size > 0) { while (size > 0) {
bytes.add((size and 0xFF).toByte()) bytes.add((size and 0xFF).toByte())
size = size ushr 8 size = size ushr 8
} }
// First byte: 0x80 | number of length bytes
out.write(0x80 or bytes.size) out.write(0x80 or bytes.size)
// Write length bytes in big-endian (reverse of how we extracted them)
for (i in bytes.indices.reversed()) { for (i in bytes.indices.reversed()) {
out.write(bytes[i].toInt()) out.write(bytes[i].toInt())
} }
@@ -0,0 +1,84 @@
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
/**
* Simulates realistic TEE hardware latency for software key generation.
*
* The delay model is derived from 64+ timing measurements across QTEE (Qualcomm) and Trustonic
* (MediaTek) hardware. It combines four independent noise sources that model different physical
* latency origins in a real TrustZone-based TEE:
*
* 1. Base crypto processing (log-normal): hardware RNG + key derivation + cert signing
* 2. Binder/kernel transit (exponential): IPC scheduling, context switches
* 3. TrustZone scheduler jitter (Gaussian): world-switch non-determinism
* 4. Cold-start penalty (half-normal): first operation after idle is slower due to TEE
* secure world re-initialization and TLB/cache warming
*
* Per-boot session bias models manufacturing variance between TEE hardware instances.
*/
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)
}
/**
* Log-normal base delay. Parameters tuned to match observed hardware profiles:
* EC P-256 on QTEE averages ~65ms, RSA-2048 ~75ms, AES ~40ms.
* Sigma kept low (0.08) to match the tight clustering seen in real measurements.
*/
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)
}
}
+162
View File
@@ -1,3 +1,165 @@
## TEESimulator-RS v5.1: Interception Architecture Rewrite
Major release. 27 files changed, 2300 lines rewritten. The entire Kotlin interception layer has been rebuilt with a clean architecture, proper AIDL alignment, and significantly lower binder overhead.
### Interception Layer Rewrite
- KeyMintSecurityLevelInterceptor completely restructured: GeneratedKeyInfo now carries full KeyMintAttestation instead of nullable stub, eliminating scattered null checks across every operation path
- SoftwareOperation rewritten with sealed CryptoPrimitive interface separating Signer, Verifier, Encryptor, and Decryptor into isolated implementations with proper JCA algorithm mapping
- KeystoreErrorCode centralized object replaces scattered magic numbers for all KeyMint and Keystore2 error codes
- listEntries moved from pre-transact parameter caching to post-transact injection, eliminating a race condition where cached params could go stale
- deleteKey now handles both APP domain (by alias) and KEY_ID domain (by nspace) resolution paths correctly
- AuthorizeCreate and AndroidPermissionUtils removed, authorization logic consolidated into the operation dispatch path
- DeviceAttestationService removed, attestation routing simplified into the main interceptor
### Software Crypto Operations
- GCM nonce returned in CreateOperationResponse.parameters for encrypt operations, matching real KeyMint HAL behavior
- updateAad correctly throws INVALID_TAG on non-AEAD operations instead of silently succeeding
- Cipher algorithm mapping cleaned up: dropped CTR block mode and RSA_PKCS1_1_5_SIGN padding that caused JCA provider mismatches
- All crypto exceptions wrapped as ServiceSpecificException with correct KeyMint error codes instead of raw exceptions
### Attestation & Certificate Generation
- CertificateGenerator rewritten with clean Kotlin Pair return type instead of Android's util.Pair
- AttestationBuilder field ordering aligned with AOSP KeyDescription ASN.1 schema
- Unique ID computation follows KeyMint HAL spec: HMAC-SHA256(temporal_counter || AAID || reset_flag, HBK) truncated to 128 bits
- Patch level logging removed from hot path to reduce logcat noise on every attestation
### Configuration & Device Properties
- ConfigurationManager target package parsing refactored: mode/package extraction deduplicated across GENERATE/PATCH/AUTO branches
- system=prop forced boot/vendor override removed, now respects explicit per-component patch level configuration
- FileObserver delete handler simplified with direct file access instead of defensive null-checks
- AndroidDeviceUtils expanded with additional device property accessors for attestation fields
### Binder Performance
- Safe parcel reads at 6 deserialization sites, replacing force-unwrap NPE paths with early-return on null. A single NPE generates a full stack trace that blocks the binder thread for ~2ms
- teeResponses cache populated on generateKey/importKey post-transact, reducing getKeyEntry from 2+ binder round-trips to 1
- pingBinder liveness check removed from pre-transact failure path, eliminating a synchronous IPC call on every failed transaction
- Native transaction code filtering at C++ level, skipping JNI entirely for PING/INTERFACE/DUMP
### Dynamic SecurityLevel Binder Registration
- Intercepts getSecurityLevel replies to register hooks on every new BBinder instance keystore2 returns, not just the initial one from setup
- Identity hash deduplication prevents double-hooking when keystore2 returns the same binder across multiple calls
- Resolves apps that call getSecurityLevel independently and receive a different binder than the one registered at startup
### Build & Packaging
- Rust native build task integrated into Gradle with cargo-ndk for aarch64/armv7/x86/x86_64
- Module ZIP includes all 4 native libraries (libTEESimulator, libsupervisor, libcertgen, libinject)
- customize.sh extraction restored for supervisor daemon and native cert gen library
- TeeLatencySimulator added as standalone utility for log-normal hardware latency emulation
---
## TEESimulator-RS v5.0: AOSP Compliance Overhaul
Major release integrating 30+ AOSP compliance improvements from upstream PR #157 analysis, layered on top of our StrongBox hardening and native cert gen architecture.
### 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)
### 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
### 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
### 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)
### 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
---
## TEESimulator-RS v4.8.1: StrongBox Op Rejection Fix
- **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 ## TEESimulator v4.2: Detection Evasion Hardening
Fixes 6 detection vectors flagged by attestation validator apps. Fixes 6 detection vectors flagged by attestation validator apps.
+8 -1
View File
@@ -15,7 +15,7 @@ fi
# --- Version Info --- # --- Version Info ---
VERSION=$(grep_prop version "${TMPDIR}/module.prop") VERSION=$(grep_prop version "${TMPDIR}/module.prop")
ui_print "- Installing TEESimulator $VERSION" ui_print "- Installing TEESimulator-RS $VERSION"
ui_print "" ui_print ""
# --- Architecture Handling --- # --- Architecture Handling ---
@@ -91,3 +91,10 @@ if [ ! -f "$CONFIG_DIR/target.txt" ]; then
ui_print "- Adding default target scope" ui_print "- Adding default target scope"
install_file "target.txt" "$CONFIG_DIR" install_file "target.txt" "$CONFIG_DIR"
fi 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
+2 -2
View File
@@ -1,7 +1,7 @@
id=tricky_store id=tricky_store
name=TEESimulator name=TEESimulator-RS
version=${REPLACEMEVER} version=${REPLACEMEVER}
versionCode=${REPLACEMEVERCODE} versionCode=${REPLACEMEVERCODE}
author=JingMatrix, Enginex0 author=JingMatrix, Enginex0
description=Software simulation for Android hardware-backed key pairs with key attestation description=Software simulation for Android hardware-backed key pairs with key attestation
updateJson=https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/update.json updateJson=https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/update.json
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"version": "v4.2", "version": "v4.5",
"versionCode": 98, "versionCode": 111,
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.2/TEESimulator-v4.2-Release.zip", "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" "changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md"
} }
+79 -2
View File
@@ -33,6 +33,36 @@ pub fn build_attestation_extension(params: &CertGenParams) -> Result<Vec<u8>> {
fn build_software_enforced(params: &CertGenParams) -> Result<Vec<u8>> { fn build_software_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
let mut fields: Vec<(u32, Vec<u8>)> = Vec::new(); let mut fields: Vec<(u32, Vec<u8>)> = Vec::new();
// Tag 303: CALLER_NONCE — NULL (presence = true)
if params.caller_nonce {
fields.push((303, enc_null()));
}
// Tag 400: ACTIVE_DATETIME — INTEGER (milliseconds)
if params.active_datetime >= 0 {
fields.push((400, enc_integer(params.active_datetime)));
}
// Tag 401: ORIGINATION_EXPIRE_DATETIME — INTEGER (milliseconds)
if params.origination_expire_datetime >= 0 {
fields.push((401, enc_integer(params.origination_expire_datetime)));
}
// Tag 402: USAGE_EXPIRE_DATETIME — INTEGER (milliseconds)
if params.usage_expire_datetime >= 0 {
fields.push((402, enc_integer(params.usage_expire_datetime)));
}
// Tag 405: USAGE_COUNT_LIMIT — INTEGER
if params.usage_count_limit >= 0 {
fields.push((405, enc_integer(params.usage_count_limit as i64)));
}
// Tag 509: UNLOCKED_DEVICE_REQUIRED — NULL
if params.unlocked_device_required {
fields.push((509, enc_null()));
}
// Tag 701: CREATION_DATETIME — INTEGER (milliseconds) // Tag 701: CREATION_DATETIME — INTEGER (milliseconds)
fields.push((701, enc_integer(params.creation_datetime))); fields.push((701, enc_integer(params.creation_datetime)));
@@ -77,8 +107,10 @@ fn build_tee_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
fields.push((10, enc_integer(curve as i32 as i64))); fields.push((10, enc_integer(curve as i32 as i64)));
} }
// Tag 503: NO_AUTH_REQUIRED — NULL (presence = true) // Tag 503: NO_AUTH_REQUIRED — NULL (conditional)
fields.push((503, enc_null())); if params.no_auth_required {
fields.push((503, enc_null()));
}
// Tag 702: ORIGIN — INTEGER 0 (GENERATED) // Tag 702: ORIGIN — INTEGER 0 (GENERATED)
fields.push((702, enc_integer(0))); fields.push((702, enc_integer(0)));
@@ -519,6 +551,44 @@ mod tests {
assert_eq!(tags, sorted, "AuthorizationList fields must be sorted by tag number"); assert_eq!(tags, sorted, "AuthorizationList fields must be sorted by tag number");
} }
#[test]
fn test_enforcement_tags_in_software_enforced() {
let mut params = make_test_params();
params.usage_count_limit = 3;
params.unlocked_device_required = true;
params.caller_nonce = true;
params.active_datetime = 1709913600000;
let sw = build_software_enforced(&params).unwrap();
let inner = skip_tlv_header(&sw);
let tags = extract_tag_numbers(inner);
assert!(tags.contains(&303), "CALLER_NONCE (303) must be in softwareEnforced");
assert!(tags.contains(&400), "ACTIVE_DATETIME (400) must be in softwareEnforced");
assert!(tags.contains(&405), "USAGE_COUNT_LIMIT (405) must be in softwareEnforced");
assert!(tags.contains(&509), "UNLOCKED_DEVICE_REQUIRED (509) must be in softwareEnforced");
}
#[test]
fn test_no_auth_required_conditional() {
let mut params = make_test_params();
params.no_auth_required = false;
let tee = build_tee_enforced(&params).unwrap();
let inner = skip_tlv_header(&tee);
let tags = extract_tag_numbers(inner);
assert!(!tags.contains(&503), "NO_AUTH_REQUIRED (503) must be absent when false");
}
#[test]
fn test_enforcement_tags_omitted_when_unset() {
let params = make_test_params();
let sw = build_software_enforced(&params).unwrap();
let inner = skip_tlv_header(&sw);
let tags = extract_tag_numbers(inner);
assert!(!tags.contains(&303), "CALLER_NONCE should be absent when false");
assert!(!tags.contains(&400), "ACTIVE_DATETIME should be absent when -1");
assert!(!tags.contains(&405), "USAGE_COUNT_LIMIT should be absent when -1");
assert!(!tags.contains(&509), "UNLOCKED_DEVICE_REQUIRED should be absent when false");
}
#[test] #[test]
fn test_full_extension_roundtrip() { fn test_full_extension_roundtrip() {
let params = make_test_params(); let params = make_test_params();
@@ -568,6 +638,13 @@ mod tests {
id_manufacturer: None, id_manufacturer: None,
id_model: None, id_model: None,
id_second_imei: None, id_second_imei: None,
active_datetime: -1,
origination_expire_datetime: -1,
usage_expire_datetime: -1,
usage_count_limit: -1,
caller_nonce: false,
unlocked_device_required: false,
no_auth_required: true,
} }
} }
+19
View File
@@ -199,6 +199,14 @@ fn extract_config(env: &mut JNIEnv, config: &JObject) -> Result<CertGenParams> {
let id_model = get_nullable_byte_array(env, config, "idModel")?; let id_model = get_nullable_byte_array(env, config, "idModel")?;
let id_second_imei = get_nullable_byte_array(env, config, "idSecondImei")?; let id_second_imei = get_nullable_byte_array(env, config, "idSecondImei")?;
let active_datetime = get_long(env, config, "activeDatetime")?;
let origination_expire_datetime = get_long(env, config, "originationExpireDatetime")?;
let usage_expire_datetime = get_long(env, config, "usageExpireDatetime")?;
let usage_count_limit = get_int(env, config, "usageCountLimit")?;
let caller_nonce = get_boolean(env, config, "callerNonce")?;
let unlocked_device_required = get_boolean(env, config, "unlockedDeviceRequired")?;
let no_auth_required = get_boolean(env, config, "noAuthRequired")?;
Ok(CertGenParams { Ok(CertGenParams {
algorithm: Algorithm::try_from(algorithm)?, algorithm: Algorithm::try_from(algorithm)?,
key_size: key_size as u32, key_size: key_size as u32,
@@ -238,6 +246,13 @@ fn extract_config(env: &mut JNIEnv, config: &JObject) -> Result<CertGenParams> {
id_manufacturer, id_manufacturer,
id_model, id_model,
id_second_imei, id_second_imei,
active_datetime,
origination_expire_datetime,
usage_expire_datetime,
usage_count_limit,
caller_nonce,
unlocked_device_required,
no_auth_required,
}) })
} }
@@ -253,6 +268,10 @@ fn get_long(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<i64> {
Ok(env.get_field(obj, name, "J")?.j()?) Ok(env.get_field(obj, name, "J")?.j()?)
} }
fn get_boolean(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<bool> {
Ok(env.get_field(obj, name, "Z")?.z()?)
}
fn get_byte_array(env: &mut JNIEnv, obj: &JObject, name: &'static str) -> Result<Vec<u8>> { fn get_byte_array(env: &mut JNIEnv, obj: &JObject, name: &'static str) -> Result<Vec<u8>> {
let field = env.get_field(obj, name, "[B")?.l()?; let field = env.get_field(obj, name, "[B")?.l()?;
if field.is_null() { if field.is_null() {
+8 -29
View File
@@ -42,35 +42,6 @@ impl TryFrom<i32> for EcCurve {
} }
} }
#[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 struct CertGenParams {
pub algorithm: Algorithm, pub algorithm: Algorithm,
pub key_size: u32, pub key_size: u32,
@@ -114,6 +85,14 @@ pub struct CertGenParams {
pub id_manufacturer: Option<Vec<u8>>, pub id_manufacturer: Option<Vec<u8>>,
pub id_model: Option<Vec<u8>>, pub id_model: Option<Vec<u8>>,
pub id_second_imei: Option<Vec<u8>>, pub id_second_imei: Option<Vec<u8>>,
pub active_datetime: i64,
pub origination_expire_datetime: i64,
pub usage_expire_datetime: i64,
pub usage_count_limit: i32,
pub caller_nonce: bool,
pub unlocked_device_required: bool,
pub no_auth_required: bool,
} }
pub struct GeneratedKeyPair { pub struct GeneratedKeyPair {
+1 -1
View File
@@ -235,7 +235,7 @@ print_summary() {
# --- Main --- # --- Main ---
echo "" echo ""
bold "TEESimulator package pipeline" bold "TEESimulator-RS package pipeline"
echo "" echo ""
[[ "$BUILD_RUST" == true ]] && build_rust [[ "$BUILD_RUST" == true ]] && build_rust
+1 -1
View File
@@ -14,7 +14,7 @@ dependencyResolutionManagement {
} }
} }
rootProject.name = "TEESimulator" rootProject.name = "TEESimulator-RS"
include(":stub") include(":stub")
@@ -13,6 +13,8 @@ public interface IPackageManager {
ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId); ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId);
int checkPermission(String permName, String pkgName, int userId);
class Stub { class Stub {
public static IPackageManager asInterface(IBinder binder) { public static IPackageManager asInterface(IBinder binder) {
throw new UnsupportedOperationException("STUB!"); throw new UnsupportedOperationException("STUB!");
@@ -0,0 +1,8 @@
package android.hardware.security.keymint;
public @interface HardwareAuthenticatorType {
int NONE = 0;
int PASSWORD = 1;
int FINGERPRINT = 2;
int ANY = -1;
}
@@ -0,0 +1,9 @@
package android.os;
/** Stub for android.os.SELinux. */
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!"); throw new UnsupportedOperationException("STUB!");
} }
public static boolean isDeclared(String name) {
throw new UnsupportedOperationException("STUB!");
}
public static String[] listServices() { public static String[] listServices() {
throw new UnsupportedOperationException("STUB!"); throw new UnsupportedOperationException("STUB!");
} }
@@ -0,0 +1,21 @@
package android.os;
/**
* Stub for android.os.ServiceSpecificException.
*
* <p>Used by AIDL-generated binder stubs to report service-specific errors with numeric codes.
* The binder framework serializes this as EX_SERVICE_SPECIFIC on the wire, preserving the integer
* error code for the client.
*/
public class ServiceSpecificException extends RuntimeException {
public final int errorCode;
public ServiceSpecificException(int errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public ServiceSpecificException(int errorCode) {
this(errorCode, null);
}
}