From c5c81b80c374ce1b234ff45a2badcc10e74aded7 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 2 Nov 2025 11:16:41 +0100 Subject: [PATCH] Add ktfmt-gradle as formatter Run `gradle format` to format all kotlin source code --- app/build.gradle.kts | 88 +-- .../beakthoven/TrickyStoreOSS/AndroidUtils.kt | 208 +++---- .../beakthoven/TrickyStoreOSS/AttestUtils.kt | 63 +- .../TrickyStoreOSS/CertificateGen.kt | 557 ++++++++++-------- .../TrickyStoreOSS/CertificateHack.kt | 204 ++++--- .../TrickyStoreOSS/CertificateUtils.kt | 149 ++--- .../github/beakthoven/TrickyStoreOSS/Main.kt | 25 +- .../beakthoven/TrickyStoreOSS/XmlParser.kt | 243 ++++---- .../TrickyStoreOSS/config/Config.kt | 276 +++++---- .../interceptors/BinderInterceptor.kt | 120 ++-- .../interceptors/InterceptorUtils.kt | 81 +-- .../interceptors/Keystore2Interceptor.kt | 74 ++- .../interceptors/KeystoreInterceptor.kt | 264 +++++---- .../interceptors/SecurityLevelInterceptor.kt | 115 ++-- .../TrickyStoreOSS/logging/Logger.kt | 41 +- build.gradle.kts | 13 +- gradle/libs.versions.toml | 2 + settings.gradle.kts | 2 + 18 files changed, 1436 insertions(+), 1089 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9976360..f61ccaf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,26 +3,24 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import java.io.ByteArrayOutputStream - plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + alias(libs.plugins.ktfmt) } +ktfmt { kotlinLangStyle() } + fun String.execute(currentWorkingDir: File = File("./")): String { val parts = this.split("\\s+".toRegex()) - val process = ProcessBuilder(parts) - .directory(currentWorkingDir) - .redirectErrorStream(true) - .start() + val process = + ProcessBuilder(parts).directory(currentWorkingDir).redirectErrorStream(true).start() val output = process.inputStream.bufferedReader().readText() process.waitFor() return output.trim() } - val gitCommitCount = "git rev-list HEAD --count".execute().toInt() val gitCommitHash = "git rev-parse --verify --short HEAD".execute() val verName = "v2.1.0" @@ -61,16 +59,14 @@ android { } } - buildFeatures { - prefab = true - } + buildFeatures { prefab = true } buildTypes { release { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } @@ -85,9 +81,7 @@ android { version = "3.28.0+" } } - buildFeatures { - viewBinding = false - } + buildFeatures { viewBinding = false } } dependencies { @@ -102,15 +96,15 @@ afterEvaluate { val variantName = variant.name val capitalized = variantName.replaceFirstChar { it.uppercase() } val tempModuleDir = project.layout.buildDirectory.dir("tmp/module-${variantName}") - + tasks.register("copyFiles${capitalized}") { dependsOn("assemble${capitalized}") val moduleFolder = project.rootDir.resolve("module") val buildDir = project.layout.buildDirectory - + doLast { val isDebug = variantName.contains("debug", ignoreCase = true) - //val apkFile = variant.outputs.first().outputFile + // val apkFile = variant.outputs.first().outputFile listOf("service.apk", "classes.dex").forEach { fileName -> val oldFile = moduleFolder.resolve(fileName) @@ -118,23 +112,32 @@ afterEvaluate { } // Select source file based on build type - val sourceFile = if (isDebug) { - variant.outputs.first().outputFile - } else { - buildDir.get().asFile.resolve("intermediates/dex/release/minifyReleaseWithR8/classes.dex") - } + val sourceFile = + if (isDebug) { + variant.outputs.first().outputFile + } else { + buildDir + .get() + .asFile + .resolve("intermediates/dex/release/minifyReleaseWithR8/classes.dex") + } val destFileName = if (isDebug) "service.apk" else "classes.dex" sourceFile.copyTo(moduleFolder.resolve(destFileName), overwrite = true) - val soDir = buildDir.get() - .asFile - .resolve("intermediates/stripped_native_libs/$variantName/strip${capitalized}DebugSymbols/out/lib") - - //apkFile.copyTo(moduleFolder.resolve("service.apk"), overwrite = true) - + val soDir = + buildDir + .get() + .asFile + .resolve( + "intermediates/stripped_native_libs/$variantName/strip${capitalized}DebugSymbols/out/lib" + ) + + // apkFile.copyTo(moduleFolder.resolve("service.apk"), overwrite = true) + val allowedLibs = setOf("libinject.so", "libTrickyStoreOSS.so") - soDir.walk() + soDir + .walk() .filter { it.isFile && it.name in allowedLibs } .forEach { soFile -> val abiFolder = soFile.parentFile.name @@ -143,21 +146,22 @@ afterEvaluate { } } } - + // Prepare temp directory with all files tasks.register("prepareModuleFiles${capitalized}") { dependsOn("copyFiles${capitalized}") val sourceDir = project.rootDir.resolve("module") - + doLast { val tempDir = tempModuleDir.get().asFile - + // Clean and create temp directory tempDir.deleteRecursively() tempDir.mkdirs() - + // Copy all files except module.prop - sourceDir.walkTopDown() + sourceDir + .walkTopDown() .filter { it.isFile && it.name != "module.prop" } .forEach { sourceFile -> val relativePath = sourceFile.relativeTo(sourceDir) @@ -165,18 +169,22 @@ afterEvaluate { destFile.parentFile.mkdirs() sourceFile.copyTo(destFile, overwrite = true) } - + // Process module.prop val sourceProp = sourceDir.resolve("module.prop") val destProp = tempDir.resolve("module.prop") val content = sourceProp.readText() - val processedContent = content - .replace("REPLACEMEVERCODE", gitCommitCount.toString()) - .replace("REPLACEMEVER", "$verName ($gitCommitCount-$gitCommitHash-$variantName)") + val processedContent = + content + .replace("REPLACEMEVERCODE", gitCommitCount.toString()) + .replace( + "REPLACEMEVER", + "$verName ($gitCommitCount-$gitCommitHash-$variantName)", + ) destProp.writeText(processedContent) } } - + // Zip task uses the temp directory val zipTask = tasks.register("zip${capitalized}") { @@ -263,7 +271,7 @@ afterEvaluate { commandLine("adb", "reboot") description = "Installs the $variantName module via APatch and reboots." } - + tasks["assemble${capitalized}"].finalizedBy("zip${capitalized}") } } 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 c29a455..2ce650a 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/AndroidUtils.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/AndroidUtils.kt @@ -14,42 +14,40 @@ 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 java.security.MessageDigest +import java.util.concurrent.ThreadLocalRandom import org.bouncycastle.asn1.ASN1Integer import org.bouncycastle.asn1.DEROctetString import org.bouncycastle.asn1.DERSequence -import java.security.MessageDigest -import java.util.concurrent.ThreadLocalRandom object AndroidUtils { - val bootKey: ByteArray by lazy { - randomBytes() - } + val bootKey: ByteArray by lazy { randomBytes() } fun setupBootHash() { - getBootHashFromProp()?.also { + 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) - } + ?: 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 } @@ -72,45 +70,51 @@ object AndroidUtils { } } - private fun randomBytes(): ByteArray = ByteArray(32).also { - ThreadLocalRandom.current().nextBytes(it) - } + private fun randomBytes(): ByteArray = + ByteArray(32).also { ThreadLocalRandom.current().nextBytes(it) } val patchLevel: Int - get() = getCustomPatchLevel("system", false) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) + get() = + getCustomPatchLevel("system", false) + ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) val patchLevelLong: Int - get() = getCustomPatchLevel("system", true) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true) + get() = + getCustomPatchLevel("system", true) + ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true) val vendorPatchLevel: Int - get() = getCustomPatchLevel("vendor", false) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) + get() = + getCustomPatchLevel("vendor", false) + ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) val vendorPatchLevelLong: Int - get() = getCustomPatchLevel("vendor", true) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true) + get() = + getCustomPatchLevel("vendor", true) + ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true) val bootPatchLevel: Int - get() = getCustomPatchLevel("boot", false) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) + get() = + getCustomPatchLevel("boot", false) + ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) val bootPatchLevelLong: Int - get() = getCustomPatchLevel("boot", true) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true) + 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 + 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 @@ -122,21 +126,19 @@ object AndroidUtils { 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 + 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 + if (isLong) year * 10000 + month * 100 else year * 100 + month } else -> { Logger.e("Invalid patch level length for $component: $normalized") @@ -149,30 +151,32 @@ object AndroidUtils { } } - 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 - ) + 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 - ) + 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 @@ -180,54 +184,56 @@ object AndroidUtils { 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) + 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) + 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() } - - 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) - } - } + 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() } -fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) } \ No newline at end of file + +fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) } diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/AttestUtils.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/AttestUtils.kt index ccac385..2a55612 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/AttestUtils.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/AttestUtils.kt @@ -9,6 +9,11 @@ import android.os.Build import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import io.github.beakthoven.TrickyStoreOSS.logging.Logger +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.SecureRandom +import java.security.cert.X509Certificate +import java.security.spec.ECGenParameterSpec import org.bouncycastle.asn1.ASN1Integer import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.ASN1OctetString @@ -16,11 +21,6 @@ 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") @@ -33,7 +33,7 @@ object AttestUtils { ) val TEEStatus: Boolean by lazy { isTEEWorking() } - val CachedAttestData: AttestationData? by lazy { getAttestData()} + val CachedAttestData: AttestationData? by lazy { getAttestData() } private val keygen_alias = "TrickyStoreOSS_attest" @@ -52,22 +52,18 @@ object AttestUtils { val keyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null) - val keyPairGenerator = KeyPairGenerator.getInstance( - KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore") + val keyPairGenerator = + KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore") - val challenge = ByteArray(16).apply { - SecureRandom().nextBytes(this) - } + 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() + 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() @@ -86,7 +82,7 @@ object AttestUtils { return if (TEEStatus) { val keyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null) - + val certChain = keyStore.getCertificateChain(keygen_alias) if (certChain == null || certChain.isEmpty()) { null @@ -104,10 +100,12 @@ object AttestUtils { 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 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() @@ -123,13 +121,18 @@ object AttestUtils { val tagged = element as ASN1TaggedObject when (tagged.tagNo) { 704 -> { // Parse Root of Trust - val rootOfTrustSeq = ASN1Sequence.getInstance(tagged.baseObject.toASN1Primitive()) + val rootOfTrustSeq = + ASN1Sequence.getInstance(tagged.baseObject.toASN1Primitive()) if (rootOfTrustSeq.size() >= 4) { - attestVerifiedBootHash = ASN1OctetString.getInstance(rootOfTrustSeq.getObjectAt(3)).octets + attestVerifiedBootHash = + ASN1OctetString.getInstance(rootOfTrustSeq.getObjectAt(3)).octets } } 705 -> { // Parse OS Version - attestOSVersion = ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive()).value.intValueExact() + attestOSVersion = + ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive()) + .value + .intValueExact() } } } @@ -143,11 +146,11 @@ object AttestUtils { verifiedBootHash = attestVerifiedBootHash, attestVersion = attestVersion, keymasterVersion = keymasterVersion, - osVersion = attestOSVersion + 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 index 429be03..2b7a752 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateGen.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateGen.kt @@ -17,6 +17,18 @@ 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 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 import org.bouncycastle.asn1.ASN1Boolean import org.bouncycastle.asn1.ASN1Encodable import org.bouncycastle.asn1.ASN1Enumerated @@ -36,24 +48,12 @@ 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 + val certificates: List, ) private data class Digest(val digest: ByteArray) { @@ -63,7 +63,7 @@ object CertificateGen { other as Digest return digest.contentEquals(other.digest) } - + override fun hashCode(): Int = digest.contentHashCode() } @@ -88,21 +88,22 @@ object CertificateGen { var imei1: ByteArray? = null, var imei2: ByteArray? = null, var meid: ByteArray? = null, - var serialno: 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.CERTIFICATE_SUBJECT -> + certificateSubject = X500Name(X500Principal(value.blob).name) Tag.RSA_PUBLIC_EXPONENT -> rsaPublicExponent = BigInteger(value.blob) Tag.EC_CURVE -> { ecCurve = value.ecCurve @@ -126,146 +127,181 @@ object CertificateGen { 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") - } - + 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) + 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(uid, 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 generateChain( + uid: Int, + params: KeyGenParameters, + keyPair: KeyPair, + securityLevel: Int = 1, + ): List? = + runCatching { + val keybox = getKeyboxForAlgorithm(uid, params.algorithm) ?: return null - fun generateKeyPair(params: KeyGenParameters): KeyPair? = runCatching { - Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) - Security.addProvider(BouncyCastleProvider()) + val issuer = X509CertificateHolder(keybox.certificates[0].encoded).subject + val leaf = buildCertificate(keyPair, keybox, params, issuer, uid, securityLevel) - 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 + val chain = buildList { + add(leaf) + addAll(keybox.certificates) + } + + CertificateUtils.run { chain.toByteArrayList() } } - 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 + .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() } - else -> { - throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}") - } - } + .onFailure { Logger.e("Failed to generate key pair", it) } + .getOrNull() - 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(uid, 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) + 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(uid, 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 } } - - 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(uid: Int, algorithm: Int): KeyBox? { val algorithmName = mapAlgorithmToName(algorithm) ?: return null @@ -273,9 +309,12 @@ object CertificateGen { return KeyBoxUtils.getKeybox(keyboxFileName, algorithmName) } - private fun getAttestationKeyInfo(uid: Int, attestKeyDescriptor: KeyDescriptor): Pair? { + 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 @@ -293,52 +332,59 @@ object CertificateGen { issuer: X500Name, uid: Int, securityLevel: Int = 1, - signingKeyPair: KeyPair = keybox.keyPair + 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 - ) - + 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 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 { + 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 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) @@ -347,51 +393,76 @@ object CertificateGen { 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), - ) + + 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))) } + + 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))) } + 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) - ) - + + val softwareEnforcedObjects = + arrayOf( + DERTaggedObject(true, 709, applicationID), + DERTaggedObject(true, 701, creationDateTime), + ) + return Extension( ATTESTATION_OID, false, - buildKeyDescriptionOctet(teeEnforcedObjects.toTypedArray(), softwareEnforcedObjects, params, securityLevel) + buildKeyDescriptionOctet( + teeEnforcedObjects.toTypedArray(), + softwareEnforcedObjects, + params, + securityLevel, + ), ) } catch (t: Throwable) { Logger.e("Failed to create attestation extension", t) @@ -403,7 +474,7 @@ object CertificateGen { teeEnforcedEncodables: Array, softwareEnforcedEncodables: Array, params: KeyGenParameters, - securityLevel: Int = 1 + securityLevel: Int = 1, ): ASN1OctetString { val attestationVersion = ASN1Integer(AndroidUtils.attestVersion.toLong()) val attestationSecurityLevel = ASN1Enumerated(securityLevel) @@ -413,18 +484,19 @@ object CertificateGen { 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 keyDescriptionEncodables = + arrayOf( + attestationVersion, + attestationSecurityLevel, + keymasterVersion, + keymasterSecurityLevel, + attestationChallenge, + uniqueId, + softwareEnforced, + teeEnforced, + ) + val keyDescriptionSeq = DERSequence(keyDescriptionEncodables) return DEROctetString(keyDescriptionSeq.encoded) } @@ -432,40 +504,51 @@ object CertificateGen { @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 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) + 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 } - 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 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) - ) + val applicationIdArray = arrayOf(DERSet(packageInfoArray), DERSet(signaturesArray)) return DEROctetString(DERSequence(applicationIdArray).encoded) } diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateHack.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateHack.kt index b129d12..ad70b0a 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateHack.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateHack.kt @@ -7,6 +7,12 @@ package io.github.beakthoven.TrickyStoreOSS import io.github.beakthoven.TrickyStoreOSS.config.PkgConfig import io.github.beakthoven.TrickyStoreOSS.logging.Logger +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 import org.bouncycastle.asn1.ASN1Boolean import org.bouncycastle.asn1.ASN1Encodable import org.bouncycastle.asn1.ASN1EncodableVector @@ -22,14 +28,8 @@ 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 { +object CertificateHack { private val certificateFactory: CertificateFactory by lazy { try { CertificateFactory.getInstance("X.509") @@ -38,40 +38,39 @@ object CertificateHack { throw RuntimeException("Cannot initialize certificate factory", t) } } - - data class KeyIdentifier( - val alias: String, - val uid: Int - ) + + data class KeyIdentifier(val alias: String, val uid: Int) val leafAlgorithms = ConcurrentHashMap() - + fun clearLeafAlgorithms() { leafAlgorithms.clear() } - + fun hackCertificateChain(certificateChain: Array?, uid: Int): 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 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) { @@ -80,86 +79,98 @@ object CertificateHack { vector.add(taggedObject) } } - + val keyboxFileName = PkgConfig.getKeyboxFileForUid(uid) val algorithmName = leaf.publicKey.algorithm - val keybox = KeyBoxUtils.getKeybox(keyboxFileName, algorithmName) ?: throw UnsupportedOperationException("Unsupported algorithm '$algorithmName' in keybox '$keyboxFileName'") - + val keybox = + KeyBoxUtils.getKeybox(keyboxFileName, algorithmName) + ?: throw UnsupportedOperationException( + "Unsupported algorithm '$algorithmName' in keybox '$keyboxFileName'" + ) + val certificates = LinkedList(keybox.certificates) - val builder = X509v3CertificateBuilder( - X509CertificateHolder(certificates[0].encoded).subject, - leafHolder.serialNumber, - leafHolder.notBefore, - leafHolder.notAfter, - leafHolder.subject, - leafHolder.subjectPublicKeyInfo - ) - + 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.addFirst( + JcaX509CertificateConverter().getCertificate(builder.build(signer)) + ) certificates.toTypedArray() } catch (t: Throwable) { Logger.e("Failed to hack certificate chain for uid=$uid", 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 algorithm = + leafAlgorithms.remove(key) + ?: throw UnsupportedOperationException("No algorithm found for key $key") + val keyboxFileName = PkgConfig.getKeyboxFileForUid(uid) - val keybox = KeyBoxUtils.getKeybox(keyboxFileName, algorithm) - ?: throw UnsupportedOperationException("Unsupported algorithm '$algorithm' in keybox '$keyboxFileName'") - + val keybox = + KeyBoxUtils.getKeybox(keyboxFileName, algorithm) + ?: throw UnsupportedOperationException( + "Unsupported algorithm '$algorithm' in keybox '$keyboxFileName'" + ) + CertificateUtils.run { keybox.certificates.toByteArray() } ?: caList } catch (t: Throwable) { Logger.e("Failed to hack CA certificate chain for uid=$uid", 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 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) { @@ -168,26 +179,30 @@ object CertificateHack { vector.add(taggedObject) } } - + val keyboxFileName = PkgConfig.getKeyboxFileForUid(uid) val algorithmName = leaf.publicKey.algorithm - val keybox = KeyBoxUtils.getKeybox(keyboxFileName, algorithmName) - ?: throw UnsupportedOperationException("Unsupported algorithm '$algorithmName' in keybox '$keyboxFileName'") - - val builder = X509v3CertificateBuilder( - X509CertificateHolder(keybox.certificates[0].encoded).subject, - leafHolder.serialNumber, - leafHolder.notBefore, - leafHolder.notAfter, - leafHolder.subject, - leafHolder.subjectPublicKeyInfo - ) - + val keybox = + KeyBoxUtils.getKeybox(keyboxFileName, algorithmName) + ?: throw UnsupportedOperationException( + "Unsupported algorithm '$algorithmName' in keybox '$keyboxFileName'" + ) + + 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)) @@ -200,47 +215,52 @@ object CertificateHack { certificate } } - + private fun hackAttestExtension( originalRootOfTrust: ASN1Encodable?, vector: ASN1EncodableVector, - originalEncodables: Array + originalEncodables: Array, ): Extension { val verifiedBootKey = AndroidUtils.bootKey var verifiedBootHash: ByteArray? = null - + try { if (originalRootOfTrust is ASN1Sequence) { - verifiedBootHash = CertificateUtils.getByteArrayFromAsn1(originalRootOfTrust.getObjectAt(3)) + 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 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, 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) } - } diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateUtils.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateUtils.kt index dc8dfca..1166a60 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateUtils.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateUtils.kt @@ -9,12 +9,6 @@ import android.system.keystore2.KeyEntryResponse import android.system.keystore2.KeyMetadata import android.util.Log import io.github.beakthoven.TrickyStoreOSS.CertificateUtils.putCertificateChain -import org.bouncycastle.asn1.ASN1Encodable -import org.bouncycastle.asn1.DEROctetString -import org.bouncycastle.openssl.PEMKeyPair -import org.bouncycastle.openssl.PEMParser -import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter -import org.bouncycastle.util.io.pem.PemReader import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.StringReader @@ -24,30 +18,41 @@ import java.security.cert.CertificateException import java.security.cert.CertificateFactory import java.security.cert.CertificateParsingException import java.security.cert.X509Certificate +import org.bouncycastle.asn1.ASN1Encodable +import org.bouncycastle.asn1.DEROctetString +import org.bouncycastle.openssl.PEMKeyPair +import org.bouncycastle.openssl.PEMParser +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter +import org.bouncycastle.util.io.pem.PemReader object CertificateUtils { private const val TAG = "TrickyStoreOSS" - + sealed class CertificateResult { data class Success(val data: T) : CertificateResult() - data class Error(val message: String, val cause: Throwable? = null) : CertificateResult() - - inline fun map(transform: (T) -> R): CertificateResult = when (this) { - is Success -> Success(transform(data)) - is Error -> this - } - - fun getOrNull(): T? = when (this) { - is Success -> data - is Error -> null - } + + data class Error(val message: String, val cause: Throwable? = null) : + CertificateResult() + + inline fun map(transform: (T) -> R): CertificateResult = + when (this) { + is Success -> Success(transform(data)) + is Error -> this + } + + fun getOrNull(): T? = + when (this) { + is Success -> data + is Error -> null + } } - + sealed class ParseResult { data class Success(val data: T) : ParseResult() + data class Error(val message: String, val cause: Throwable? = null) : ParseResult() } - + fun ByteArray?.toCertificate(): X509Certificate? { return this?.let { bytes -> try { @@ -59,73 +64,74 @@ object CertificateUtils { } } } - + fun ByteArray.toCertificateResult(): CertificateResult { return try { val certFactory = CertificateFactory.getInstance("X.509") - val certificate = certFactory.generateCertificate(ByteArrayInputStream(this)) as X509Certificate + val certificate = + certFactory.generateCertificate(ByteArrayInputStream(this)) as X509Certificate CertificateResult.Success(certificate) } catch (e: CertificateException) { CertificateResult.Error("Failed to parse certificate", e) } } - + @Suppress("UNCHECKED_CAST") fun ByteArray?.toCertificates(): Collection { return this?.let { bytes -> try { val certFactory = CertificateFactory.getInstance("X.509") - certFactory.generateCertificates(ByteArrayInputStream(bytes)) as Collection + certFactory.generateCertificates(ByteArrayInputStream(bytes)) + as Collection } catch (e: CertificateException) { Log.w(TAG, "Couldn't parse certificates in keystore", e) emptyList() } } ?: emptyList() } - - fun Collection.toByteArray(): ByteArray? = runCatching { - ByteArrayOutputStream().use { outputStream -> - forEach { cert -> outputStream.write(cert.encoded) } - outputStream.toByteArray() - } - }.onFailure { - Log.w(TAG, "Failed to convert certificates to byte array", it) - }.getOrNull() - - fun Collection.toByteArrayList(): List? = runCatching { - map { it.encoded } - }.onFailure { - Log.w(TAG, "Failed to convert certificates to byte array list", it) - }.getOrNull() - + + fun Collection.toByteArray(): ByteArray? = + runCatching { + ByteArrayOutputStream().use { outputStream -> + forEach { cert -> outputStream.write(cert.encoded) } + outputStream.toByteArray() + } + } + .onFailure { Log.w(TAG, "Failed to convert certificates to byte array", it) } + .getOrNull() + + fun Collection.toByteArrayList(): List? = + runCatching { map { it.encoded } } + .onFailure { Log.w(TAG, "Failed to convert certificates to byte array list", it) } + .getOrNull() + fun KeyEntryResponse?.getCertificateChain(): Array? { val metadata = this?.metadata ?: return null val leafCert = metadata.certificate?.toCertificate() ?: return null - + return when (val chainBytes = metadata.certificateChain) { null -> arrayOf(leafCert) else -> { val additionalCerts = chainBytes.toCertificates() buildList { - add(leafCert) - addAll(additionalCerts) - }.toTypedArray() + add(leafCert) + addAll(additionalCerts) + } + .toTypedArray() } } } - + fun KeyEntryResponse.putCertificateChain(chain: Array): Result { - return runCatching { - metadata.putCertificateChain(chain) - } + return runCatching { metadata.putCertificateChain(chain) } } - + fun KeyMetadata.putCertificateChain(chain: Array): Result { return runCatching { if (chain.isEmpty()) return@runCatching - + certificate = chain[0].encoded - + if (chain.size > 1) { ByteArrayOutputStream().use { output -> for (i in 1 until chain.size) { @@ -138,7 +144,7 @@ object CertificateUtils { } } } - + // Certificate parsing utilities fun parseKeyPair(keyContent: String): ParseResult { return try { @@ -154,54 +160,59 @@ object CertificateUtils { ParseResult.Error("Failed to parse PEM key pair", t) } } - + fun parseCertificate(certContent: String): ParseResult { return try { PemReader(StringReader(certContent.trimLine())).use { reader -> val pemObject = reader.readPemObject() - val certificate = CertificateFactory.getInstance("X.509").generateCertificate( - ByteArrayInputStream(pemObject.content) - ) + val certificate = + CertificateFactory.getInstance("X.509") + .generateCertificate(ByteArrayInputStream(pemObject.content)) ParseResult.Success(certificate) } } catch (t: Throwable) { ParseResult.Error("Failed to parse certificate", t) } } - + fun convertPemToKeyPair(pemKeyPair: PEMKeyPair): KeyPair { return JcaPEMKeyConverter().getKeyPair(pemKeyPair) } - + @Throws(CertificateParsingException::class) fun getByteArrayFromAsn1(asn1Encodable: ASN1Encodable): ByteArray { return when (asn1Encodable) { is DEROctetString -> asn1Encodable.octets - else -> throw CertificateParsingException("Expected DEROctetString, got ${asn1Encodable::class.simpleName}") + else -> + throw CertificateParsingException( + "Expected DEROctetString, got ${asn1Encodable::class.simpleName}" + ) } } } -fun ByteArray?.toX509Certificate(): X509Certificate? = CertificateUtils.run { this@toX509Certificate.toCertificate() } +fun ByteArray?.toX509Certificate(): X509Certificate? = + CertificateUtils.run { this@toX509Certificate.toCertificate() } -fun ByteArray?.toX509Certificates(): Collection = CertificateUtils.run { this@toX509Certificates.toCertificates() } +fun ByteArray?.toX509Certificates(): Collection = + CertificateUtils.run { this@toX509Certificates.toCertificates() } -fun Collection.encodedBytes(): ByteArray? = CertificateUtils.run { this@encodedBytes.toByteArray() } +fun Collection.encodedBytes(): ByteArray? = + CertificateUtils.run { this@encodedBytes.toByteArray() } -fun Collection.encodedBytesList(): List? = CertificateUtils.run { this@encodedBytesList.toByteArrayList() } +fun Collection.encodedBytesList(): List? = + CertificateUtils.run { this@encodedBytesList.toByteArrayList() } fun KeyEntryResponse.putCertificateChain(chain: Array): Result { - return runCatching { - metadata.putCertificateChain(chain).getOrThrow() - } + return runCatching { metadata.putCertificateChain(chain).getOrThrow() } } fun KeyMetadata.putCertificateChain(chain: Array): Result { return runCatching { if (chain.isEmpty()) return@runCatching - + certificate = chain[0].encoded - + if (chain.size > 1) { ByteArrayOutputStream().use { output -> for (i in 1 until chain.size) { @@ -213,4 +224,4 @@ fun KeyMetadata.putCertificateChain(chain: Array): Result { certificateChain = 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 9150641..077a966 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/Main.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/Main.kt @@ -16,7 +16,7 @@ private const val SERVICE_SLEEP_MS = 1000000L fun main(args: Array) { Logger.i("Welcome to TrickyStoreOSS!") - + try { AndroidUtils.setupBootHash() initializeInterceptors() @@ -29,26 +29,27 @@ fun main(args: Array) { private fun initializeInterceptors() { val interceptor = selectKeystoreInterceptor() - + while (!interceptor.tryRunKeystoreInterceptor()) { Logger.d("Retrying interceptor initialization...") Thread.sleep(RETRY_DELAY_MS) } - + PkgConfig.initialize() Logger.i("Interceptors initialized successfully") } -private fun selectKeystoreInterceptor() = when { - Build.VERSION.SDK_INT in Build.VERSION_CODES.Q..Build.VERSION_CODES.R -> { - Logger.i("Using KeystoreInterceptor for Android Q/R (SDK ${Build.VERSION.SDK_INT})") - KeystoreInterceptor +private fun selectKeystoreInterceptor() = + when { + Build.VERSION.SDK_INT in Build.VERSION_CODES.Q..Build.VERSION_CODES.R -> { + Logger.i("Using KeystoreInterceptor for Android Q/R (SDK ${Build.VERSION.SDK_INT})") + KeystoreInterceptor + } + else -> { + Logger.i("Using Keystore2Interceptor for Android S+ (SDK ${Build.VERSION.SDK_INT})") + Keystore2Interceptor + } } - else -> { - Logger.i("Using Keystore2Interceptor for Android S+ (SDK ${Build.VERSION.SDK_INT})") - Keystore2Interceptor - } -} private fun maintainService() { Logger.i("Service started, entering maintenance mode") 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 7f6cfa9..3a88c51 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/XmlParser.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/XmlParser.kt @@ -10,29 +10,30 @@ import io.github.beakthoven.TrickyStoreOSS.CertificateGen.KeyBox import io.github.beakthoven.TrickyStoreOSS.CertificateHack.clearLeafAlgorithms import io.github.beakthoven.TrickyStoreOSS.config.PkgConfig 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.File import java.io.IOException import java.io.StringReader import java.security.cert.Certificate import java.util.concurrent.ConcurrentHashMap +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import org.xmlpull.v1.XmlPullParserFactory class XmlParser(private val xmlContent: String) { - + sealed class ParseResult { data class Success(val attributes: Map) : ParseResult() + data class Error(val message: String, val cause: Throwable? = null) : ParseResult() } - + fun obtainPath(path: String): ParseResult { return try { val factory = XmlPullParserFactory.newInstance() val parser = factory.newPullParser() parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false) parser.setInput(StringReader(xmlContent)) - + val tags = path.split(".").toTypedArray() val result = readData(parser, tags, 0, mutableMapOf()) ParseResult.Success(result) @@ -44,7 +45,7 @@ class XmlParser(private val xmlContent: String) { ParseResult.Error("Unexpected error: ${e.message}", e) } } - + @Throws(Exception::class) fun obtainPathLegacy(path: String): Map { when (val result = obtainPath(path)) { @@ -52,24 +53,24 @@ class XmlParser(private val xmlContent: String) { is ParseResult.Error -> throw result.cause ?: Exception(result.message) } } - + @Throws(IOException::class, XmlPullParserException::class) private fun readData( parser: XmlPullParser, tags: Array, index: Int, - tagCounts: MutableMap + tagCounts: MutableMap, ): Map { while (parser.next() != XmlPullParser.END_DOCUMENT) { if (parser.eventType != XmlPullParser.START_TAG) { continue } - + val currentTag = parser.name ?: continue val targetTag = tags[index] val tagParts = targetTag.split("[") val baseTagName = tagParts[0] - + if (currentTag == baseTagName) { return if (tagParts.size > 1) { handleIndexedTag(parser, tags, index, tagCounts, currentTag, tagParts[1]) @@ -80,10 +81,10 @@ class XmlParser(private val xmlContent: String) { skipCurrentElement(parser) } } - + throw XmlPullParserException("Path not found: ${tags.joinToString(".")}") } - + @Throws(IOException::class, XmlPullParserException::class) private fun handleIndexedTag( parser: XmlPullParser, @@ -91,13 +92,14 @@ class XmlParser(private val xmlContent: String) { index: Int, tagCounts: MutableMap, currentTag: String, - indexPart: String + indexPart: String, ): Map { - val targetIndex = indexPart.replace("]", "").toIntOrNull() - ?: throw XmlPullParserException("Invalid index in tag: $indexPart") - + val targetIndex = + indexPart.replace("]", "").toIntOrNull() + ?: throw XmlPullParserException("Invalid index in tag: $indexPart") + val currentCount = tagCounts.getOrDefault(currentTag, 0) - + return if (currentCount < targetIndex) { tagCounts[currentTag] = currentCount + 1 readData(parser, tags, index, tagCounts) @@ -109,12 +111,12 @@ class XmlParser(private val xmlContent: String) { } } } - + @Throws(IOException::class, XmlPullParserException::class) private fun handleRegularTag( parser: XmlPullParser, tags: Array, - index: Int + index: Int, ): Map { return if (index == tags.size - 1) { readAttributes(parser) @@ -122,11 +124,11 @@ class XmlParser(private val xmlContent: String) { readData(parser, tags, index + 1, mutableMapOf()) } } - + @Throws(IOException::class, XmlPullParserException::class) private fun readAttributes(parser: XmlPullParser): Map { val attributes = mutableMapOf() - + for (i in 0 until parser.attributeCount) { val name = parser.getAttributeName(i) val value = parser.getAttributeValue(i) @@ -134,22 +136,20 @@ class XmlParser(private val xmlContent: String) { attributes[name] = value } } - + if (parser.next() == XmlPullParser.TEXT) { - parser.text?.let { text -> - attributes["text"] = text - } + parser.text?.let { text -> attributes["text"] = text } } - + return attributes } - + @Throws(XmlPullParserException::class, IOException::class) private fun skipCurrentElement(parser: XmlPullParser) { if (parser.eventType != XmlPullParser.START_TAG) { throw IllegalStateException("Parser must be positioned at START_TAG") } - + var depth = 1 while (depth != 0) { when (parser.next()) { @@ -164,18 +164,19 @@ object KeyBoxUtils { private val loadedKeyboxFiles = ConcurrentHashMap>() /** - * The primary public function to get a specific KeyBox for a given algorithm and file. - * It will load and cache the file on demand if it hasn't been seen before. + * The primary public function to get a specific KeyBox for a given algorithm and file. It will + * load and cache the file on demand if it hasn't been seen before. * * @param keyboxFileName The simple name of the keybox file (e.g., "keybox.xml"). * @param algorithm The algorithm key (e.g., KeyProperties.KEY_ALGORITHM_EC). * @return The requested KeyBox, or null if not found in the specified file. */ fun getKeybox(keyboxFileName: String, algorithm: String): KeyBox? { - val keyboxesForFile = loadedKeyboxFiles.getOrPut(keyboxFileName) { - // If this file is not in our cache, load it now. - readFromFile(keyboxFileName) - } + val keyboxesForFile = + loadedKeyboxFiles.getOrPut(keyboxFileName) { + // If this file is not in our cache, load it now. + readFromFile(keyboxFileName) + } Logger.i("Retriving keybox $keyboxFileName [$algorithm]") return keyboxesForFile[algorithm] } @@ -196,10 +197,16 @@ object KeyBoxUtils { val xmlParser = XmlParser(xmlData.sanitizeXml()) val numberOfKeyboxesResult = xmlParser.obtainPath("AndroidAttestation.NumberOfKeyboxes") - val numberOfKeyboxes = when (numberOfKeyboxesResult) { - is XmlParser.ParseResult.Success -> numberOfKeyboxesResult.attributes["text"]?.toIntOrNull() ?: 1 - is XmlParser.ParseResult.Error -> throw Exception(numberOfKeyboxesResult.message, numberOfKeyboxesResult.cause) - } + val numberOfKeyboxes = + when (numberOfKeyboxesResult) { + is XmlParser.ParseResult.Success -> + numberOfKeyboxesResult.attributes["text"]?.toIntOrNull() ?: 1 + is XmlParser.ParseResult.Error -> + throw Exception( + numberOfKeyboxesResult.message, + numberOfKeyboxesResult.cause, + ) + } repeat(numberOfKeyboxes) { i -> val (algorithmName, keyBox) = processKeybox(xmlParser, i) @@ -214,9 +221,11 @@ object KeyBoxUtils { return keyboxes } - fun hasKeyboxes(): Boolean = loadedKeyboxFiles.isNotEmpty() && loadedKeyboxFiles.values.any { it.isNotEmpty() } + fun hasKeyboxes(): Boolean = + loadedKeyboxFiles.isNotEmpty() && loadedKeyboxFiles.values.any { it.isNotEmpty() } - // This function is now deprecated and should be removed. We keep it for now to show the transition. + // This function is now deprecated and should be removed. We keep it for now to show the + // transition. // Its logic is now inside readFromFile. @Deprecated("Use getKeybox(fileName, algorithm) instead for dynamic loading.") fun readFromXml(xmlData: String?) { @@ -224,26 +233,30 @@ object KeyBoxUtils { // We could make this load into a default keybox for backward compatibility if needed. loadedKeyboxFiles.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) - } - + 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) @@ -253,11 +266,7 @@ object KeyBoxUtils { private fun String.sanitizeXml(): String { var content = this - val boms = listOf( - "\uFEFF", - "\uFFFE", - "\u0000\uFEFF" - ) + val boms = listOf("\uFEFF", "\uFFFE", "\u0000\uFEFF") content = content.trimStart() for (bom in boms) { content = content.removePrefix(bom) @@ -270,60 +279,82 @@ object KeyBoxUtils { private fun processKeybox(xmlParser: XmlParser, index: Int): Pair { 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 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) - } - + 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) + 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 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 - } - + + val algorithmName = + when (keyboxAlgorithm.lowercase()) { + "ecdsa" -> KeyProperties.KEY_ALGORITHM_EC + "rsa" -> KeyProperties.KEY_ALGORITHM_RSA + else -> keyboxAlgorithm + } + return algorithmName to KeyBox(pemKeyPair, keyPair, certificateChain) - } catch (t: Throwable) { Logger.e("Error processing keybox $index", t) throw t diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/config/Config.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/config/Config.kt index 740a83f..829a7b4 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/config/Config.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/config/Config.kt @@ -25,71 +25,79 @@ object PkgConfig { private val keyboxRegex = Regex("^\\[([a-zA-Z0-9_.-]+\\.xml)]$") private const val DEFAULT_KEYBOX_FILE = "keybox.xml" - fun getKeyboxFileForUid(callingUid: Int): String = runCatching { - val ps = getPm()?.getPackagesForUid(callingUid) ?: return DEFAULT_KEYBOX_FILE - for (pkg in ps) { - packageKeyboxes[pkg]?.let { return it } - } - return DEFAULT_KEYBOX_FILE - }.getOrDefault(DEFAULT_KEYBOX_FILE) + fun getKeyboxFileForUid(callingUid: Int): String = + runCatching { + val ps = getPm()?.getPackagesForUid(callingUid) ?: return DEFAULT_KEYBOX_FILE + for (pkg in ps) { + packageKeyboxes[pkg]?.let { + return it + } + } + return DEFAULT_KEYBOX_FILE + } + .getOrDefault(DEFAULT_KEYBOX_FILE) enum class Mode { - AUTO, LEAF_HACK, GENERATE + AUTO, + LEAF_HACK, + GENERATE, } - private fun updateTargetPackages(f: File?) = runCatching { - hackPackages.clear() - generatePackages.clear() - packageModes.clear() - packageKeyboxes.clear() + private fun updateTargetPackages(f: File?) = + runCatching { + hackPackages.clear() + generatePackages.clear() + packageModes.clear() + packageKeyboxes.clear() - var currentKeyboxFile = DEFAULT_KEYBOX_FILE + var currentKeyboxFile = DEFAULT_KEYBOX_FILE - f?.readLines()?.forEach { line -> - val n = line.trim() - if (n.isBlank() || n.startsWith("#")) { - return@forEach // Skip comments and empty lines - } + f?.readLines()?.forEach { line -> + val n = line.trim() + if (n.isBlank() || n.startsWith("#")) { + return@forEach // Skip comments and empty lines + } - val matchResult = keyboxRegex.find(n) - if (matchResult != null) { - currentKeyboxFile = matchResult.groupValues[1] - Logger.i("Switched to keybox file: $currentKeyboxFile for subsequent packages") - return@forEach - } + val matchResult = keyboxRegex.find(n) + if (matchResult != null) { + currentKeyboxFile = matchResult.groupValues[1] + Logger.i( + "Switched to keybox file: $currentKeyboxFile for subsequent packages" + ) + return@forEach + } - when { - n.endsWith("!") -> { - val pkg = n.removeSuffix("!").trim() - generatePackages.add(pkg) - packageModes[pkg] = Mode.GENERATE - packageKeyboxes[pkg] = currentKeyboxFile - } - n.endsWith("?") -> { - val pkg = n.removeSuffix("?").trim() - hackPackages.add(pkg) - packageModes[pkg] = Mode.LEAF_HACK - packageKeyboxes[pkg] = currentKeyboxFile - } - else -> { - // Auto mode - packageModes[n] = Mode.AUTO - packageKeyboxes[n] = currentKeyboxFile + when { + n.endsWith("!") -> { + val pkg = n.removeSuffix("!").trim() + generatePackages.add(pkg) + packageModes[pkg] = Mode.GENERATE + packageKeyboxes[pkg] = currentKeyboxFile + } + n.endsWith("?") -> { + val pkg = n.removeSuffix("?").trim() + hackPackages.add(pkg) + packageModes[pkg] = Mode.LEAF_HACK + packageKeyboxes[pkg] = currentKeyboxFile + } + else -> { + // Auto mode + packageModes[n] = Mode.AUTO + packageKeyboxes[n] = currentKeyboxFile + } + } } + Logger.i( + "update hack packages: $hackPackages, generate packages=$generatePackages, packageModes=$packageModes, , packageKeyboxes=$packageKeyboxes" + ) } - } - Logger.i("update hack packages: $hackPackages, generate packages=$generatePackages, packageModes=$packageModes, , packageKeyboxes=$packageKeyboxes") - }.onFailure { - Logger.e("failed to update target files", it) - } + .onFailure { Logger.e("failed to update target files", it) } // This function is now deprecated in favor of a more dynamic approach, but kept for simplicity. // The key logic is now in KeyBoxUtils which will be called from the interceptors. - private fun updateKeyBox(f: File?) = runCatching { - KeyBoxUtils.readFromXml(f?.readText()) - }.onFailure { - Logger.e("failed to update keybox", it) - } + private fun updateKeyBox(f: File?) = + runCatching { KeyBoxUtils.readFromXml(f?.readText()) } + .onFailure { Logger.e("failed to update keybox", it) } const val CONFIG_PATH = "/data/adb/tricky_store" private const val TARGET_FILE = "target.txt" @@ -97,15 +105,14 @@ object PkgConfig { private const val PATCHLEVEL_FILE = "security_patch.txt" private val root = File(CONFIG_PATH) - @Volatile - private var teeBroken: Boolean? = null + @Volatile private var teeBroken: Boolean? = null private fun storeTEEStatus(root: File) { val statusFile = File(root, TEE_STATUS_FILE) teeBroken = !TEEStatus try { statusFile.writeText("teeBroken=${teeBroken}") - Logger.i("TEE status written to $statusFile: teeBroken=$teeBroken") + Logger.i("TEE status written to $statusFile: teeBroken=$teeBroken") } catch (e: Exception) { Logger.e("Failed to write TEE status: ${e.message}") } @@ -124,16 +131,21 @@ object PkgConfig { object ConfigObserver : FileObserver(root, CLOSE_WRITE or DELETE or MOVED_FROM or MOVED_TO) { override fun onEvent(event: Int, path: String?) { path ?: return - val f = when (event) { - CLOSE_WRITE, MOVED_TO -> File(root, path) - DELETE, MOVED_FROM -> null - else -> return - } + val f = + when (event) { + CLOSE_WRITE, + MOVED_TO -> File(root, path) + DELETE, + MOVED_FROM -> null + else -> return + } when { path == TARGET_FILE -> updateTargetPackages(f) path.endsWith(".xml") -> { - // This is a simplification. A more robust solution would be to reload the specific keybox if it's in use. - // For now, we assume any XML change might affect the active keyboxes, prompting a reload where needed. + // This is a simplification. A more robust solution would be to reload the + // specific keybox if it's in use. + // For now, we assume any XML change might affect the active keyboxes, prompting + // a reload where needed. // The main logic for loading is now handled dynamically in KeyBoxUtils. Logger.i("Keybox file $path changed. It will be re-read on next use.") } @@ -163,12 +175,13 @@ object PkgConfig { } private var iPm: IPackageManager? = null - private val packageManagerDeathRecipient = object : IBinder.DeathRecipient { - override fun binderDied() { - (iPm as? IInterface)?.asBinder()?.unlinkToDeath(this, 0) - iPm = null + private val packageManagerDeathRecipient = + object : IBinder.DeathRecipient { + override fun binderDied() { + (iPm as? IInterface)?.asBinder()?.unlinkToDeath(this, 0) + iPm = null + } } - } fun getPm(): IPackageManager? { if (iPm == null) { @@ -179,72 +192,83 @@ object PkgConfig { return iPm } - fun needHack(callingUid: Int): Boolean = kotlin.runCatching { - val ps = getPm()?.getPackagesForUid(callingUid) ?: return false - if (teeBroken == null) loadTEEStatus(root) - for (pkg in ps) { - when (packageModes[pkg]) { - Mode.LEAF_HACK -> return true - Mode.AUTO -> { - if (teeBroken == false) return true + fun needHack(callingUid: Int): Boolean = + kotlin + .runCatching { + val ps = getPm()?.getPackagesForUid(callingUid) ?: return false + if (teeBroken == null) loadTEEStatus(root) + for (pkg in ps) { + when (packageModes[pkg]) { + Mode.LEAF_HACK -> return true + Mode.AUTO -> { + if (teeBroken == false) return true + } + else -> {} + } } - else -> {} + return false } - } - return false - }.onFailure { Logger.e("failed to get packages", it) }.getOrNull() ?: false + .onFailure { Logger.e("failed to get packages", it) } + .getOrNull() ?: false - fun needGenerate(callingUid: Int): Boolean = kotlin.runCatching { - val ps = getPm()?.getPackagesForUid(callingUid) ?: return false - if (teeBroken == null) loadTEEStatus(root) - for (pkg in ps) { - when (packageModes[pkg]) { - Mode.GENERATE -> return true - Mode.AUTO -> { - if (teeBroken == true) return true + fun needGenerate(callingUid: Int): Boolean = + kotlin + .runCatching { + val ps = getPm()?.getPackagesForUid(callingUid) ?: return false + if (teeBroken == null) loadTEEStatus(root) + for (pkg in ps) { + when (packageModes[pkg]) { + Mode.GENERATE -> return true + Mode.AUTO -> { + if (teeBroken == true) return true + } + else -> {} + } } - else -> {} + return false } - } - return false - }.onFailure { Logger.e("failed to get packages", it) }.getOrNull() ?: false + .onFailure { Logger.e("failed to get packages", it) } + .getOrNull() ?: false - @Volatile - var _customPatchLevel: CustomPatchLevel? = null + @Volatile var _customPatchLevel: CustomPatchLevel? = null - fun updatePatchLevel(f: File?) = runCatching { - if (f == null || !f.exists()) { - _customPatchLevel = null - return@runCatching - } - val lines = f.readLines().map { it.trim() }.filter { it.isNotEmpty() && !it.startsWith("#") } - if (lines.isEmpty()) { - _customPatchLevel = null - return@runCatching - } - if (lines.size == 1 && !lines[0].contains("=")) { - _customPatchLevel = CustomPatchLevel(all = lines[0]) - return@runCatching - } - val map = mutableMapOf() - for (line in lines) { - val idx = line.indexOf('=') - if (idx > 0) { - val key = line.substring(0, idx).trim().lowercase() - val value = line.substring(idx + 1).trim() - map[key] = value + fun updatePatchLevel(f: File?) = + runCatching { + if (f == null || !f.exists()) { + _customPatchLevel = null + return@runCatching + } + val lines = + f.readLines() + .map { it.trim() } + .filter { it.isNotEmpty() && !it.startsWith("#") } + if (lines.isEmpty()) { + _customPatchLevel = null + return@runCatching + } + if (lines.size == 1 && !lines[0].contains("=")) { + _customPatchLevel = CustomPatchLevel(all = lines[0]) + return@runCatching + } + val map = mutableMapOf() + for (line in lines) { + val idx = line.indexOf('=') + if (idx > 0) { + val key = line.substring(0, idx).trim().lowercase() + val value = line.substring(idx + 1).trim() + map[key] = value + } + } + val all = map["all"] + _customPatchLevel = + CustomPatchLevel( + system = map["system"] ?: all, + vendor = map["vendor"] ?: all, + boot = map["boot"] ?: all, + all = all, + ) } - } - val all = map["all"] - _customPatchLevel = CustomPatchLevel( - system = map["system"] ?: all, - vendor = map["vendor"] ?: all, - boot = map["boot"] ?: all, - all = all - ) - }.onFailure { - Logger.e("failed to update patch level", it) - } + .onFailure { Logger.e("failed to update patch level", it) } private fun waitAndGetSystemService(name: String): IBinder? { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { @@ -270,5 +294,5 @@ data class CustomPatchLevel( val system: String? = null, val vendor: String? = null, val boot: String? = null, - val all: String? = null + val all: String? = null, ) 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 808d504..154a820 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 @@ -11,34 +11,34 @@ import android.os.Parcel import io.github.beakthoven.TrickyStoreOSS.logging.Logger open class BinderInterceptor : Binder() { - + sealed class Result - + data object Skip : Result() - + data object Continue : Result() - + data class OverrideData(val data: Parcel) : Result() - + data class OverrideReply(val code: Int = 0, val reply: Parcel) : Result() companion object { private const val BACKDOOR_TRANSACTION_CODE = 0xdeadbeef.toInt() - + private const val REGISTER_INTERCEPTOR_CODE = 1 - + private const val PRE_TRANSACT_CODE = 1 private const val POST_TRANSACT_CODE = 2 - + private const val RESULT_SKIP = 1 private const val RESULT_CONTINUE = 2 private const val RESULT_OVERRIDE_REPLY = 3 private const val RESULT_OVERRIDE_DATA = 4 - + fun getBinderBackdoor(binder: IBinder): IBinder? { val data = Parcel.obtain() val reply = Parcel.obtain() - + return try { val success = binder.transact(BACKDOOR_TRANSACTION_CODE, data, reply, 0) if (success) { @@ -58,13 +58,13 @@ open class BinderInterceptor : Binder() { } fun registerBinderInterceptor( - backdoor: IBinder, - target: IBinder, - interceptor: BinderInterceptor + backdoor: IBinder, + target: IBinder, + interceptor: BinderInterceptor, ) { val data = Parcel.obtain() val reply = Parcel.obtain() - + try { data.writeStrongBinder(target) data.writeStrongBinder(interceptor) @@ -80,36 +80,37 @@ open class BinderInterceptor : Binder() { } open fun onPreTransact( - target: IBinder, - code: Int, - flags: Int, - callingUid: Int, - callingPid: Int, - data: Parcel + target: IBinder, + code: Int, + flags: Int, + callingUid: Int, + callingPid: Int, + data: Parcel, ): Result = Skip - + open fun onPostTransact( - target: IBinder, - code: Int, - flags: Int, - callingUid: Int, - callingPid: Int, - data: Parcel, - reply: Parcel?, - resultCode: Int + target: IBinder, + code: Int, + flags: Int, + callingUid: Int, + callingPid: Int, + data: Parcel, + reply: Parcel?, + resultCode: Int, ): Result = Skip override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { - val result = when (code) { - PRE_TRANSACT_CODE -> handlePreTransact(data) - POST_TRANSACT_CODE -> handlePostTransact(data) - else -> return super.onTransact(code, data, reply, flags) - } - + val result = + when (code) { + PRE_TRANSACT_CODE -> handlePreTransact(data) + POST_TRANSACT_CODE -> handlePostTransact(data) + else -> return super.onTransact(code, data, reply, flags) + } + writeResultToReply(result, reply!!) return true } - + private fun handlePreTransact(data: Parcel): Result { val target = data.readStrongBinder() val transactionCode = data.readInt() @@ -117,17 +118,24 @@ open class BinderInterceptor : Binder() { val callingUid = data.readInt() val callingPid = data.readInt() val dataSize = data.readLong() - + val transactionData = Parcel.obtain() return try { transactionData.appendFrom(data, data.dataPosition(), dataSize.toInt()) transactionData.setDataPosition(0) - onPreTransact(target, transactionCode, transactionFlags, callingUid, callingPid, transactionData) + onPreTransact( + target, + transactionCode, + transactionFlags, + callingUid, + callingPid, + transactionData, + ) } finally { transactionData.recycle() } } - + private fun handlePostTransact(data: Parcel): Result { val target = data.readStrongBinder() val transactionCode = data.readInt() @@ -135,30 +143,40 @@ open class BinderInterceptor : Binder() { val callingUid = data.readInt() val callingPid = data.readInt() val resultCode = data.readInt() - + val transactionData = Parcel.obtain() val transactionReply = Parcel.obtain() - + return try { val dataSize = data.readLong().toInt() transactionData.appendFrom(data, data.dataPosition(), dataSize) transactionData.setDataPosition(0) data.setDataPosition(data.dataPosition() + dataSize) - + val replySize = data.readLong().toInt() - val reply = if (replySize > 0) { - transactionReply.appendFrom(data, data.dataPosition(), replySize) - transactionReply.setDataPosition(0) - transactionReply - } else null - - onPostTransact(target, transactionCode, transactionFlags, callingUid, callingPid, transactionData, reply, resultCode) + val reply = + if (replySize > 0) { + transactionReply.appendFrom(data, data.dataPosition(), replySize) + transactionReply.setDataPosition(0) + transactionReply + } else null + + onPostTransact( + target, + transactionCode, + transactionFlags, + callingUid, + callingPid, + transactionData, + reply, + resultCode, + ) } finally { transactionData.recycle() transactionReply.recycle() } } - + private fun writeResultToReply(result: Result, reply: Parcel) { when (result) { Skip -> reply.writeInt(RESULT_SKIP) @@ -178,4 +196,4 @@ open class BinderInterceptor : Binder() { } } } -} \ No newline at end of file +} 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 c13f675..f9ce1a5 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 @@ -15,91 +15,91 @@ import io.github.beakthoven.TrickyStoreOSS.logging.Logger import kotlin.system.exitProcess abstract class BaseKeystoreInterceptor : BinderInterceptor() { - + protected lateinit var keystore: IBinder protected var triedCount = 0 protected var injected = false protected open val maxRetries: Int = 3 - + protected abstract val serviceName: String protected abstract val injectionCommand: String protected abstract val processName: String - + fun tryRunKeystoreInterceptor(): Boolean { Logger.i("Trying to register ${this::class.simpleName} (attempt $triedCount)...") - + val service = getService() ?: return false val backdoor = getBinderBackdoor(service) - + return if (backdoor != null) { setupInterceptor(service, backdoor) } else { handleMissingBackdoor() } } - + protected open fun getService(): IBinder? = ServiceManager.getService(serviceName) - + protected open fun setupInterceptor(service: IBinder, backdoor: IBinder): Boolean { keystore = service Logger.i("Registering for $serviceName: $keystore") - + registerBinderInterceptor(backdoor, service, this) service.linkToDeath(createDeathRecipient(), 0) onInterceptorSetup(service, backdoor) - + return true } - + private fun handleMissingBackdoor(): Boolean { if (triedCount >= maxRetries) { Logger.e("Tried injection $maxRetries times but still no backdoor, exiting") exitProcess(1) } - + if (!injected) { performInjection() injected = true } - + triedCount++ return false } - + protected open fun performInjection() { Logger.i("Attempting to inject into $processName...") - + val command = arrayOf("/system/bin/sh", "-c", injectionCommand) Logger.d("Injection command: ${command.joinToString(" ")}") - + val process = Runtime.getRuntime().exec(command) - + if (process.waitFor() != 0) { Logger.e("Injection failed! Daemon will exit") exitProcess(1) } - + Logger.i("Injection completed successfully") } - - protected open fun createDeathRecipient(): IBinder.DeathRecipient = object : IBinder.DeathRecipient { - override fun binderDied() { - Logger.d("$serviceName died, daemon restarting") - exitProcess(0) + + protected open fun createDeathRecipient(): IBinder.DeathRecipient = + object : IBinder.DeathRecipient { + override fun binderDied() { + Logger.d("$serviceName died, daemon restarting") + exitProcess(0) + } } - } - + protected open fun onInterceptorSetup(service: IBinder, backdoor: IBinder) { // Default implementation does nothing } } object InterceptorUtils { - + fun getTransactCode(clazz: Class<*>, method: String): Int = - clazz.getDeclaredField("TRANSACTION_$method").apply { isAccessible = true } - .getInt(null) - + clazz.getDeclaredField("TRANSACTION_$method").apply { isAccessible = true }.getInt(null) + fun createSuccessKeystoreResponse(): KeystoreResponse { val parcel = Parcel.obtain() try { @@ -111,36 +111,43 @@ object InterceptorUtils { parcel.recycle() } } - + fun createSuccessReply(resultCode: Int = KeyStore.NO_ERROR): BinderInterceptor.OverrideReply { val parcel = Parcel.obtain() parcel.writeNoException() parcel.writeInt(resultCode) return BinderInterceptor.OverrideReply(0, parcel) } - - fun createByteArrayReply(data: ByteArray, resultCode: Int = KeyStore.NO_ERROR): BinderInterceptor.OverrideReply { + + fun createByteArrayReply( + data: ByteArray, + resultCode: Int = KeyStore.NO_ERROR, + ): BinderInterceptor.OverrideReply { val parcel = Parcel.obtain() parcel.writeNoException() parcel.writeByteArray(data) return BinderInterceptor.OverrideReply(resultCode, parcel) } - - fun createTypedObjectReply(obj: T, flags: Int = 0, resultCode: Int = 0): BinderInterceptor.OverrideReply { + + fun createTypedObjectReply( + obj: T, + flags: Int = 0, + resultCode: Int = 0, + ): BinderInterceptor.OverrideReply { val parcel = Parcel.obtain() parcel.writeNoException() parcel.writeTypedObject(obj, flags) return BinderInterceptor.OverrideReply(resultCode, parcel) } - + fun String.extractAlias(): String { return when { - contains("_") -> split("_")[1] + contains("_") -> split("_")[1] else -> this } } - + fun Parcel.hasException(): Boolean { return kotlin.runCatching { readException() }.exceptionOrNull() != null } -} \ No newline at end of file +} 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 745d14c..392ab97 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 @@ -28,23 +28,25 @@ object Keystore2Interceptor : BaseKeystoreInterceptor() { getTransactCode(IKeystoreService.Stub::class.java, "getKeyEntry") private val deleteKeyTransaction = getTransactCode(IKeystoreService.Stub::class.java, "deleteKey") - + override val serviceName = "android.system.keystore2.IKeystoreService/default" override val processName = "keystore2" override val injectionCommand = "exec ./inject `pidof keystore2` libTrickyStoreOSS.so entry" private var teeInterceptor: SecurityLevelInterceptor? = null private var strongBoxInterceptor: SecurityLevelInterceptor? = null - + override fun onInterceptorSetup(service: IBinder, backdoor: IBinder) { setupSecurityLevelInterceptors(service, backdoor) } - + private fun setupSecurityLevelInterceptors(service: IBinder, backdoor: IBinder) { val ks = IKeystoreService.Stub.asInterface(service) - - val tee = kotlin.runCatching { ks.getSecurityLevel(SecurityLevel.TRUSTED_ENVIRONMENT) } - .getOrNull() + + val tee = + kotlin + .runCatching { ks.getSecurityLevel(SecurityLevel.TRUSTED_ENVIRONMENT) } + .getOrNull() if (tee != null) { Logger.i("Registering for TEE SecurityLevel: $tee") val interceptor = SecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT) @@ -53,9 +55,9 @@ object Keystore2Interceptor : BaseKeystoreInterceptor() { } else { Logger.i("No TEE SecurityLevel found") } - - val strongBox = kotlin.runCatching { ks.getSecurityLevel(SecurityLevel.STRONGBOX) } - .getOrNull() + + val strongBox = + kotlin.runCatching { ks.getSecurityLevel(SecurityLevel.STRONGBOX) }.getOrNull() if (strongBox != null) { Logger.i("Registering for StrongBox SecurityLevel: $strongBox") val interceptor = SecurityLevelInterceptor(strongBox, SecurityLevel.STRONGBOX) @@ -72,40 +74,62 @@ object Keystore2Interceptor : BaseKeystoreInterceptor() { flags: Int, callingUid: Int, callingPid: Int, - data: Parcel + data: Parcel, ): Result { if (code == getKeyEntryTransaction) { if (KeyBoxUtils.hasKeyboxes()) { - Logger.d("intercept pre $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()}") + Logger.d( + "intercept pre $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()}" + ) try { data.enforceInterface(IKeystoreService.DESCRIPTOR) val descriptor = data.readTypedObject(KeyDescriptor.CREATOR) ?: return Skip if (PkgConfig.needGenerate(callingUid)) { - val response = SecurityLevelInterceptor.getKeyResponse(callingUid, descriptor.alias) + val response = + SecurityLevelInterceptor.getKeyResponse(callingUid, descriptor.alias) if (response != null) { - Logger.i("Found generated response for uid=$callingUid alias=${descriptor.alias}") + Logger.i( + "Found generated response for uid=$callingUid alias=${descriptor.alias}" + ) return createTypedObjectReply(response) } else { - Logger.e("No generated response found for uid=$callingUid alias=${descriptor.alias}") + Logger.e( + "No generated response found for uid=$callingUid alias=${descriptor.alias}" + ) val nullParcel = Parcel.obtain() nullParcel.writeTypedObject(null as KeyEntryResponse?, 0) return OverrideReply(0, nullParcel) } } else if (PkgConfig.needHack(callingUid)) { - if (SecurityLevelInterceptor.shouldSkipLeafHack(callingUid, descriptor.alias)) { + if ( + SecurityLevelInterceptor.shouldSkipLeafHack( + callingUid, + descriptor.alias, + ) + ) { Logger.i("skip leaf hack for uid=$callingUid alias=${descriptor.alias}") - val response = SecurityLevelInterceptor.getKeyResponse(callingUid, descriptor.alias) + val response = + SecurityLevelInterceptor.getKeyResponse( + callingUid, + descriptor.alias, + ) if (response != null) { - Logger.i("Found generated response for uid=$callingUid alias=${descriptor.alias}") + Logger.i( + "Found generated response for uid=$callingUid alias=${descriptor.alias}" + ) return createTypedObjectReply(response) } else { - Logger.e("No generated response found for uid=$callingUid alias=${descriptor.alias}") + Logger.e( + "No generated response found for uid=$callingUid alias=${descriptor.alias}" + ) val nullParcel = Parcel.obtain() nullParcel.writeTypedObject(null as KeyEntryResponse?, 0) return OverrideReply(0, nullParcel) } } else { - Logger.i("proceeding with leaf hack for uid=$callingUid alias=${descriptor.alias}") + Logger.i( + "proceeding with leaf hack for uid=$callingUid alias=${descriptor.alias}" + ) return Continue } } @@ -127,12 +151,14 @@ object Keystore2Interceptor : BaseKeystoreInterceptor() { callingPid: Int, data: Parcel, reply: Parcel?, - resultCode: Int + resultCode: Int, ): Result { if (target != keystore || reply == null) return Skip if (reply.hasException()) return Skip val p = Parcel.obtain() - Logger.d("intercept post $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()} replySz=${reply.dataSize()}") + Logger.d( + "intercept post $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()} replySz=${reply.dataSize()}" + ) if (code == deleteKeyTransaction && resultCode == 0) { data.enforceInterface("android.system.keystore2.IKeystoreService") @@ -140,7 +166,9 @@ object Keystore2Interceptor : BaseKeystoreInterceptor() { val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR) if (keyDescriptor == null || keyDescriptor.domain == 0) return Skip - SecurityLevelInterceptor.keys.remove(SecurityLevelInterceptor.Key(callingUid, keyDescriptor.alias)) + SecurityLevelInterceptor.keys.remove( + SecurityLevelInterceptor.Key(callingUid, keyDescriptor.alias) + ) return Skip } else if (code == getKeyEntryTransaction) { @@ -149,7 +177,7 @@ object Keystore2Interceptor : BaseKeystoreInterceptor() { val response = reply.readTypedObject(KeyEntryResponse.CREATOR) if (response != null) { val chain = CertificateUtils.run { response.getCertificateChain() } - if (chain != null) { + if (chain != null) { val newChain = CertificateHack.hackCertificateChain(chain, callingUid) response.putCertificateChain(newChain).getOrThrow() Logger.i("Hacked certificate for uid=$callingUid") 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 1f29d53..748005a 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 @@ -36,8 +36,7 @@ import java.util.Date @SuppressLint("BlockedPrivateApi") object KeystoreInterceptor : BaseKeystoreInterceptor() { - private val getTransaction = - getTransactCode(IKeystoreService.Stub::class.java, "get") + private val getTransaction = getTransactCode(IKeystoreService.Stub::class.java, "get") private val generateKeyTransaction = getTransactCode(IKeystoreService.Stub::class.java, "generateKey") private val getKeyCharacteristicsTransaction = @@ -46,7 +45,7 @@ object KeystoreInterceptor : BaseKeystoreInterceptor() { getTransactCode(IKeystoreService.Stub::class.java, "exportKey") private val attestKeyTransaction = getTransactCode(IKeystoreService.Stub::class.java, "attestKey") - + override val serviceName = "android.security.keystore" override val processName = "keystore" override val injectionCommand = "exec ./inject `pidof keystore` libTrickyStoreOSS.so entry" @@ -64,7 +63,7 @@ object KeystoreInterceptor : BaseKeystoreInterceptor() { flags: Int, callingUid: Int, callingPid: Int, - data: Parcel + data: Parcel, ): Result { if (KeyBoxUtils.hasKeyboxes()) { if (code == getTransaction) { @@ -76,123 +75,168 @@ object KeystoreInterceptor : BaseKeystoreInterceptor() { } else if (PkgConfig.needGenerate(callingUid)) { when (code) { generateKeyTransaction -> { - kotlin.runCatching { - data.enforceInterface(DESCRIPTOR) - val callback = IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder()) - val alias = data.readString()!!.extractAlias() - Logger.i("generateKeyTransaction uid $callingUid alias $alias") - val check = data.readInt() - val kma = KeymasterArguments() - 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.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()) - if (kgp.algorithm == KeymasterDefs.KM_ALGORITHM_RSA) { - try { - val getArgumentByTag = KeymasterArguments::class.java.getDeclaredMethods().first { it.name == "getArgumentByTag" } - getArgumentByTag.isAccessible = true - val rsaArgument = getArgumentByTag.invoke(kma, KeymasterDefs.KM_TAG_RSA_PUBLIC_EXPONENT) + kotlin + .runCatching { + data.enforceInterface(DESCRIPTOR) + val callback = + IKeystoreKeyCharacteristicsCallback.Stub.asInterface( + data.readStrongBinder() + ) + val alias = data.readString()!!.extractAlias() + Logger.i("generateKeyTransaction uid $callingUid alias $alias") + val check = data.readInt() + val kma = KeymasterArguments() + 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.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()) + if (kgp.algorithm == KeymasterDefs.KM_ALGORITHM_RSA) { + try { + val getArgumentByTag = + KeymasterArguments::class + .java + .getDeclaredMethods() + .first { it.name == "getArgumentByTag" } + getArgumentByTag.isAccessible = true + val rsaArgument = + getArgumentByTag.invoke( + kma, + KeymasterDefs.KM_TAG_RSA_PUBLIC_EXPONENT, + ) - val getLongTagValue = KeymasterArguments::class.java.getDeclaredMethods().first { it.name == "getLongTagValue" } - getLongTagValue.isAccessible = true - kgp.rsaPublicExponent = getLongTagValue.invoke(kma, rsaArgument) as BigInteger - } catch (ex: Exception) { - Logger.e("Read rsaPublicExponent error", ex) + val getLongTagValue = + KeymasterArguments::class + .java + .getDeclaredMethods() + .first { it.name == "getLongTagValue" } + getLongTagValue.isAccessible = true + kgp.rsaPublicExponent = + getLongTagValue.invoke(kma, rsaArgument) + as BigInteger + } catch (ex: Exception) { + Logger.e("Read rsaPublicExponent error", ex) + } } + keyArguments[Key(callingUid, alias)] = kgp } - keyArguments[Key(callingUid, alias)] = kgp + + val kc = KeyCharacteristics() + kc.swEnforced = KeymasterArguments() + kc.hwEnforced = kma + + val ksr = createSuccessKeystoreResponse() + callback.onFinished(ksr, kc) + + return createSuccessReply() } - - val kc = KeyCharacteristics() - kc.swEnforced = KeymasterArguments() - kc.hwEnforced = kma - - val ksr = createSuccessKeystoreResponse() - callback.onFinished(ksr, kc) - - return createSuccessReply() - }.onFailure { - Logger.e("generateKeyTransaction error", it) - } + .onFailure { Logger.e("generateKeyTransaction error", it) } } getKeyCharacteristicsTransaction -> { - kotlin.runCatching { - data.enforceInterface(DESCRIPTOR) - val callback = IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder()) - val alias = data.readString()!!.extractAlias() - Logger.i("getKeyCharacteristicsTransaction uid $callingUid alias $alias") - val kc = KeyCharacteristics() - val kma = KeymasterArguments() - kma.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, keyArguments[Key(callingUid, alias)]!!.algorithm) - kc.swEnforced = KeymasterArguments() - kc.hwEnforced = kma + kotlin + .runCatching { + data.enforceInterface(DESCRIPTOR) + val callback = + IKeystoreKeyCharacteristicsCallback.Stub.asInterface( + data.readStrongBinder() + ) + val alias = data.readString()!!.extractAlias() + Logger.i( + "getKeyCharacteristicsTransaction uid $callingUid alias $alias" + ) + val kc = KeyCharacteristics() + val kma = KeymasterArguments() + kma.addEnum( + KeymasterDefs.KM_TAG_ALGORITHM, + keyArguments[Key(callingUid, alias)]!!.algorithm, + ) + kc.swEnforced = KeymasterArguments() + kc.hwEnforced = kma - val ksr = createSuccessKeystoreResponse() - callback.onFinished(ksr, kc) + val ksr = createSuccessKeystoreResponse() + callback.onFinished(ksr, kc) - return createSuccessReply() - }.onFailure { - Logger.e("getKeyCharacteristicsTransaction error", it) - } + return createSuccessReply() + } + .onFailure { Logger.e("getKeyCharacteristicsTransaction error", it) } } exportKeyTransaction -> { - kotlin.runCatching { - data.enforceInterface(DESCRIPTOR) - val callback = IKeystoreExportKeyCallback.Stub.asInterface(data.readStrongBinder()) - val alias = data.readString()!!.extractAlias() - Logger.i("exportKeyTransaction uid $callingUid alias $alias") - val kp = CertificateGen.generateKeyPair(keyArguments[Key(callingUid, alias)]!!) - keyPairs[Key(callingUid, alias)] = kp!! + kotlin + .runCatching { + data.enforceInterface(DESCRIPTOR) + val callback = + IKeystoreExportKeyCallback.Stub.asInterface( + data.readStrongBinder() + ) + val alias = data.readString()!!.extractAlias() + Logger.i("exportKeyTransaction uid $callingUid alias $alias") + val kp = + CertificateGen.generateKeyPair( + keyArguments[Key(callingUid, alias)]!! + ) + keyPairs[Key(callingUid, alias)] = kp!! - val erP = Parcel.obtain() - erP.writeInt(KeyStore.NO_ERROR) - erP.writeByteArray(kp.public.encoded) - erP.setDataPosition(0) - val er = ExportResult.CREATOR.createFromParcel(erP) - erP.recycle() + val erP = Parcel.obtain() + erP.writeInt(KeyStore.NO_ERROR) + erP.writeByteArray(kp.public.encoded) + erP.setDataPosition(0) + val er = ExportResult.CREATOR.createFromParcel(erP) + erP.recycle() - callback.onFinished(er) + callback.onFinished(er) - return createSuccessReply() - }.onFailure { - Logger.e("exportKeyTransaction error", it) - } + return createSuccessReply() + } + .onFailure { Logger.e("exportKeyTransaction error", it) } } attestKeyTransaction -> { - kotlin.runCatching { - data.enforceInterface(DESCRIPTOR) - val callback = IKeystoreCertificateChainCallback.Stub.asInterface(data.readStrongBinder()) - val alias = data.readString()!!.extractAlias() - Logger.i("attestKeyTransaction uid $callingUid alias $alias") - val check = data.readInt() - val kma = KeymasterArguments() - if (check == 1) { - kma.readFromParcel(data) - val attestationChallenge = kma.getBytes(KeymasterDefs.KM_TAG_ATTESTATION_CHALLENGE, ByteArray(0)) + kotlin + .runCatching { + data.enforceInterface(DESCRIPTOR) + val callback = + IKeystoreCertificateChainCallback.Stub.asInterface( + data.readStrongBinder() + ) + val alias = data.readString()!!.extractAlias() + Logger.i("attestKeyTransaction uid $callingUid alias $alias") + val check = data.readInt() + val kma = KeymasterArguments() + if (check == 1) { + kma.readFromParcel(data) + val attestationChallenge = + kma.getBytes( + KeymasterDefs.KM_TAG_ATTESTATION_CHALLENGE, + ByteArray(0), + ) - val ksr = createSuccessKeystoreResponse() + val ksr = createSuccessKeystoreResponse() - val key = Key(callingUid, alias) - val ka = keyArguments[key]!! - ka.attestationChallenge = attestationChallenge - val chain = CertificateGen.generateChain(callingUid, ka, keyPairs[key]!!) + val key = Key(callingUid, alias) + val ka = keyArguments[key]!! + ka.attestationChallenge = attestationChallenge + val chain = + CertificateGen.generateChain( + callingUid, + ka, + keyPairs[key]!!, + ) - val kcc = KeymasterCertificateChain(chain) - callback.onFinished(ksr, kcc) + val kcc = KeymasterCertificateChain(chain) + callback.onFinished(ksr, kcc) + } + + return createSuccessReply() } - - return createSuccessReply() - }.onFailure { - Logger.e("attestKeyTransaction error", it) - } + .onFailure { Logger.e("attestKeyTransaction error", it) } } } } @@ -208,24 +252,36 @@ object KeystoreInterceptor : BaseKeystoreInterceptor() { callingPid: Int, data: Parcel, reply: Parcel?, - resultCode: Int + resultCode: Int, ): Result { if (target != keystore || code != getTransaction || reply == null) return Skip if (reply.hasException()) return Skip val p = Parcel.obtain() - Logger.d("intercept post $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()} replySz=${reply.dataSize()}") + Logger.d( + "intercept post $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()} replySz=${reply.dataSize()}" + ) try { data.enforceInterface(DESCRIPTOR) val alias = data.readString() ?: "" var response = reply.createByteArray() when { alias.startsWith(Credentials.USER_CERTIFICATE) -> { - response = CertificateHack.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 = CertificateHack.hackCACertificateChain(response!!, alias.extractAlias(), callingUid) + response = + CertificateHack.hackCACertificateChain( + response!!, + alias.extractAlias(), + callingUid, + ) Logger.i("Hacked CA certificate chain for uid=$callingUid") return createByteArrayReply(response) } @@ -237,4 +293,4 @@ object KeystoreInterceptor : BaseKeystoreInterceptor() { } return Skip } -} \ No newline at end of file +} 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 183b0ca..e713387 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 @@ -27,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap class SecurityLevelInterceptor( private val original: IKeystoreSecurityLevel, - private val level: Int + private val level: Int, ) : BinderInterceptor() { companion object { private val generateKeyTransaction = @@ -37,14 +37,11 @@ class SecurityLevelInterceptor( private val createOperationTransaction = getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "createOperation") - @Keep - val keys = ConcurrentHashMap() + @Keep val keys = ConcurrentHashMap() - @Keep - val keyPairs = ConcurrentHashMap>>() + @Keep val keyPairs = ConcurrentHashMap>>() - @Keep - val skipLeafHacks = ConcurrentHashMap() + @Keep val skipLeafHacks = ConcurrentHashMap() @Keep fun getKeyResponse(uid: Int, alias: String): KeyEntryResponse? = @@ -60,6 +57,7 @@ class SecurityLevelInterceptor( } data class Key(val uid: Int, val alias: String) + data class Info(val keyPair: KeyPair, val response: KeyEntryResponse) override fun onPreTransact( @@ -68,51 +66,80 @@ class SecurityLevelInterceptor( flags: Int, callingUid: Int, callingPid: Int, - data: Parcel + data: Parcel, ): Result { if (code == generateKeyTransaction) { Logger.i("intercept key gen uid=$callingUid pid=$callingPid") - kotlin.runCatching { - data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) - val keyDescriptor = - data.readTypedObject(KeyDescriptor.CREATOR) ?: return@runCatching - val attestationKeyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR) - val params = data.createTypedArray(KeyParameter.CREATOR)!! - val aFlags = data.readInt() - val entropy = data.createByteArray() - 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) - keys[Key(callingUid, keyDescriptor.alias)] = Info(pair.first, response) - val p = Parcel.obtain() - p.writeNoException() - p.writeTypedObject(response.metadata, 0) - return OverrideReply(0, p) - } 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 = 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) + kotlin + .runCatching { + data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) + val keyDescriptor = + data.readTypedObject(KeyDescriptor.CREATOR) ?: return@runCatching + val attestationKeyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR) + val params = data.createTypedArray(KeyParameter.CREATOR)!! + val aFlags = data.readInt() + val entropy = data.createByteArray() + 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, + ) keys[Key(callingUid, keyDescriptor.alias)] = Info(pair.first, response) - SecurityLevelInterceptor.skipLeafHacks[Key(callingUid, keyDescriptor.alias)] = true val p = Parcel.obtain() p.writeNoException() p.writeTypedObject(response.metadata, 0) return OverrideReply(0, p) - } else { - skipLeafHacks.remove(Key(callingUid, keyDescriptor.alias)) - Logger.i("Cleared skip flag for non-attestation key: uid=$callingUid alias=${keyDescriptor.alias}") - return Skip + } 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 = + 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, + ) + keys[Key(callingUid, keyDescriptor.alias)] = Info(pair.first, response) + SecurityLevelInterceptor.skipLeafHacks[ + Key(callingUid, keyDescriptor.alias)] = true + val p = Parcel.obtain() + p.writeNoException() + p.writeTypedObject(response.metadata, 0) + return OverrideReply(0, p) + } else { + skipLeafHacks.remove(Key(callingUid, keyDescriptor.alias)) + Logger.i( + "Cleared skip flag for non-attestation key: uid=$callingUid alias=${keyDescriptor.alias}" + ) + return Skip + } } } - }.onFailure { - Logger.e("parse key gen request", it) - } + .onFailure { Logger.e("parse key gen request", it) } } return Skip } @@ -120,7 +147,7 @@ class SecurityLevelInterceptor( private fun buildResponse( chain: List, params: CertificateGen.KeyGenParameters, - descriptor: KeyDescriptor + descriptor: KeyDescriptor, ): KeyEntryResponse { val response = KeyEntryResponse() val metadata = KeyMetadata() @@ -177,4 +204,4 @@ class SecurityLevelInterceptor( response.iSecurityLevel = original return response } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/logging/Logger.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/logging/Logger.kt index 6039951..a0cdbdb 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/logging/Logger.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/logging/Logger.kt @@ -9,56 +9,65 @@ import android.util.Log object Logger { const val TAG = "TrickyStoreOSS" - + sealed class LogLevel(val priority: Int) { object Debug : LogLevel(Log.DEBUG) + object Info : LogLevel(Log.INFO) + object Warning : LogLevel(Log.WARN) + object Error : LogLevel(Log.ERROR) + object Verbose : LogLevel(Log.VERBOSE) } - + fun d(message: String) { Log.d(TAG, message) } - + fun e(message: String) { Log.e(TAG, message) } - + fun e(message: String, throwable: Throwable) { Log.e(TAG, "fatal: $message", throwable) } - + fun i(message: String) { Log.i(TAG, message) } - + fun w(message: String) { Log.w(TAG, message) } - + fun w(message: String, throwable: Throwable) { Log.w(TAG, message, throwable) } - + fun v(message: String) { Log.v(TAG, message) } - + fun log(level: LogLevel, message: String, throwable: Throwable? = null) { when (level) { - is LogLevel.Debug -> if (throwable != null) Log.d(TAG, message, throwable) else Log.d(TAG, message) - is LogLevel.Info -> if (throwable != null) Log.i(TAG, message, throwable) else Log.i(TAG, message) - is LogLevel.Warning -> if (throwable != null) Log.w(TAG, message, throwable) else Log.w(TAG, message) - is LogLevel.Error -> if (throwable != null) Log.e(TAG, message, throwable) else Log.e(TAG, message) - is LogLevel.Verbose -> if (throwable != null) Log.v(TAG, message, throwable) else Log.v(TAG, message) + is LogLevel.Debug -> + if (throwable != null) Log.d(TAG, message, throwable) else Log.d(TAG, message) + is LogLevel.Info -> + if (throwable != null) Log.i(TAG, message, throwable) else Log.i(TAG, message) + is LogLevel.Warning -> + if (throwable != null) Log.w(TAG, message, throwable) else Log.w(TAG, message) + is LogLevel.Error -> + if (throwable != null) Log.e(TAG, message, throwable) else Log.e(TAG, message) + is LogLevel.Verbose -> + if (throwable != null) Log.v(TAG, message, throwable) else Log.v(TAG, message) } } - + fun logIf(level: LogLevel, condition: Boolean = true, messageProvider: () -> String) { if (condition && Log.isLoggable(TAG, level.priority)) { log(level, messageProvider()) } } -} \ No newline at end of file +} diff --git a/build.gradle.kts b/build.gradle.kts index f7ba7ac..415fab0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,9 +3,20 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask + // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.android) apply false -} \ No newline at end of file + alias(libs.plugins.ktfmt) +} + +tasks.register("format") { + source = project.fileTree(rootDir) + include("*.gradle.kts", "app/*.gradle.kts") + dependsOn(":app:ktfmtFormat") +} + +ktfmt { kotlinLangStyle() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0649b71..64d4bfe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ annotation = "1.9.1" jdk18on = "1.81" kotlin = "2.2.10" libcxx = "28.1.13356709" +ktfmt = "0.25.0" [libraries] annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } @@ -14,4 +15,5 @@ org-lsposed-libcxx-libcxx = { module = "org.lsposed.libcxx:libcxx", version.ref android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 841f68a..71cebfe 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,6 +16,7 @@ pluginManagement { gradlePluginPortal() } } + dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { @@ -25,4 +26,5 @@ dependencyResolutionManagement { } rootProject.name = "Tricky Store OSS" + include(":app", ":stub")