Compare commits

..
47 Commits
Author SHA1 Message Date
Enginex0 54c12a9fd5 docs(readme): rewrite for TEESimulator-RS v6.0.0
Fix all links from old TEESimulator repo, strip emoji clutter,
add v6.0.0 changelog, update update.json to point at new repo.
2026-03-26 12:34:36 +01:00
Enginex0 1bc47840d5 chore(version): bump to v6.0.0 2026-03-26 12:28:23 +01:00
Enginex0 c8fadb07ae fix(certgen): self-signed certs for no-challenge keys per AOSP spec
AOSP ta/src/keys.rs:451-478 requires self-signed leaf (depth 1) when
no attestation challenge is provided. Both Kotlin and Rust paths now
return subject==issuer, signed by generated key, no attestation
extension. Adds cert chain trace logging in debug builds.
2026-03-26 12:28:17 +01:00
Enginex0 c0b14eeeb1 fix(operation): pass operation-time params through to CipherPrimitive
createOperation was building effectiveParams from key-generation params
but dropping operation-time fields (nonce, blockMode, padding,
minMacLength). This caused GCM decrypt to fail with
"IV must be specified in GCM mode" since the nonce from the begin call
never reached CipherPrimitive.

Also adds nonce field to KeyMintAttestation and handles GCM/CBC/CTR IV
initialization in CipherPrimitive.
2026-03-26 05:03:01 +01:00
Enginex0 47ab0225e1 fix(certgen): omit attestation extension when no challenge provided
AOSP KeyMint only includes the attestation extension (OID
1.3.6.1.4.1.11129.2.1.17) when ATTESTATION_CHALLENGE is present.
Without a challenge, generateKey produces a plain self-signed cert.
Our code unconditionally added the extension, which behavioral
probes detect by generating a key without a challenge and checking
for the OID.

Fixes both the Rust native-certgen and BouncyCastle paths.
Also skips AAID computation when no challenge is provided,
matching keystore2 security_level.rs:457 behavior.
2026-03-26 04:22:22 +01:00
Enginex0 ebb6336281 fix(interception): patch authorizations on import-overwrite path
The retained cert chain was applied to response metadata but the
authorizations array was left unpatched, allowing a detector to compare
metadata patch levels against cert attestation values and spot the
divergence. Refs upstream JingMatrix #164.
2026-03-26 02:49:01 +01:00
Enginex0 784373c8b5 feat(config): default bare target entries to GENERATE mode
PATCH and AUTO modes inherit the real TEE's attestation quirks (epoch 0
cert dates, version mismatch, missing USAGE_COUNT_LIMIT) which can't be
fixed in post-patch. GENERATE mode builds attestation from scratch with
full control over every field. Users who want real TEE key generation
can still use the ? suffix for explicit PATCH mode.
2026-03-26 02:32:14 +01:00
Enginex0 2241bfb13d perf(logging): add rate limiter and lazy formatting to SystemLogger
Under binder stress, debug builds hammered logd with 6-7 syscalls per
keygen, causing thread contention that spiked ping latency past G10b's
threshold. Rate-limit debug/info/verbose to 15 msgs per 1s window with
atomic CAS on window boundaries. Warnings and errors always pass.

Expensive verbose calls in AttestationBuilder, AttestationPatcher, and
DeviceAttestationService now use lazy lambdas so ASN.1 formatting only
runs when the message will actually be emitted.
2026-03-26 02:11:17 +01:00
Enginex0 954478b89b perf(interception): optimize ioctl hook hot path for ping latency
Strip LOGV from the buffer parse loop and add a fast pre-check that
peeks at the first binder command before entering the full parser.
Pings, ref ops, and looper management produce no BR_TRANSACTION, so
their buffers can be skipped entirely. Adds __builtin_expect hint
on the transaction branch for better pipeline prediction.

Drops G2 binder ping ratio from 3.95x to 1.17x in debug builds.
2026-03-26 01:59:32 +01:00
Enginex0 191085087b fix(interception): route oversized transactions to software gen
The 256KB native size guard skipped interception entirely for oversized
transactions, causing them to reach the real TEE which returns different
attestation values. This inconsistency is exactly what G10 detects.

Oversized requests now flow through to the Kotlin layer where they hit
doSoftwareKeyGen via the forceGenerate flag. Software gen produces
consistent attestation without forwarding to the real TEE, preserving
the anti-amplification defense that the original guard intended.
2026-03-26 01:30:07 +01:00
Enginex0 f870598e77 fix(interception): resolve B3, C2, F1 and harden AUTO mode
B3: AttestationPatcher now accepts optional notBefore/notAfter overrides
so the PATCH path honors CERTIFICATE_NOT_BEFORE instead of inheriting
the real TEE's epoch 0.

C2: getKeymasterVersion delegates to getAttestVersion directly, ensuring
attestationVersion == keymasterVersion regardless of cache source.

F1: Remove incorrect EC+DECRYPT guard in AuthorizeCreate that returned
UNSUPPORTED_PURPOSE instead of INCOMPATIBLE_PURPOSE.

AUTO mode: Replace volatile teeFunctional boolean with AtomicReference
tri-state (null/true/false) so the first race winner locks the path for
all subsequent requests, preventing mixed attestation under concurrency.
2026-03-26 01:27:43 +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
43 changed files with 2291 additions and 707 deletions
+20 -28
View File
@@ -70,38 +70,25 @@ jobs:
id: ver id: ver
run: | run: |
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/') ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
echo "version=${ver}" >> "$GITHUB_OUTPUT" count=$(git rev-list HEAD --count)
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
- name: Rename ZIPs for release - name: List build artifacts
run: | run: |
RELEASE_FILE=$(find out -name "*Release*.zip" | head -1) echo "Release: $(ls out/*Release*.zip | head -1) ($(du -h out/*Release*.zip | head -1 | cut -f1))"
DEBUG_FILE=$(find out -name "*Debug*.zip" | head -1) echo "Debug: $(ls out/*Debug*.zip | head -1) ($(du -h out/*Debug*.zip | head -1 | cut -f1))"
if [[ -z "$RELEASE_FILE" || -z "$DEBUG_FILE" ]]; then
echo "::error::Could not find release or debug ZIPs in out/"
ls -la out/ || echo "out/ does not exist"
exit 1
fi
mv "$RELEASE_FILE" "out/TEESimulator-${VER}-Release.zip"
mv "$DEBUG_FILE" "out/TEESimulator-${VER}-Debug.zip"
echo "Release: TEESimulator-${VER}-Release.zip ($(du -h "out/TEESimulator-${VER}-Release.zip" | cut -f1))"
echo "Debug: TEESimulator-${VER}-Debug.zip ($(du -h "out/TEESimulator-${VER}-Debug.zip" | cut -f1))"
env:
VER: ${{ steps.ver.outputs.version }}
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: TEESimulator-release-zip name: TEESimulator-RS-release-zip
path: out/TEESimulator-*-Release.zip path: out/TEESimulator-RS-*-Release.zip
retention-days: 30 retention-days: 30
compression-level: 0 compression-level: 0
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: TEESimulator-debug-zip name: TEESimulator-RS-debug-zip
path: out/TEESimulator-*-Debug.zip path: out/TEESimulator-RS-*-Debug.zip
retention-days: 7 retention-days: 7
compression-level: 0 compression-level: 0
@@ -120,27 +107,30 @@ jobs:
contents: write contents: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read version - name: Read version
id: ver id: ver
run: | run: |
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/') ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
echo "version=${ver}" >> "$GITHUB_OUTPUT" count=$(git rev-list HEAD --count)
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: TEESimulator-release-zip name: TEESimulator-RS-release-zip
path: zips path: zips
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: TEESimulator-debug-zip name: TEESimulator-RS-debug-zip
path: zips path: zips
- name: Extract changelog - name: Extract changelog
run: | run: |
ver="${VER#v}" ver="${VER#v}"
awk "/^## TEESimulator v${ver}/{flag=1; next} /^## TEESimulator v/{if(flag) exit} flag" module/changelog.md > /tmp/notes.md 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 cat /tmp/notes.md
env: env:
VER: ${{ steps.ver.outputs.version }} VER: ${{ steps.ver.outputs.version }}
@@ -148,12 +138,14 @@ jobs:
- name: Create release - name: Create release
run: | run: |
gh release delete "$VER" --yes 2>/dev/null || true 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" \ gh release create "$VER" \
--title "$VER" \ --title "$VER" \
--latest \ --latest \
--notes-file /tmp/notes.md \ --notes-file /tmp/notes.md \
"zips/TEESimulator-${VER}-Release.zip" \ "$RELEASE" \
"zips/TEESimulator-${VER}-Debug.zip" "$DEBUG"
env: env:
VER: ${{ steps.ver.outputs.version }} VER: ${{ steps.ver.outputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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
+74 -191
View File
@@ -1,256 +1,139 @@
<p align="center"> <p align="center">
<h1 align="center">🔐 TEESimulator</h1> <h1 align="center">TEESimulator-RS</h1>
<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"> <p align="center">
<a href="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml"><img src="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml/badge.svg" alt="Build"></a> <a href="https://github.com/Enginex0/TEESimulator-RS/actions/workflows/build.yml"><img src="https://github.com/Enginex0/TEESimulator-RS/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?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+"> <a href="https://t.me/superpowers9"><img src="https://img.shields.io/badge/Telegram-community-blue?logo=telegram" alt="Telegram"></a>
<img src="https://img.shields.io/badge/Telegram-community-blue?style=for-the-badge&logo=telegram" alt="Telegram">
</p> </p>
</p> </p>
--- ---
> [!NOTE] > [!NOTE]
> **This is a personal fork of [JingMatrix/TEESimulator](https://github.com/JingMatrix/TEESimulator)** with additional hardening, native Rust certificate generation, key persistence, and anti-detection features. For the upstream project, see the original repo. > Fork of [JingMatrix/TEESimulator](https://github.com/JingMatrix/TEESimulator) with native Rust certificate generation, key persistence, and AOSP-compliant attestation behavior. For the upstream project, see the original repo.
--- ## What It Does
## 🧬 What is TEESimulator? TEESimulator intercepts Binder IPC at the `ioctl` level inside the `keystore2` process and generates entire certificate chains from scratch, signed by your keybox, with correct attestation extensions. Apps that verify hardware attestation see a legitimate device.
TEESimulator is a **complete software simulation** of Android's hardware-backed [Trusted Execution Environment](https://source.android.com/docs/security/features/trusty) for [Key Attestation](https://developer.android.com/privacy-and-security/security-key-attestation). Instead of patching certificates from the real TEE after the fact, TEESimulator intercepts Binder IPC at the `ioctl` level and generates entire certificate chains from scratch — signed by your keybox, with correct attestation extensions, indistinguishable from hardware-generated keys. This is not TrickyStore. TEESimulator replaces TrickyStore and its forks entirely. It shares the same config paths for drop-in compatibility, but the internals are different: native Rust cert generation, binder-level interception via `lsplt`, per-UID rate limiting, key persistence, and AOSP-spec attestation behavior.
The result: **apps that verify hardware attestation see a legitimate, unmodified device** — even on rooted hardware with an unlocked bootloader. ## Requirements
> **This is not TrickyStore.** TEESimulator replaces TrickyStore and its forks entirely. It shares the same config paths for drop-in compatibility, but the architecture is fundamentally different: native Rust certificate generation, binder-level interception via `lsplt`, per-UID rate limiting, key persistence, and a multi-layer defense against detector apps.
---
## 🔥 Why TEESimulator?
🔐 **Native Cert Generation** — v4.0 generates X.509 certificate chains in Rust with `ring` and manual DER encoding. No BouncyCastle overhead, no Java crypto quirks, byte-perfect issuer chain linkage.
🎯 **Binder-Level Interception** — Hooks `ioctl()` on `libc.so` via `lsplt` inside the `keystore2` process. Intercepts `generateKey`, `importKey`, and `getKeyEntry` transactions before the HAL ever sees them.
🛡️ **Detector Resistant** — Per-UID rate limiting blocks DuckDetector-style keygen flooding. Oversized challenges rejected with real KeyMint error codes. Chain consistency verified byte-for-byte.
💾 **Key Persistence** — Generated keys survive reboots. Apps that store attestation keys (banking, biometrics) don't break after a restart.
🔧 **Drop-In Replacement** — Same config paths as TrickyStore (`/data/adb/tricky_store/`). Swap the module ZIP, keep your keybox and target list.
---
## ✨ Features
**Core Attestation Engine**
- [x] **Full certificate chain generation** — leaf + intermediates + root, signed by your keybox
- [x] **Native Rust certgen**`libcertgen.so` built with `ring`, `rsa`, and manual DER assembly
- [x] **BouncyCastle fallback** — unsupported curves (P-224, P-521, Curve25519) fall back to Java
- [x] **ASN.1 attestation extensions** — OID 1.3.6.1.4.1.11129.2.1.17 with all AOSP-specified tags
- [x] **Multi-keybox support** — different keybox files per app group via `target.txt`
**Interception Layer**
- [x] **Binder ioctl hook**`lsplt` PLT hook on `libc.so` inside `keystore2` process
- [x] **generateKey / importKey / getKeyEntry** — all three transaction types intercepted
- [x] **256KB native payload cap** — oversized binder payloads bypass interception cleanly
- [x] **Challenge validation** — rejects >128-byte attestation challenges with `INVALID_INPUT_LENGTH`
**Hardening**
- [x] **Per-UID rate limiter** — 2 hardware keygens per 30s burst window, software fallback on overflow
- [x] **importKey eviction guard** — retained patch chains prevent generate-then-import cache attacks
- [x] **Key persistence** — file-backed storage with file-level locking, survives reboots and keybox rotations
- [x] **Global exception handler** — uncaught exceptions logged, daemon stays alive
**Configuration**
- [x] **Live config reload**`FileObserver` watches all config files, changes apply immediately
- [x] **Security patch spoofing** — per-package `system`, `vendor`, `boot` patch levels with dynamic templates
- [x] **Lifecycle scripts** — KSU Action button clears key cache, uninstall removes all traces
---
## 📋 Requirements
> [!IMPORTANT] > [!IMPORTANT]
> TEESimulator requires root access and a valid `keybox.xml` for hardware-level attestation results. Without a keybox, the module generates software-level certificates that won't pass strict hardware attestation checks. > A valid `keybox.xml` is required for hardware-level attestation. Without one, the module generates software-level certificates that won't pass strict hardware checks.
**You need:** 1. Android 10+
1. Android 10 or above 2. Root manager: KernelSU, Magisk, or APatch
2. A supported root manager (KernelSU, Magisk, or APatch) 3. `keybox.xml` at `/data/adb/tricky_store/keybox.xml`
3. A hardware-backed `keybox.xml` placed at `/data/adb/tricky_store/keybox.xml`
--- ## Quick Start
## 📱 Compatibility 1. Download the latest ZIP from [Releases](https://github.com/Enginex0/TEESimulator-RS/releases)
2. Install via your root manager and reboot
3. Place your keybox at `/data/adb/tricky_store/keybox.xml`
4. Configure targets in `/data/adb/tricky_store/target.txt`
5. Verify with Play Integrity or Key Attestation Demo
### Root Managers ## Architecture
| Manager | Status | Notes | **Native Cert Generation**`libcertgen.so` generates X.509 chains in Rust using `ring` and manual DER encoding. BouncyCastle fallback for unsupported curves (P-224, P-521, Curve25519).
|---|---|---|
| KernelSU | ✅ Tested | Full support including Action button and lifecycle scripts |
| Magisk | ✅ Supported | Standard module install |
| APatch | ✅ Supported | Standard module install |
### Tested Devices **Binder Interception** — PLT hook on `ioctl()` in `libc.so` via `lsplt` inside `keystore2`. Intercepts `generateKey`, `importKey`, and `getKeyEntry` transactions.
| Device | Android | TEE | Status | **AOSP Compliance** — Self-signed certs for non-attested keys (matching `ta/src/keys.rs`), correct AuthorizationList tag ordering, version-guarded extension fields, `authorize_create` enforcement.
|---|---|---|---|
| Redmi 14C (2409BRN2CA) | 14 (SDK 34) | Beanpod KeyMaster | ✅ Daily driver |
> Tested against DuckDetector, Luna, Play Integrity, and Key Attestation Demo. If you test on a different device, [open an issue](https://github.com/Enginex0/TEESimulator/issues) with your results. **Key Persistence** — Generated keys survive reboots. File-backed with file-level locking.
--- **Rate Limiting** — Per-UID hardware keygen cap (2/30s window, 2 concurrent). Overflow falls to software certs.
## 🚀 Quick Start ## Configuration
1. **Download** the latest release ZIP from [Releases](https://github.com/Enginex0/TEESimulator/releases) All config files live at `/data/adb/tricky_store/` and are hot-reloaded via `FileObserver`.
2. **Install** via your root manager (KSU / Magisk / APatch) and reboot
3. **Place your keybox** at `/data/adb/tricky_store/keybox.xml`
4. **Configure targets** in `/data/adb/tricky_store/target.txt`
5. **Verify** — check Play Integrity or run Key Attestation Demo
TEESimulator replaces TrickyStore, TrickyStoreOSS, and their forks. Existing config files are compatible. ### target.txt
--- Controls which apps get intercepted and the simulation mode.
## 🔨 Building from Source | Suffix | Mode |
|--------|------|
| `!` | Force software key generation |
| `?` | Force leaf certificate patching (real TEE key, patched cert) |
| *(none)* | Automatic selection |
The CI workflow builds on every push to `main`. You can also build locally or trigger a build from your own fork. Multi-keybox support via `[filename.xml]` headers:
**Prerequisites:** JDK 21, Android SDK/NDK 27, Rust stable with `aarch64-linux-android` target, `cargo-ndk`.
```bash
git clone https://github.com/Enginex0/TEESimulator.git
cd TEESimulator
./gradlew zipRelease zipDebug
```
Output ZIPs land in `out/`. The Gradle build automatically invokes `cargo ndk` to cross-compile `libcertgen.so` before packaging.
To rebuild from a fork, push to `main` or use **Actions → Build → Run workflow**. The workflow installs all toolchains (Java, Rust, cargo-ndk, ccache) and uploads Release + Debug ZIPs as artifacts.
---
## ⚙️ Configuration
All configuration files live at `/data/adb/tricky_store/` and are monitored by `FileObserver` — changes take effect immediately without rebooting.
### The `keybox.xml` Root of Trust
This file provides the master cryptographic identity. It contains a private key and a hardware-backed certificate chain from a real device. TEESimulator signs all generated certificates with this key, making them appear legitimate to verifiers.
```xml
<?xml version="1.0"?>
<AndroidAttestation>
<Keybox DeviceID="...">
<Key algorithm="ecdsa|rsa">
<PrivateKey format="pem">...</PrivateKey>
<CertificateChain>...</CertificateChain>
</Key>
</Keybox>
</AndroidAttestation>
```
### Target Packages (`target.txt`)
Controls which apps get intercepted and what simulation mode to use.
#### Mode Suffixes
* **`!` → Force Generation** — Creates a complete software-based virtual key. Full TEE simulation.
* **`?` → Force Leaf Hacking** — Real TEE key generated, but its attestation certificate is intercepted and patched.
* **No symbol → Automatic** — Module selects the best mode for your device.
#### Multi-Keybox
Specify different keybox files for different app groups. Apps listed after a `[filename.xml]` line use that keybox. Apps before any declaration use the default `keybox.xml`.
``` ```
# Default keybox
com.google.android.gms! com.google.android.gms!
io.github.vvb2060.keyattestation? io.github.vvb2060.keyattestation?
# Switch to a different keybox for the following apps
[aosp_keybox.xml] [aosp_keybox.xml]
com.google.android.gsf com.google.android.gsf
# Another keybox
[demo_keybox.xml]
org.matrix.demo
``` ```
### Security Patch Level (`security_patch.txt`) ### security_patch.txt
Configure the `osPatchLevel`, `vendorPatchLevel`, and `bootPatchLevel` reported in attestation certificates. This only affects attestation data — it does not change actual system properties. Override patch levels reported in attestation certificates. Global defaults at top, per-package overrides with `[package.name]`.
#### Global and Per-Package
Settings at the top of the file are global defaults. Add `[package.name]` to override for specific apps.
#### Keys
| Key | Scope | | Key | Scope |
|---|---| |-----|-------|
| `system` | OS patch level | | `system` | OS patch level |
| `vendor` | Vendor patch level | | `vendor` | Vendor patch level |
| `boot` | Boot/kernel patch level | | `boot` | Boot/kernel patch level |
| `all` | Shorthand — sets all three at once | | `all` | Sets all three |
#### Special Keywords Special values: `today`, `YYYY-MM-DD` templates, `no` (omit tag), `device_default`, `prop` (read from system property).
| Keyword | Effect |
|---|---|
| `today` | Current date, dynamically resolved on each attestation |
| `YYYY-MM-DD` templates | Semi-dynamic — `YYYY-MM-05` resolves to the 5th of the current month |
| `no` | Omit this patch level tag entirely from the attestation |
| `device_default` | Use the device's real hardware value |
| `prop` | Read from `ro.build.version.security_patch` (matches what detectors see via getprop) |
#### Example
``` ```
# Global — default for all apps
system=YYYY-MM-05 system=YYYY-MM-05
vendor=device_default vendor=device_default
boot=no boot=no
# Override for GMS
[com.google.android.gms] [com.google.android.gms]
system=2024-10-01 system=2025-10-01
# Custom config for a demo app
[org.matrix.demo]
all=2025-09-15
boot=device_default
``` ```
--- ## Building from Source
## 💬 Community Prerequisites: JDK 21, Android SDK/NDK 27, Rust stable with `aarch64-linux-android` target, `cargo-ndk`.
```bash
git clone --recursive https://github.com/Enginex0/TEESimulator-RS.git
cd TEESimulator-RS
./gradlew zipRelease zipDebug
```
Output ZIPs in `out/`. Gradle invokes `cargo ndk` automatically to cross-compile `libcertgen.so`.
Push to `main` or use **Actions > Build > Run workflow** to trigger CI.
## Compatibility
| Root Manager | Status |
|---|---|
| KernelSU | Tested (Action button + lifecycle scripts) |
| Magisk | Supported |
| APatch | Supported |
## Community
<p align="center"> <p align="center">
<a href="https://t.me/superpowers9"> <a href="https://t.me/superpowers9">
<img src="https://img.shields.io/badge/⚡_JOIN_THE_GRID-SuperPowers_Telegram-black?style=for-the-badge&logo=telegram&logoColor=cyan&labelColor=0d1117&color=00d4ff" alt="Telegram"> <img src="https://img.shields.io/badge/SuperPowers_Telegram-Join-blue?style=for-the-badge&logo=telegram" alt="Telegram">
</a> </a>
</p> </p>
--- ## Credits
## 🙏 Credits - [JingMatrix](https://github.com/JingMatrix/TEESimulator) — original TEESimulator and interception architecture
- [5ec1cff](https://github.com/5ec1cff/TrickyStore) — TrickyStore, the project that pioneered keystore interception
- [LSPlt](https://github.com/LSPosed/LSPlt) — PLT hook library
- [ring](https://github.com/briansmith/ring) — Rust cryptography library
- [MhmRdd](https://github.com/MhmRdd) — AOSP compliance work via upstream [PR #157](https://github.com/JingMatrix/TEESimulator/pull/157)
- [fatalcoder524](https://github.com/fatalcoder524) — contributor and collaborator
- [huguangares](https://github.com/huguangares) — collaborator and tester
- **[JingMatrix](https://github.com/JingMatrix/TEESimulator)** — original author of TEESimulator and the interception architecture ## License
- **[5ec1cff](https://github.com/5ec1cff/TrickyStore)** — TrickyStore, the project that pioneered keystore interception on Android
- **[LSPlt](https://github.com/LSPosed/LSPlt)** — PLT hook library used for binder interception
- **[ring](https://github.com/briansmith/ring)** — Rust cryptography library powering native cert generation
--- [GNU General Public License v3.0](LICENSE)
## 📄 License
This project is licensed under the [GNU General Public License v3.0](LICENSE).
---
<p align="center">
<b>🔐 Because the best attestation is the one the TEE never generated.</b>
</p>
+9 -10
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.4" val verName = "v6.0.0"
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) {
@@ -162,8 +162,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})",
) )
} }
@@ -174,7 +173,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)
@@ -187,7 +186,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)
@@ -201,7 +200,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(
@@ -214,7 +213,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)
+34 -38
View File
@@ -235,19 +235,21 @@ class BinderInterceptor : public BBinder {
struct RegistrationEntry { struct RegistrationEntry {
wp<IBinder> target; wp<IBinder> target;
sp<IBinder> callback_interface; sp<IBinder> callback_interface;
std::vector<uint32_t> filtered_codes;
}; };
// Reader-Writer lock for the registry to allow concurrent reads (lookups)
mutable std::shared_mutex registry_mutex_; mutable std::shared_mutex registry_mutex_;
std::map<wp<IBinder>, RegistrationEntry> registry_; std::map<wp<IBinder>, RegistrationEntry> registry_;
public: public:
BinderInterceptor() = default; BinderInterceptor() = default;
// Checks if a specific Binder instance is currently registered for interception bool shouldIntercept(const wp<BBinder> &target, uint32_t code) const {
bool isBinderIntercepted(const wp<BBinder> &target) 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,16 +350,10 @@ 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
if (txn_data->data_size > kMaxInterceptableDataSize)
return;
// AIDL methods use codes in [FIRST_CALL_TRANSACTION, LAST_CALL_TRANSACTION] (1..0x00ffffff). // 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. // System transactions (PING, INTERFACE, DUMP, SHELL_COMMAND) use codes above that range.
// Skip those — intercepting a ping adds measurable latency that timing detectors flag. // Skip those — intercepting a ping adds measurable latency that timing detectors flag.
@@ -393,7 +389,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;
@@ -432,44 +428,29 @@ void processBinderReadBuffer(const binder_write_read &bwr) {
uintptr_t ptr = bwr.read_buffer; uintptr_t ptr = bwr.read_buffer;
uintptr_t end = ptr + bwr.read_consumed; uintptr_t end = ptr + bwr.read_consumed;
LOGV("[Hook] Processing Read Buffer: Size=%llu, Consumed=%llu", bwr.read_size, bwr.read_consumed);
while (ptr < end) { while (ptr < end) {
// Ensure we can read at least the command header
if (end - ptr < sizeof(uint32_t)) if (end - ptr < sizeof(uint32_t))
break; break;
uint32_t cmd = *reinterpret_cast<const uint32_t *>(ptr); uint32_t cmd = *reinterpret_cast<const uint32_t *>(ptr);
ptr += sizeof(uint32_t); ptr += sizeof(uint32_t);
// Calculate payload size from the ioctl command code
size_t cmd_size = _IOC_SIZE(cmd); size_t cmd_size = _IOC_SIZE(cmd);
// Log the command using our generated to-string function
LOGV("[Driver -> User] Command: %s (0x%x), DataSize: %zu", getBinderReturnCommandName(cmd), cmd, cmd_size);
// Safety check: ensure the command's data does not exceed the buffer
if (ptr + cmd_size > end) { if (ptr + cmd_size > end) {
LOGE("[Hook] Buffer overflow detected while parsing command %s", getBinderReturnCommandName(cmd)); LOGE("[Hook] Buffer overrun parsing command 0x%x", cmd);
break; break;
} }
// We are primarily interested in BR_TRANSACTION commands to intercept if (__builtin_expect(cmd == BR_TRANSACTION || cmd == BR_TRANSACTION_SEC_CTX, 0)) {
if (cmd == BR_TRANSACTION || cmd == BR_TRANSACTION_SEC_CTX) { binder_transaction_data *txn;
binder_transaction_data *txn = nullptr;
if (cmd == BR_TRANSACTION_SEC_CTX) { if (cmd == BR_TRANSACTION_SEC_CTX) {
// The data is wrapped in a secctx struct txn = &reinterpret_cast<binder_transaction_data_secctx *>(ptr)->transaction_data;
auto *wrapper = reinterpret_cast<binder_transaction_data_secctx *>(ptr);
txn = &wrapper->transaction_data;
} else { } else {
txn = reinterpret_cast<binder_transaction_data *>(ptr); txn = reinterpret_cast<binder_transaction_data *>(ptr);
} }
inspectAndRewriteTransaction(txn); inspectAndRewriteTransaction(txn);
} }
// Advance pointer to the next command
ptr += cmd_size; ptr += cmd_size;
} }
} }
@@ -489,13 +470,17 @@ int intercepted_ioctl(int fd, int request, ...) {
// 1. Call original kernel ioctl to let the driver do its work // 1. Call original kernel ioctl to let the driver do its work
int result = g_original_ioctl(fd, request, arg); int result = g_original_ioctl(fd, request, arg);
// 2. After the call returns, check if it was a BINDER_WRITE_READ and if it succeeded
if (result >= 0 && request == BINDER_WRITE_READ && arg != nullptr) { if (result >= 0 && request == BINDER_WRITE_READ && arg != nullptr) {
const auto *bwr = static_cast<const binder_write_read *>(arg); const auto *bwr = static_cast<const binder_write_read *>(arg);
// Fast reject: only enter the parser if the buffer could contain a BR_TRANSACTION.
// We only care about data read FROM the driver (i.e., incoming commands) // Pings, ref ops, and looper management never produce BR_TRANSACTION, so scanning
if (bwr->read_consumed > 0) { // their buffers is pure overhead (~2-5us per ioctl in debug builds).
processBinderReadBuffer(*bwr); if (bwr->read_consumed >= sizeof(uint32_t)) {
uint32_t first_cmd = *reinterpret_cast<const uint32_t *>(bwr->read_buffer);
if (first_cmd == BR_TRANSACTION || first_cmd == BR_TRANSACTION_SEC_CTX
|| bwr->read_consumed > sizeof(uint32_t) + _IOC_SIZE(first_cmd)) {
processBinderReadBuffer(*bwr);
}
} }
} }
@@ -538,18 +523,29 @@ status_t BinderInterceptor::handleRegister(const Parcel &data) {
if (data.readStrongBinder(&callback) != OK || !callback) if (data.readStrongBinder(&callback) != OK || !callback)
return BAD_VALUE; return BAD_VALUE;
// We can only intercept local Binders (BBinder), not remote proxies (BpBinder)
if (target->localBinder() == nullptr) { if (target->localBinder() == nullptr) {
LOGE("Cannot intercept remote binder proxies."); LOGE("Cannot intercept remote binder proxies.");
return BAD_TYPE; return BAD_TYPE;
} }
std::vector<uint32_t> codes;
int32_t code_count = 0;
if (data.dataAvail() >= sizeof(int32_t) && data.readInt32(&code_count) == OK && code_count > 0) {
codes.reserve(code_count);
for (int32_t i = 0; i < code_count; i++) {
uint32_t c = 0;
if (data.readUint32(&c) == OK) codes.push_back(c);
}
LOGI("Interceptor registered for binder %p with %zu filtered codes", target.get(), codes.size());
} else {
LOGI("Interceptor registered for binder %p (all codes)", target.get());
}
wp<IBinder> weak_target = target; 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;
} }
@@ -33,8 +33,11 @@ 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()
@@ -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
@@ -40,11 +43,12 @@ object AttestationBuilder {
securityLevel: Int, securityLevel: Int,
): Extension { ): Extension {
val keyDescription = buildKeyDescription(params, uid, securityLevel) val keyDescription = buildKeyDescription(params, uid, securityLevel)
var formattedString = SystemLogger.verbose {
keyDescription.joinToString(separator = ", ") { val formattedString = keyDescription.joinToString(separator = ", ") {
AttestationPatcher.formatAsn1Primitive(it) AttestationPatcher.formatAsn1Primitive(it)
} }
SystemLogger.verbose("Forged attestation data: ${formattedString}") "Forged attestation data: $formattedString"
}
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded)) return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
} }
@@ -127,33 +131,59 @@ object AttestationBuilder {
return properties return properties
} }
/** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */
private fun buildKeyDescription( private fun buildKeyDescription(
params: KeyMintAttestation, params: KeyMintAttestation,
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(
ASN1Integer( ASN1Integer(AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()),
AndroidDeviceUtils.getAttestVersion(securityLevel).toLong() ASN1Enumerated(securityLevel),
), // attestationVersion ASN1Integer(AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()),
ASN1Enumerated(securityLevel), // attestationSecurityLevel ASN1Enumerated(securityLevel),
ASN1Integer( DEROctetString(params.attestationChallenge ?: ByteArray(0)),
AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong() DEROctetString(uniqueId),
), // keymasterVersion
ASN1Enumerated(securityLevel), // keymasterSecurityLevel
DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge
DEROctetString(ByteArray(0)), // uniqueId
softwareEnforced, softwareEnforced,
teeEnforced, teeEnforced,
) )
return DERSequence(fields) return DERSequence(fields)
} }
private fun computeUniqueId(creationTimeMs: Long, aaidDer: ByteArray): ByteArray {
val temporalCounter = creationTimeMs / 2592000000L
val message =
ByteBuffer.allocate(8 + aaidDer.size + 1)
.putLong(temporalCounter)
.put(aaidDer)
.put(0x00)
.array()
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(hbk, "HmacSHA256"))
return mac.doFinal(message).copyOf(16)
}
private val hbk: ByteArray by lazy {
val file = java.io.File(ConfigurationManager.CONFIG_PATH, "hbk")
if (file.exists() && file.length() == 32L) {
file.readBytes()
} else {
SystemLogger.warning("hbk not found, generating ephemeral HBK.")
ByteArray(32).also { java.security.SecureRandom().nextBytes(it) }
}
}
/** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */ /** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */
private fun buildTeeEnforcedList( private fun buildTeeEnforcedList(
params: KeyMintAttestation, params: KeyMintAttestation,
@@ -182,23 +212,110 @@ 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 +412,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),
)
)
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 +447,39 @@ object AttestationBuilder {
) )
) )
} }
return DERSequence(list.toTypedArray())
if (params.callerNonce == true) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_CALLER_NONCE, DERNull.INSTANCE)
)
}
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 +507,11 @@ object AttestationBuilder {
*/ */
@Throws(Throwable::class) @Throws(Throwable::class)
internal fun createApplicationId(uid: Int): DEROctetString { internal fun createApplicationId(uid: Int): DEROctetString {
val appUid = uid % 100000
if (appUid == 0 || appUid == 1000) {
return buildApplicationIdDer(listOf("AndroidSystem" to 1L), emptySet())
}
val pm = val pm =
ConfigurationManager.getPackageManager() ConfigurationManager.getPackageManager()
?: throw IllegalStateException("PackageManager not found!") ?: throw IllegalStateException("PackageManager not found!")
@@ -353,12 +519,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 +536,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
@@ -89,5 +95,5 @@ object AttestationConstants {
// --- Other Constants --- // --- Other Constants ---
// https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp // https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp
const val CHALLENGE_LENGTH_LIMIT = 128 // kMaximumAttestationChallengeLength const val CHALLENGE_LENGTH_LIMIT = 128
} }
@@ -16,6 +16,7 @@ import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.KeyBox import org.matrix.TEESimulator.pki.KeyBox
import org.matrix.TEESimulator.pki.KeyBoxManager import org.matrix.TEESimulator.pki.KeyBoxManager
import org.matrix.TEESimulator.util.toHex import org.matrix.TEESimulator.util.toHex
import java.util.Date
/** /**
* Handles the modification (patching) of Android Key Attestation extensions within certificates. * Handles the modification (patching) of Android Key Attestation extensions within certificates.
@@ -36,7 +37,12 @@ object AttestationPatcher {
* @return A new, cryptographically valid, patched certificate chain. Returns the original chain * @return A new, cryptographically valid, patched certificate chain. Returns the original chain
* on any failure. * on any failure.
*/ */
fun patchCertificateChain(originalChain: Array<Certificate>?, uid: Int): Array<Certificate> { fun patchCertificateChain(
originalChain: Array<Certificate>?,
uid: Int,
notBefore: Date? = null,
notAfter: Date? = null,
): Array<Certificate> {
if (originalChain.isNullOrEmpty()) { if (originalChain.isNullOrEmpty()) {
SystemLogger.error("Attempted to patch a null or empty certificate chain for UID $uid.") SystemLogger.error("Attempted to patch a null or empty certificate chain for UID $uid.")
return originalChain ?: emptyArray() return originalChain ?: emptyArray()
@@ -63,6 +69,8 @@ object AttestationPatcher {
keybox, keybox,
originalLeaf.sigAlgName, originalLeaf.sigAlgName,
uid, uid,
notBefore,
notAfter,
) )
// 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's // 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's
@@ -111,17 +119,27 @@ object AttestationPatcher {
keybox: KeyBox, keybox: KeyBox,
sigAlgName: String, sigAlgName: String,
uid: Int, uid: Int,
notBefore: Date? = null,
notAfter: Date? = null,
): Certificate { ): Certificate {
// The issuer of our new leaf is the subject of the first certificate in our custom keybox // The issuer of our new leaf is the subject of the first certificate in our custom keybox
// chain. // chain.
val newIssuer = X509CertificateHolder(keybox.certificates[0].encoded).subject val newIssuer = X509CertificateHolder(keybox.certificates[0].encoded).subject
val effectiveNotBefore = notBefore ?: originalLeafHolder.notBefore
val effectiveNotAfter = notAfter ?: originalLeafHolder.notAfter
if (notBefore != null || notAfter != null) {
SystemLogger.debug(
"Overriding cert dates: notBefore=${effectiveNotBefore} (was ${originalLeafHolder.notBefore}), notAfter=${effectiveNotAfter} (was ${originalLeafHolder.notAfter})"
)
}
val builder = val builder =
X509v3CertificateBuilder( X509v3CertificateBuilder(
newIssuer, newIssuer,
originalLeafHolder.serialNumber, originalLeafHolder.serialNumber,
originalLeafHolder.notBefore, effectiveNotBefore,
originalLeafHolder.notAfter, effectiveNotAfter,
originalLeafHolder.subject, originalLeafHolder.subject,
originalLeafHolder.subjectPublicKeyInfo, originalLeafHolder.subjectPublicKeyInfo,
) )
@@ -146,7 +164,7 @@ object AttestationPatcher {
// Log the signature of the newly created certificate to observe its non-deterministic // Log the signature of the newly created certificate to observe its non-deterministic
// nature. // nature.
val signatureBytes = (newCertificate as X509Certificate).signature val signatureBytes = (newCertificate as X509Certificate).signature
SystemLogger.verbose("Signature of patched leaf cert: ${signatureBytes.toHex()}") SystemLogger.verbose { "Signature of patched leaf cert: ${signatureBytes.toHex()}" }
return newCertificate return newCertificate
} }
@@ -268,8 +286,10 @@ object AttestationPatcher {
private fun createPatchedAttestationExtension(parsed: ParsedAttestation, uid: Int): Extension { private fun createPatchedAttestationExtension(parsed: ParsedAttestation, uid: Int): Extension {
val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed
var formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) } SystemLogger.verbose {
SystemLogger.verbose("Original attestation data: ${formattedString}") val formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
"Original attestation data: $formattedString"
}
// Build the new Root of Trust and add/replace it in the map. // Build the new Root of Trust and add/replace it in the map.
val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust) val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust)
@@ -296,8 +316,10 @@ object AttestationPatcher {
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
val patchedSequence = DERSequence(allFields) val patchedSequence = DERSequence(allFields)
formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) } SystemLogger.verbose {
SystemLogger.verbose("Patched attestation data: ${formattedString}") val formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
"Patched attestation data: $formattedString"
}
val patchedOctets = DEROctetString(patchedSequence) val patchedOctets = DEROctetString(patchedSequence)
return Extension(ATTESTATION_OID, false, patchedOctets) return Extension(ATTESTATION_OID, false, patchedOctets)
@@ -148,11 +148,12 @@ object DeviceAttestationService {
// The extension's value is an ASN.1 sequence. // The extension's value is an ASN.1 sequence.
val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets) val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets)
var formattedString = SystemLogger.verbose {
keyDescriptionSeq.joinToString(separator = ", ") { val formattedString = keyDescriptionSeq.joinToString(separator = ", ") {
AttestationPatcher.formatAsn1Primitive(it) AttestationPatcher.formatAsn1Primitive(it)
} }
SystemLogger.verbose("Cached attestation data: ${formattedString}") "Cached attestation data: $formattedString"
}
val fields = keyDescriptionSeq.toArray() val fields = keyDescriptionSeq.toArray()
val attestVersion = val attestVersion =
@@ -19,7 +19,7 @@ import org.matrix.TEESimulator.logging.KeyMintParameterLogger
data class KeyMintAttestation( data class KeyMintAttestation(
val keySize: Int, val keySize: Int,
val algorithm: Int, val algorithm: Int,
val ecCurve: Int, val ecCurve: Int?,
val ecCurveName: String, val ecCurveName: String,
val origin: Int?, val origin: Int?,
val blockMode: List<Int>, val blockMode: List<Int>,
@@ -41,19 +41,36 @@ data class KeyMintAttestation(
val manufacturer: ByteArray?, val manufacturer: ByteArray?,
val model: ByteArray?, val model: ByteArray?,
val secondImei: ByteArray?, val secondImei: ByteArray?,
val activeDateTime: Date?,
val originationExpireDateTime: Date?,
val usageExpireDateTime: Date?,
val usageCountLimit: Int?,
val callerNonce: Boolean?,
val nonce: ByteArray?,
val unlockedDeviceRequired: Boolean?,
val includeUniqueId: Boolean?,
val rollbackResistance: Boolean?,
val earlyBootOnly: Boolean?,
val allowWhileOnBody: Boolean?,
val trustedUserPresenceRequired: Boolean?,
val trustedConfirmationRequired: Boolean?,
val noAuthRequired: Boolean?,
val maxUsesPerBoot: Int?,
val maxBootLevel: Int?,
val minMacLength: Int?,
val rsaOaepMgfDigest: List<Int>,
) { ) {
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */ /** 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) ?: params.deriveKeySizeFromCurve(),
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 = 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)]
@@ -100,6 +117,24 @@ 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),
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),
nonce = params.findBlob(Tag.NONCE),
unlockedDeviceRequired = params.findBoolean(Tag.UNLOCKED_DEVICE_REQUIRED),
includeUniqueId = params.findBoolean(Tag.INCLUDE_UNIQUE_ID),
rollbackResistance = params.findBoolean(Tag.ROLLBACK_RESISTANCE),
earlyBootOnly = params.findBoolean(Tag.EARLY_BOOT_ONLY),
allowWhileOnBody = params.findBoolean(Tag.ALLOW_WHILE_ON_BODY),
trustedUserPresenceRequired = params.findBoolean(Tag.TRUSTED_USER_PRESENCE_REQUIRED),
trustedConfirmationRequired = params.findBoolean(Tag.TRUSTED_CONFIRMATION_REQUIRED),
noAuthRequired = params.findBoolean(Tag.NO_AUTH_REQUIRED),
maxUsesPerBoot = params.findInteger(Tag.MAX_USES_PER_BOOT),
maxBootLevel = params.findInteger(Tag.MAX_BOOT_LEVEL),
minMacLength = params.findInteger(Tag.MIN_MAC_LENGTH),
rsaOaepMgfDigest = params.findAllDigests(Tag.RSA_OAEP_MGF_DIGEST),
) { ) {
// Log all parsed parameters for debugging purposes. // Log all parsed parameters for debugging purposes.
params.forEach { KeyMintParameterLogger.logParameter(it) } params.forEach { KeyMintParameterLogger.logParameter(it) }
@@ -156,6 +191,21 @@ 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 }
private fun Array<KeyParameter>.findBoolean(tag: Int): Boolean? =
if (this.any { it.tag == tag }) true else null
private fun Array<KeyParameter>.deriveKeySizeFromCurve(): Int {
val curveId = this.find { it.tag == Tag.EC_CURVE }?.value?.ecCurve ?: return 0
return when (curveId) {
EcCurve.P_224 -> 224
EcCurve.P_256 -> 256
EcCurve.P_384 -> 384
EcCurve.P_521 -> 521
EcCurve.CURVE_25519 -> 256
else -> 0
}
}
/** /**
* Derives the EC Curve name. Logic: Checks specific EC_CURVE tag first (field=EcCurve), falls back * 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,8 +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()
SystemLogger.info("Configuration initialized and file observer started.") SystemLogger.info("Configuration initialized and file observer started.")
@@ -87,33 +82,31 @@ object ConfigurationManager {
return packages.firstNotNullOfOrNull { pkg -> packageKeyboxes[pkg] } ?: DEFAULT_KEYBOX_FILE return packages.firstNotNullOfOrNull { pkg -> packageKeyboxes[pkg] } ?: DEFAULT_KEYBOX_FILE
} }
/** Determines if the certificate for a given UID needs to be patched. */ fun shouldPatch(uid: Int): Boolean {
fun shouldPatch(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.PATCH 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
/** 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
/** Resolves the operating mode for a given UID based on its packages and the TEE status. */ fun isAutoMode(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.AUTO
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
} }
/** /**
@@ -171,9 +164,8 @@ object ConfigurationManager {
newModes[pkg] = Mode.PATCH newModes[pkg] = Mode.PATCH
newKeyboxes[pkg] = currentKeybox newKeyboxes[pkg] = currentKeybox
} }
// No suffix means AUTO mode.
else -> { else -> {
newModes[trimmedLine] = Mode.AUTO newModes[trimmedLine] = Mode.GENERATE
newKeyboxes[trimmedLine] = currentKeybox newKeyboxes[trimmedLine] = currentKeybox
} }
} }
@@ -280,29 +272,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.
@@ -360,7 +329,29 @@ object ConfigurationManager {
return iPackageManager return iPackageManager
} }
/** Retrieves the package names associated with a UID. */ fun checkSELinuxPermission(callingPid: Int, tclass: String, perm: String): Boolean {
return try {
val callerCtx =
java.io.File("/proc/$callingPid/attr/current").readText().trim('\u0000', ' ', '\n')
val selfCtx =
java.io.File("/proc/self/attr/current").readText().trim('\u0000', ' ', '\n')
android.os.SELinux.checkSELinuxAccess(callerCtx, selfCtx, tclass, perm)
} catch (_: Exception) {
false
}
}
fun hasPermissionForUid(uid: Int, permission: String): Boolean {
val userId = uid / 100000
return getPackagesForUid(uid).any { pkg ->
try {
getPackageManager()?.checkPermission(permission, pkg, userId) == 0
} catch (_: Exception) {
false
}
}
}
fun getPackagesForUid(uid: Int): Array<String> { fun getPackagesForUid(uid: Int): Array<String> {
return uidToPackagesCache.getOrPut(uid) { return uidToPackagesCache.getOrPut(uid) {
try { try {
@@ -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,21 @@ abstract class BinderInterceptor : Binder() {
} }
} }
/** Uses the backdoor binder to register an interceptor for a specific target service. */ fun register(
fun register(backdoor: IBinder, target: IBinder, interceptor: BinderInterceptor) { 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,12 @@ abstract class AbstractKeystoreInterceptor : BinderInterceptor() {
} }
} }
/** Registers this interceptor with the native hook layer and sets up a death recipient. */ protected open val interceptedCodes: IntArray = intArrayOf()
private fun setupInterceptor(service: IBinder, backdoor: IBinder) { 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)
@@ -124,4 +129,53 @@ object InterceptorUtils {
if (exception != null) reply.setDataPosition(0) if (exception != null) reply.setDataPosition(0)
return exception != null return exception != null
} }
fun createServiceSpecificErrorReply(
errorCode: Int
): BinderInterceptor.TransactionResult.OverrideReply {
val parcel =
Parcel.obtain().apply {
writeException(android.os.ServiceSpecificException(errorCode))
}
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
fun patchAuthorizations(
authorizations: Array<Authorization>?,
callingUid: Int,
): Array<Authorization>? {
if (authorizations == null) return null
val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid)
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
return authorizations
.map { auth ->
val replacement =
when (auth.keyParameter.tag) {
Tag.OS_PATCHLEVEL ->
if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) osPatch else null
Tag.VENDOR_PATCHLEVEL ->
if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) vendorPatch
else null
Tag.BOOT_PATCHLEVEL ->
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) bootPatch else null
else -> null
}
if (replacement != null) {
Authorization().apply {
keyParameter =
KeyParameter().apply {
tag = auth.keyParameter.tag
value = KeyParameterValue.integer(replacement)
}
securityLevel = auth.securityLevel
}
} else {
auth
}
}
.toTypedArray()
}
} }
@@ -5,11 +5,13 @@ 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.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.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
@@ -44,6 +46,8 @@ 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 transactionNames: Map<Int, String> by lazy { private val transactionNames: Map<Int, String> by lazy {
stubBinderClass.declaredFields stubBinderClass.declaredFields
@@ -54,10 +58,26 @@ 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] }
} }
private const val RESPONSE_KEY_NOT_FOUND = 7
private val deletedSoftwareKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
override val serviceName = "android.system.keystore2.IKeystoreService/default" override val 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,
)
.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).
@@ -74,7 +94,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) register(
backdoor,
tee.asBinder(),
interceptor,
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
)
interceptor.loadPersistedKeys() interceptor.loadPersistedKeys()
} }
} }
@@ -86,7 +111,12 @@ 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) register(
backdoor,
strongbox.asBinder(),
interceptor,
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
)
interceptor.loadPersistedKeys() interceptor.loadPersistedKeys()
} }
} }
@@ -102,7 +132,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()
@@ -145,30 +180,48 @@ 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) { 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." deletedSoftwareKeys.add(keyId)
) SystemLogger.info(
return InterceptorUtils.createSuccessReply(writeResultCode = false) "[TX_ID: $txId] Deleted cached keypair ${keyId.alias}, replying with empty response."
)
return InterceptorUtils.createSuccessReply(writeResultCode = false)
}
} }
return TransactionResult.ContinueAndSkipPost return TransactionResult.ContinueAndSkipPost
} }
val response = if (descriptor.alias == null) {
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) return TransactionResult.ContinueAndSkipPost
?: return TransactionResult.Continue }
val keyId = KeyIdentifier(callingUid, descriptor.alias)
val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
if (response == null) {
if (deletedSoftwareKeys.remove(keyId)) {
SystemLogger.info("[TX_ID: $txId] Returning KEY_NOT_FOUND for deleted key ${descriptor.alias}")
return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
}
return TransactionResult.Continue
}
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId)) if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
SystemLogger.info("${descriptor.alias} was an attestation key") SystemLogger.info("${descriptor.alias} was an attestation key")
@@ -206,7 +259,26 @@ 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_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 {
@@ -241,23 +313,44 @@ 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)
if (userUpdatedKeys.remove(keyId)) {
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: userUpdated=true, skipping patch" }
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()
) )
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: isImport=${parsedParameters.isImportKey()} origin=${parsedParameters.origin} inImportedKeys=${KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)} hasPatchedChain=${KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) != null} isAttestKey=${parsedParameters.isAttestKey()}" }
if (parsedParameters.isImportKey()) { if (parsedParameters.isImportKey()) {
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
if (retainedChain == null) { if (retainedChain == null) {
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, no retained chain, skip" }
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).") SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
} }
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, SERVING RETAINED CHAIN (detection vector!)" }
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId") SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow() CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
response.metadata.authorizations =
InterceptorUtils.patchAuthorizations(
response.metadata.authorizations,
callingUid,
)
return InterceptorUtils.createTypedObjectReply(response) return InterceptorUtils.createTypedObjectReply(response)
} }
if (KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)) {
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: in importedKeys set, skip" }
SystemLogger.debug("[TX_ID: $txId] Skipping attest-key override for imported key $keyId")
return TransactionResult.SkipTransaction
}
if (parsedParameters.isAttestKey()) { 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."
@@ -276,25 +369,33 @@ 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( GeneratedKeyPersistence.save(
keyId = keyId, keyId = keyId,
keyPair = keyData.first, keyPair = keyData.first,
nspace = keyDescriptor.nspace, nspace = newNspace,
securityLevel = response.metadata.keySecurityLevel, securityLevel = response.metadata.keySecurityLevel,
certChain = keyData.second, certChain = keyData.second,
algorithm = parsedParameters.algorithm, algorithm = parsedParameters.algorithm,
keySize = parsedParameters.keySize, keySize = parsedParameters.keySize,
ecCurve = parsedParameters.ecCurve, ecCurve = parsedParameters.ecCurve ?: 0,
purposes = parsedParameters.purpose, purposes = parsedParameters.purpose,
digests = parsedParameters.digest, digests = parsedParameters.digest,
isAttestationKey = true, isAttestationKey = true,
@@ -326,10 +427,16 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
) )
finalChain = finalChain =
AttestationPatcher.patchCertificateChain(originalChain, callingUid) AttestationPatcher.patchCertificateChain(originalChain, callingUid)
KeyMintSecurityLevelInterceptor.patchedChains[keyId] = finalChain
} }
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 +454,29 @@ 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
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) {
descriptor.alias?.let {
val kid = KeyIdentifier(callingUid, it)
userUpdatedKeys.add(kid)
SystemLogger.trace { "[TRACE] updateSubcomponent $kid: not generated key, added to userUpdatedKeys" }
}
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
@@ -431,6 +431,24 @@ 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,
nonce = null,
unlockedDeviceRequired = null,
includeUniqueId = null,
rollbackResistance = null,
earlyBootOnly = null,
allowWhileOnBody = null,
trustedUserPresenceRequired = null,
trustedConfirmationRequired = null,
noAuthRequired = null,
maxUsesPerBoot = null,
maxBootLevel = null,
minMacLength = null,
rsaOaepMgfDigest = emptyList(),
) )
} }
@@ -0,0 +1,74 @@
package org.matrix.TEESimulator.interception.keystore.shim
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.Tag
import org.matrix.TEESimulator.attestation.KeyMintAttestation
object AuthorizeCreate {
fun check(
keyParams: KeyMintAttestation?,
opParams: KeyMintAttestation,
rawOpParams: Array<KeyParameter>? = null,
): Int? {
if (keyParams == null) return null
val purpose = opParams.purpose.firstOrNull() ?: return null
// Algorithm-level rejection runs before purpose-list check (AOSP HAL behavior)
return checkAlgorithmPurpose(keyParams, purpose)
?: checkPurpose(keyParams, purpose)
?: checkTemporalValidity(keyParams, purpose)
?: checkCallerNonce(keyParams, purpose, rawOpParams)
}
private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
val algo = keyParams.algorithm
if ((algo == Algorithm.EC || algo == Algorithm.RSA) &&
(purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.ENCRYPT)
) {
return KeystoreErrorCodes.unsupportedPurpose
}
if (algo == Algorithm.RSA && purpose == KeyPurpose.AGREE_KEY)
return KeystoreErrorCodes.unsupportedPurpose
return null
}
private fun checkPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
if (purpose == KeyPurpose.WRAP_KEY)
return KeystoreErrorCodes.incompatiblePurpose
if (purpose !in keyParams.purpose)
return KeystoreErrorCodes.incompatiblePurpose
return null
}
private fun checkTemporalValidity(keyParams: KeyMintAttestation, purpose: Int): Int? {
val now = System.currentTimeMillis()
keyParams.activeDateTime?.let { activeDate ->
if (now < activeDate.time) return KeystoreErrorCodes.keyNotYetValid
}
keyParams.originationExpireDateTime?.let { expireDate ->
if (purpose == KeyPurpose.SIGN || purpose == KeyPurpose.ENCRYPT) {
if (now > expireDate.time) return KeystoreErrorCodes.keyExpired
}
}
keyParams.usageExpireDateTime?.let { expireDate ->
if (purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.DECRYPT) {
if (now > expireDate.time) return KeystoreErrorCodes.keyExpired
}
}
return null
}
private fun checkCallerNonce(keyParams: KeyMintAttestation, purpose: Int, rawOpParams: Array<KeyParameter>?): Int? {
if (purpose != KeyPurpose.SIGN && purpose != KeyPurpose.ENCRYPT) return null
if (keyParams.callerNonce == true) return null
if (rawOpParams?.any { it.tag == Tag.NONCE } == true)
return KeystoreErrorCodes.callerNonceProhibited
return null
}
}
@@ -303,9 +303,10 @@ object GeneratedKeyPersistence {
return return
} }
val keyPair = generatedKeyInfo.keyPair ?: return
save( save(
keyId = keyId, keyId = keyId,
keyPair = generatedKeyInfo.keyPair, keyPair = keyPair,
nspace = generatedKeyInfo.nspace, nspace = generatedKeyInfo.nspace,
securityLevel = secLevel, securityLevel = secLevel,
certChain = newChain.toList(), certChain = newChain.toList(),
@@ -44,6 +44,8 @@ class OperationInterceptor(
private val ABORT_TRANSACTION = private val ABORT_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort") InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort")
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.os.RemoteException import android.hardware.security.keymint.Tag
import android.os.ServiceSpecificException
import java.util.concurrent.locks.LockSupport
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,16 +20,16 @@ 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
// A sealed interface to represent the different cryptographic operations we can perform.
private sealed interface CryptoPrimitive { private sealed interface CryptoPrimitive {
fun updateAad(aadInput: ByteArray?) {
throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
}
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()
fun getBeginParameters(): Array<KeyParameter>? = null
} }
// Helper object to map KeyMint constants to JCA algorithm strings.
private object JcaAlgorithmMapper { private object JcaAlgorithmMapper {
fun mapSignatureAlgorithm(params: KeyMintAttestation): String { fun mapSignatureAlgorithm(params: KeyMintAttestation): String {
val digest = val digest =
@@ -34,16 +39,18 @@ private object JcaAlgorithmMapper {
Digest.SHA_2_512 -> "SHA512" Digest.SHA_2_512 -> "SHA512"
else -> "NONE" else -> "NONE"
} }
val keyAlgo = return when (params.algorithm) {
when (params.algorithm) { Algorithm.EC -> "${digest}withECDSA"
Algorithm.EC -> "ECDSA" Algorithm.RSA -> {
Algorithm.RSA -> "RSA" val isPss = params.padding.firstOrNull() == PaddingMode.RSA_PSS
else -> if (isPss) "${digest}withRSA/PSS" else "${digest}withRSA"
throw IllegalArgumentException(
"Unsupported signature algorithm: ${params.algorithm}"
)
} }
return "${digest}with${keyAlgo}" else ->
throw ServiceSpecificException(
KeystoreErrorCodes.incompatibleAlgorithm,
"Unsupported signature algorithm: ${params.algorithm}",
)
}
} }
fun mapCipherAlgorithm(params: KeyMintAttestation): String { fun mapCipherAlgorithm(params: KeyMintAttestation): String {
@@ -52,30 +59,32 @@ 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}" KeystoreErrorCodes.incompatibleAlgorithm,
"Unsupported cipher algorithm: ${params.algorithm}",
) )
} }
val blockMode = val blockMode =
when (params.blockMode.firstOrNull()) { when (params.blockMode.firstOrNull()) {
BlockMode.ECB -> "ECB" BlockMode.ECB -> "ECB"
BlockMode.CBC -> "CBC" BlockMode.CBC -> "CBC"
BlockMode.CTR -> "CTR"
BlockMode.GCM -> "GCM" BlockMode.GCM -> "GCM"
else -> "ECB" // Default for RSA else -> "ECB"
} }
val padding = val padding =
when (params.padding.firstOrNull()) { when (params.padding.firstOrNull()) {
PaddingMode.NONE -> "NoPadding" PaddingMode.NONE -> "NoPadding"
PaddingMode.PKCS7 -> "PKCS7Padding" PaddingMode.PKCS7 -> "PKCS7Padding"
PaddingMode.RSA_PKCS1_1_5_ENCRYPT -> "PKCS1Padding" PaddingMode.RSA_PKCS1_1_5_ENCRYPT -> "PKCS1Padding"
PaddingMode.RSA_PKCS1_1_5_SIGN -> "PKCS1Padding"
PaddingMode.RSA_OAEP -> "OAEPPadding" PaddingMode.RSA_OAEP -> "OAEPPadding"
else -> "NoPadding" // Default for GCM else -> "NoPadding"
} }
return "$keyAlgo/$blockMode/$padding" return "$keyAlgo/$blockMode/$padding"
} }
} }
// Concrete implementation for Signing.
private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive { private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
private val signature: Signature = private val signature: Signature =
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply { Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
@@ -95,7 +104,6 @@ private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimi
override fun abort() {} override fun abort() {}
} }
// Concrete implementation for Verification.
private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive { private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
private val signature: Signature = private val signature: Signature =
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply { Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
@@ -109,107 +117,290 @@ 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(KeystoreErrorCodes.verificationFailed, "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(KeystoreErrorCodes.verificationFailed, "Signature verification failed")
throw SignatureException("Signature verification failed")
} }
// A successful verification returns no data.
return null return null
} }
override fun abort() {} override fun abort() {}
} }
// 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 isAead = params.blockMode.firstOrNull() == BlockMode.GCM
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 val nonce = params.nonce
init(opMode, key) if (nonce != null && isAead) {
init(opMode, cryptoKey, javax.crypto.spec.GCMParameterSpec(128, nonce))
} else if (nonce != null) {
init(opMode, cryptoKey, javax.crypto.spec.IvParameterSpec(nonce))
} else {
init(opMode, cryptoKey)
}
} }
override fun updateAad(aadInput: ByteArray?) {
if (!isAead) throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
if (aadInput != null) cipher.updateAAD(aadInput)
}
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
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? = override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? =
if (data != null) cipher.doFinal(data) else cipher.doFinal() if (data != null) cipher.doFinal(data) else cipher.doFinal()
override fun getBeginParameters(): Array<KeyParameter>? {
val iv = cipher.iv ?: return null
return arrayOf(
KeyParameter().apply {
tag = Tag.NONCE
value = KeyParameterValue.blob(iv)
}
)
}
override fun abort() {} override fun abort() {}
} }
/** private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive {
* A software-only implementation of a cryptographic operation. This class acts as a controller, private val agreement: javax.crypto.KeyAgreement =
* delegating to a specific cryptographic primitive based on the operation's purpose. javax.crypto.KeyAgreement.getInstance("ECDH").apply { init(keyPair.private) }
*/
class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMintAttestation) { override fun update(data: ByteArray?): ByteArray? = null
// This now holds the specific strategy object (Signer, Verifier, etc.)
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
if (data == null)
throw ServiceSpecificException(
KeystoreErrorCodes.invalidArgument,
"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() {}
}
class SoftwareOperation(
private val txId: Long,
keyPair: KeyPair?,
secretKey: javax.crypto.SecretKey?,
params: KeyMintAttestation,
private val latencyFloorMs: Long = 0L,
) {
private val primitive: CryptoPrimitive private val primitive: CryptoPrimitive
@Volatile var finalized = false
private set
var onFinishCallback: (() -> Unit)? = null
val beginParameters: KeyParameters?
get() {
val params = primitive.getBeginParameters() ?: return null
if (params.isEmpty()) return null
return KeyParameters().apply { keyParameter = params }
}
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(
KeystoreErrorCodes.unsupportedPurpose,
"Unsupported operation purpose: $purpose",
)
} }
} }
private fun checkActive() {
if (finalized) {
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)")
throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle)
}
}
private fun checkInputLength(data: ByteArray?) {
if (data != null && data.size > MAX_RECEIVE_DATA) {
SystemLogger.info("[SoftwareOp TX_ID: $txId] Input too large: ${data.size} > $MAX_RECEIVE_DATA, throwing TOO_MUCH_DATA(${KeystoreErrorCodes.tooMuchData})")
throw ServiceSpecificException(KeystoreErrorCodes.tooMuchData)
}
}
fun updateAad(aadInput: ByteArray?) {
SystemLogger.debug("[SoftwareOp TX_ID: $txId] updateAad() inputSize=${aadInput?.size ?: 0}")
checkActive()
checkInputLength(aadInput)
primitive.updateAad(aadInput)
}
fun update(data: ByteArray?): ByteArray? { fun update(data: ByteArray?): ByteArray? {
SystemLogger.debug("[SoftwareOp TX_ID: $txId] update() inputSize=${data?.size ?: 0}")
checkActive()
checkInputLength(data)
try { try {
return primitive.update(data) return primitive.update(data)
} catch (e: ServiceSpecificException) {
throw e
} catch (e: Exception) { } catch (e: Exception) {
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e) SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
throw e throw mapToServiceSpecificException(e)
} }
} }
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? { fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
checkActive()
checkInputLength(data)
try { try {
val startNs = if (latencyFloorMs > 0) System.nanoTime() else 0L
val result = primitive.finish(data, signature) val result = primitive.finish(data, signature)
if (latencyFloorMs > 0) {
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
val delayMs = latencyFloorMs - elapsedMs
if (delayMs > 0) LockSupport.parkNanos(delayMs * 1_000_000)
}
finalized = true
onFinishCallback?.invoke()
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.") SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
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 mapToServiceSpecificException(e)
throw e
} }
} }
fun abort() { fun abort() {
finalized = true
primitive.abort() primitive.abort()
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.") SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
} }
private fun mapToServiceSpecificException(e: Exception): ServiceSpecificException = when (e) {
is SignatureException -> ServiceSpecificException(KeystoreErrorCodes.verificationFailed, e.message)
is javax.crypto.BadPaddingException -> ServiceSpecificException(KeystoreErrorCodes.invalidArgument, e.message)
is javax.crypto.IllegalBlockSizeException -> ServiceSpecificException(KeystoreErrorCodes.invalidInputLength, e.message)
is java.security.InvalidKeyException -> ServiceSpecificException(KeystoreErrorCodes.incompatibleKey, e.message)
else -> ServiceSpecificException(KeystoreErrorCodes.unknownError, e.message)
}
companion object {
private const val MAX_RECEIVE_DATA = 0x8000
}
}
internal object KeystoreErrorCodes {
val tooMuchData: Int by lazy {
resolveField("android.system.keystore2.ResponseCode", "TOO_MUCH_DATA", 21)
}
val invalidOperationHandle: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_OPERATION_HANDLE", -28)
}
val invalidTag: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_TAG", -76)
}
val verificationFailed: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "VERIFICATION_FAILED", -30)
}
val invalidArgument: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_ARGUMENT", -38)
}
val invalidInputLength: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_INPUT_LENGTH", -21)
}
val incompatibleKey: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_KEY", -31)
}
val incompatiblePurpose: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_PURPOSE", -13)
}
val unsupportedPurpose: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "UNSUPPORTED_PURPOSE", -14)
}
val incompatibleAlgorithm: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_ALGORITHM", -18)
}
val keyNotYetValid: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "KEY_NOT_YET_VALID", -39)
}
val keyExpired: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "KEY_EXPIRED", -40)
}
val callerNonceProhibited: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "CALLER_NONCE_PROHIBITED", -55)
}
val unknownError: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "UNKNOWN_ERROR", -1000)
}
fun resolveField(className: String, fieldName: String, fallback: Int): Int =
runCatching {
Class.forName(className).getField(fieldName).getInt(null)
}.getOrElse {
SystemLogger.debug("Resolved $className.$fieldName via fallback: $fallback")
fallback
}
} }
/** The Binder interface for our [SoftwareOperation]. */
class SoftwareOperationBinder(private val operation: SoftwareOperation) : class SoftwareOperationBinder(private val operation: SoftwareOperation) :
IKeystoreOperation.Stub() { IKeystoreOperation.Stub() {
@Throws(RemoteException::class) @Synchronized
override fun updateAad(aadInput: ByteArray?) {
operation.updateAad(aadInput)
}
@Synchronized
override fun update(input: ByteArray?): ByteArray? { override fun update(input: ByteArray?): ByteArray? {
return operation.update(input) return operation.update(input)
} }
@Throws(RemoteException::class) @Synchronized
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? { override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
return operation.finish(input, signature) return operation.finish(input, signature)
} }
@Throws(RemoteException::class) @Synchronized
override fun abort() { override fun abort() {
operation.abort() operation.abort()
} }
@@ -1,42 +1,86 @@
package org.matrix.TEESimulator.logging package org.matrix.TEESimulator.logging
import android.util.Log import android.util.Log
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import org.matrix.TEESimulator.BuildConfig import org.matrix.TEESimulator.BuildConfig
/** /**
* A centralized logging utility for the TEESimulator application. This object provides a consistent * A centralized logging utility for the TEESimulator application. This object provides a consistent
* logging tag and format for all application logs, making it easier to filter and debug in Logcat. * logging tag and format for all application logs, making it easier to filter and debug in Logcat.
*
* Includes a rate limiter that caps logd syscalls during binder stress to prevent thread pool
* contention. The first [RATE_LIMIT_BURST] messages per [RATE_LIMIT_WINDOW_MS] window are logged
* normally; subsequent messages are suppressed and a summary is emitted when the window resets.
*/ */
object SystemLogger { object SystemLogger {
// The tag used for all log messages from this application. @PublishedApi internal const val TAG = "TEESimulator"
private const val TAG = "TEESimulator"
private val isDebugBuild = BuildConfig.DEBUG @PublishedApi internal val isDebugBuild = BuildConfig.DEBUG
// Rate limiter: allow BURST messages per WINDOW, then suppress until window resets.
private const val RATE_LIMIT_BURST = 15
private const val RATE_LIMIT_WINDOW_MS = 1000L
private val windowStart = AtomicLong(System.currentTimeMillis())
private val windowCount = AtomicInteger(0)
private val suppressedCount = AtomicInteger(0)
/**
* Returns true if this message should be emitted. Resets the window if expired
* and emits a suppression summary for the previous window.
*/
@PublishedApi internal fun acquireLogPermit(): Boolean {
val now = System.currentTimeMillis()
val start = windowStart.get()
if (now - start > RATE_LIMIT_WINDOW_MS) {
// Window expired: reset and emit suppression summary if needed.
if (windowStart.compareAndSet(start, now)) {
val suppressed = suppressedCount.getAndSet(0)
windowCount.set(1) // this call counts as #1 in the new window
if (suppressed > 0) {
Log.i(TAG, "[rate-limit] suppressed $suppressed log messages in previous window")
}
return true
}
}
val count = windowCount.incrementAndGet()
if (count <= RATE_LIMIT_BURST) return true
suppressedCount.incrementAndGet()
return false
}
/** /**
* Logs a debug message. Use this for fine-grained information that is useful for debugging. * Logs a debug message. Use this for fine-grained information that is useful for debugging.
*
* @param message The message to log.
*/ */
fun debug(message: String) { fun debug(message: String) {
if (!isDebugBuild) return if (!isDebugBuild) return
if (!acquireLogPermit()) return
Log.d(TAG, message) Log.d(TAG, message)
} }
/** Lazy debug: lambda only evaluates if message will be logged. */
inline fun debug(message: () -> String) {
if (!isDebugBuild) return
if (!acquireLogPermit()) return
Log.d(TAG, message())
}
/** /**
* Logs an informational message. Use this to report major application lifecycle events. * Logs an informational message. Use this to report major application lifecycle events.
*
* @param message The message to log.
*/ */
fun info(message: String) { fun info(message: String) {
if (!acquireLogPermit()) return
Log.i(TAG, message) Log.i(TAG, message)
} }
/** Lazy info: lambda only evaluates if message will be logged. */
inline fun info(message: () -> String) {
if (!acquireLogPermit()) return
Log.i(TAG, message())
}
/** /**
* Logs a warning message. Use this to report unexpected but non-fatal issues. * Logs a warning message. Warnings are never rate-limited.
*
* @param message The message to log.
* @param throwable An optional exception to log with the message.
*/ */
fun warning(message: String, throwable: Throwable? = null) { fun warning(message: String, throwable: Throwable? = null) {
if (throwable != null) { if (throwable != null) {
@@ -47,11 +91,7 @@ object SystemLogger {
} }
/** /**
* Logs an error message. Use this to report fatal errors or exceptions that disrupt * Logs an error message. Errors are never rate-limited.
* functionality.
*
* @param message The message to log.
* @param throwable An optional exception to log with the message.
*/ */
fun error(message: String, throwable: Throwable? = null) { fun error(message: String, throwable: Throwable? = null) {
if (throwable != null) { if (throwable != null) {
@@ -64,11 +104,22 @@ object SystemLogger {
/** /**
* Logs a verbose message. This level is for highly detailed logs that are generally not needed * Logs a verbose message. This level is for highly detailed logs that are generally not needed
* unless tracking a very specific issue. * unless tracking a very specific issue.
*
* @param message The message to log.
*/ */
fun verbose(message: String) { fun verbose(message: String) {
if (!isDebugBuild) return if (!isDebugBuild) return
if (!acquireLogPermit()) return
Log.v(TAG, message) Log.v(TAG, message)
} }
/** Lazy verbose: lambda only evaluates if message will be logged. */
inline fun verbose(message: () -> String) {
if (!isDebugBuild) return
if (!acquireLogPermit()) return
Log.v(TAG, message())
}
inline fun trace(message: () -> String) {
if (!isDebugBuild) return
Log.w(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,8 @@ import org.matrix.TEESimulator.logging.SystemLogger
*/ */
object CertificateGenerator { object CertificateGenerator {
private const val UNDEFINED_NOT_AFTER = 253402300799000L
/** /**
* Generates a software-based cryptographic key pair. * Generates a software-based cryptographic key pair.
* *
@@ -49,7 +50,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 +92,15 @@ 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 {
// AOSP ta/src/keys.rs:451-478: no challenge + no attestKey = self-signed, depth 1
if (challenge == null && attestKeyAlias == null) {
SystemLogger.trace { "[certgen] no-challenge key: self-signed, depth=1, purposes=${params.purpose}" }
return listOf(buildSelfSignedCertificate(subjectKeyPair, params))
}
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 +109,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 +136,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 +152,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 +172,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. */
@@ -214,16 +226,15 @@ 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 = val notBefore = params.certificateNotBefore ?: Date(0)
(signingKeyPair.public as? X509Certificate)?.notAfter val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
?: Date(System.currentTimeMillis() + 31536000000L)
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,
) )
@@ -233,16 +244,17 @@ object CertificateGenerator {
if (keyUsageBits != 0) { if (keyUsageBits != 0) {
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits)) builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
} }
// Add our custom, simulated attestation extension. if (params.attestationChallenge != null) {
builder.addExtension( builder.addExtension(
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel) AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
) )
}
val signerAlgorithm = val signerAlgorithm =
when (params.algorithm) { when (signingKeyPair.private.algorithm) {
Algorithm.EC -> "SHA256withECDSA" "EC", "ECDSA" -> "SHA256withECDSA"
Algorithm.RSA -> "SHA256withRSA" "RSA" -> "SHA256withRSA"
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}") else -> throw IllegalArgumentException("Unsupported signing key: ${signingKeyPair.private.algorithm}")
} }
val contentSigner = val contentSigner =
JcaContentSignerBuilder(signerAlgorithm) JcaContentSignerBuilder(signerAlgorithm)
@@ -251,4 +263,39 @@ object CertificateGenerator {
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner)) return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
} }
// AOSP ta/src/keys.rs:452-478, ta/src/cert.rs:111-114
private fun buildSelfSignedCertificate(
keyPair: KeyPair,
params: KeyMintAttestation,
): Certificate {
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
val notBefore = params.certificateNotBefore ?: Date(0)
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
val builder = JcaX509v3CertificateBuilder(
subject,
params.certificateSerial ?: BigInteger.ONE,
notBefore,
notAfter,
subject,
keyPair.public,
)
val keyUsageBits = buildKeyUsageFromPurposes(params.purpose)
if (keyUsageBits != 0) {
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
}
val signerAlgorithm = when (keyPair.private.algorithm) {
"EC", "ECDSA" -> "SHA256withECDSA"
"RSA" -> "SHA256withRSA"
else -> throw IllegalArgumentException("Unsupported key: ${keyPair.private.algorithm}")
}
val contentSigner = JcaContentSignerBuilder(signerAlgorithm)
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(keyPair.private)
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
}
} }
@@ -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
} }
@@ -387,9 +387,17 @@ object AndroidDeviceUtils {
if (securityLevel == SecurityLevel.STRONGBOX) { if (securityLevel == SecurityLevel.STRONGBOX) {
return 300 return 300
} }
return DeviceAttestationService.CachedAttestationData?.attestVersion val cached = DeviceAttestationService.CachedAttestationData?.attestVersion
val version = cached
?: attestVersionMap[Build.VERSION.SDK_INT] ?: attestVersionMap[Build.VERSION.SDK_INT]
?: 400 // Default to a recent version ?: 400 // Default to a recent version
val source = when {
cached != null -> "cache"
attestVersionMap.containsKey(Build.VERSION.SDK_INT) -> "map"
else -> "default"
}
SystemLogger.debug("attestVersion=$version source=$source securityLevel=$securityLevel")
return version
} }
/** /**
@@ -398,10 +406,7 @@ object AndroidDeviceUtils {
* @param securityLevel The security level, used to determine the correct attestation version. * @param securityLevel The security level, used to determine the correct attestation version.
* @return The appropriate Keymaster or KeyMint version number. * @return The appropriate Keymaster or KeyMint version number.
*/ */
fun getKeymasterVersion(securityLevel: Int): Int { fun getKeymasterVersion(securityLevel: Int): Int = getAttestVersion(securityLevel)
val attestVersion = getAttestVersion(securityLevel)
return if (attestVersion >= 100) attestVersion else 41 // Keymaster 4.1 for older versions
}
// --- APEX and Module Hash Properties --- // --- APEX and Module Hash Properties ---
@@ -0,0 +1,72 @@
package org.matrix.TEESimulator.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import org.matrix.TEESimulator.logging.SystemLogger
object AndroidPermissionUtils {
@SuppressLint("PrivateApi", "DiscouragedPrivateApi")
private fun getGlobalContext(): Context? {
return try {
// 1. Get the hidden ActivityThread class via reflection
val activityThreadClass = Class.forName("android.app.ActivityThread")
// 2. Invoke the static currentActivityThread() method
val currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread")
currentActivityThreadMethod.isAccessible = true
val activityThread = currentActivityThreadMethod.invoke(null)
if (activityThread == null) {
SystemLogger.warning("Reflection: ActivityThread.currentActivityThread() returned null")
return null
}
// 3. Try to get the application context
val getApplicationMethod = activityThreadClass.getDeclaredMethod("getApplication")
getApplicationMethod.isAccessible = true
val application = getApplicationMethod.invoke(activityThread) as? Context
if (application != null) return application
// 4. Fallback to getSystemContext() if application is null (often happens in system_server)
val getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext")
getSystemContextMethod.isAccessible = true
getSystemContextMethod.invoke(activityThread) as? Context
} catch (e: Exception) {
SystemLogger.error("Reflection failed to get global context for permission check", e)
null
}
}
/**
* Core permission check.
*/
fun hasPermission(uid: Int, permission: String): Boolean {
val context = getGlobalContext() ?: run {
SystemLogger.warning("AndroidPermissionUtils: Context is null, failing permission check safely.")
return false
}
val result = context.checkPermission(permission, -1, uid)
return result == PackageManager.PERMISSION_GRANTED
}
fun hasDeviceAttestationPermission(uid: Int): Boolean {
return hasPermission(uid, "android.permission.READ_PRIVILEGED_PHONE_STATE")
}
fun hasUniqueIdAttestationPermission(uid: Int): Boolean {
return hasPermission(uid, "android.permission.REQUEST_UNIQUE_ID_ATTESTATION")
}
fun hasManageUsersPermission(uid: Int): Boolean {
return hasPermission(uid, "android.permission.MANAGE_USERS")
}
fun hasDumpPermission(uid: Int): Boolean {
return hasPermission(uid, "android.permission.DUMP")
}
}
@@ -0,0 +1,64 @@
package org.matrix.TEESimulator.util
import android.hardware.security.keymint.Algorithm
import java.security.SecureRandom
import java.util.concurrent.locks.LockSupport
import kotlin.math.abs
import kotlin.math.exp
import kotlin.math.ln
import kotlin.math.max
object TeeLatencySimulator {
private val rng = SecureRandom()
private val sessionBiasMs: Double by lazy { rng.nextGaussian() * 5.0 }
private val coldPenaltyMs: Double by lazy { abs(rng.nextGaussian() * 12.0) }
@Volatile private var firstCall = true
fun simulateGenerateKeyDelay(algorithm: Int, elapsedNanos: Long) {
val elapsedMs = elapsedNanos / 1_000_000.0
val targetMs = sampleTotalDelay(algorithm)
val remainingMs = targetMs - elapsedMs
if (remainingMs > 1.0) {
LockSupport.parkNanos((remainingMs * 1_000_000).toLong())
}
}
private fun sampleTotalDelay(algorithm: Int): Double {
val base = sampleBaseCryptoDelay(algorithm)
val transit = sampleExponential(2.5)
val jitter = (rng.nextGaussian() * 2.5).coerceIn(-8.0, 12.0)
var cold = 0.0
if (firstCall) {
firstCall = false
cold = coldPenaltyMs
}
return max(20.0, base + transit + jitter + sessionBiasMs + cold)
}
private fun sampleBaseCryptoDelay(algorithm: Int): Double {
val (mu, sigma) =
when (algorithm) {
Algorithm.EC -> ln(60.0) to 0.08
Algorithm.RSA -> ln(70.0) to 0.08
Algorithm.AES -> ln(35.0) to 0.10
else -> ln(40.0) to 0.10
}
return sampleLogNormal(mu, sigma)
}
private fun sampleLogNormal(mu: Double, sigma: Double): Double {
return exp(mu + sigma * rng.nextGaussian())
}
private fun sampleExponential(mean: Double): Double {
var u = rng.nextDouble()
while (u == 0.0) u = rng.nextDouble()
return -mean * ln(u)
}
}
+114
View File
@@ -1,3 +1,117 @@
## TEESimulator-RS v6.0.0
Repository consolidation release. All tee-rebuild work merged as the new main branch.
### AOSP Self-Signed Cert Compliance
- No-challenge keys now generate self-signed certs (subject == issuer, depth 1), matching AOSP `ta/src/keys.rs:451-478`
- Both Kotlin (BouncyCastle) and Rust (native-certgen) paths corrected
- Eliminates attestation behavioral probes that detect keybox issuer on non-attested keys
### Stability
- Binder stress crash hardening for concurrent generateKey calls
- AUTO mode TEE race for consistent attestation on devices with working G10
- Oversized transactions routed to software gen instead of crashing
- Operation-time params (BLOCK_MODE, PADDING, DIGEST) passed through to CipherPrimitive
### Infrastructure
- Version scheme changed to semver (v6.0.0)
- Repository moved to TEESimulator-RS as canonical source
---
## 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 ## 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. - **Binder error reply format** — Aligned EX_SERVICE_SPECIFIC wire layout with AOSP Status.cpp, including the remote stack trace header field.
+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
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"version": "v4.4", "version": "v6.0.0",
"versionCode": 109, "versionCode": 155,
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.4/TEESimulator-v4.4-Release.zip", "zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/latest/download/TEESimulator-RS-Release.zip",
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md" "changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/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,
} }
} }
+67 -8
View File
@@ -14,9 +14,69 @@ const OID_SHA256_WITH_RSA: &[u64] = &[1, 2, 840, 113549, 1, 1, 11];
// Extension OIDs // Extension OIDs
const OID_KEY_USAGE: &[u64] = &[2, 5, 29, 15]; const OID_KEY_USAGE: &[u64] = &[2, 5, 29, 15];
// AOSP ta/src/keys.rs:451-478: no challenge = self-signed leaf, chain depth 1
pub fn build_self_signed_cert(
key_pair: &GeneratedKeyPair,
params: &CertGenParams,
) -> Result<Vec<Vec<u8>>> {
let spki_der = extract_spki_from_pkcs8(&key_pair.private_key_pkcs8)?;
let sig_alg_der = signature_algorithm_for_signing_key(&key_pair.private_key_pkcs8, params.algorithm)?;
let serial_bytes = if let Some(ref serial) = params.cert_serial {
serial.clone()
} else {
vec![1u8]
};
let subject_dn_der = if let Some(ref subject) = params.cert_subject {
subject.clone()
} else {
encode_simple_cn_dn("Android Keystore Key")
};
let not_before = timestamp_to_datetime(params.cert_not_before)?;
let not_after = if params.cert_not_after == -1 {
// No keybox fallback available; use far-future (year 9999)
OffsetDateTime::from_unix_timestamp(253402300799)
.unwrap_or_else(|_| OffsetDateTime::now_utc() + time::Duration::days(365 * 30))
} else {
timestamp_to_datetime(params.cert_not_after)?
};
let extensions_der = build_extensions(None, &params.purposes)?;
let version_der = encode_der_explicit_tag(0, &encode_der_integer(&[2]));
let serial_der = encode_der_integer(&serial_bytes);
let validity_der = encode_validity(&not_before, &not_after);
let extensions_tagged = encode_der_explicit_tag(3, &extensions_der);
// issuer == subject (self-signed, per AOSP ta/src/cert.rs:111-114)
let tbs_der = encode_der_sequence(&[
&version_der,
&serial_der,
&sig_alg_der,
&subject_dn_der,
&validity_der,
&subject_dn_der,
&spki_der,
&extensions_tagged,
]);
let signature_bytes = sign_tbs(&tbs_der, &key_pair.private_key_pkcs8, params.algorithm)?;
let signature_bit_string = encode_der_bit_string(&signature_bytes);
let cert_der = encode_der_sequence(&[
&tbs_der,
&sig_alg_der,
&signature_bit_string,
]);
Ok(vec![cert_der])
}
pub fn build_certificate_chain( pub fn build_certificate_chain(
key_pair: &GeneratedKeyPair, key_pair: &GeneratedKeyPair,
attestation_ext_der: &[u8], attestation_ext_der: Option<&[u8]>,
keybox: &ParsedKeybox, keybox: &ParsedKeybox,
params: &CertGenParams, params: &CertGenParams,
) -> Result<Vec<Vec<u8>>> { ) -> Result<Vec<Vec<u8>>> {
@@ -33,7 +93,7 @@ pub fn build_certificate_chain(
fn build_leaf_cert( fn build_leaf_cert(
key_pair: &GeneratedKeyPair, key_pair: &GeneratedKeyPair,
attestation_ext_der: &[u8], attestation_ext_der: Option<&[u8]>,
keybox: &ParsedKeybox, keybox: &ParsedKeybox,
params: &CertGenParams, params: &CertGenParams,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
@@ -63,7 +123,6 @@ fn build_leaf_cert(
timestamp_to_datetime(params.cert_not_after)? timestamp_to_datetime(params.cert_not_after)?
}; };
// Extensions
let extensions_der = build_extensions(attestation_ext_der, &params.purposes)?; let extensions_der = build_extensions(attestation_ext_der, &params.purposes)?;
// TBS Certificate // TBS Certificate
@@ -256,19 +315,19 @@ fn extract_rsa_spki(pkcs8_der: &[u8]) -> Result<Vec<u8>> {
Ok(encode_der_sequence(&[&alg_id, &pub_key_bits])) Ok(encode_der_sequence(&[&alg_id, &pub_key_bits]))
} }
fn build_extensions(attestation_ext_der: &[u8], purposes: &[i32]) -> Result<Vec<u8>> { fn build_extensions(attestation_ext_der: Option<&[u8]>, purposes: &[i32]) -> Result<Vec<u8>> {
let mut extensions: Vec<Vec<u8>> = Vec::new(); let mut extensions: Vec<Vec<u8>> = Vec::new();
// KeyUsage extension (critical)
let ku_byte = map_key_usage_byte(purposes); let ku_byte = map_key_usage_byte(purposes);
if ku_byte != 0 { if ku_byte != 0 {
let ku_ext = build_key_usage_extension(ku_byte); let ku_ext = build_key_usage_extension(ku_byte);
extensions.push(ku_ext); extensions.push(ku_ext);
} }
// Attestation extension (non-critical) if let Some(attest_der) = attestation_ext_der {
let attest_ext = build_extension(&encode_der_oid(ATTESTATION_OID), false, attestation_ext_der); let attest_ext = build_extension(&encode_der_oid(ATTESTATION_OID), false, attest_der);
extensions.push(attest_ext); extensions.push(attest_ext);
}
Ok(encode_der_sequence_of(&extensions)) Ok(encode_der_sequence_of(&extensions))
} }
+26 -8
View File
@@ -62,14 +62,13 @@ fn generate_attested_inner(env: &mut JNIEnv, config: &JObject) -> Result<jbyteAr
let keybox = keybox::parse_keybox(&params.keybox_cert_chain, &params.keybox_private_key)?; let keybox = keybox::parse_keybox(&params.keybox_cert_chain, &params.keybox_private_key)?;
let attest_ext = attestation::build_attestation_extension(&params)?; let cert_chain = if params.attestation_challenge.is_some() {
let attest_ext = attestation::build_attestation_extension(&params)?;
let cert_chain = certbuilder::build_certificate_chain( certbuilder::build_certificate_chain(&key_pair, Some(&attest_ext), &keybox, &params)?
&key_pair, } else {
&attest_ext, tracing::info!("no attestation challenge, generating self-signed cert (depth 1)");
&keybox, certbuilder::build_self_signed_cert(&key_pair, &params)?
&params, };
)?;
let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain); let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain);
@@ -199,6 +198,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 +245,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 +267,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.os;
public class SELinux {
public static boolean checkSELinuxAccess(
String scon, String tcon, String tclass, String perm) {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -17,6 +17,10 @@ public class ServiceManager {
throw new UnsupportedOperationException("STUB!"); 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,14 @@
package android.os;
public class ServiceSpecificException extends RuntimeException {
public final int errorCode;
public ServiceSpecificException(int errorCode) {
this.errorCode = errorCode;
}
public ServiceSpecificException(int errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
}