diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index fbfc04f..74f8f1d 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -24,7 +24,7 @@ public static void main(java.lang.String[]); } --assumenosideeffects class io.github.beakthoven.TrickyStoreOSS.core.logging.Logger { +-assumenosideeffects class io.github.beakthoven.TrickyStoreOSS.logging.Logger { public static void d(java.lang.String); public static void dd(java.lang.String); public static void v(java.lang.String); diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/AndroidUtils.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/AndroidUtils.kt index b2972e7..c29a455 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/AndroidUtils.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/AndroidUtils.kt @@ -10,227 +10,223 @@ import android.content.pm.PackageManager import android.os.Build import android.os.ServiceManager import android.os.SystemProperties -import io.github.beakthoven.TrickyStoreOSS.core.config.Config -import io.github.beakthoven.TrickyStoreOSS.core.config.CustomPatchLevel -import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger +import io.github.beakthoven.TrickyStoreOSS.AttestUtils.CachedAttestData +import io.github.beakthoven.TrickyStoreOSS.config.CustomPatchLevel +import io.github.beakthoven.TrickyStoreOSS.config.PkgConfig +import io.github.beakthoven.TrickyStoreOSS.logging.Logger import org.bouncycastle.asn1.ASN1Integer import org.bouncycastle.asn1.DEROctetString import org.bouncycastle.asn1.DERSequence import java.security.MessageDigest import java.util.concurrent.ThreadLocalRandom -fun getTransactCode(clazz: Class<*>, method: String): Int = - clazz.getDeclaredField("TRANSACTION_$method").apply { isAccessible = true } - .getInt(null) +object AndroidUtils { -// cache attest data to avoid running attestation multiple times -private val cachedAttestData: AttestationData? by lazy { - getAttestData() // from CertHacker -} - -val bootKey: ByteArray by lazy { - randomBytes() -} - -fun setupBootHash() { - getBootHashFromProp()?.also { - Logger.d("Using boot hash from system property: ${it.toHex()}") - } - ?: getBootHashFromAttestation()?.also { - Logger.d("Using boot hash from attestation: ${it.toHex()}") - setBootHashProp(it) - } - ?: randomBytes().also { - Logger.d("Generating random boot hash: ${it.toHex()}") - setBootHashProp(it) - } -} - -@OptIn(ExperimentalStdlibApi::class) -fun getBootHashFromProp(): ByteArray? { - val digest = SystemProperties.get("ro.boot.vbmeta.digest", null) ?: return null - Logger.d("System property ro.boot.vbmeta.digest: $digest") - - if (digest.isBlank()) { - Logger.d("Property is blank") - return null - } - - return if (digest.length == 64) digest.hexToByteArray() else null -} - -private fun getBootHashFromAttestation(): ByteArray? { - return try { - cachedAttestData?.verifiedBootHash - } catch (e: Exception) { - Logger.e("Failed to get boot hash from attestation: ${e.message}") - null - } -} - -private fun setBootHashProp(bytes: ByteArray) { - val hex = bytes.toHex() - try { - Logger.d("Setting ro.boot.vbmeta.digest to: $hex") - SystemProperties.set("ro.boot.vbmeta.digest", hex) - } catch (e: Exception) { - Logger.e("Exception setting vbmeta digest: ${e.message}") - } -} - -private fun randomBytes(): ByteArray = ByteArray(32).also { - ThreadLocalRandom.current().nextBytes(it) -} - -val patchLevel: Int - get() = getCustomPatchLevel("system", false) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) - -val patchLevelLong: Int - get() = getCustomPatchLevel("system", true) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true) - -val vendorPatchLevel: Int - get() = getCustomPatchLevel("vendor", false) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) - -val vendorPatchLevelLong: Int - get() = getCustomPatchLevel("vendor", true) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true) - -val bootPatchLevel: Int - get() = getCustomPatchLevel("boot", false) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) - -val bootPatchLevelLong: Int - get() = getCustomPatchLevel("boot", true) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true) - -private val customPatchLevel: CustomPatchLevel? - get() = Config._customPatchLevel - -private fun getCustomPatchLevel(component: String, isLong: Boolean): Int? { - val config = customPatchLevel ?: return null - val value = when (component) { - "system" -> config.system ?: config.all - "vendor" -> config.vendor ?: config.all - "boot" -> config.boot ?: config.all - else -> config.all - } ?: return null - - when { - value.equals("no", ignoreCase = true) -> return null - value.equals("prop", ignoreCase = true) -> return null + val bootKey: ByteArray by lazy { + randomBytes() } - return parsePatchLevelValue(value, component, isLong) -} - -private fun parsePatchLevelValue(value: String, component: String, isLong: Boolean): Int? { - val normalized = value.replace("-", "") - - return try { - when (normalized.length) { - 8 -> { - val year = normalized.substring(0, 4).toInt() - val month = normalized.substring(4, 6).toInt() - val day = normalized.substring(6, 8).toInt() - if (isLong) year * 10000 + month * 100 + day - else year * 100 + month - } - 6 -> { - val year = normalized.substring(0, 4).toInt() - val month = normalized.substring(4, 6).toInt() - if (isLong) year * 10000 + month * 100 - else year * 100 + month - } - else -> { - Logger.e("Invalid patch level length for $component: $normalized") - null - } + fun setupBootHash() { + getBootHashFromProp()?.also { + Logger.d("Using boot hash from system property: ${it.toHex()}") + } + ?: getBootHashFromAttestation()?.also { + Logger.d("Using boot hash from attestation: ${it.toHex()}") + setBootHashProp(it) + } + ?: randomBytes().also { + Logger.d("Generating random boot hash: ${it.toHex()}") + setBootHashProp(it) } - } catch (e: NumberFormatException) { - Logger.e("Patch level parse error for $component=$value", e) - null } -} -private val osVersionMap = mapOf( - Build.VERSION_CODES.BAKLAVA to 160000, - Build.VERSION_CODES.VANILLA_ICE_CREAM to 150000, - Build.VERSION_CODES.UPSIDE_DOWN_CAKE to 140000, - Build.VERSION_CODES.TIRAMISU to 130000, - Build.VERSION_CODES.S_V2 to 120100, - Build.VERSION_CODES.S to 120000, - Build.VERSION_CODES.R to 110000, - Build.VERSION_CODES.Q to 100000 -) - -val osVersion: Int - get() = cachedAttestData?.osVersion ?: osVersionMap[Build.VERSION.SDK_INT] ?: 160000 - -private val attestVersionMap = mapOf( - Build.VERSION_CODES.Q to 4, // Keymaster 4.1 - Build.VERSION_CODES.R to 4, // Keymaster 4.1 - Build.VERSION_CODES.S to 100, // KeyMint 1.0 - Build.VERSION_CODES.S_V2 to 100, // KeyMint 1.0 - Build.VERSION_CODES.TIRAMISU to 200, // KeyMint 2.0 - Build.VERSION_CODES.UPSIDE_DOWN_CAKE to 300, // KeyMint 3.0 - Build.VERSION_CODES.VANILLA_ICE_CREAM to 300, // KeyMint 3.0 - Build.VERSION_CODES.BAKLAVA to 400 // KeyMint 4.0 -) - -val attestVersion: Int - get() = cachedAttestData?.attestVersion ?: attestVersionMap[Build.VERSION.SDK_INT] ?: 400 - -val keymasterVersion: Int - get() = cachedAttestData?.keymasterVersion ?: if (attestVersion == 4) 41 else attestVersion - -fun String.convertPatchLevel(isLong: Boolean): Int = runCatching { - val parts = split("-") - when { - isLong && parts.size >= 3 -> parts[0].toInt() * 10000 + parts[1].toInt() * 100 + parts[2].toInt() - parts.size >= 2 -> parts[0].toInt() * 100 + parts[1].toInt() - else -> throw IllegalArgumentException("Invalid patch level format: $this") - } -}.onFailure { - Logger.e("Invalid patch level format: $this", it) -}.getOrDefault(202404) - -val apexInfos: List> by lazy { - runCatching { - val packageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package")) - val packages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - packageManager.getInstalledPackages(PackageManager.MATCH_APEX.toLong(), 0) - } else { - @Suppress("DEPRECATION") - packageManager.getInstalledPackages(PackageManager.MATCH_APEX, 0) + @OptIn(ExperimentalStdlibApi::class) + fun getBootHashFromProp(): ByteArray? { + val digest = SystemProperties.get("ro.boot.vbmeta.digest", null) ?: return null + Logger.d("System property ro.boot.vbmeta.digest: $digest") + + if (digest.isBlank()) { + Logger.d("Property is blank") + return null } - packages.list - .map { it.packageName to it.longVersionCode } - .sortedBy { it.first } - }.getOrElse { - Logger.e("Failed to get APEX package information") - emptyList() + return if (digest.length == 64) digest.hexToByteArray() else null } -} -val moduleHash: ByteArray by lazy { - runCatching { - val encodables = apexInfos.flatMap { (packageName, versionCode) -> - listOf( - DEROctetString(packageName.toByteArray()), - ASN1Integer(versionCode) - ) + private fun getBootHashFromAttestation(): ByteArray? { + return try { + CachedAttestData?.verifiedBootHash + } catch (e: Exception) { + Logger.e("Failed to get boot hash from attestation: ${e.message}") + null } - - val sequence = DERSequence(encodables.toTypedArray()) - MessageDigest.getInstance("SHA-256").digest(sequence.encoded) - }.getOrElse { - Logger.e("Failed to compute module hash", it) - ByteArray(32) } + + private fun setBootHashProp(bytes: ByteArray) { + val hex = bytes.toHex() + try { + Logger.d("Setting ro.boot.vbmeta.digest to: $hex") + SystemProperties.set("ro.boot.vbmeta.digest", hex) + } catch (e: Exception) { + Logger.e("Exception setting vbmeta digest: ${e.message}") + } + } + + private fun randomBytes(): ByteArray = ByteArray(32).also { + ThreadLocalRandom.current().nextBytes(it) + } + + val patchLevel: Int + get() = getCustomPatchLevel("system", false) + ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) + + val patchLevelLong: Int + get() = getCustomPatchLevel("system", true) + ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true) + + val vendorPatchLevel: Int + get() = getCustomPatchLevel("vendor", false) + ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) + + val vendorPatchLevelLong: Int + get() = getCustomPatchLevel("vendor", true) + ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true) + + val bootPatchLevel: Int + get() = getCustomPatchLevel("boot", false) + ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) + + val bootPatchLevelLong: Int + get() = getCustomPatchLevel("boot", true) + ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true) + + private val customPatchLevel: CustomPatchLevel? + get() = PkgConfig._customPatchLevel + + private fun getCustomPatchLevel(component: String, isLong: Boolean): Int? { + val config = customPatchLevel ?: return null + val value = when (component) { + "system" -> config.system ?: config.all + "vendor" -> config.vendor ?: config.all + "boot" -> config.boot ?: config.all + else -> config.all + } ?: return null + + when { + value.equals("no", ignoreCase = true) -> return null + value.equals("prop", ignoreCase = true) -> return null + } + + return parsePatchLevelValue(value, component, isLong) + } + + private fun parsePatchLevelValue(value: String, component: String, isLong: Boolean): Int? { + val normalized = value.replace("-", "") + + return try { + when (normalized.length) { + 8 -> { + val year = normalized.substring(0, 4).toInt() + val month = normalized.substring(4, 6).toInt() + val day = normalized.substring(6, 8).toInt() + if (isLong) year * 10000 + month * 100 + day + else year * 100 + month + } + 6 -> { + val year = normalized.substring(0, 4).toInt() + val month = normalized.substring(4, 6).toInt() + if (isLong) year * 10000 + month * 100 + else year * 100 + month + } + else -> { + Logger.e("Invalid patch level length for $component: $normalized") + null + } + } + } catch (e: NumberFormatException) { + Logger.e("Patch level parse error for $component=$value", e) + null + } + } + + private val osVersionMap = mapOf( + Build.VERSION_CODES.BAKLAVA to 160000, + Build.VERSION_CODES.VANILLA_ICE_CREAM to 150000, + Build.VERSION_CODES.UPSIDE_DOWN_CAKE to 140000, + Build.VERSION_CODES.TIRAMISU to 130000, + Build.VERSION_CODES.S_V2 to 120100, + Build.VERSION_CODES.S to 120000, + Build.VERSION_CODES.R to 110000, + Build.VERSION_CODES.Q to 100000 + ) + + val osVersion: Int + get() = CachedAttestData?.osVersion ?: osVersionMap[Build.VERSION.SDK_INT] ?: 160000 + + private val attestVersionMap = mapOf( + Build.VERSION_CODES.Q to 4, // Keymaster 4.1 + Build.VERSION_CODES.R to 4, // Keymaster 4.1 + Build.VERSION_CODES.S to 100, // KeyMint 1.0 + Build.VERSION_CODES.S_V2 to 100, // KeyMint 1.0 + Build.VERSION_CODES.TIRAMISU to 200, // KeyMint 2.0 + Build.VERSION_CODES.UPSIDE_DOWN_CAKE to 300, // KeyMint 3.0 + Build.VERSION_CODES.VANILLA_ICE_CREAM to 300, // KeyMint 3.0 + Build.VERSION_CODES.BAKLAVA to 400 // KeyMint 4.0 + ) + + val attestVersion: Int + get() = CachedAttestData?.attestVersion ?: attestVersionMap[Build.VERSION.SDK_INT] ?: 400 + + val keymasterVersion: Int + get() = CachedAttestData?.keymasterVersion ?: if (attestVersion == 4) 41 else attestVersion + + fun String.convertPatchLevel(isLong: Boolean): Int = runCatching { + val parts = split("-") + when { + isLong && parts.size >= 3 -> parts[0].toInt() * 10000 + parts[1].toInt() * 100 + parts[2].toInt() + parts.size >= 2 -> parts[0].toInt() * 100 + parts[1].toInt() + else -> throw IllegalArgumentException("Invalid patch level format: $this") + } + }.onFailure { + Logger.e("Invalid patch level format: $this", it) + }.getOrDefault(202404) + + val apexInfos: List> by lazy { + runCatching { + val packageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package")) + val packages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getInstalledPackages(PackageManager.MATCH_APEX.toLong(), 0) + } else { + @Suppress("DEPRECATION") + packageManager.getInstalledPackages(PackageManager.MATCH_APEX, 0) + } + + packages.list + .map { it.packageName to it.longVersionCode } + .sortedBy { it.first } + }.getOrElse { + Logger.e("Failed to get APEX package information") + emptyList() + } + } + + val moduleHash: ByteArray by lazy { + runCatching { + val encodables = apexInfos.flatMap { (packageName, versionCode) -> + listOf( + DEROctetString(packageName.toByteArray()), + ASN1Integer(versionCode) + ) + } + + val sequence = DERSequence(encodables.toTypedArray()) + MessageDigest.getInstance("SHA-256").digest(sequence.encoded) + }.getOrElse { + Logger.e("Failed to compute module hash", it) + ByteArray(32) + } + } + } fun String.trimLine(): String = trim().split("\n").joinToString("\n") { it.trim() } diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/AttestUtils.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/AttestUtils.kt new file mode 100644 index 0000000..ccac385 --- /dev/null +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/AttestUtils.kt @@ -0,0 +1,153 @@ +/* + * Copyright 2025 Dakkshesh + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package io.github.beakthoven.TrickyStoreOSS + +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import io.github.beakthoven.TrickyStoreOSS.logging.Logger +import org.bouncycastle.asn1.ASN1Integer +import org.bouncycastle.asn1.ASN1ObjectIdentifier +import org.bouncycastle.asn1.ASN1OctetString +import org.bouncycastle.asn1.ASN1Sequence +import org.bouncycastle.asn1.ASN1TaggedObject +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.cert.X509CertificateHolder +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.SecureRandom +import java.security.cert.X509Certificate +import java.security.spec.ECGenParameterSpec + +val ATTESTATION_OID = ASN1ObjectIdentifier("1.3.6.1.4.1.11129.2.1.17") + +object AttestUtils { + data class AttestationData( + val verifiedBootHash: ByteArray?, + val attestVersion: Int?, + val keymasterVersion: Int?, + val osVersion: Int?, + ) + + val TEEStatus: Boolean by lazy { isTEEWorking() } + val CachedAttestData: AttestationData? by lazy { getAttestData()} + + private val keygen_alias = "TrickyStoreOSS_attest" + + private fun isTEEWorking(): Boolean { + return try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + android.app.ActivityThread.initializeMainlineModules() + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + android.security.keystore2.AndroidKeyStoreProvider.install() + } else { + android.security.keystore.AndroidKeyStoreProvider.install() + } + + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + + val keyPairGenerator = KeyPairGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore") + + val challenge = ByteArray(16).apply { + SecureRandom().nextBytes(this) + } + + val parameterSpec = KeyGenParameterSpec.Builder( + keygen_alias, + KeyProperties.PURPOSE_SIGN + ) + .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) + .setDigests(KeyProperties.DIGEST_SHA256) + .setAttestationChallenge(challenge) + .setIsStrongBoxBacked(false) + .build() + + keyPairGenerator.initialize(parameterSpec) + keyPairGenerator.generateKeyPair() + + Logger.d("TEE check: successful") + + // keyStore.deleteEntry(keygen_alias) + true + } catch (e: Exception) { + Logger.w("TEE check failure: ${e.message}") + false + } + } + + private fun getAttestCert(): X509Certificate? { + return if (TEEStatus) { + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + + val certChain = keyStore.getCertificateChain(keygen_alias) + if (certChain == null || certChain.isEmpty()) { + null + } else { + keyStore.deleteEntry(keygen_alias) + certChain[0] as X509Certificate + } + } else { + null + } + } + + private fun getAttestData(): AttestationData? { + val leaf: X509Certificate = getAttestCert() ?: return null + + return try { + val leafHolder = X509CertificateHolder(leaf.encoded) + val ext: Extension = leafHolder.getExtension(ATTESTATION_OID) ?: run { + Logger.i("No attestation extension found on certificate") + return null + } + + val keyDescriptionSeq = ASN1Sequence.getInstance(ext.extnValue.octets) + val encodables = keyDescriptionSeq.toArray() + + val attestVersion = ASN1Integer.getInstance(encodables[0]).value.intValueExact() + val keymasterVersion = ASN1Integer.getInstance(encodables[2]).value.intValueExact() + var attestVerifiedBootHash: ByteArray? = null + var attestOSVersion: Int? = null + + val teeEnforced = ASN1Sequence.getInstance(encodables[7]) + + teeEnforced.forEach { element -> + val tagged = element as ASN1TaggedObject + when (tagged.tagNo) { + 704 -> { // Parse Root of Trust + val rootOfTrustSeq = ASN1Sequence.getInstance(tagged.baseObject.toASN1Primitive()) + if (rootOfTrustSeq.size() >= 4) { + attestVerifiedBootHash = ASN1OctetString.getInstance(rootOfTrustSeq.getObjectAt(3)).octets + } + } + 705 -> { // Parse OS Version + attestOSVersion = ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive()).value.intValueExact() + } + } + } + + Logger.i("Extracted attestationVersion: $attestVersion") + Logger.i("Extracted keymasterVersion: $keymasterVersion") + Logger.i("Extracted verifiedBootHash: ${attestVerifiedBootHash?.toHex() ?: 0}") + Logger.i("Extracted osVersion: $attestOSVersion") + + AttestationData( + verifiedBootHash = attestVerifiedBootHash, + attestVersion = attestVersion, + keymasterVersion = keymasterVersion, + osVersion = attestOSVersion + ) + } catch (e: Exception) { + Logger.e("Failed to parse attestation data", e) + null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateGen.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateGen.kt new file mode 100644 index 0000000..3bdd92d --- /dev/null +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateGen.kt @@ -0,0 +1,471 @@ +/* + * Copyright 2025 Dakkshesh + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package io.github.beakthoven.TrickyStoreOSS + +import android.content.pm.PackageManager +import android.hardware.security.keymint.Algorithm +import android.hardware.security.keymint.EcCurve +import android.hardware.security.keymint.KeyParameter +import android.hardware.security.keymint.Tag +import android.os.Build +import android.security.keystore.KeyProperties +import android.system.keystore2.KeyDescriptor +import android.util.Pair +import io.github.beakthoven.TrickyStoreOSS.config.PkgConfig +import io.github.beakthoven.TrickyStoreOSS.interceptors.SecurityLevelInterceptor +import io.github.beakthoven.TrickyStoreOSS.logging.Logger +import org.bouncycastle.asn1.ASN1Boolean +import org.bouncycastle.asn1.ASN1Encodable +import org.bouncycastle.asn1.ASN1Enumerated +import org.bouncycastle.asn1.ASN1Integer +import org.bouncycastle.asn1.ASN1OctetString +import org.bouncycastle.asn1.DERNull +import org.bouncycastle.asn1.DEROctetString +import org.bouncycastle.asn1.DERSequence +import org.bouncycastle.asn1.DERSet +import org.bouncycastle.asn1.DERTaggedObject +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.KeyUsage +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.openssl.PEMKeyPair +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import java.math.BigInteger +import java.nio.charset.StandardCharsets +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.MessageDigest +import java.security.Security +import java.security.cert.Certificate +import java.security.cert.X509Certificate +import java.security.spec.ECGenParameterSpec +import java.security.spec.RSAKeyGenParameterSpec +import java.util.Date +import javax.security.auth.x500.X500Principal + +object CertificateGen { + data class KeyBox( + val pemKeyPair: PEMKeyPair, + val keyPair: KeyPair, + val certificates: List + ) + + private data class Digest(val digest: ByteArray) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Digest + return digest.contentEquals(other.digest) + } + + override fun hashCode(): Int = digest.contentHashCode() + } + + data class KeyGenParameters( + var keySize: Int = 0, + var algorithm: Int = 0, + var certificateSerial: BigInteger? = null, + var certificateNotBefore: Date? = null, + var certificateNotAfter: Date? = null, + var certificateSubject: X500Name? = null, + var rsaPublicExponent: BigInteger? = null, + var ecCurve: Int = 0, + var ecCurveName: String? = null, + var purpose: MutableList = mutableListOf(), + var digest: MutableList = mutableListOf(), + var attestationChallenge: ByteArray? = null, + var brand: ByteArray? = null, + var device: ByteArray? = null, + var product: ByteArray? = null, + var manufacturer: ByteArray? = null, + var model: ByteArray? = null, + var imei1: ByteArray? = null, + var imei2: ByteArray? = null, + var meid: ByteArray? = null, + var serialno: ByteArray? = null + ) { + + constructor(params: Array) : this() { + params.forEach { param -> + Logger.d("Processing key parameter: ${param.tag}") + val value = param.value + + when (param.tag) { + Tag.KEY_SIZE -> keySize = value.integer + Tag.ALGORITHM -> algorithm = value.algorithm + Tag.CERTIFICATE_SERIAL -> certificateSerial = BigInteger(value.blob) + Tag.CERTIFICATE_NOT_BEFORE -> certificateNotBefore = Date(value.dateTime) + Tag.CERTIFICATE_NOT_AFTER -> certificateNotAfter = Date(value.dateTime) + Tag.CERTIFICATE_SUBJECT -> certificateSubject = X500Name(X500Principal(value.blob).name) + Tag.RSA_PUBLIC_EXPONENT -> rsaPublicExponent = BigInteger(value.blob) + Tag.EC_CURVE -> { + ecCurve = value.ecCurve + ecCurveName = getEcCurveName(ecCurve) + } + Tag.PURPOSE -> purpose.add(value.keyPurpose) + Tag.DIGEST -> digest.add(value.digest) + Tag.ATTESTATION_CHALLENGE -> attestationChallenge = value.blob + Tag.ATTESTATION_ID_BRAND -> brand = value.blob + Tag.ATTESTATION_ID_DEVICE -> device = value.blob + Tag.ATTESTATION_ID_PRODUCT -> product = value.blob + Tag.ATTESTATION_ID_MANUFACTURER -> manufacturer = value.blob + Tag.ATTESTATION_ID_MODEL -> model = value.blob + Tag.ATTESTATION_ID_IMEI -> imei1 = value.blob + Tag.ATTESTATION_ID_SECOND_IMEI -> imei2 = value.blob + Tag.ATTESTATION_ID_MEID -> meid = value.blob + } + } + // Fallback: if no EC curve tag but we know key size + if (ecCurveName == null && keySize != 0) { + ecCurveName = ecCurveMapKeySize(keySize) + } + } + + private fun ecCurveMapKeySize(curveSize: Int): String = when (curveSize) { + 224 -> "secp224r1" + 256 -> "secp256r1" + 384 -> "secp384r1" + 521 -> "secp521r1" + else -> "secp256r1" // default fallback + } + + + private fun getEcCurveName(curve: Int): String = when (curve) { + EcCurve.CURVE_25519 -> "CURVE_25519" + EcCurve.P_224 -> "secp224r1" + EcCurve.P_256 -> "secp256r1" + EcCurve.P_384 -> "secp384r1" + EcCurve.P_521 -> "secp521r1" + else -> throw IllegalArgumentException("Unknown EC curve: $curve") + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as KeyGenParameters + + return keySize == other.keySize && + algorithm == other.algorithm && + certificateSerial == other.certificateSerial && + certificateNotBefore == other.certificateNotBefore && + certificateNotAfter == other.certificateNotAfter && + certificateSubject == other.certificateSubject && + rsaPublicExponent == other.rsaPublicExponent && + ecCurve == other.ecCurve && + ecCurveName == other.ecCurveName && + purpose == other.purpose && + digest == other.digest && + attestationChallenge.contentEquals(other.attestationChallenge) && + brand.contentEquals(other.brand) && + device.contentEquals(other.device) && + product.contentEquals(other.product) && + manufacturer.contentEquals(other.manufacturer) && + model.contentEquals(other.model) && + imei1.contentEquals(other.imei1) && + imei2.contentEquals(other.imei2) && + meid.contentEquals(other.meid) && + serialno.contentEquals(other.serialno) + } + } + + fun generateChain(uid: Int, params: KeyGenParameters, keyPair: KeyPair, securityLevel: Int = 1): List? = runCatching { + val keybox = getKeyboxForAlgorithm(params.algorithm) ?: return null + + val issuer = X509CertificateHolder(keybox.certificates[0].encoded).subject + val leaf = buildCertificate(keyPair, keybox, params, issuer, uid, securityLevel) + + val chain = buildList { + add(leaf) + addAll(keybox.certificates) + } + + CertificateUtils.run { chain.toByteArrayList() } + }.onFailure { + Logger.e("Failed to generate certificate chain", it) + }.getOrNull() + + fun generateKeyPair(params: KeyGenParameters): KeyPair? = runCatching { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) + Security.addProvider(BouncyCastleProvider()) + + val (keyPairGenerator, spec) = when (params.algorithm) { + Algorithm.EC -> { + Logger.d("Generating EC keypair of size ${params.keySize}") + val spec = ECGenParameterSpec(params.ecCurveName) + val kpg = KeyPairGenerator.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME) + kpg to spec + } + Algorithm.RSA -> { + Logger.d("Generating RSA keypair of size ${params.keySize}") + val spec = RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent) + val kpg = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME) + kpg to spec + } + else -> { + throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}") + } + } + + keyPairGenerator.initialize(spec) + keyPairGenerator.generateKeyPair() + }.onFailure { + Logger.e("Failed to generate key pair", it) + }.getOrNull() + + fun generateKeyPair( + uid: Int, + descriptor: KeyDescriptor, + attestKeyDescriptor: KeyDescriptor?, + params: KeyGenParameters, + securityLevel: Int = 1 + ): Pair>? = runCatching { + Logger.i("Requested KeyPair with alias: ${descriptor.alias}") + + val hasAttestKey = attestKeyDescriptor != null + if (hasAttestKey) { + Logger.i("Requested KeyPair with attestKey: ${attestKeyDescriptor?.alias}") + } + + val keyPair = generateKeyPair(params) ?: return null + val keybox = getKeyboxForAlgorithm(params.algorithm) ?: return null + + val (signingKeyPair, issuer) = if (hasAttestKey) { + getAttestationKeyInfo(uid, attestKeyDescriptor!!)?.let { + it.first to it.second + } ?: (keybox.keyPair to X509CertificateHolder(keybox.certificates[0].encoded).subject) + } else { + keybox.keyPair to X509CertificateHolder(keybox.certificates[0].encoded).subject + } + + val leaf = buildCertificate(keyPair, keybox, params, issuer, uid, securityLevel, signingKeyPair) + val chain = buildList { + add(leaf) + if (!hasAttestKey) { + addAll(keybox.certificates) + } + } + + Logger.d("Successfully generated certificate for alias: ${descriptor.alias}") + Pair(keyPair, chain) + }.onFailure { + Logger.e("Failed to generate key pair with certificates", it) + }.getOrNull() + + private fun mapAlgorithmToName(algorithm: Int): String? = when (algorithm) { + Algorithm.EC -> KeyProperties.KEY_ALGORITHM_EC + Algorithm.RSA -> KeyProperties.KEY_ALGORITHM_RSA + else -> { + Logger.e("Unsupported algorithm: $algorithm") + null + } + } + + private fun getKeyboxForAlgorithm(algorithm: Int): KeyBox? { + val algorithmName = mapAlgorithmToName(algorithm) ?: return null + return KeyBoxUtils.keyboxes[algorithmName] + } + + private fun getAttestationKeyInfo(uid: Int, attestKeyDescriptor: KeyDescriptor): Pair? { + Logger.d("Looking for attestation key: uid=$uid alias=${attestKeyDescriptor.alias}") + + val keyInfo = SecurityLevelInterceptor.getKeyPairs(uid, attestKeyDescriptor.alias) + return if (keyInfo != null) { + val issuer = X509CertificateHolder(keyInfo.second[0].encoded).subject + Pair(keyInfo.first, issuer) + } else { + Logger.e("Attestation key info not found, falling back to default keybox") + null + } + } + + private fun buildCertificate( + keyPair: KeyPair, + keybox: KeyBox, + params: KeyGenParameters, + issuer: X500Name, + uid: Int, + securityLevel: Int = 1, + signingKeyPair: KeyPair = keybox.keyPair + ): Certificate { + val builder = JcaX509v3CertificateBuilder( + issuer, + params.certificateSerial ?: BigInteger.ONE, + params.certificateNotBefore ?: Date(), + params.certificateNotAfter ?: (keybox.certificates[0] as X509Certificate).notAfter, + params.certificateSubject ?: X500Name("CN=Android KeyStore Key"), + keyPair.public + ) + + builder.addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.keyCertSign)) + builder.addExtension(buildAttestExtension(params, uid, securityLevel)) + + val signerAlgorithm = when (params.algorithm) { + Algorithm.EC -> "SHA256withECDSA" + Algorithm.RSA -> "SHA256withRSA" + else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}") + } + val contentSigner = JcaContentSignerBuilder(signerAlgorithm).build(signingKeyPair.private) + + return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner)) + } + + private fun buildAttestExtension(params: KeyGenParameters, uid: Int, securityLevel: Int = 1): Extension { + try { + val key = AndroidUtils.bootKey + val hash = AndroidUtils.getBootHashFromProp() + + Logger.d("Using boothash ${hash?.toHex() ?: 0}") + + val rootOfTrustEncodables = arrayOf( + DEROctetString(key), + ASN1Boolean.TRUE, + ASN1Enumerated(0), + DEROctetString(hash) + ) + val rootOfTrustSeq = DERSequence(rootOfTrustEncodables) + + val purpose = DERSet(params.purpose.map { ASN1Integer(it.toLong()) }.toTypedArray()) + val algorithm = ASN1Integer(params.algorithm.toLong()) + val keySize = ASN1Integer(params.keySize.toLong()) + val digest = DERSet(params.digest.map { ASN1Integer(it.toLong()) }.toTypedArray()) + val ecCurve = ASN1Integer(params.ecCurve.toLong()) + val noAuthRequired = DERNull.INSTANCE + + val osVersion = ASN1Integer(AndroidUtils.osVersion.toLong()) + val osPatchLevel = ASN1Integer(AndroidUtils.patchLevel.toLong()) + val applicationID = createApplicationId(uid) + val bootPatchLevel = ASN1Integer(AndroidUtils.bootPatchLevelLong.toLong()) + val vendorPatchLevel = ASN1Integer(AndroidUtils.vendorPatchLevelLong.toLong()) + val creationDateTime = ASN1Integer(System.currentTimeMillis()) + val origin = ASN1Integer(0L) + val moduleHash = DEROctetString(AndroidUtils.moduleHash) + + val teeEnforcedObjects = mutableListOf( + DERTaggedObject(true, 1, purpose), + DERTaggedObject(true, 2, algorithm), + DERTaggedObject(true, 3, keySize), + DERTaggedObject(true, 5, digest), + DERTaggedObject(true, 10, ecCurve), + DERTaggedObject(true, 503, noAuthRequired), + DERTaggedObject(true, 702, origin), + DERTaggedObject(true, 704, rootOfTrustSeq), + DERTaggedObject(true, 705, osVersion), + DERTaggedObject(true, 706, osPatchLevel), + DERTaggedObject(true, 718, vendorPatchLevel), + DERTaggedObject(true, 719, bootPatchLevel), + ) + + if (AndroidUtils.attestVersion >= 400) { + teeEnforcedObjects.add(DERTaggedObject(true, 724, moduleHash)) + } + + params.brand?.let { teeEnforcedObjects.add(DERTaggedObject(true, 710, DEROctetString(it))) } + params.device?.let { teeEnforcedObjects.add(DERTaggedObject(true, 711, DEROctetString(it))) } + params.product?.let { teeEnforcedObjects.add(DERTaggedObject(true, 712, DEROctetString(it))) } + params.manufacturer?.let { teeEnforcedObjects.add(DERTaggedObject(true, 716, DEROctetString(it))) } + params.model?.let { teeEnforcedObjects.add(DERTaggedObject(true, 717, DEROctetString(it))) } + + params.serialno?.let { teeEnforcedObjects.add(DERTaggedObject(true, 713, DEROctetString(it))) } + params.imei1?.let { teeEnforcedObjects.add(DERTaggedObject(true, 714, DEROctetString(it))) } + params.meid?.let { teeEnforcedObjects.add(DERTaggedObject(true, 715, DEROctetString(it))) } + + if (AndroidUtils.attestVersion >= 300) { + params.imei2?.let { teeEnforcedObjects.add(DERTaggedObject(true, 723, DEROctetString(it))) } + } + + teeEnforcedObjects.sortBy { it.tagNo } + + val softwareEnforcedObjects = arrayOf( + DERTaggedObject(true, 709, applicationID), + DERTaggedObject(true, 701, creationDateTime) + ) + + return Extension( + ATTESTATION_OID, + false, + buildKeyDescriptionOctet(teeEnforcedObjects.toTypedArray(), softwareEnforcedObjects, params, securityLevel) + ) + } catch (t: Throwable) { + Logger.e("Failed to create attestation extension", t) + throw t + } + } + + private fun buildKeyDescriptionOctet( + teeEnforcedEncodables: Array, + softwareEnforcedEncodables: Array, + params: KeyGenParameters, + securityLevel: Int = 1 + ): ASN1OctetString { + val attestationVersion = ASN1Integer(AndroidUtils.attestVersion.toLong()) + val attestationSecurityLevel = ASN1Enumerated(securityLevel) + val keymasterVersion = ASN1Integer(AndroidUtils.keymasterVersion.toLong()) + val keymasterSecurityLevel = ASN1Enumerated(securityLevel) + val attestationChallenge = DEROctetString(params.attestationChallenge ?: ByteArray(0)) + val uniqueId = DEROctetString(ByteArray(0)) + val softwareEnforced = DERSequence(softwareEnforcedEncodables) + val teeEnforced = DERSequence(teeEnforcedEncodables) + + val keyDescriptionEncodables = arrayOf( + attestationVersion, + attestationSecurityLevel, + keymasterVersion, + keymasterSecurityLevel, + attestationChallenge, + uniqueId, + softwareEnforced, + teeEnforced + ) + + val keyDescriptionSeq = DERSequence(keyDescriptionEncodables) + return DEROctetString(keyDescriptionSeq.encoded) + } + + @Throws(Throwable::class) + private fun createApplicationId(uid: Int): DEROctetString { + val pm = PkgConfig.getPm() ?: throw IllegalStateException("PackageManager not found!") + val packages = pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid") + + val messageDigest = MessageDigest.getInstance("SHA-256") + val signatures = mutableSetOf() + + val packageInfos = packages.map { packageName -> + val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES.toLong(), uid / 100000) + } else { + pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, uid / 100000) + } + + info.signingInfo?.signingCertificateHistory?.forEach { signature -> + signatures.add(Digest(messageDigest.digest(signature.toByteArray()))) + } + + info + } + + val packageInfoArray = packageInfos.map { info -> + DERSequence( + arrayOf( + DEROctetString(info.packageName.toByteArray(StandardCharsets.UTF_8)), + ASN1Integer(info.longVersionCode) + ) + ) + }.toTypedArray() + + val signaturesArray = signatures.map { DEROctetString(it.digest) }.toTypedArray() + + val applicationIdArray = arrayOf( + DERSet(packageInfoArray), + DERSet(signaturesArray) + ) + + return DEROctetString(DERSequence(applicationIdArray).encoded) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateHack.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateHack.kt new file mode 100644 index 0000000..97e6017 --- /dev/null +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateHack.kt @@ -0,0 +1,241 @@ +/* + * Copyright 2025 Dakkshesh + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package io.github.beakthoven.TrickyStoreOSS + +import io.github.beakthoven.TrickyStoreOSS.logging.Logger +import org.bouncycastle.asn1.ASN1Boolean +import org.bouncycastle.asn1.ASN1Encodable +import org.bouncycastle.asn1.ASN1EncodableVector +import org.bouncycastle.asn1.ASN1Enumerated +import org.bouncycastle.asn1.ASN1Integer +import org.bouncycastle.asn1.ASN1Sequence +import org.bouncycastle.asn1.ASN1TaggedObject +import org.bouncycastle.asn1.DEROctetString +import org.bouncycastle.asn1.DERSequence +import org.bouncycastle.asn1.DERTaggedObject +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import java.io.ByteArrayInputStream +import java.security.cert.Certificate +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.util.LinkedList +import java.util.concurrent.ConcurrentHashMap + +object CertificateHack { + private val certificateFactory: CertificateFactory by lazy { + try { + CertificateFactory.getInstance("X.509") + } catch (t: Throwable) { + Logger.e("Failed to initialize certificate factory", t) + throw RuntimeException("Cannot initialize certificate factory", t) + } + } + + data class KeyIdentifier( + val alias: String, + val uid: Int + ) + + val leafAlgorithms = ConcurrentHashMap() + + fun clearLeafAlgorithms() { + leafAlgorithms.clear() + } + + fun hackCertificateChain(certificateChain: Array?): Array { + if (certificateChain == null) { + throw UnsupportedOperationException("Certificate chain is null!") + } + + return try { + val leaf = certificateFactory.generateCertificate( + ByteArrayInputStream(certificateChain[0].encoded) + ) as X509Certificate + + val extensionBytes = leaf.getExtensionValue(ATTESTATION_OID.id) + ?: return certificateChain // No attestation extension, return original + + val leafHolder = X509CertificateHolder(leaf.encoded) + val extension = leafHolder.getExtension(ATTESTATION_OID) + val sequence = ASN1Sequence.getInstance(extension.extnValue.octets) + val encodables = sequence.toArray() + val teeEnforced = encodables[7] as ASN1Sequence + + val vector = ASN1EncodableVector() + var rootOfTrust: ASN1Encodable? = null + + teeEnforced.forEach { element -> + val taggedObject = element as ASN1TaggedObject + if (taggedObject.tagNo == 704) { + rootOfTrust = taggedObject.baseObject.toASN1Primitive() + } else { + vector.add(taggedObject) + } + } + + val keybox = KeyBoxUtils.keyboxes[leaf.publicKey.algorithm] + ?: throw UnsupportedOperationException("Unsupported algorithm: ${leaf.publicKey.algorithm}") + + val certificates = LinkedList(keybox.certificates) + val builder = X509v3CertificateBuilder( + X509CertificateHolder(certificates[0].encoded).subject, + leafHolder.serialNumber, + leafHolder.notBefore, + leafHolder.notAfter, + leafHolder.subject, + leafHolder.subjectPublicKeyInfo + ) + + val signer = JcaContentSignerBuilder(leaf.sigAlgName).build(keybox.keyPair.private) + + val hackedExtension = hackAttestExtension(rootOfTrust, vector, encodables) + builder.addExtension(hackedExtension) + + leafHolder.extensions.extensionOIDs.forEach { oid -> + if (oid.id != ATTESTATION_OID.id) { + builder.addExtension(leafHolder.getExtension(oid)) + } + } + + certificates.addFirst(JcaX509CertificateConverter().getCertificate(builder.build(signer))) + certificates.toTypedArray() + } catch (t: Throwable) { + Logger.e("Failed to hack certificate chain", t) + certificateChain + } + } + + fun hackCACertificateChain(caList: ByteArray?, alias: String, uid: Int): ByteArray { + if (caList == null) { + throw UnsupportedOperationException("CA list is null!") + } + + return try { + val key = KeyIdentifier(alias, uid) + val algorithm = leafAlgorithms.remove(key) + ?: throw UnsupportedOperationException("No algorithm found for key $key") + + val keybox = KeyBoxUtils.keyboxes[algorithm] + ?: throw UnsupportedOperationException("Unsupported algorithm: $algorithm") + + CertificateUtils.run { keybox.certificates.toByteArray() } ?: caList + } catch (t: Throwable) { + Logger.e("Failed to hack CA certificate chain", t) + caList + } + } + + fun hackUserCertificate(certificate: ByteArray?, alias: String, uid: Int): ByteArray { + if (certificate == null) { + throw UnsupportedOperationException("Leaf certificate is null!") + } + + return try { + val leaf = certificateFactory.generateCertificate( + ByteArrayInputStream(certificate) + ) as X509Certificate + + val extensionBytes = leaf.getExtensionValue(ATTESTATION_OID.id) + ?: return certificate // No attestation extension, return original + + val keyIdentifier = KeyIdentifier(alias, uid) + leafAlgorithms[keyIdentifier] = leaf.publicKey.algorithm + + val leafHolder = X509CertificateHolder(leaf.encoded) + val extension = leafHolder.getExtension(ATTESTATION_OID) + val sequence = ASN1Sequence.getInstance(extension.extnValue.octets) + val encodables = sequence.toArray() + val teeEnforced = encodables[7] as ASN1Sequence + + val vector = ASN1EncodableVector() + var rootOfTrust: ASN1Encodable? = null + + teeEnforced.forEach { element -> + val taggedObject = element as ASN1TaggedObject + if (taggedObject.tagNo == 704) { + rootOfTrust = taggedObject.baseObject.toASN1Primitive() + } else { + vector.add(taggedObject) + } + } + + val keybox = KeyBoxUtils.keyboxes[leaf.publicKey.algorithm] + ?: throw UnsupportedOperationException("Unsupported algorithm: ${leaf.publicKey.algorithm}") + + val builder = X509v3CertificateBuilder( + X509CertificateHolder(keybox.certificates[0].encoded).subject, + leafHolder.serialNumber, + leafHolder.notBefore, + leafHolder.notAfter, + leafHolder.subject, + leafHolder.subjectPublicKeyInfo + ) + + val signer = JcaContentSignerBuilder(leaf.sigAlgName).build(keybox.keyPair.private) + + val hackedExtension = hackAttestExtension(rootOfTrust, vector, encodables) + builder.addExtension(hackedExtension) + + leafHolder.extensions.extensionOIDs.forEach { oid -> + if (oid.id != ATTESTATION_OID.id) { + builder.addExtension(leafHolder.getExtension(oid)) + } + } + + JcaX509CertificateConverter().getCertificate(builder.build(signer)).encoded + } catch (t: Throwable) { + Logger.e("Failed to hack user certificate", t) + certificate + } + } + + private fun hackAttestExtension( + originalRootOfTrust: ASN1Encodable?, + vector: ASN1EncodableVector, + originalEncodables: Array + ): Extension { + val verifiedBootKey = AndroidUtils.bootKey + var verifiedBootHash: ByteArray? = null + + try { + if (originalRootOfTrust is ASN1Sequence) { + verifiedBootHash = CertificateUtils.getByteArrayFromAsn1(originalRootOfTrust.getObjectAt(3)) + } + } catch (t: Throwable) { + Logger.e("Failed to get verified boot hash from original, using generated", t) + } + + if (verifiedBootHash == null) { + verifiedBootHash = AndroidUtils.getBootHashFromProp() + } + + val rootOfTrustElements = arrayOf( + DEROctetString(verifiedBootKey), + ASN1Boolean.TRUE, + ASN1Enumerated(0), + DEROctetString(verifiedBootHash) + ) + val hackedRootOfTrust = DERSequence(rootOfTrustElements) + + vector.add(DERTaggedObject(true, 718, ASN1Integer(AndroidUtils.vendorPatchLevelLong.toLong()))) + vector.add(DERTaggedObject(true, 719, ASN1Integer(AndroidUtils.bootPatchLevelLong.toLong()))) + vector.add(DERTaggedObject(true, 706, ASN1Integer(AndroidUtils.patchLevel.toLong()))) + vector.add(DERTaggedObject(true, 705, ASN1Integer(AndroidUtils.osVersion.toLong()))) + vector.add(DERTaggedObject(704, hackedRootOfTrust)) + + val hackEnforced = DERSequence(vector) + originalEncodables[7] = hackEnforced + val hackedSequence = DERSequence(originalEncodables) + val hackedSequenceOctets = DEROctetString(hackedSequence) + + return Extension(ATTESTATION_OID, false, hackedSequenceOctets) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateHacker.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateHacker.kt deleted file mode 100644 index ac5efa2..0000000 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateHacker.kt +++ /dev/null @@ -1,984 +0,0 @@ -/* - * Copyright 2025 Dakkshesh - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package io.github.beakthoven.TrickyStoreOSS - -import android.content.pm.PackageManager -import android.hardware.security.keymint.Algorithm -import android.hardware.security.keymint.EcCurve -import android.hardware.security.keymint.KeyParameter -import android.hardware.security.keymint.Tag -import android.os.Build -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyProperties -import android.system.keystore2.KeyDescriptor -import android.util.Pair -import io.github.beakthoven.TrickyStoreOSS.core.config.Config -import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger -import io.github.beakthoven.TrickyStoreOSS.interceptors.SecurityLevelInterceptor -import org.bouncycastle.asn1.ASN1Boolean -import org.bouncycastle.asn1.ASN1Encodable -import org.bouncycastle.asn1.ASN1EncodableVector -import org.bouncycastle.asn1.ASN1Enumerated -import org.bouncycastle.asn1.ASN1Integer -import org.bouncycastle.asn1.ASN1ObjectIdentifier -import org.bouncycastle.asn1.ASN1OctetString -import org.bouncycastle.asn1.ASN1Sequence -import org.bouncycastle.asn1.ASN1TaggedObject -import org.bouncycastle.asn1.DERNull -import org.bouncycastle.asn1.DEROctetString -import org.bouncycastle.asn1.DERSequence -import org.bouncycastle.asn1.DERSet -import org.bouncycastle.asn1.DERTaggedObject -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.Extension -import org.bouncycastle.asn1.x509.KeyUsage -import org.bouncycastle.cert.X509CertificateHolder -import org.bouncycastle.cert.X509v3CertificateBuilder -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter -import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.bouncycastle.openssl.PEMKeyPair -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder -import java.io.ByteArrayInputStream -import java.math.BigInteger -import java.nio.charset.StandardCharsets -import java.security.KeyPair -import java.security.KeyPairGenerator -import java.security.KeyStore -import java.security.MessageDigest -import java.security.SecureRandom -import java.security.Security -import java.security.cert.Certificate -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.security.spec.ECGenParameterSpec -import java.security.spec.RSAKeyGenParameterSpec -import java.util.Date -import java.util.LinkedList -import java.util.concurrent.ConcurrentHashMap -import javax.security.auth.x500.X500Principal - -object CertificateHacker { - - private val ATTESTATION_OID = ASN1ObjectIdentifier("1.3.6.1.4.1.11129.2.1.17") - - private val certificateFactory: CertificateFactory by lazy { - try { - CertificateFactory.getInstance("X.509") - } catch (t: Throwable) { - Logger.e("Failed to initialize certificate factory", t) - throw RuntimeException("Cannot initialize certificate factory", t) - } - } - - data class KeyBox( - val pemKeyPair: PEMKeyPair, - val keyPair: KeyPair, - val certificates: List - ) - - data class KeyIdentifier( - val alias: String, - val uid: Int - ) - - sealed class ParseResult { - data class Success(val data: T) : ParseResult() - data class Error(val message: String, val cause: Throwable? = null) : ParseResult() - } - - sealed class HackResult { - data class Success(val data: T) : HackResult() - data class Error(val message: String, val cause: Throwable? = null) : HackResult() - } - - data class KeyGenParameters( - var keySize: Int = 0, - var algorithm: Int = 0, - var certificateSerial: BigInteger? = null, - var certificateNotBefore: Date? = null, - var certificateNotAfter: Date? = null, - var certificateSubject: X500Name? = null, - var rsaPublicExponent: BigInteger? = null, - var ecCurve: Int = 0, - var ecCurveName: String? = null, - var purpose: MutableList = mutableListOf(), - var digest: MutableList = mutableListOf(), - var attestationChallenge: ByteArray? = null, - var brand: ByteArray? = null, - var device: ByteArray? = null, - var product: ByteArray? = null, - var manufacturer: ByteArray? = null, - var model: ByteArray? = null, - var imei1: ByteArray? = null, - var imei2: ByteArray? = null, - var meid: ByteArray? = null, - var serialno: ByteArray? = null - ) { - - constructor(params: Array) : this() { - parseKeyParameters(params) - } - - private fun parseKeyParameters(params: Array) { - params.forEach { param -> - Logger.d("Processing key parameter: ${param.tag}") - val value = param.value - - when (param.tag) { - Tag.KEY_SIZE -> keySize = value.integer - Tag.ALGORITHM -> algorithm = value.algorithm - Tag.CERTIFICATE_SERIAL -> certificateSerial = BigInteger(value.blob) - Tag.CERTIFICATE_NOT_BEFORE -> certificateNotBefore = Date(value.dateTime) - Tag.CERTIFICATE_NOT_AFTER -> certificateNotAfter = Date(value.dateTime) - Tag.CERTIFICATE_SUBJECT -> certificateSubject = X500Name(X500Principal(value.blob).name) - Tag.RSA_PUBLIC_EXPONENT -> rsaPublicExponent = BigInteger(value.blob) - Tag.EC_CURVE -> { - ecCurve = value.ecCurve - ecCurveName = getEcCurveName(ecCurve) - } - Tag.PURPOSE -> purpose.add(value.keyPurpose) - Tag.DIGEST -> digest.add(value.digest) - Tag.ATTESTATION_CHALLENGE -> attestationChallenge = value.blob - Tag.ATTESTATION_ID_BRAND -> brand = value.blob - Tag.ATTESTATION_ID_DEVICE -> device = value.blob - Tag.ATTESTATION_ID_PRODUCT -> product = value.blob - Tag.ATTESTATION_ID_MANUFACTURER -> manufacturer = value.blob - Tag.ATTESTATION_ID_MODEL -> model = value.blob - Tag.ATTESTATION_ID_IMEI -> imei1 = value.blob - Tag.ATTESTATION_ID_SECOND_IMEI -> imei2 = value.blob - Tag.ATTESTATION_ID_MEID -> meid = value.blob - } - } - } - - fun setEcCurveName(curveSize: Int) { - ecCurveName = when (curveSize) { - 224 -> "secp224r1" - 256 -> "secp256r1" - 384 -> "secp384r1" - 521 -> "secp521r1" - else -> "secp256r1" - } - } - - companion object { - private fun getEcCurveName(curve: Int): String = when (curve) { - EcCurve.CURVE_25519 -> "CURVE_25519" - EcCurve.P_224 -> "secp224r1" - EcCurve.P_256 -> "secp256r1" - EcCurve.P_384 -> "secp384r1" - EcCurve.P_521 -> "secp521r1" - else -> throw IllegalArgumentException("Unknown EC curve: $curve") - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as KeyGenParameters - - return keySize == other.keySize && - algorithm == other.algorithm && - certificateSerial == other.certificateSerial && - certificateNotBefore == other.certificateNotBefore && - certificateNotAfter == other.certificateNotAfter && - certificateSubject == other.certificateSubject && - rsaPublicExponent == other.rsaPublicExponent && - ecCurve == other.ecCurve && - ecCurveName == other.ecCurveName && - purpose == other.purpose && - digest == other.digest && - attestationChallenge.contentEquals(other.attestationChallenge) && - brand.contentEquals(other.brand) && - device.contentEquals(other.device) && - product.contentEquals(other.product) && - manufacturer.contentEquals(other.manufacturer) && - model.contentEquals(other.model) && - imei1.contentEquals(other.imei1) && - imei2.contentEquals(other.imei2) && - meid.contentEquals(other.meid) && - serialno.contentEquals(other.serialno) - } - - override fun hashCode(): Int { - var result = keySize - result = 31 * result + algorithm - result = 31 * result + (certificateSerial?.hashCode() ?: 0) - result = 31 * result + (certificateNotBefore?.hashCode() ?: 0) - result = 31 * result + (certificateNotAfter?.hashCode() ?: 0) - result = 31 * result + (certificateSubject?.hashCode() ?: 0) - result = 31 * result + (rsaPublicExponent?.hashCode() ?: 0) - result = 31 * result + ecCurve - result = 31 * result + (ecCurveName?.hashCode() ?: 0) - result = 31 * result + purpose.hashCode() - result = 31 * result + digest.hashCode() - result = 31 * result + (attestationChallenge?.contentHashCode() ?: 0) - result = 31 * result + (brand?.contentHashCode() ?: 0) - result = 31 * result + (device?.contentHashCode() ?: 0) - result = 31 * result + (product?.contentHashCode() ?: 0) - result = 31 * result + (manufacturer?.contentHashCode() ?: 0) - result = 31 * result + (model?.contentHashCode() ?: 0) - result = 31 * result + (imei1?.contentHashCode() ?: 0) - result = 31 * result + (imei2?.contentHashCode() ?: 0) - result = 31 * result + (meid?.contentHashCode() ?: 0) - result = 31 * result + (serialno?.contentHashCode() ?: 0) - return result - } - } - - private val keyboxes = ConcurrentHashMap() - private val leafAlgorithms = ConcurrentHashMap() - - - - fun hasKeyboxes(): Boolean = keyboxes.isNotEmpty() - - private data class Digest(val digest: ByteArray) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - other as Digest - return digest.contentEquals(other.digest) - } - - override fun hashCode(): Int = digest.contentHashCode() - } - - fun String.sanitizeXml(): String { - var content = this - - val boms = listOf( - "\uFEFF", - "\uFFFE", - "\u0000\uFEFF" - ) - content = content.trimStart() - for (bom in boms) { - content = content.removePrefix(bom) - } - content = content.trimStart() - - return content.trimEnd() - } - - fun readFromXml(xmlData: String?) { - keyboxes.clear() - leafAlgorithms.clear() - - if (xmlData == null) { - Logger.i("Clearing all keyboxes") - return - } - - try { - val xmlParser = XmlParser(xmlData.sanitizeXml()) - - val numberOfKeyboxesResult = xmlParser.obtainPath("AndroidAttestation.NumberOfKeyboxes") - val numberOfKeyboxes = when (numberOfKeyboxesResult) { - is XmlParser.ParseResult.Success -> numberOfKeyboxesResult.attributes["text"]?.toIntOrNull() - ?: throw IllegalArgumentException("Invalid number of keyboxes") - is XmlParser.ParseResult.Error -> throw Exception(numberOfKeyboxesResult.message, numberOfKeyboxesResult.cause) - } - - repeat(numberOfKeyboxes) { i -> - processKeybox(xmlParser, i) - } - - Logger.i("Successfully updated $numberOfKeyboxes keyboxes") - } catch (t: Throwable) { - Logger.e("Error loading XML file (keyboxes cleared)", t) - } - } - - private fun processKeybox(xmlParser: XmlParser, index: Int) { - try { - val algorithmResult = xmlParser.obtainPath("AndroidAttestation.Keybox.Key[$index]") - val keyboxAlgorithm = when (algorithmResult) { - is XmlParser.ParseResult.Success -> algorithmResult.attributes["algorithm"] - ?: throw IllegalArgumentException("Missing algorithm attribute") - is XmlParser.ParseResult.Error -> throw Exception(algorithmResult.message, algorithmResult.cause) - } - - val privateKeyResult = xmlParser.obtainPath("AndroidAttestation.Keybox.Key[$index].PrivateKey") - val privateKeyContent = when (privateKeyResult) { - is XmlParser.ParseResult.Success -> privateKeyResult.attributes["text"] - ?: throw IllegalArgumentException("Missing private key text") - is XmlParser.ParseResult.Error -> throw Exception(privateKeyResult.message, privateKeyResult.cause) - } - - val numberOfCertificatesResult = xmlParser.obtainPath( - "AndroidAttestation.Keybox.Key[$index].CertificateChain.NumberOfCertificates" - ) - val numberOfCertificates = when (numberOfCertificatesResult) { - is XmlParser.ParseResult.Success -> numberOfCertificatesResult.attributes["text"]?.toIntOrNull() - ?: throw IllegalArgumentException("Invalid number of certificates") - is XmlParser.ParseResult.Error -> throw Exception(numberOfCertificatesResult.message, numberOfCertificatesResult.cause) - } - - val certificateChain = mutableListOf() - repeat(numberOfCertificates) { j -> - val certResult = xmlParser.obtainPath( - "AndroidAttestation.Keybox.Key[$index].CertificateChain.Certificate[$j]" - ) - val certContent = when (certResult) { - is XmlParser.ParseResult.Success -> certResult.attributes["text"] - ?: throw IllegalArgumentException("Missing certificate text") - is XmlParser.ParseResult.Error -> throw Exception(certResult.message, certResult.cause) - } - - when (val certParseResult = CertificateUtils.parseCertificate(certContent)) { - is CertificateUtils.ParseResult.Success -> certificateChain.add(certParseResult.data) - is CertificateUtils.ParseResult.Error -> throw Exception(certParseResult.message, certParseResult.cause) - } - } - - val pemKeyPair = when (val keyParseResult = CertificateUtils.parseKeyPair(privateKeyContent)) { - is CertificateUtils.ParseResult.Success -> keyParseResult.data - is CertificateUtils.ParseResult.Error -> throw Exception(keyParseResult.message, keyParseResult.cause) - } - - val keyPair = CertificateUtils.convertPemToKeyPair(pemKeyPair) - - val algorithmName = when (keyboxAlgorithm.lowercase()) { - "ecdsa" -> KeyProperties.KEY_ALGORITHM_EC - "rsa" -> KeyProperties.KEY_ALGORITHM_RSA - else -> keyboxAlgorithm - } - - keyboxes[algorithmName] = KeyBox(pemKeyPair, keyPair, certificateChain) - - } catch (t: Throwable) { - Logger.e("Error processing keybox $index", t) - throw t - } - } - - fun hackCertificateChain(certificateChain: Array?): Array { - if (certificateChain == null) { - throw UnsupportedOperationException("Certificate chain is null!") - } - - return try { - val leaf = certificateFactory.generateCertificate( - ByteArrayInputStream(certificateChain[0].encoded) - ) as X509Certificate - - val extensionBytes = leaf.getExtensionValue(ATTESTATION_OID.id) - ?: return certificateChain // No attestation extension, return original - - val leafHolder = X509CertificateHolder(leaf.encoded) - val extension = leafHolder.getExtension(ATTESTATION_OID) - val sequence = ASN1Sequence.getInstance(extension.extnValue.octets) - val encodables = sequence.toArray() - val teeEnforced = encodables[7] as ASN1Sequence - - val vector = ASN1EncodableVector() - var rootOfTrust: ASN1Encodable? = null - - teeEnforced.forEach { element -> - val taggedObject = element as ASN1TaggedObject - if (taggedObject.tagNo == 704) { - rootOfTrust = taggedObject.baseObject.toASN1Primitive() - } else { - vector.add(taggedObject) - } - } - - val keybox = keyboxes[leaf.publicKey.algorithm] - ?: throw UnsupportedOperationException("Unsupported algorithm: ${leaf.publicKey.algorithm}") - - val certificates = LinkedList(keybox.certificates) - val builder = X509v3CertificateBuilder( - X509CertificateHolder(certificates[0].encoded).subject, - leafHolder.serialNumber, - leafHolder.notBefore, - leafHolder.notAfter, - leafHolder.subject, - leafHolder.subjectPublicKeyInfo - ) - - val signer = JcaContentSignerBuilder(leaf.sigAlgName).build(keybox.keyPair.private) - - val hackedExtension = createHackedAttestationExtension(rootOfTrust, vector, encodables) - builder.addExtension(hackedExtension) - - leafHolder.extensions.extensionOIDs.forEach { oid -> - if (oid.id != ATTESTATION_OID.id) { - builder.addExtension(leafHolder.getExtension(oid)) - } - } - - certificates.addFirst(JcaX509CertificateConverter().getCertificate(builder.build(signer))) - certificates.toTypedArray() - } catch (t: Throwable) { - Logger.e("Failed to hack certificate chain", t) - certificateChain - } - } - - fun hackCACertificateChain(caList: ByteArray?, alias: String, uid: Int): ByteArray { - if (caList == null) { - throw UnsupportedOperationException("CA list is null!") - } - - return try { - val key = KeyIdentifier(alias, uid) - val algorithm = leafAlgorithms.remove(key) - ?: throw UnsupportedOperationException("No algorithm found for key $key") - - val keybox = keyboxes[algorithm] - ?: throw UnsupportedOperationException("Unsupported algorithm: $algorithm") - - CertificateUtils.run { keybox.certificates.toByteArray() } ?: caList - } catch (t: Throwable) { - Logger.e("Failed to hack CA certificate chain", t) - caList - } - } - - fun hackUserCertificate(certificate: ByteArray?, alias: String, uid: Int): ByteArray { - if (certificate == null) { - throw UnsupportedOperationException("Leaf certificate is null!") - } - - return try { - val leaf = certificateFactory.generateCertificate( - ByteArrayInputStream(certificate) - ) as X509Certificate - - val extensionBytes = leaf.getExtensionValue(ATTESTATION_OID.id) - ?: return certificate // No attestation extension, return original - - val keyIdentifier = KeyIdentifier(alias, uid) - leafAlgorithms[keyIdentifier] = leaf.publicKey.algorithm - - val leafHolder = X509CertificateHolder(leaf.encoded) - val extension = leafHolder.getExtension(ATTESTATION_OID) - val sequence = ASN1Sequence.getInstance(extension.extnValue.octets) - val encodables = sequence.toArray() - val teeEnforced = encodables[7] as ASN1Sequence - - val vector = ASN1EncodableVector() - var rootOfTrust: ASN1Encodable? = null - - teeEnforced.forEach { element -> - val taggedObject = element as ASN1TaggedObject - if (taggedObject.tagNo == 704) { - rootOfTrust = taggedObject.baseObject.toASN1Primitive() - } else { - vector.add(taggedObject) - } - } - - val keybox = keyboxes[leaf.publicKey.algorithm] - ?: throw UnsupportedOperationException("Unsupported algorithm: ${leaf.publicKey.algorithm}") - - val builder = X509v3CertificateBuilder( - X509CertificateHolder(keybox.certificates[0].encoded).subject, - leafHolder.serialNumber, - leafHolder.notBefore, - leafHolder.notAfter, - leafHolder.subject, - leafHolder.subjectPublicKeyInfo - ) - - val signer = JcaContentSignerBuilder(leaf.sigAlgName).build(keybox.keyPair.private) - - val hackedExtension = createHackedAttestationExtension(rootOfTrust, vector, encodables) - builder.addExtension(hackedExtension) - - leafHolder.extensions.extensionOIDs.forEach { oid -> - if (oid.id != ATTESTATION_OID.id) { - builder.addExtension(leafHolder.getExtension(oid)) - } - } - - JcaX509CertificateConverter().getCertificate(builder.build(signer)).encoded - } catch (t: Throwable) { - Logger.e("Failed to hack user certificate", t) - certificate - } - } - - fun generateKeyPair(params: KeyGenParameters): KeyPair? = runCatching { - when (params.algorithm) { - Algorithm.EC -> { - Logger.d("Generating EC keypair of size ${params.keySize}") - buildECKeyPair(params) - } - Algorithm.RSA -> { - Logger.d("Generating RSA keypair of size ${params.keySize}") - buildRSAKeyPair(params) - } - else -> { - Logger.e("Unsupported algorithm: ${params.algorithm}") - null - } - } - }.onFailure { - Logger.e("Failed to generate key pair", it) - }.getOrNull() - - fun generateChain(uid: Int, params: KeyGenParameters, keyPair: KeyPair, securityLevel: Int = 1): List? = runCatching { - val keybox = getKeyboxForAlgorithm(params.algorithm) ?: return null - - val issuer = X509CertificateHolder(keybox.certificates[0].encoded).subject - val leaf = buildCertificate(keyPair, keybox, params, issuer, uid, securityLevel) - - val chain = buildList { - add(leaf) - addAll(keybox.certificates) - } - - CertificateUtils.run { chain.toByteArrayList() } - }.onFailure { - Logger.e("Failed to generate certificate chain", it) - }.getOrNull() - - fun generateKeyPair( - uid: Int, - descriptor: KeyDescriptor, - attestKeyDescriptor: KeyDescriptor?, - params: KeyGenParameters, - securityLevel: Int = 1 - ): Pair>? = runCatching { - Logger.i("Requested KeyPair with alias: ${descriptor.alias}") - - val hasAttestKey = attestKeyDescriptor != null - if (hasAttestKey) { - Logger.i("Requested KeyPair with attestKey: ${attestKeyDescriptor?.alias}") - } - - val keyPair = generateKeyPair(params) ?: return null - val keybox = getKeyboxForAlgorithm(params.algorithm) ?: return null - - val (signingKeyPair, issuer) = if (hasAttestKey) { - getAttestationKeyInfo(uid, attestKeyDescriptor!!)?.let { - it.first to it.second - } ?: (keybox.keyPair to X509CertificateHolder(keybox.certificates[0].encoded).subject) - } else { - keybox.keyPair to X509CertificateHolder(keybox.certificates[0].encoded).subject - } - - val leaf = buildCertificate(keyPair, keybox, params, issuer, uid, securityLevel, signingKeyPair) - val chain = buildList { - add(leaf) - if (!hasAttestKey) { - addAll(keybox.certificates) - } - } - - Logger.d("Successfully generated certificate for alias: ${descriptor.alias}") - Pair(keyPair, chain) - }.onFailure { - Logger.e("Failed to generate key pair with certificates", it) - }.getOrNull() - - private fun buildECKeyPair(params: KeyGenParameters): KeyPair { - setupBouncyCastle() - val spec = ECGenParameterSpec(params.ecCurveName) - val keyPairGenerator = KeyPairGenerator.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME) - keyPairGenerator.initialize(spec) - return keyPairGenerator.generateKeyPair() - } - - private fun buildRSAKeyPair(params: KeyGenParameters): KeyPair { - setupBouncyCastle() - val spec = RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent) - val keyPairGenerator = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME) - keyPairGenerator.initialize(spec) - return keyPairGenerator.generateKeyPair() - } - - private fun setupBouncyCastle() { - Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) - Security.addProvider(BouncyCastleProvider()) - } - - private fun mapAlgorithmToName(algorithm: Int): String? = when (algorithm) { - Algorithm.EC -> KeyProperties.KEY_ALGORITHM_EC - Algorithm.RSA -> KeyProperties.KEY_ALGORITHM_RSA - else -> { - Logger.e("Unsupported algorithm: $algorithm") - null - } - } - - private fun getKeyboxForAlgorithm(algorithm: Int): KeyBox? { - val algorithmName = mapAlgorithmToName(algorithm) ?: return null - return keyboxes[algorithmName] - } - - private fun getAttestationKeyInfo(uid: Int, attestKeyDescriptor: KeyDescriptor): Pair? { - Logger.d("Looking for attestation key: uid=$uid alias=${attestKeyDescriptor.alias}") - - val keyInfo = SecurityLevelInterceptor.getKeyPairs(uid, attestKeyDescriptor.alias) - return if (keyInfo != null) { - val issuer = X509CertificateHolder(keyInfo.second[0].encoded).subject - Pair(keyInfo.first, issuer) - } else { - Logger.e("Attestation key info not found, falling back to default keybox") - null - } - } - - private fun createHackedAttestationExtension( - originalRootOfTrust: ASN1Encodable?, - vector: ASN1EncodableVector, - originalEncodables: Array - ): Extension { - val verifiedBootKey = bootKey - var verifiedBootHash: ByteArray? = null - - try { - if (originalRootOfTrust is ASN1Sequence) { - verifiedBootHash = CertificateUtils.getByteArrayFromAsn1(originalRootOfTrust.getObjectAt(3)) - } - } catch (t: Throwable) { - Logger.e("Failed to get verified boot hash from original, using generated", t) - } - - if (verifiedBootHash == null) { - verifiedBootHash = getBootHashFromProp() - } - - val rootOfTrustElements = arrayOf( - DEROctetString(verifiedBootKey), - ASN1Boolean.TRUE, - ASN1Enumerated(0), - DEROctetString(verifiedBootHash) - ) - val hackedRootOfTrust = DERSequence(rootOfTrustElements) - - vector.add(DERTaggedObject(true, 718, ASN1Integer(vendorPatchLevelLong.toLong()))) - vector.add(DERTaggedObject(true, 719, ASN1Integer(bootPatchLevelLong.toLong()))) - vector.add(DERTaggedObject(true, 706, ASN1Integer(patchLevel.toLong()))) - vector.add(DERTaggedObject(true, 705, ASN1Integer(osVersion.toLong()))) - vector.add(DERTaggedObject(704, hackedRootOfTrust)) - - val hackEnforced = DERSequence(vector) - originalEncodables[7] = hackEnforced - val hackedSequence = DERSequence(originalEncodables) - val hackedSequenceOctets = DEROctetString(hackedSequence) - - return Extension(ATTESTATION_OID, false, hackedSequenceOctets) - } - - private fun buildCertificate( - keyPair: KeyPair, - keybox: KeyBox, - params: KeyGenParameters, - issuer: X500Name, - uid: Int, - securityLevel: Int = 1, - signingKeyPair: KeyPair = keybox.keyPair - ): Certificate { - val builder = JcaX509v3CertificateBuilder( - issuer, - params.certificateSerial ?: BigInteger.ONE, - params.certificateNotBefore ?: Date(), - params.certificateNotAfter ?: (keybox.certificates[0] as X509Certificate).notAfter, - params.certificateSubject ?: X500Name("CN=Android KeyStore Key"), - keyPair.public - ) - - builder.addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.keyCertSign)) - builder.addExtension(createAttestationExtension(params, uid, securityLevel)) - - val signerAlgorithm = when (params.algorithm) { - Algorithm.EC -> "SHA256withECDSA" - Algorithm.RSA -> "SHA256withRSA" - else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}") - } - val contentSigner = JcaContentSignerBuilder(signerAlgorithm).build(signingKeyPair.private) - - return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner)) - } - - private fun createAttestationExtension(params: KeyGenParameters, uid: Int, securityLevel: Int = 1): Extension { - try { - val key = bootKey - val hash = getBootHashFromProp() - - Logger.d("Using boothash ${hash?.toHex() ?: 0}") - - val rootOfTrustEncodables = arrayOf( - DEROctetString(key), - ASN1Boolean.TRUE, - ASN1Enumerated(0), - DEROctetString(hash) - ) - val rootOfTrustSeq = DERSequence(rootOfTrustEncodables) - - val purpose = DERSet(params.purpose.map { ASN1Integer(it.toLong()) }.toTypedArray()) - val algorithm = ASN1Integer(params.algorithm.toLong()) - val keySize = ASN1Integer(params.keySize.toLong()) - val digest = DERSet(params.digest.map { ASN1Integer(it.toLong()) }.toTypedArray()) - val ecCurve = ASN1Integer(params.ecCurve.toLong()) - val noAuthRequired = DERNull.INSTANCE - - val osVersion = ASN1Integer(io.github.beakthoven.TrickyStoreOSS.osVersion.toLong()) - val osPatchLevel = ASN1Integer(io.github.beakthoven.TrickyStoreOSS.patchLevel.toLong()) - val applicationID = createApplicationId(uid) - val bootPatchLevel = ASN1Integer(bootPatchLevelLong.toLong()) - val vendorPatchLevel = ASN1Integer(vendorPatchLevelLong.toLong()) - val creationDateTime = ASN1Integer(System.currentTimeMillis()) - val origin = ASN1Integer(0L) - val moduleHash = DEROctetString(io.github.beakthoven.TrickyStoreOSS.moduleHash) - - val teeEnforcedObjects = mutableListOf( - DERTaggedObject(true, 1, purpose), - DERTaggedObject(true, 2, algorithm), - DERTaggedObject(true, 3, keySize), - DERTaggedObject(true, 5, digest), - DERTaggedObject(true, 10, ecCurve), - DERTaggedObject(true, 503, noAuthRequired), - DERTaggedObject(true, 702, origin), - DERTaggedObject(true, 704, rootOfTrustSeq), - DERTaggedObject(true, 705, osVersion), - DERTaggedObject(true, 706, osPatchLevel), - DERTaggedObject(true, 718, vendorPatchLevel), - DERTaggedObject(true, 719, bootPatchLevel), - ) - - if (io.github.beakthoven.TrickyStoreOSS.attestVersion >= 400) { - teeEnforcedObjects.add(DERTaggedObject(true, 724, moduleHash)) - } - - params.brand?.let { teeEnforcedObjects.add(DERTaggedObject(true, 710, DEROctetString(it))) } - params.device?.let { teeEnforcedObjects.add(DERTaggedObject(true, 711, DEROctetString(it))) } - params.product?.let { teeEnforcedObjects.add(DERTaggedObject(true, 712, DEROctetString(it))) } - params.manufacturer?.let { teeEnforcedObjects.add(DERTaggedObject(true, 716, DEROctetString(it))) } - params.model?.let { teeEnforcedObjects.add(DERTaggedObject(true, 717, DEROctetString(it))) } - - params.serialno?.let { teeEnforcedObjects.add(DERTaggedObject(true, 713, DEROctetString(it))) } - params.imei1?.let { teeEnforcedObjects.add(DERTaggedObject(true, 714, DEROctetString(it))) } - params.meid?.let { teeEnforcedObjects.add(DERTaggedObject(true, 715, DEROctetString(it))) } - - if (io.github.beakthoven.TrickyStoreOSS.attestVersion >= 300) { - params.imei2?.let { teeEnforcedObjects.add(DERTaggedObject(true, 723, DEROctetString(it))) } - } - - teeEnforcedObjects.sortBy { it.tagNo } - - val softwareEnforcedObjects = arrayOf( - DERTaggedObject(true, 709, applicationID), - DERTaggedObject(true, 701, creationDateTime) - ) - - return Extension( - ATTESTATION_OID, - false, - getAsn1OctetString(teeEnforcedObjects.toTypedArray(), softwareEnforcedObjects, params, securityLevel) - ) - } catch (t: Throwable) { - Logger.e("Failed to create attestation extension", t) - throw t - } - } - - - - private fun getAsn1OctetString( - teeEnforcedEncodables: Array, - softwareEnforcedEncodables: Array, - params: KeyGenParameters, - securityLevel: Int = 1 - ): ASN1OctetString { - val attestationVersion = ASN1Integer(io.github.beakthoven.TrickyStoreOSS.attestVersion.toLong()) - val attestationSecurityLevel = ASN1Enumerated(securityLevel) - val keymasterVersion = ASN1Integer(io.github.beakthoven.TrickyStoreOSS.keymasterVersion.toLong()) - val keymasterSecurityLevel = ASN1Enumerated(securityLevel) - val attestationChallenge = DEROctetString(params.attestationChallenge ?: ByteArray(0)) - val uniqueId = DEROctetString(ByteArray(0)) - val softwareEnforced = DERSequence(softwareEnforcedEncodables) - val teeEnforced = DERSequence(teeEnforcedEncodables) - - val keyDescriptionEncodables = arrayOf( - attestationVersion, - attestationSecurityLevel, - keymasterVersion, - keymasterSecurityLevel, - attestationChallenge, - uniqueId, - softwareEnforced, - teeEnforced - ) - - val keyDescriptionSeq = DERSequence(keyDescriptionEncodables) - return DEROctetString(keyDescriptionSeq.encoded) - } - - @Throws(Throwable::class) - private fun createApplicationId(uid: Int): DEROctetString { - val pm = Config.getPm() ?: throw IllegalStateException("PackageManager not found!") - val packages = pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid") - - val messageDigest = MessageDigest.getInstance("SHA-256") - val signatures = mutableSetOf() - - val packageInfos = packages.map { packageName -> - val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES.toLong(), uid / 100000) - } else { - pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, uid / 100000) - } - - info.signingInfo?.signingCertificateHistory?.forEach { signature -> - signatures.add(Digest(messageDigest.digest(signature.toByteArray()))) - } - - info - } - - val packageInfoArray = packageInfos.map { info -> - DERSequence( - arrayOf( - DEROctetString(info.packageName.toByteArray(StandardCharsets.UTF_8)), - ASN1Integer(info.longVersionCode) - ) - ) - }.toTypedArray() - - val signaturesArray = signatures.map { DEROctetString(it.digest) }.toTypedArray() - - val applicationIdArray = arrayOf( - DERSet(packageInfoArray), - DERSet(signaturesArray) - ) - - return DEROctetString(DERSequence(applicationIdArray).encoded) - } - -} - -data class AttestationData( - val verifiedBootHash: ByteArray?, - val attestVersion: Int?, - val keymasterVersion: Int?, - val osVersion: Int?, -) - -val keygen_alias = "tricky_store_oss_attest" - -val teeStatus: Boolean by lazy { isTEEWorking() } - -private fun isTEEWorking(): Boolean { - return try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - android.app.ActivityThread.initializeMainlineModules() - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - android.security.keystore2.AndroidKeyStoreProvider.install() - } else { - android.security.keystore.AndroidKeyStoreProvider.install() - } - - val keyStore = KeyStore.getInstance("AndroidKeyStore") - keyStore.load(null) - - val keyPairGenerator = KeyPairGenerator.getInstance( - KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore") - - val challenge = ByteArray(16).apply { - SecureRandom().nextBytes(this) - } - - val parameterSpec = KeyGenParameterSpec.Builder( - keygen_alias, - KeyProperties.PURPOSE_SIGN - ) - .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) - .setDigests(KeyProperties.DIGEST_SHA256) - .setAttestationChallenge(challenge) - .setIsStrongBoxBacked(false) - .build() - - keyPairGenerator.initialize(parameterSpec) - keyPairGenerator.generateKeyPair() - - Logger.d("TEE check: successful") - - // keyStore.deleteEntry(keygen_alias) - true - } catch (e: Exception) { - Logger.w("TEE check failure: ${e.message}") - false - } -} - -private fun getAttestCert(): X509Certificate? { - return if (teeStatus) { - val keyStore = KeyStore.getInstance("AndroidKeyStore") - keyStore.load(null) - - val certChain = keyStore.getCertificateChain(keygen_alias) - if (certChain == null || certChain.isEmpty()) { - null - } else { - keyStore.deleteEntry(keygen_alias) - certChain[0] as X509Certificate - } - } else { - null - } -} - -fun getAttestData(): AttestationData? { - val leaf: X509Certificate = getAttestCert() ?: return null - val ATTESTATION_OID = ASN1ObjectIdentifier("1.3.6.1.4.1.11129.2.1.17") - - return try { - val leafHolder = X509CertificateHolder(leaf.encoded) - val ext: Extension = leafHolder.getExtension(ATTESTATION_OID) ?: run { - Logger.i("No attestation extension found on certificate") - return null - } - - val keyDescriptionSeq = ASN1Sequence.getInstance(ext.extnValue.octets) - val encodables = keyDescriptionSeq.toArray() - - val attestVersion = ASN1Integer.getInstance(encodables[0]).value.intValueExact() - val keymasterVersion = ASN1Integer.getInstance(encodables[2]).value.intValueExact() - var attestVerifiedBootHash: ByteArray? = null - var attestOSVersion: Int? = null - - val teeEnforced = ASN1Sequence.getInstance(encodables[7]) - - teeEnforced.forEach { element -> - val tagged = element as ASN1TaggedObject - when (tagged.tagNo) { - 704 -> { // Parse Root of Trust - val rootOfTrustSeq = ASN1Sequence.getInstance(tagged.baseObject.toASN1Primitive()) - if (rootOfTrustSeq.size() >= 4) { - attestVerifiedBootHash = ASN1OctetString.getInstance(rootOfTrustSeq.getObjectAt(3)).octets - } - } - 705 -> { // Parse OS Version - attestOSVersion = ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive()).value.intValueExact() - } - } - } - - Logger.i("Extracted attestationVersion: $attestVersion") - Logger.i("Extracted keymasterVersion: $keymasterVersion") - Logger.i("Extracted verifiedBootHash: ${attestVerifiedBootHash?.toHex() ?: 0}") - Logger.i("Extracted osVersion: $attestOSVersion") - - AttestationData( - verifiedBootHash = attestVerifiedBootHash, - attestVersion = attestVersion, - keymasterVersion = keymasterVersion, - osVersion = attestOSVersion - ) - } catch (e: Exception) { - Logger.e("Failed to parse attestation data", e) - null - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/Main.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/Main.kt index 503ef7c..9150641 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/Main.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/Main.kt @@ -6,10 +6,10 @@ package io.github.beakthoven.TrickyStoreOSS import android.os.Build -import io.github.beakthoven.TrickyStoreOSS.core.config.Config -import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger +import io.github.beakthoven.TrickyStoreOSS.config.PkgConfig import io.github.beakthoven.TrickyStoreOSS.interceptors.Keystore2Interceptor import io.github.beakthoven.TrickyStoreOSS.interceptors.KeystoreInterceptor +import io.github.beakthoven.TrickyStoreOSS.logging.Logger private const val RETRY_DELAY_MS = 1000L private const val SERVICE_SLEEP_MS = 1000000L @@ -18,7 +18,7 @@ fun main(args: Array) { Logger.i("Welcome to TrickyStoreOSS!") try { - setupBootHash() + AndroidUtils.setupBootHash() initializeInterceptors() maintainService() } catch (e: Exception) { @@ -35,7 +35,7 @@ private fun initializeInterceptors() { Thread.sleep(RETRY_DELAY_MS) } - Config.initialize() + PkgConfig.initialize() Logger.i("Interceptors initialized successfully") } diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/XmlParser.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/XmlParser.kt index c093a75..e47bacf 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/XmlParser.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/XmlParser.kt @@ -5,11 +5,17 @@ package io.github.beakthoven.TrickyStoreOSS +import android.security.keystore.KeyProperties +import io.github.beakthoven.TrickyStoreOSS.CertificateGen.KeyBox +import io.github.beakthoven.TrickyStoreOSS.CertificateHack.clearLeafAlgorithms +import io.github.beakthoven.TrickyStoreOSS.logging.Logger import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserFactory import java.io.IOException import java.io.StringReader +import java.security.cert.Certificate +import java.util.concurrent.ConcurrentHashMap class XmlParser(private val xmlContent: String) { @@ -152,4 +158,117 @@ class XmlParser(private val xmlContent: String) { } } -fun String.toXmlParser(): XmlParser = XmlParser(this) \ No newline at end of file +object KeyBoxUtils { + val keyboxes = ConcurrentHashMap() + + fun hasKeyboxes(): Boolean = keyboxes.isNotEmpty() + + fun readFromXml(xmlData: String?) { + keyboxes.clear() + clearLeafAlgorithms() + + if (xmlData == null) { + Logger.i("Clearing all keyboxes") + return + } + + try { + val xmlParser = XmlParser(xmlData.sanitizeXml()) + + val numberOfKeyboxesResult = xmlParser.obtainPath("AndroidAttestation.NumberOfKeyboxes") + val numberOfKeyboxes = when (numberOfKeyboxesResult) { + is XmlParser.ParseResult.Success -> numberOfKeyboxesResult.attributes["text"]?.toIntOrNull() + ?: throw IllegalArgumentException("Invalid number of keyboxes") + is XmlParser.ParseResult.Error -> throw Exception(numberOfKeyboxesResult.message, numberOfKeyboxesResult.cause) + } + + repeat(numberOfKeyboxes) { i -> + processKeybox(xmlParser, i) + } + + Logger.i("Successfully updated $numberOfKeyboxes keyboxes") + } catch (t: Throwable) { + Logger.e("Error loading XML file (keyboxes cleared)", t) + } + } + + private fun String.sanitizeXml(): String { + var content = this + + val boms = listOf( + "\uFEFF", + "\uFFFE", + "\u0000\uFEFF" + ) + content = content.trimStart() + for (bom in boms) { + content = content.removePrefix(bom) + } + content = content.trimStart() + + return content.trimEnd() + } + + private fun processKeybox(xmlParser: XmlParser, index: Int) { + try { + val algorithmResult = xmlParser.obtainPath("AndroidAttestation.Keybox.Key[$index]") + val keyboxAlgorithm = when (algorithmResult) { + is XmlParser.ParseResult.Success -> algorithmResult.attributes["algorithm"] + ?: throw IllegalArgumentException("Missing algorithm attribute") + is XmlParser.ParseResult.Error -> throw Exception(algorithmResult.message, algorithmResult.cause) + } + + val privateKeyResult = xmlParser.obtainPath("AndroidAttestation.Keybox.Key[$index].PrivateKey") + val privateKeyContent = when (privateKeyResult) { + is XmlParser.ParseResult.Success -> privateKeyResult.attributes["text"] + ?: throw IllegalArgumentException("Missing private key text") + is XmlParser.ParseResult.Error -> throw Exception(privateKeyResult.message, privateKeyResult.cause) + } + + val numberOfCertificatesResult = xmlParser.obtainPath( + "AndroidAttestation.Keybox.Key[$index].CertificateChain.NumberOfCertificates" + ) + val numberOfCertificates = when (numberOfCertificatesResult) { + is XmlParser.ParseResult.Success -> numberOfCertificatesResult.attributes["text"]?.toIntOrNull() + ?: throw IllegalArgumentException("Invalid number of certificates") + is XmlParser.ParseResult.Error -> throw Exception(numberOfCertificatesResult.message, numberOfCertificatesResult.cause) + } + + val certificateChain = mutableListOf() + repeat(numberOfCertificates) { j -> + val certResult = xmlParser.obtainPath( + "AndroidAttestation.Keybox.Key[$index].CertificateChain.Certificate[$j]" + ) + val certContent = when (certResult) { + is XmlParser.ParseResult.Success -> certResult.attributes["text"] + ?: throw IllegalArgumentException("Missing certificate text") + is XmlParser.ParseResult.Error -> throw Exception(certResult.message, certResult.cause) + } + + when (val certParseResult = CertificateUtils.parseCertificate(certContent)) { + is CertificateUtils.ParseResult.Success -> certificateChain.add(certParseResult.data) + is CertificateUtils.ParseResult.Error -> throw Exception(certParseResult.message, certParseResult.cause) + } + } + + val pemKeyPair = when (val keyParseResult = CertificateUtils.parseKeyPair(privateKeyContent)) { + is CertificateUtils.ParseResult.Success -> keyParseResult.data + is CertificateUtils.ParseResult.Error -> throw Exception(keyParseResult.message, keyParseResult.cause) + } + + val keyPair = CertificateUtils.convertPemToKeyPair(pemKeyPair) + + val algorithmName = when (keyboxAlgorithm.lowercase()) { + "ecdsa" -> KeyProperties.KEY_ALGORITHM_EC + "rsa" -> KeyProperties.KEY_ALGORITHM_RSA + else -> keyboxAlgorithm + } + + keyboxes[algorithmName] = KeyBox(pemKeyPair, keyPair, certificateChain) + + } catch (t: Throwable) { + Logger.e("Error processing keybox $index", t) + throw t + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/core/config/Config.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/config/Config.kt similarity index 95% rename from app/src/main/java/io/github/beakthoven/TrickyStoreOSS/core/config/Config.kt rename to app/src/main/java/io/github/beakthoven/TrickyStoreOSS/config/Config.kt index c6b3735..1c047fd 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/core/config/Config.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/config/Config.kt @@ -3,19 +3,19 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -package io.github.beakthoven.TrickyStoreOSS.core.config +package io.github.beakthoven.TrickyStoreOSS.config import android.content.pm.IPackageManager import android.os.FileObserver import android.os.IBinder import android.os.IInterface import android.os.ServiceManager -import io.github.beakthoven.TrickyStoreOSS.CertificateHacker -import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger -import io.github.beakthoven.TrickyStoreOSS.teeStatus +import io.github.beakthoven.TrickyStoreOSS.AttestUtils.TEEStatus +import io.github.beakthoven.TrickyStoreOSS.KeyBoxUtils +import io.github.beakthoven.TrickyStoreOSS.logging.Logger import java.io.File -object Config { +object PkgConfig { private val hackPackages = mutableSetOf() private val generatePackages = mutableSetOf() private val packageModes = mutableMapOf() @@ -55,7 +55,7 @@ object Config { } private fun updateKeyBox(f: File?) = runCatching { - CertificateHacker.readFromXml(f?.readText()) + KeyBoxUtils.readFromXml(f?.readText()) }.onFailure { Logger.e("failed to update keybox", it) } @@ -72,7 +72,7 @@ object Config { private fun storeTEEStatus(root: File) { val statusFile = File(root, TEE_STATUS_FILE) - teeBroken = !teeStatus + teeBroken = !TEEStatus try { statusFile.writeText("teeBroken=${teeBroken}") Logger.i("TEE status written to $statusFile: teeBroken=$teeBroken") diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/BinderInterceptor.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/BinderInterceptor.kt index d241b49..808d504 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/BinderInterceptor.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/BinderInterceptor.kt @@ -8,7 +8,7 @@ package io.github.beakthoven.TrickyStoreOSS.interceptors import android.os.Binder import android.os.IBinder import android.os.Parcel -import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger +import io.github.beakthoven.TrickyStoreOSS.logging.Logger open class BinderInterceptor : Binder() { diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/InterceptorUtils.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/InterceptorUtils.kt index e07299b..c13f675 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/InterceptorUtils.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/InterceptorUtils.kt @@ -11,7 +11,7 @@ import android.os.Parcelable import android.os.ServiceManager import android.security.KeyStore import android.security.keystore.KeystoreResponse -import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger +import io.github.beakthoven.TrickyStoreOSS.logging.Logger import kotlin.system.exitProcess abstract class BaseKeystoreInterceptor : BinderInterceptor() { @@ -96,6 +96,10 @@ abstract class BaseKeystoreInterceptor : BinderInterceptor() { object InterceptorUtils { + fun getTransactCode(clazz: Class<*>, method: String): Int = + clazz.getDeclaredField("TRANSACTION_$method").apply { isAccessible = true } + .getInt(null) + fun createSuccessKeystoreResponse(): KeystoreResponse { val parcel = Parcel.obtain() try { diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/Keystore2Interceptor.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/Keystore2Interceptor.kt index 369f1cf..dbf7f18 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/Keystore2Interceptor.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/Keystore2Interceptor.kt @@ -12,13 +12,14 @@ import android.os.Parcel import android.system.keystore2.IKeystoreService import android.system.keystore2.KeyDescriptor import android.system.keystore2.KeyEntryResponse -import io.github.beakthoven.TrickyStoreOSS.CertificateHacker +import io.github.beakthoven.TrickyStoreOSS.CertificateHack import io.github.beakthoven.TrickyStoreOSS.CertificateUtils -import io.github.beakthoven.TrickyStoreOSS.core.config.Config -import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger -import io.github.beakthoven.TrickyStoreOSS.getTransactCode +import io.github.beakthoven.TrickyStoreOSS.KeyBoxUtils +import io.github.beakthoven.TrickyStoreOSS.config.PkgConfig import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createTypedObjectReply +import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.getTransactCode import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.hasException +import io.github.beakthoven.TrickyStoreOSS.logging.Logger import io.github.beakthoven.TrickyStoreOSS.putCertificateChain @SuppressLint("BlockedPrivateApi") @@ -74,17 +75,17 @@ object Keystore2Interceptor : BaseKeystoreInterceptor() { data: Parcel ): Result { if (code == getKeyEntryTransaction) { - if (CertificateHacker.hasKeyboxes()) { + if (KeyBoxUtils.hasKeyboxes()) { Logger.d("intercept pre $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()}") kotlin.runCatching { data.enforceInterface(IKeystoreService.DESCRIPTOR) val descriptor = data.readTypedObject(KeyDescriptor.CREATOR) ?: return@runCatching - if (Config.needGenerate(callingUid)) { + if (PkgConfig.needGenerate(callingUid)) { val response = SecurityLevelInterceptor.getKeyResponse(callingUid, descriptor.alias) ?: return@runCatching Logger.i("Generate key for uid=$callingUid alias=${descriptor.alias}") return createTypedObjectReply(response) - } else if (Config.needHack(callingUid)) { + } else if (PkgConfig.needHack(callingUid)) { if (SecurityLevelInterceptor.shouldSkipLeafHack(callingUid, descriptor.alias)) { Logger.i("skip leaf hack for uid=$callingUid alias=${descriptor.alias}") val response = SecurityLevelInterceptor.getKeyResponse(callingUid, descriptor.alias) @@ -138,7 +139,7 @@ object Keystore2Interceptor : BaseKeystoreInterceptor() { if (response != null) { val chain = CertificateUtils.run { response.getCertificateChain() } if (chain != null) { - val newChain = CertificateHacker.hackCertificateChain(chain) + val newChain = CertificateHack.hackCertificateChain(chain) response.putCertificateChain(newChain).getOrThrow() Logger.i("Hacked certificate for uid=$callingUid") return createTypedObjectReply(response) diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/KeystoreInterceptor.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/KeystoreInterceptor.kt index 8c9b697..1f29d53 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/KeystoreInterceptor.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/KeystoreInterceptor.kt @@ -19,15 +19,17 @@ import android.security.keystore.IKeystoreCertificateChainCallback import android.security.keystore.IKeystoreExportKeyCallback import android.security.keystore.IKeystoreKeyCharacteristicsCallback import android.security.keystore.IKeystoreService -import io.github.beakthoven.TrickyStoreOSS.CertificateHacker -import io.github.beakthoven.TrickyStoreOSS.core.config.Config -import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger -import io.github.beakthoven.TrickyStoreOSS.getTransactCode +import io.github.beakthoven.TrickyStoreOSS.CertificateGen +import io.github.beakthoven.TrickyStoreOSS.CertificateHack +import io.github.beakthoven.TrickyStoreOSS.KeyBoxUtils +import io.github.beakthoven.TrickyStoreOSS.config.PkgConfig import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createByteArrayReply import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createSuccessKeystoreResponse import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createSuccessReply import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.extractAlias +import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.getTransactCode import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.hasException +import io.github.beakthoven.TrickyStoreOSS.logging.Logger import java.math.BigInteger import java.security.KeyPair import java.util.Date @@ -51,7 +53,7 @@ object KeystoreInterceptor : BaseKeystoreInterceptor() { private const val DESCRIPTOR = "android.security.keystore.IKeystoreService" - private val keyArguments = HashMap() + private val keyArguments = HashMap() private val keyPairs = HashMap() data class Key(val uid: Int, val alias: String) @@ -64,14 +66,14 @@ object KeystoreInterceptor : BaseKeystoreInterceptor() { callingPid: Int, data: Parcel ): Result { - if (CertificateHacker.hasKeyboxes()) { + if (KeyBoxUtils.hasKeyboxes()) { if (code == getTransaction) { - if (Config.needHack(callingUid)) { + if (PkgConfig.needHack(callingUid)) { return Continue - } else if (Config.needGenerate(callingUid)) { + } else if (PkgConfig.needGenerate(callingUid)) { return Skip } - } else if (Config.needGenerate(callingUid)) { + } else if (PkgConfig.needGenerate(callingUid)) { when (code) { generateKeyTransaction -> { kotlin.runCatching { @@ -81,12 +83,12 @@ object KeystoreInterceptor : BaseKeystoreInterceptor() { Logger.i("generateKeyTransaction uid $callingUid alias $alias") val check = data.readInt() val kma = KeymasterArguments() - val kgp = CertificateHacker.KeyGenParameters() + val kgp = CertificateGen.KeyGenParameters() if (check == 1) { kma.readFromParcel(data) kgp.algorithm = kma.getEnum(KeymasterDefs.KM_TAG_ALGORITHM, 0) kgp.keySize = kma.getUnsignedInt(KeymasterDefs.KM_TAG_KEY_SIZE, 0).toInt() - kgp.setEcCurveName(kgp.keySize) + //kgp.setEcCurveName(kgp.keySize) kgp.purpose = kma.getEnums(KeymasterDefs.KM_TAG_PURPOSE) kgp.digest = kma.getEnums(KeymasterDefs.KM_TAG_DIGEST) kgp.certificateNotBefore = kma.getDate(KeymasterDefs.KM_TAG_ACTIVE_DATETIME, Date()) @@ -146,7 +148,7 @@ object KeystoreInterceptor : BaseKeystoreInterceptor() { val callback = IKeystoreExportKeyCallback.Stub.asInterface(data.readStrongBinder()) val alias = data.readString()!!.extractAlias() Logger.i("exportKeyTransaction uid $callingUid alias $alias") - val kp = CertificateHacker.generateKeyPair(keyArguments[Key(callingUid, alias)]!!) + val kp = CertificateGen.generateKeyPair(keyArguments[Key(callingUid, alias)]!!) keyPairs[Key(callingUid, alias)] = kp!! val erP = Parcel.obtain() @@ -181,7 +183,7 @@ object KeystoreInterceptor : BaseKeystoreInterceptor() { val key = Key(callingUid, alias) val ka = keyArguments[key]!! ka.attestationChallenge = attestationChallenge - val chain = CertificateHacker.generateChain(callingUid, ka, keyPairs[key]!!) + val chain = CertificateGen.generateChain(callingUid, ka, keyPairs[key]!!) val kcc = KeymasterCertificateChain(chain) callback.onFinished(ksr, kcc) @@ -218,12 +220,12 @@ object KeystoreInterceptor : BaseKeystoreInterceptor() { var response = reply.createByteArray() when { alias.startsWith(Credentials.USER_CERTIFICATE) -> { - response = CertificateHacker.hackUserCertificate(response!!, alias.extractAlias(), callingUid) + response = CertificateHack.hackUserCertificate(response!!, alias.extractAlias(), callingUid) Logger.i("Hacked leaf certificate for uid=$callingUid") return createByteArrayReply(response) } alias.startsWith(Credentials.CA_CERTIFICATE) -> { - response = CertificateHacker.hackCACertificateChain(response!!, alias.extractAlias(), callingUid) + response = CertificateHack.hackCACertificateChain(response!!, alias.extractAlias(), callingUid) Logger.i("Hacked CA certificate chain for uid=$callingUid") return createByteArrayReply(response) } diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/SecurityLevelInterceptor.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/SecurityLevelInterceptor.kt index 1679fba..183b0ca 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/SecurityLevelInterceptor.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/SecurityLevelInterceptor.kt @@ -16,10 +16,10 @@ import android.system.keystore2.KeyDescriptor import android.system.keystore2.KeyEntryResponse import android.system.keystore2.KeyMetadata import androidx.annotation.Keep -import io.github.beakthoven.TrickyStoreOSS.CertificateHacker -import io.github.beakthoven.TrickyStoreOSS.core.config.Config -import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger -import io.github.beakthoven.TrickyStoreOSS.getTransactCode +import io.github.beakthoven.TrickyStoreOSS.CertificateGen +import io.github.beakthoven.TrickyStoreOSS.config.PkgConfig +import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.getTransactCode +import io.github.beakthoven.TrickyStoreOSS.logging.Logger import io.github.beakthoven.TrickyStoreOSS.putCertificateChain import java.security.KeyPair import java.security.cert.Certificate @@ -80,9 +80,9 @@ class SecurityLevelInterceptor( val params = data.createTypedArray(KeyParameter.CREATOR)!! val aFlags = data.readInt() val entropy = data.createByteArray() - val kgp = CertificateHacker.KeyGenParameters(params) - if (Config.needGenerate(callingUid)) { - val pair = CertificateHacker.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp, level) + val kgp = CertificateGen.KeyGenParameters(params) + if (PkgConfig.needGenerate(callingUid)) { + val pair = CertificateGen.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp, level) ?: return@runCatching keyPairs[Key(callingUid, keyDescriptor.alias)] = Pair(pair.first, pair.second) val response = buildResponse(pair.second, kgp, attestationKeyDescriptor ?: keyDescriptor) @@ -91,10 +91,10 @@ class SecurityLevelInterceptor( p.writeNoException() p.writeTypedObject(response.metadata, 0) return OverrideReply(0, p) - } else if (Config.needHack(callingUid)) { + } else if (PkgConfig.needHack(callingUid)) { if ((kgp.purpose.contains(7)) || (attestationKeyDescriptor != null)) { Logger.i("Generating key in generation mode for attestation: uid=$callingUid alias=${keyDescriptor.alias}") - val pair = CertificateHacker.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp, level) + val pair = CertificateGen.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp, level) ?: return@runCatching keyPairs[Key(callingUid, keyDescriptor.alias)] = Pair(pair.first, pair.second) val response = buildResponse(pair.second, kgp, attestationKeyDescriptor ?: keyDescriptor) @@ -119,7 +119,7 @@ class SecurityLevelInterceptor( private fun buildResponse( chain: List, - params: CertificateHacker.KeyGenParameters, + params: CertificateGen.KeyGenParameters, descriptor: KeyDescriptor ): KeyEntryResponse { val response = KeyEntryResponse() diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/core/logging/Logger.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/logging/Logger.kt similarity index 94% rename from app/src/main/java/io/github/beakthoven/TrickyStoreOSS/core/logging/Logger.kt rename to app/src/main/java/io/github/beakthoven/TrickyStoreOSS/logging/Logger.kt index 3249a6a..6039951 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/core/logging/Logger.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/logging/Logger.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -package io.github.beakthoven.TrickyStoreOSS.core.logging +package io.github.beakthoven.TrickyStoreOSS.logging import android.util.Log @@ -27,7 +27,7 @@ object Logger { } fun e(message: String, throwable: Throwable) { - Log.e(TAG, "wtf: $message", throwable) + Log.e(TAG, "fatal: $message", throwable) } fun i(message: String) {