Compare commits

...
6 Commits
Author SHA1 Message Date
JingMatrix 4e67371193 Release TEESimulator v2.1 2025-11-28 20:00:07 +01:00
JingMatrixandGitHub 2ef89f15c6 Fix date format of vendor patch level (#24)
This was a mistake during the refactoring of TrickyStoreOSS.
After correcting it, we can obtain STRONG integrity (instead of DEVICE) with a valid keybox.

The correct format can be easily found using the `Key Attestation` app.
2025-11-28 19:45:53 +01:00
JingMatrixandGitHub 4f608247fe Set boot digest via resetprop (#22)
The stub method `SystemProperties.set` has wrong signature and is unable to set read-only system properties.
2025-11-28 13:11:54 +01:00
QingandJingMatrix 22cbe5a9a7 Clear generated key cache on keybox updates for Android 12+ (#16)
Ensures that the cache of generated keys is invalidated and cleared whenever a keybox file is updated. This prevents the system from using stale certificates after a keybox change.

Co-authored-by: JingMatrix <jingmatrix@gmail.com>
2025-11-27 23:29:43 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a6fa137e32 Bump org.bouncycastle:bcpkix-jdk18on from 1.82 to 1.83 (#13)
Bumps [org.bouncycastle:bcpkix-jdk18on](https://github.com/bcgit/bc-java) from 1.82 to 1.83.
- [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html)
- [Commits](https://github.com/bcgit/bc-java/commits)

---
updated-dependencies:
- dependency-name: org.bouncycastle:bcpkix-jdk18on
  dependency-version: '1.83'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-27 22:40:33 +01:00
JingMatrixandGitHub 5afefba7bd Clean up cached keys on successful import (#18)
Generated and attestation keys are cached, and if a key is imported with the same name, the cached key would be returned instead of the newly imported one.

This change invalidates the cached key when a key is successfully imported with the same alias.
Close #17 as fixed.

The logging has also been improved to be more consistent across the different interceptors.
2025-11-27 15:43:56 +01:00
10 changed files with 112 additions and 61 deletions
+1 -1
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 = "v2.0" val verName = "v2.1"
android { android {
namespace = "org.matrix.TEESimulator" namespace = "org.matrix.TEESimulator"
@@ -81,7 +81,7 @@ object AttestationBuilder {
DERTaggedObject( DERTaggedObject(
true, true,
AttestationConstants.TAG_VENDOR_PATCHLEVEL, AttestationConstants.TAG_VENDOR_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.vendorPatchLevel.toLong()), ASN1Integer(AndroidDeviceUtils.vendorPatchLevelLong.toLong()),
) )
) )
vector.add( vector.add(
@@ -165,7 +165,7 @@ object AttestationBuilder {
DERTaggedObject( DERTaggedObject(
true, true,
AttestationConstants.TAG_VENDOR_PATCHLEVEL, AttestationConstants.TAG_VENDOR_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.vendorPatchLevel.toLong()), ASN1Integer(AndroidDeviceUtils.vendorPatchLevelLong.toLong()),
), ),
DERTaggedObject( DERTaggedObject(
true, true,
@@ -247,14 +247,20 @@ object ConfigurationManager {
when (path) { when (path) {
TARGET_PACKAGES_FILE -> loadTargetPackages(file!!) TARGET_PACKAGES_FILE -> loadTargetPackages(file!!)
PATCH_LEVEL_FILE -> loadPatchLevelConfig(file!!) PATCH_LEVEL_FILE -> loadPatchLevelConfig(file!!)
// Any change to an XML file is assumed to be a keybox. The cache in KeyBoxUtils // Any change to an XML file is assumed to be a keybox.
// will handle reloading it on its next use. // The cache in KeyBoxManager will handle reloading it on its next use.
else -> else ->
if (path.endsWith(".xml")) { if (path.endsWith(".xml")) {
SystemLogger.info( SystemLogger.info(
"Keybox file $path may have changed. It will be reloaded on next access." "Keybox file $path may have changed. It will be reloaded on next access."
) )
KeyBoxManager.invalidateCache(path) KeyBoxManager.invalidateCache(path)
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
// Clear cached keys possibly containing old certificates
org.matrix.TEESimulator.interception.keystore.shim
.KeyMintSecurityLevelInterceptor
.clearAllGeneratedKeys("updating $file")
}
} }
} }
} }
@@ -89,24 +89,17 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
data: Parcel, data: Parcel,
): TransactionResult { ): TransactionResult {
if (code == GET_KEY_ENTRY_TRANSACTION || code == DELETE_KEY_TRANSACTION) { if (code == GET_KEY_ENTRY_TRANSACTION || code == DELETE_KEY_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
data.enforceInterface(IKeystoreService.DESCRIPTOR) data.enforceInterface(IKeystoreService.DESCRIPTOR)
val descriptor = val descriptor =
data.readTypedObject(KeyDescriptor.CREATOR) data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction ?: return TransactionResult.SkipTransaction
logTransaction(
txId,
"${transactionNames[code]} (alias=${descriptor.alias})",
callingUid,
callingPid,
)
if (ConfigurationManager.shouldSkipUid(callingUid)) { if (ConfigurationManager.shouldSkipUid(callingUid))
SystemLogger.debug(
"[TX_ID: $txId] Skip post-transaction hook for UID=${callingUid}"
)
return TransactionResult.ContinueAndSkipPost return TransactionResult.ContinueAndSkipPost
}
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
val keyId = KeyIdentifier(callingUid, descriptor.alias) val keyId = KeyIdentifier(callingUid, descriptor.alias)
if (code == DELETE_KEY_TRANSACTION) { if (code == DELETE_KEY_TRANSACTION) {
@@ -119,7 +112,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
?: return TransactionResult.Continue ?: return TransactionResult.Continue
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId)) if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
SystemLogger.debug("${descriptor.alias} was an attestation key") SystemLogger.info("${descriptor.alias} was an attestation key")
SystemLogger.info("[TX_ID: $txId] Found generated response for ${descriptor.alias}:") SystemLogger.info("[TX_ID: $txId] Found generated response for ${descriptor.alias}:")
response.metadata?.authorizations?.forEach { response.metadata?.authorizations?.forEach {
@@ -155,20 +148,17 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
if (code == GET_KEY_ENTRY_TRANSACTION) { if (code == GET_KEY_ENTRY_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
data.enforceInterface(IKeystoreService.DESCRIPTOR) data.enforceInterface(IKeystoreService.DESCRIPTOR)
val keyDescriptor = val keyDescriptor =
data.readTypedObject(KeyDescriptor.CREATOR) data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction ?: return TransactionResult.SkipTransaction
logTransaction(
txId,
"post-getKeyEntry (alias=${keyDescriptor.alias})",
callingUid,
callingPid,
)
if (!ConfigurationManager.shouldPatch(callingUid)) if (!ConfigurationManager.shouldPatch(callingUid))
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
SystemLogger.info("Handling post-${transactionNames[code]!!} ${keyDescriptor.alias}")
return try { return try {
val response = val response =
reply.readTypedObject(KeyEntryResponse.CREATOR) reply.readTypedObject(KeyEntryResponse.CREATOR)
@@ -40,11 +40,20 @@ class KeyMintSecurityLevelInterceptor(
callingPid: Int, callingPid: Int,
data: Parcel, data: Parcel,
): TransactionResult { ): TransactionResult {
// This interceptor only handles the 'generateKey' transaction directly.
if (code == GENERATE_KEY_TRANSACTION) { if (code == GENERATE_KEY_TRANSACTION) {
logTransaction(txId, "generateKey", callingUid, callingPid) logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
return handleGenerateKey(callingUid, data) return handleGenerateKey(callingUid, data)
} else if (code == IMPORT_KEY_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val alias =
data.readTypedObject(KeyDescriptor.CREATOR)?.alias
?: return TransactionResult.ContinueAndSkipPost
SystemLogger.info("Handling post-${transactionNames[code]} ${alias}")
return TransactionResult.Continue
} else { } else {
logTransaction( logTransaction(
txId, txId,
@@ -57,6 +66,35 @@ class KeyMintSecurityLevelInterceptor(
return TransactionResult.ContinueAndSkipPost return TransactionResult.ContinueAndSkipPost
} }
override fun onPostTransact(
txId: Long,
target: IBinder,
code: Int,
flags: Int,
callingUid: Int,
callingPid: Int,
data: Parcel,
reply: Parcel?,
resultCode: Int,
): TransactionResult {
// We only care about successful 'importKey' transactions to clean cached keys.
if (
code == IMPORT_KEY_TRANSACTION &&
resultCode == 0 &&
reply != null &&
!InterceptorUtils.hasException(reply)
) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor =
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction
cleanupKeyData(KeyIdentifier(callingUid, keyDescriptor.alias))
}
return TransactionResult.SkipTransaction
}
/** /**
* Handles the `generateKey` transaction. Based on the configuration for the calling UID, it * Handles the `generateKey` transaction. Based on the configuration for the calling UID, it
* either generates a key in software or lets the call pass through to the hardware. * either generates a key in software or lets the call pass through to the hardware.
@@ -66,7 +104,7 @@ class KeyMintSecurityLevelInterceptor(
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
val attestationKey = data.readTypedObject(KeyDescriptor.CREATOR) val attestationKey = data.readTypedObject(KeyDescriptor.CREATOR)
SystemLogger.debug( SystemLogger.debug(
"[key, attestationKey]: ${keyDescriptor.alias}, ${attestationKey?.alias}" "Handling generateKey ${keyDescriptor.alias}, attestKey=${attestationKey?.alias}"
) )
val params = data.createTypedArray(KeyParameter.CREATOR)!! val params = data.createTypedArray(KeyParameter.CREATOR)!!
val parsedParams = KeyMintAttestation(params) val parsedParams = KeyMintAttestation(params)
@@ -84,9 +122,7 @@ class KeyMintSecurityLevelInterceptor(
isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias))) isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias)))
if (needsSoftwareGeneration) { if (needsSoftwareGeneration) {
SystemLogger.info( SystemLogger.info("Generating software key for ${keyId}.")
"Generating software key for alias '${keyDescriptor.alias}' (UID: $callingUid)."
)
// Generate the key pair and certificate chain. // Generate the key pair and certificate chain.
val keyData = val keyData =
@@ -116,11 +152,11 @@ class KeyMintSecurityLevelInterceptor(
// If not generating, clear any stale state for this alias and let the call proceed. // If not generating, clear any stale state for this alias and let the call proceed.
cleanupKeyData(keyId) cleanupKeyData(keyId)
TransactionResult.Continue TransactionResult.ContinueAndSkipPost
} }
.getOrElse { .getOrElse {
SystemLogger.error("Error during generateKey handling for UID $callingUid.", it) SystemLogger.error("Error during generateKey handling for UID $callingUid.", it)
TransactionResult.Continue // Fallback to original service on error. TransactionResult.ContinueAndSkipPost
} }
} }
@@ -175,8 +211,21 @@ class KeyMintSecurityLevelInterceptor(
fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId) fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId)
fun cleanupKeyData(keyId: KeyIdentifier) { fun cleanupKeyData(keyId: KeyIdentifier) {
generatedKeys.remove(keyId) if (generatedKeys.remove(keyId) != null) {
attestationKeys.remove(keyId) SystemLogger.debug("Remove generated key ${keyId}")
}
if (attestationKeys.remove(keyId)) {
SystemLogger.debug("Remove cached attestaion key ${keyId}")
}
}
// Clears all cached keys.
fun clearAllGeneratedKeys(reason: String? = null) {
val count = generatedKeys.size
val reasonMessage = reason?.let { " due to $it" } ?: ""
generatedKeys.clear()
attestationKeys.clear()
SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.")
} }
} }
} }
@@ -70,7 +70,7 @@ object AndroidDeviceUtils {
} }
/** /**
* Sets the `ro.boot.vbmeta.digest` system property. * Sets the `ro.boot.vbmeta.digest` system property using the `resetprop` command.
* *
* @param bytes The 32-byte digest to set. * @param bytes The 32-byte digest to set.
*/ */
@@ -78,9 +78,24 @@ object AndroidDeviceUtils {
val hex = bytes.toHex() val hex = bytes.toHex()
try { try {
SystemLogger.debug("Setting system property 'ro.boot.vbmeta.digest' to: $hex") SystemLogger.debug("Setting system property 'ro.boot.vbmeta.digest' to: $hex")
SystemProperties.set("ro.boot.vbmeta.digest", hex)
// Construct the command to be executed
val command = arrayOf("resetprop", "ro.boot.vbmeta.digest", hex)
// Execute the command
val process = Runtime.getRuntime().exec(command)
// Wait for the process to complete and check the exit code for errors
val exitCode = process.waitFor()
if (exitCode != 0) {
val errorOutput = process.errorStream.bufferedReader().readText()
SystemLogger.error(
"resetprop command failed with exit code $exitCode: $errorOutput"
)
}
} catch (e: Exception) { } catch (e: Exception) {
SystemLogger.error("Failed to set vbmeta digest property.", e) SystemLogger.error("Failed to set vbmeta digest property by executing resetprop.", e)
} }
} }
@@ -95,10 +110,10 @@ object AndroidDeviceUtils {
getCustomPatchLevelFor("system", isLong = false) getCustomPatchLevelFor("system", isLong = false)
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = false) ?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = false)
val vendorPatchLevel: Int val vendorPatchLevelLong: Int
get() = get() =
getCustomPatchLevelFor("vendor", isLong = false) getCustomPatchLevelFor("vendor", isLong = true)
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = false) ?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true)
val bootPatchLevelLong: Int val bootPatchLevelLong: Int
get() = get() =
+1 -1
View File
@@ -1,7 +1,7 @@
[versions] [versions]
agp = "8.13.1" agp = "8.13.1"
annotation = "1.9.1" annotation = "1.9.1"
jdk18on = "1.82" jdk18on = "1.83"
kotlin = "2.2.21" kotlin = "2.2.21"
ktfmt = "0.25.0" ktfmt = "0.25.0"
+9 -14
View File
@@ -1,19 +1,14 @@
**Key Highlights:** 🚀 **TEESimulator v2.1 Hotfix Release is Live!** 🚀
* 🚀 **Complete Refactoring:** TEESimulator v2.0 has been entirely rebuilt and is no longer based on its predecessors, [TrickyStore](https://github.com/5ec1cff/TrickyStore) and [TrickyStoreOSS](https://github.com/beakthoven/TrickyStoreOSS), resulting in a more streamlined and maintainable codebase. This urgent hotfix addresses several critical issues identified in the previous v2.0 release.
* 🛡️ **Enhanced Bypass Capabilities:** The simulator now successfully bypasses well-known detection mechanisms, including [TamperedAttestation](https://github.com/JingMatrix/TamperedAttestation) and [KeyAttestation](https://github.com/JingMatrix/KeyAttestation). The v2.0 update, a significant refactoring effort, unfortunately introduced a few unexpected behaviors and bugs that we are now rectifying.
* 💳 **Revolut Detection Bypass:** With a valid keybox, users can now circumvent the detection measures implemented in the [Revolut](https://play.google.com/store/apps/details?id=com.revolut.revolut) application. **Key fixes in this release include:**
1. **Google Play Integrity:** Resolved an issue preventing the attainment of STRONG integrity for Google Play verdicts, caused by an incorrect vendor patch level format. ✅
2. **Application Stability:** Fixed a critical crash related to an incorrect signature for the `SystemProperties.set` stub method. 🐛
3. **Stealth Enhancement:** Implemented a fix to bypass detection by the `Android Native Detector`. 👻
**Current Limitations:** 🔬 We are actively investigating a recent detection method to further enhance stealth capabilities.
* ⚠️ **Google Play Verdict:** Bypassing the detections within the Google Play verdict remains an unresolved challenge. We are actively seeking solutions and welcome any insights from the community regarding potential system module-based bypasses. 🙏 Support for TEE-broken devices and Android 10/11 remains an area of ongoing improvement. We highly encourage you to submit any issues you encounter to help us refine these aspects! 🤝
**Platform Support:**
* 📱 **Android 10 & 11:** TEESimulator v2.0 has not yet been tested on Android 10 or 11. We encourage users on these platforms to report any issues and provide logs to help us improve compatibility.
**Contributing:**
* 🤝 We welcome and encourage community contributions. Please feel free to submit issues and pull requests to help improve the project.
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"version": "v2.0", "version": "v2.1",
"versionCode": 14, "versionCode": 20,
"zipUrl": "https://github.com/JingMatrix/TEESimulator/releases/download/v2.0/TEESimulator-v2.0-14-release.zip", "zipUrl": "https://github.com/JingMatrix/TEESimulator/releases/download/v2.1/TEESimulator-v2.1-20-release.zip",
"changelog": "https://raw.githubusercontent.com/JingMatrix/TEESimulator/main/module/changelog.md" "changelog": "https://raw.githubusercontent.com/JingMatrix/TEESimulator/main/module/changelog.md"
} }
@@ -4,8 +4,4 @@ public class SystemProperties {
public static String get(String key, String def) { public static String get(String key, String def) {
throw new UnsupportedOperationException("STUB!"); throw new UnsupportedOperationException("STUB!");
} }
public static String set(String key, String val) {
throw new UnsupportedOperationException("STUB!");
}
} }