Implement multi-keybox support (#1)

This commit adds support for using different `keybox.xml` files for different applications. This allows for greater flexibility, enabling the use of distinct cryptographic identities for specific groups of apps.

The `target.txt` configuration now supports a new syntax. A line with a filename in brackets, like `[demo_keybox.xml]`, sets the active keybox for all subsequent packages in the file.

The implementation refactors the configuration and keybox loading logic:

- PkgConfig now parses the new syntax and maps packages to their designated keybox files.

- KeyBoxUtils is updated to dynamically load and cache multiple keybox files on demand, instead of relying on a single global state.

- Certificate generation and hacking functions now use this new configuration to select the correct keybox for each operation based on the application's UID.
This commit is contained in:
JingMatrix
2025-11-02 11:07:50 +01:00
committed by GitHub
parent ad81149700
commit 093c1bbc13
6 changed files with 171 additions and 57 deletions
+22 -6
View File
@@ -40,22 +40,38 @@ This file provides the master cryptographic identity for the simulator. It conta
</AndroidAttestation> </AndroidAttestation>
``` ```
### Mode Configuration (`target.txt`) ### Mode and Keybox Configuration (`target.txt`)
TEESimulator currently operates in two primary modes as it transitions towards full emulation. You can control this behavior on a per-package basis. TEESimulator currently operates in two primary modes as it transitions towards full emulation.
You can control the simulation mode and the specific keybox.xml file used on a per-package basis.
#### Mode Suffixes
* **`!` → Force Generation Mode:** Creates a complete, software-based virtual key. This is the foundation of the full TEE simulation. * **`!` → Force Generation Mode:** Creates a complete, software-based virtual key. This is the foundation of the full TEE simulation.
* **`?` → Force Leaf Hacking Mode:** A legacy mode where a real TEE key is generated, but its attestation certificate is intercepted and modified. * **`?` → Force Leaf Hacking Mode:** A legacy mode where a real TEE key is generated, but its attestation certificate is intercepted and modified.
* **No symbol → Automatic Mode:** The module selects the most appropriate mode for the device. * **No symbol → Automatic Mode:** The module selects the most appropriate mode for the device.
#### Multi-Keybox Configuration
You can specify different keybox files for different groups of applications. This is done by adding a line with the filename in square brackets (e.g., [demo_keybox.xml]).
All applications listed after this line will use the specified keybox file, until a new keybox is declared. Applications listed before any custom keybox declaration will use the default `keybox.xml`.
For example: For example:
``` ```
# target.txt # These two apps will use the default /data/adb/tricky_store/keybox.xml
# Use full generation/simulation for this app
com.google.android.gms! com.google.android.gms!
# Use the legacy leaf hacking mode
io.github.vvb2060.keyattestation? io.github.vvb2060.keyattestation?
# Switch to a different keybox for the following apps.
# The file must be located at /data/adb/tricky_store/aosp_keybox.xml
[aosp_keybox.xml]
com.google.android.gsf
# Switch again to another keybox.
# The file must be located at /data/adb/tricky_store/demo_keybox.xml
[demo_keybox.xml]
org.matrix.demo
``` ```
### Security Patch Level (`security_patch.txt`) ### Security Patch Level (`security_patch.txt`)
@@ -176,7 +176,7 @@ object CertificateGen {
} }
fun generateChain(uid: Int, params: KeyGenParameters, keyPair: KeyPair, securityLevel: Int = 1): List<ByteArray>? = runCatching { fun generateChain(uid: Int, params: KeyGenParameters, keyPair: KeyPair, securityLevel: Int = 1): List<ByteArray>? = runCatching {
val keybox = getKeyboxForAlgorithm(params.algorithm) ?: return null val keybox = getKeyboxForAlgorithm(uid, params.algorithm) ?: return null
val issuer = X509CertificateHolder(keybox.certificates[0].encoded).subject val issuer = X509CertificateHolder(keybox.certificates[0].encoded).subject
val leaf = buildCertificate(keyPair, keybox, params, issuer, uid, securityLevel) val leaf = buildCertificate(keyPair, keybox, params, issuer, uid, securityLevel)
@@ -234,7 +234,7 @@ object CertificateGen {
} }
val keyPair = generateKeyPair(params) ?: return null val keyPair = generateKeyPair(params) ?: return null
val keybox = getKeyboxForAlgorithm(params.algorithm) ?: return null val keybox = getKeyboxForAlgorithm(uid, params.algorithm) ?: return null
val (signingKeyPair, issuer) = if (hasAttestKey) { val (signingKeyPair, issuer) = if (hasAttestKey) {
getAttestationKeyInfo(uid, attestKeyDescriptor!!)?.let { getAttestationKeyInfo(uid, attestKeyDescriptor!!)?.let {
@@ -267,9 +267,10 @@ object CertificateGen {
} }
} }
private fun getKeyboxForAlgorithm(algorithm: Int): KeyBox? { private fun getKeyboxForAlgorithm(uid: Int, algorithm: Int): KeyBox? {
val algorithmName = mapAlgorithmToName(algorithm) ?: return null val algorithmName = mapAlgorithmToName(algorithm) ?: return null
return KeyBoxUtils.keyboxes[algorithmName] val keyboxFileName = PkgConfig.getKeyboxFileForUid(uid)
return KeyBoxUtils.getKeybox(keyboxFileName, algorithmName)
} }
private fun getAttestationKeyInfo(uid: Int, attestKeyDescriptor: KeyDescriptor): Pair<KeyPair, X500Name>? { private fun getAttestationKeyInfo(uid: Int, attestKeyDescriptor: KeyDescriptor): Pair<KeyPair, X500Name>? {
@@ -468,4 +469,4 @@ object CertificateGen {
return DEROctetString(DERSequence(applicationIdArray).encoded) return DEROctetString(DERSequence(applicationIdArray).encoded)
} }
} }
@@ -5,6 +5,7 @@
package io.github.beakthoven.TrickyStoreOSS package io.github.beakthoven.TrickyStoreOSS
import io.github.beakthoven.TrickyStoreOSS.config.PkgConfig
import io.github.beakthoven.TrickyStoreOSS.logging.Logger import io.github.beakthoven.TrickyStoreOSS.logging.Logger
import org.bouncycastle.asn1.ASN1Boolean import org.bouncycastle.asn1.ASN1Boolean
import org.bouncycastle.asn1.ASN1Encodable import org.bouncycastle.asn1.ASN1Encodable
@@ -49,7 +50,7 @@ object CertificateHack {
leafAlgorithms.clear() leafAlgorithms.clear()
} }
fun hackCertificateChain(certificateChain: Array<Certificate>?): Array<Certificate> { fun hackCertificateChain(certificateChain: Array<Certificate>?, uid: Int): Array<Certificate> {
if (certificateChain == null) { if (certificateChain == null) {
throw UnsupportedOperationException("Certificate chain is null!") throw UnsupportedOperationException("Certificate chain is null!")
} }
@@ -80,8 +81,9 @@ object CertificateHack {
} }
} }
val keybox = KeyBoxUtils.keyboxes[leaf.publicKey.algorithm] val keyboxFileName = PkgConfig.getKeyboxFileForUid(uid)
?: throw UnsupportedOperationException("Unsupported algorithm: ${leaf.publicKey.algorithm}") val algorithmName = leaf.publicKey.algorithm
val keybox = KeyBoxUtils.getKeybox(keyboxFileName, algorithmName) ?: throw UnsupportedOperationException("Unsupported algorithm '$algorithmName' in keybox '$keyboxFileName'")
val certificates = LinkedList(keybox.certificates) val certificates = LinkedList(keybox.certificates)
val builder = X509v3CertificateBuilder( val builder = X509v3CertificateBuilder(
@@ -107,7 +109,7 @@ object CertificateHack {
certificates.addFirst(JcaX509CertificateConverter().getCertificate(builder.build(signer))) certificates.addFirst(JcaX509CertificateConverter().getCertificate(builder.build(signer)))
certificates.toTypedArray() certificates.toTypedArray()
} catch (t: Throwable) { } catch (t: Throwable) {
Logger.e("Failed to hack certificate chain", t) Logger.e("Failed to hack certificate chain for uid=$uid", t)
certificateChain certificateChain
} }
} }
@@ -122,12 +124,13 @@ object CertificateHack {
val algorithm = leafAlgorithms.remove(key) val algorithm = leafAlgorithms.remove(key)
?: throw UnsupportedOperationException("No algorithm found for key $key") ?: throw UnsupportedOperationException("No algorithm found for key $key")
val keybox = KeyBoxUtils.keyboxes[algorithm] val keyboxFileName = PkgConfig.getKeyboxFileForUid(uid)
?: throw UnsupportedOperationException("Unsupported algorithm: $algorithm") val keybox = KeyBoxUtils.getKeybox(keyboxFileName, algorithm)
?: throw UnsupportedOperationException("Unsupported algorithm '$algorithm' in keybox '$keyboxFileName'")
CertificateUtils.run { keybox.certificates.toByteArray() } ?: caList CertificateUtils.run { keybox.certificates.toByteArray() } ?: caList
} catch (t: Throwable) { } catch (t: Throwable) {
Logger.e("Failed to hack CA certificate chain", t) Logger.e("Failed to hack CA certificate chain for uid=$uid", t)
caList caList
} }
} }
@@ -166,8 +169,10 @@ object CertificateHack {
} }
} }
val keybox = KeyBoxUtils.keyboxes[leaf.publicKey.algorithm] val keyboxFileName = PkgConfig.getKeyboxFileForUid(uid)
?: throw UnsupportedOperationException("Unsupported algorithm: ${leaf.publicKey.algorithm}") val algorithmName = leaf.publicKey.algorithm
val keybox = KeyBoxUtils.getKeybox(keyboxFileName, algorithmName)
?: throw UnsupportedOperationException("Unsupported algorithm '$algorithmName' in keybox '$keyboxFileName'")
val builder = X509v3CertificateBuilder( val builder = X509v3CertificateBuilder(
X509CertificateHolder(keybox.certificates[0].encoded).subject, X509CertificateHolder(keybox.certificates[0].encoded).subject,
@@ -191,7 +196,7 @@ object CertificateHack {
JcaX509CertificateConverter().getCertificate(builder.build(signer)).encoded JcaX509CertificateConverter().getCertificate(builder.build(signer)).encoded
} catch (t: Throwable) { } catch (t: Throwable) {
Logger.e("Failed to hack user certificate", t) Logger.e("Failed to hack user certificate for uid=$uid", t)
certificate certificate
} }
} }
@@ -238,4 +243,4 @@ object CertificateHack {
return Extension(ATTESTATION_OID, false, hackedSequenceOctets) return Extension(ATTESTATION_OID, false, hackedSequenceOctets)
} }
} }
@@ -8,10 +8,12 @@ package io.github.beakthoven.TrickyStoreOSS
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import io.github.beakthoven.TrickyStoreOSS.CertificateGen.KeyBox import io.github.beakthoven.TrickyStoreOSS.CertificateGen.KeyBox
import io.github.beakthoven.TrickyStoreOSS.CertificateHack.clearLeafAlgorithms import io.github.beakthoven.TrickyStoreOSS.CertificateHack.clearLeafAlgorithms
import io.github.beakthoven.TrickyStoreOSS.config.PkgConfig
import io.github.beakthoven.TrickyStoreOSS.logging.Logger import io.github.beakthoven.TrickyStoreOSS.logging.Logger
import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory import org.xmlpull.v1.XmlPullParserFactory
import java.io.File
import java.io.IOException import java.io.IOException
import java.io.StringReader import java.io.StringReader
import java.security.cert.Certificate import java.security.cert.Certificate
@@ -159,12 +161,68 @@ class XmlParser(private val xmlContent: String) {
} }
object KeyBoxUtils { object KeyBoxUtils {
val keyboxes = ConcurrentHashMap<String, KeyBox>() private val loadedKeyboxFiles = ConcurrentHashMap<String, ConcurrentHashMap<String, KeyBox>>()
fun hasKeyboxes(): Boolean = keyboxes.isNotEmpty() /**
* The primary public function to get a specific KeyBox for a given algorithm and file.
* It will load and cache the file on demand if it hasn't been seen before.
*
* @param keyboxFileName The simple name of the keybox file (e.g., "keybox.xml").
* @param algorithm The algorithm key (e.g., KeyProperties.KEY_ALGORITHM_EC).
* @return The requested KeyBox, or null if not found in the specified file.
*/
fun getKeybox(keyboxFileName: String, algorithm: String): KeyBox? {
val keyboxesForFile = loadedKeyboxFiles.getOrPut(keyboxFileName) {
// If this file is not in our cache, load it now.
readFromFile(keyboxFileName)
}
Logger.i("Retriving keybox $keyboxFileName [$algorithm]")
return keyboxesForFile[algorithm]
}
private fun readFromFile(fileName: String): ConcurrentHashMap<String, KeyBox> {
val filePath = File(PkgConfig.CONFIG_PATH, fileName)
Logger.i("Loading keybox file: $filePath")
val keyboxes = ConcurrentHashMap<String, KeyBox>()
if (!filePath.exists()) {
Logger.e("Keybox file not found: $filePath")
return keyboxes // Return an empty map if file doesn't exist
}
try {
val xmlData = filePath.readText()
val xmlParser = XmlParser(xmlData.sanitizeXml())
val numberOfKeyboxesResult = xmlParser.obtainPath("AndroidAttestation.NumberOfKeyboxes")
val numberOfKeyboxes = when (numberOfKeyboxesResult) {
is XmlParser.ParseResult.Success -> numberOfKeyboxesResult.attributes["text"]?.toIntOrNull() ?: 1
is XmlParser.ParseResult.Error -> throw Exception(numberOfKeyboxesResult.message, numberOfKeyboxesResult.cause)
}
repeat(numberOfKeyboxes) { i ->
val (algorithmName, keyBox) = processKeybox(xmlParser, i)
keyboxes[algorithmName] = keyBox
}
Logger.i("Successfully loaded ${keyboxes.size} keyboxes from $fileName")
} catch (t: Throwable) {
Logger.e("Error loading XML file ($fileName)", t)
}
return keyboxes
}
fun hasKeyboxes(): Boolean = loadedKeyboxFiles.isNotEmpty() && loadedKeyboxFiles.values.any { it.isNotEmpty() }
// This function is now deprecated and should be removed. We keep it for now to show the transition.
// Its logic is now inside readFromFile.
@Deprecated("Use getKeybox(fileName, algorithm) instead for dynamic loading.")
fun readFromXml(xmlData: String?) { fun readFromXml(xmlData: String?) {
keyboxes.clear() // The old global state is gone. This function's logic is now in readFromFile.
// We could make this load into a default keybox for backward compatibility if needed.
loadedKeyboxFiles.clear()
clearLeafAlgorithms() clearLeafAlgorithms()
if (xmlData == null) { if (xmlData == null) {
@@ -209,7 +267,7 @@ object KeyBoxUtils {
return content.trimEnd() return content.trimEnd()
} }
private fun processKeybox(xmlParser: XmlParser, index: Int) { private fun processKeybox(xmlParser: XmlParser, index: Int): Pair<String, KeyBox> {
try { try {
val algorithmResult = xmlParser.obtainPath("AndroidAttestation.Keybox.Key[$index]") val algorithmResult = xmlParser.obtainPath("AndroidAttestation.Keybox.Key[$index]")
val keyboxAlgorithm = when (algorithmResult) { val keyboxAlgorithm = when (algorithmResult) {
@@ -264,11 +322,11 @@ object KeyBoxUtils {
else -> keyboxAlgorithm else -> keyboxAlgorithm
} }
keyboxes[algorithmName] = KeyBox(pemKeyPair, keyPair, certificateChain) return algorithmName to KeyBox(pemKeyPair, keyPair, certificateChain)
} catch (t: Throwable) { } catch (t: Throwable) {
Logger.e("Error processing keybox $index", t) Logger.e("Error processing keybox $index", t)
throw t throw t
} }
} }
} }
@@ -21,6 +21,18 @@ object PkgConfig {
private val generatePackages = mutableSetOf<String>() private val generatePackages = mutableSetOf<String>()
private val packageModes = mutableMapOf<String, Mode>() private val packageModes = mutableMapOf<String, Mode>()
private val packageKeyboxes = mutableMapOf<String, String>()
private val keyboxRegex = Regex("^\\[([a-zA-Z0-9_.-]+\\.xml)]$")
private const val DEFAULT_KEYBOX_FILE = "keybox.xml"
fun getKeyboxFileForUid(callingUid: Int): String = runCatching {
val ps = getPm()?.getPackagesForUid(callingUid) ?: return DEFAULT_KEYBOX_FILE
for (pkg in ps) {
packageKeyboxes[pkg]?.let { return it }
}
return DEFAULT_KEYBOX_FILE
}.getOrDefault(DEFAULT_KEYBOX_FILE)
enum class Mode { enum class Mode {
AUTO, LEAF_HACK, GENERATE AUTO, LEAF_HACK, GENERATE
} }
@@ -29,41 +41,58 @@ object PkgConfig {
hackPackages.clear() hackPackages.clear()
generatePackages.clear() generatePackages.clear()
packageModes.clear() packageModes.clear()
f?.readLines()?.forEach { packageKeyboxes.clear()
if (it.isNotBlank() && !it.startsWith("#")) {
val n = it.trim() var currentKeyboxFile = DEFAULT_KEYBOX_FILE
when {
n.endsWith("!") -> { f?.readLines()?.forEach { line ->
val pkg = n.removeSuffix("!").trim() val n = line.trim()
generatePackages.add(pkg) if (n.isBlank() || n.startsWith("#")) {
packageModes[pkg] = Mode.GENERATE return@forEach // Skip comments and empty lines
} }
n.endsWith("?") -> {
val pkg = n.removeSuffix("?").trim() val matchResult = keyboxRegex.find(n)
hackPackages.add(pkg) if (matchResult != null) {
packageModes[pkg] = Mode.LEAF_HACK currentKeyboxFile = matchResult.groupValues[1]
} Logger.i("Switched to keybox file: $currentKeyboxFile for subsequent packages")
else -> { return@forEach
// Auto mode }
packageModes[n] = Mode.AUTO
} when {
n.endsWith("!") -> {
val pkg = n.removeSuffix("!").trim()
generatePackages.add(pkg)
packageModes[pkg] = Mode.GENERATE
packageKeyboxes[pkg] = currentKeyboxFile
}
n.endsWith("?") -> {
val pkg = n.removeSuffix("?").trim()
hackPackages.add(pkg)
packageModes[pkg] = Mode.LEAF_HACK
packageKeyboxes[pkg] = currentKeyboxFile
}
else -> {
// Auto mode
packageModes[n] = Mode.AUTO
packageKeyboxes[n] = currentKeyboxFile
} }
} }
} }
Logger.i("update hack packages: $hackPackages, generate packages=$generatePackages, packageModes=$packageModes") Logger.i("update hack packages: $hackPackages, generate packages=$generatePackages, packageModes=$packageModes, , packageKeyboxes=$packageKeyboxes")
}.onFailure { }.onFailure {
Logger.e("failed to update target files", it) Logger.e("failed to update target files", it)
} }
// This function is now deprecated in favor of a more dynamic approach, but kept for simplicity.
// The key logic is now in KeyBoxUtils which will be called from the interceptors.
private fun updateKeyBox(f: File?) = runCatching { private fun updateKeyBox(f: File?) = runCatching {
KeyBoxUtils.readFromXml(f?.readText()) KeyBoxUtils.readFromXml(f?.readText())
}.onFailure { }.onFailure {
Logger.e("failed to update keybox", it) Logger.e("failed to update keybox", it)
} }
private const val CONFIG_PATH = "/data/adb/tricky_store" const val CONFIG_PATH = "/data/adb/tricky_store"
private const val TARGET_FILE = "target.txt" private const val TARGET_FILE = "target.txt"
private const val KEYBOX_FILE = "keybox.xml"
private const val TEE_STATUS_FILE = "tee_status" private const val TEE_STATUS_FILE = "tee_status"
private const val PATCHLEVEL_FILE = "security_patch.txt" private const val PATCHLEVEL_FILE = "security_patch.txt"
private val root = File(CONFIG_PATH) private val root = File(CONFIG_PATH)
@@ -100,10 +129,15 @@ object PkgConfig {
DELETE, MOVED_FROM -> null DELETE, MOVED_FROM -> null
else -> return else -> return
} }
when (path) { when {
TARGET_FILE -> updateTargetPackages(f) path == TARGET_FILE -> updateTargetPackages(f)
KEYBOX_FILE -> updateKeyBox(f) path.endsWith(".xml") -> {
PATCHLEVEL_FILE -> updatePatchLevel(f) // This is a simplification. A more robust solution would be to reload the specific keybox if it's in use.
// For now, we assume any XML change might affect the active keyboxes, prompting a reload where needed.
// The main logic for loading is now handled dynamically in KeyBoxUtils.
Logger.i("Keybox file $path changed. It will be re-read on next use.")
}
path == PATCHLEVEL_FILE -> updatePatchLevel(f)
} }
} }
} }
@@ -116,9 +150,9 @@ object PkgConfig {
} else { } else {
Logger.e("target.txt file not found, please put it to $scope !") Logger.e("target.txt file not found, please put it to $scope !")
} }
val keybox = File(root, KEYBOX_FILE) val keybox = File(root, DEFAULT_KEYBOX_FILE)
if (!keybox.exists()) { if (!keybox.exists()) {
Logger.e("keybox file not found, please put it to $keybox !") Logger.e("default keybox file not found, please put it to $keybox !")
} else { } else {
updateKeyBox(keybox) updateKeyBox(keybox)
} }
@@ -150,7 +150,7 @@ object Keystore2Interceptor : BaseKeystoreInterceptor() {
if (response != null) { if (response != null) {
val chain = CertificateUtils.run { response.getCertificateChain() } val chain = CertificateUtils.run { response.getCertificateChain() }
if (chain != null) { if (chain != null) {
val newChain = CertificateHack.hackCertificateChain(chain) val newChain = CertificateHack.hackCertificateChain(chain, callingUid)
response.putCertificateChain(newChain).getOrThrow() response.putCertificateChain(newChain).getOrThrow()
Logger.i("Hacked certificate for uid=$callingUid") Logger.i("Hacked certificate for uid=$callingUid")
return createTypedObjectReply(response) return createTypedObjectReply(response)
@@ -167,4 +167,4 @@ object Keystore2Interceptor : BaseKeystoreInterceptor() {
} }
return Skip return Skip
} }
} }