diff --git a/.clang-format b/.clang-format index ab38316..b643df2 100644 --- a/.clang-format +++ b/.clang-format @@ -1,17 +1,17 @@ -BasedOnStyle: LLVM - -Language: Cpp -Standard: c++20 - -ColumnLimit: 135 - -AlignEscapedNewlines: Left -AllowShortFunctionsOnASingleLine: Empty -AllowShortLambdasOnASingleLine: Empty -AlwaysBreakTemplateDeclarations: true -IndentPPDirectives: AfterHash - -AccessModifierOffset: -4 -IndentWidth: 4 -UseTab: Never - +BasedOnStyle: LLVM + +Language: Cpp +Standard: c++20 + +ColumnLimit: 135 + +AlignEscapedNewlines: Left +AllowShortFunctionsOnASingleLine: Empty +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: true +IndentPPDirectives: AfterHash + +AccessModifierOffset: -4 +IndentWidth: 4 +UseTab: Never + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b1b618c..7c6f541 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ - - - + + + \ No newline at end of file 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 f23d04c..a6f0e6f 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/AndroidUtils.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/AndroidUtils.kt @@ -1,188 +1,188 @@ -/* - * Copyright 2025 Dakkshesh - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package io.github.beakthoven.TrickyStoreOSS - -import android.content.pm.IPackageManager -import android.content.pm.PackageManager -import android.os.Build -import android.os.ServiceManager -import android.os.SystemProperties -import io.github.beakthoven.TrickyStoreOSS.core.config.Config -import io.github.beakthoven.TrickyStoreOSS.core.config.CustomPatchLevel -import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger -import org.bouncycastle.asn1.ASN1Integer -import org.bouncycastle.asn1.DEROctetString -import org.bouncycastle.asn1.DERSequence -import java.security.MessageDigest -import java.util.concurrent.ThreadLocalRandom - -fun getTransactCode(clazz: Class<*>, method: String): Int = - clazz.getDeclaredField("TRANSACTION_$method").apply { isAccessible = true } - .getInt(null) - -val bootHash: ByteArray by lazy { - getBootHashFromProp() ?: randomBytes() -} - -val bootKey: ByteArray by lazy { - randomBytes() -} - -@OptIn(ExperimentalStdlibApi::class) -private fun getBootHashFromProp(): ByteArray? { - val digest = SystemProperties.get("ro.boot.vbmeta.digest", null) ?: return null - return if (digest.length == 64) digest.hexToByteArray() else null -} - -private fun randomBytes(): ByteArray = ByteArray(32).also { - ThreadLocalRandom.current().nextBytes(it) -} - -val patchLevel: Int - get() = getCustomPatchLevel("system", false) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) - -val patchLevelLong: Int - get() = getCustomPatchLevel("system", true) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true) - -val vendorPatchLevel: Int - get() = getCustomPatchLevel("vendor", false) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) - -val vendorPatchLevelLong: Int - get() = getCustomPatchLevel("vendor", true) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true) - -val bootPatchLevel: Int - get() = getCustomPatchLevel("boot", false) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) - -val bootPatchLevelLong: Int - get() = getCustomPatchLevel("boot", true) - ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true) - -private val customPatchLevel: CustomPatchLevel? - get() = Config._customPatchLevel - -private fun getCustomPatchLevel(component: String, isLong: Boolean): Int? { - val config = customPatchLevel ?: return null - val value = when (component) { - "system" -> config.system ?: config.all - "vendor" -> config.vendor ?: config.all - "boot" -> config.boot ?: config.all - else -> config.all - } ?: return null - - when { - value.equals("no", ignoreCase = true) -> return null - value.equals("prop", ignoreCase = true) -> return null - } - - return parsePatchLevelValue(value, component, isLong) -} - -private fun parsePatchLevelValue(value: String, component: String, isLong: Boolean): Int? { - val normalized = value.replace("-", "") - - return try { - when (normalized.length) { - 8 -> { - val year = normalized.substring(0, 4).toInt() - val month = normalized.substring(4, 6).toInt() - val day = normalized.substring(6, 8).toInt() - if (isLong) year * 10000 + month * 100 + day - else year * 100 + month - } - 6 -> { - val year = normalized.substring(0, 4).toInt() - val month = normalized.substring(4, 6).toInt() - if (isLong) year * 10000 + month * 100 - else year * 100 + month - } - else -> { - Logger.e("Invalid patch level length for $component: $normalized") - null - } - } - } catch (e: NumberFormatException) { - Logger.e("Patch level parse error for $component=$value", e) - null - } -} - -val osVersion: Int - get() = getOsVersion(Build.VERSION.SDK_INT) - -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 fun getOsVersion(sdkVersion: Int): Int = osVersionMap[sdkVersion] ?: 160000 - -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 IPackageManager.getPackageInfoCompat(name: String, flags: Long, userId: Int) = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getPackageInfo(name, flags, userId) - } else { - @Suppress("DEPRECATION") - getPackageInfo(name, flags.toInt(), userId) - } - -val apexInfos: List> by lazy { - runCatching { - val packageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package")) - val packages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - packageManager.getInstalledPackages(PackageManager.MATCH_APEX.toLong(), 0) - } else { - @Suppress("DEPRECATION") - packageManager.getInstalledPackages(PackageManager.MATCH_APEX, 0) - } - - packages.list - .map { it.packageName to it.longVersionCode } - .sortedBy { it.first } - }.getOrElse { - Logger.e("Failed to get APEX package information") - emptyList() - } -} - -val moduleHash: ByteArray by lazy { - runCatching { - val encodables = apexInfos.flatMap { (packageName, versionCode) -> - listOf( - DEROctetString(packageName.toByteArray()), - ASN1Integer(versionCode) - ) - } - - val sequence = DERSequence(encodables.toTypedArray()) - MessageDigest.getInstance("SHA-256").digest(sequence.encoded) - }.getOrElse { - Logger.e("Failed to compute module hash", it) - ByteArray(32) - } -} - +/* + * Copyright 2025 Dakkshesh + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package io.github.beakthoven.TrickyStoreOSS + +import android.content.pm.IPackageManager +import android.content.pm.PackageManager +import android.os.Build +import android.os.ServiceManager +import android.os.SystemProperties +import io.github.beakthoven.TrickyStoreOSS.core.config.Config +import io.github.beakthoven.TrickyStoreOSS.core.config.CustomPatchLevel +import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger +import org.bouncycastle.asn1.ASN1Integer +import org.bouncycastle.asn1.DEROctetString +import org.bouncycastle.asn1.DERSequence +import java.security.MessageDigest +import java.util.concurrent.ThreadLocalRandom + +fun getTransactCode(clazz: Class<*>, method: String): Int = + clazz.getDeclaredField("TRANSACTION_$method").apply { isAccessible = true } + .getInt(null) + +val bootHash: ByteArray by lazy { + getBootHashFromProp() ?: randomBytes() +} + +val bootKey: ByteArray by lazy { + randomBytes() +} + +@OptIn(ExperimentalStdlibApi::class) +private fun getBootHashFromProp(): ByteArray? { + val digest = SystemProperties.get("ro.boot.vbmeta.digest", null) ?: return null + return if (digest.length == 64) digest.hexToByteArray() else null +} + +private fun randomBytes(): ByteArray = ByteArray(32).also { + ThreadLocalRandom.current().nextBytes(it) +} + +val patchLevel: Int + get() = getCustomPatchLevel("system", false) + ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) + +val patchLevelLong: Int + get() = getCustomPatchLevel("system", true) + ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true) + +val vendorPatchLevel: Int + get() = getCustomPatchLevel("vendor", false) + ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) + +val vendorPatchLevelLong: Int + get() = getCustomPatchLevel("vendor", true) + ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true) + +val bootPatchLevel: Int + get() = getCustomPatchLevel("boot", false) + ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false) + +val bootPatchLevelLong: Int + get() = getCustomPatchLevel("boot", true) + ?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true) + +private val customPatchLevel: CustomPatchLevel? + get() = Config._customPatchLevel + +private fun getCustomPatchLevel(component: String, isLong: Boolean): Int? { + val config = customPatchLevel ?: return null + val value = when (component) { + "system" -> config.system ?: config.all + "vendor" -> config.vendor ?: config.all + "boot" -> config.boot ?: config.all + else -> config.all + } ?: return null + + when { + value.equals("no", ignoreCase = true) -> return null + value.equals("prop", ignoreCase = true) -> return null + } + + return parsePatchLevelValue(value, component, isLong) +} + +private fun parsePatchLevelValue(value: String, component: String, isLong: Boolean): Int? { + val normalized = value.replace("-", "") + + return try { + when (normalized.length) { + 8 -> { + val year = normalized.substring(0, 4).toInt() + val month = normalized.substring(4, 6).toInt() + val day = normalized.substring(6, 8).toInt() + if (isLong) year * 10000 + month * 100 + day + else year * 100 + month + } + 6 -> { + val year = normalized.substring(0, 4).toInt() + val month = normalized.substring(4, 6).toInt() + if (isLong) year * 10000 + month * 100 + else year * 100 + month + } + else -> { + Logger.e("Invalid patch level length for $component: $normalized") + null + } + } + } catch (e: NumberFormatException) { + Logger.e("Patch level parse error for $component=$value", e) + null + } +} + +val osVersion: Int + get() = getOsVersion(Build.VERSION.SDK_INT) + +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 fun getOsVersion(sdkVersion: Int): Int = osVersionMap[sdkVersion] ?: 160000 + +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 IPackageManager.getPackageInfoCompat(name: String, flags: Long, userId: Int) = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getPackageInfo(name, flags, userId) + } else { + @Suppress("DEPRECATION") + getPackageInfo(name, flags.toInt(), userId) + } + +val apexInfos: List> by lazy { + runCatching { + val packageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package")) + val packages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getInstalledPackages(PackageManager.MATCH_APEX.toLong(), 0) + } else { + @Suppress("DEPRECATION") + packageManager.getInstalledPackages(PackageManager.MATCH_APEX, 0) + } + + packages.list + .map { it.packageName to it.longVersionCode } + .sortedBy { it.first } + }.getOrElse { + Logger.e("Failed to get APEX package information") + emptyList() + } +} + +val moduleHash: ByteArray by lazy { + runCatching { + val encodables = apexInfos.flatMap { (packageName, versionCode) -> + listOf( + DEROctetString(packageName.toByteArray()), + ASN1Integer(versionCode) + ) + } + + val sequence = DERSequence(encodables.toTypedArray()) + MessageDigest.getInstance("SHA-256").digest(sequence.encoded) + }.getOrElse { + Logger.e("Failed to compute module hash", it) + ByteArray(32) + } +} + fun String.trimLine(): String = trim().split("\n").joinToString("\n") { it.trim() } \ No newline at end of file diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateHacker.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateHacker.kt index 445c3e3..47e2846 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateHacker.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateHacker.kt @@ -1,868 +1,868 @@ -/* - * Copyright 2025 Dakkshesh - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package io.github.beakthoven.TrickyStoreOSS - -import android.content.pm.PackageManager -import android.hardware.security.keymint.Algorithm -import android.hardware.security.keymint.EcCurve -import android.hardware.security.keymint.KeyParameter -import android.hardware.security.keymint.Tag -import android.security.keystore.KeyProperties -import android.system.keystore2.KeyDescriptor -import android.util.Pair -import io.github.beakthoven.TrickyStoreOSS.* -import io.github.beakthoven.TrickyStoreOSS.core.config.Config -import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger -import io.github.beakthoven.TrickyStoreOSS.interceptors.SecurityLevelInterceptor -import org.bouncycastle.asn1.* -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.Extension -import org.bouncycastle.asn1.x509.KeyUsage -import org.bouncycastle.cert.X509CertificateHolder -import org.bouncycastle.cert.X509v3CertificateBuilder -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter -import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.bouncycastle.openssl.PEMKeyPair -import org.bouncycastle.openssl.PEMParser -import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder -import org.bouncycastle.util.io.pem.PemReader -import java.io.ByteArrayInputStream -import java.io.StringReader -import java.math.BigInteger -import java.nio.charset.StandardCharsets -import java.security.* -import java.security.cert.Certificate -import java.security.cert.CertificateFactory -import java.security.cert.CertificateParsingException -import java.security.cert.X509Certificate -import java.security.spec.ECGenParameterSpec -import java.security.spec.RSAKeyGenParameterSpec -import java.util.* -import java.util.concurrent.ConcurrentHashMap -import javax.security.auth.x500.X500Principal - -object CertificateHacker { - - private val ATTESTATION_OID = ASN1ObjectIdentifier("1.3.6.1.4.1.11129.2.1.17") - - private val certificateFactory: CertificateFactory by lazy { - try { - CertificateFactory.getInstance("X.509") - } catch (t: Throwable) { - Logger.e("Failed to initialize certificate factory", t) - throw RuntimeException("Cannot initialize certificate factory", t) - } - } - - data class KeyBox( - val pemKeyPair: PEMKeyPair, - val keyPair: KeyPair, - val certificates: List - ) - - data class KeyIdentifier( - val alias: String, - val uid: Int - ) - - sealed class ParseResult { - data class Success(val data: T) : ParseResult() - data class Error(val message: String, val cause: Throwable? = null) : ParseResult() - } - - sealed class HackResult { - data class Success(val data: T) : HackResult() - data class Error(val message: String, val cause: Throwable? = null) : HackResult() - } - - data class KeyGenParameters( - var keySize: Int = 0, - var algorithm: Int = 0, - var certificateSerial: BigInteger? = null, - var certificateNotBefore: Date? = null, - var certificateNotAfter: Date? = null, - var certificateSubject: X500Name? = null, - var rsaPublicExponent: BigInteger? = null, - var ecCurve: Int = 0, - var ecCurveName: String? = null, - var purpose: MutableList = mutableListOf(), - var digest: MutableList = mutableListOf(), - var attestationChallenge: ByteArray? = null, - var brand: ByteArray? = null, - var device: ByteArray? = null, - var product: ByteArray? = null, - var manufacturer: ByteArray? = null, - var model: ByteArray? = null, - var imei1: ByteArray? = null, - var imei2: ByteArray? = null, - var meid: ByteArray? = null, - var serialno: ByteArray? = null - ) { - - constructor(params: Array) : this() { - parseKeyParameters(params) - } - - private fun parseKeyParameters(params: Array) { - params.forEach { param -> - Logger.d("Processing key parameter: ${param.tag}") - val value = param.value - - when (param.tag) { - Tag.KEY_SIZE -> keySize = value.integer - Tag.ALGORITHM -> algorithm = value.algorithm - Tag.CERTIFICATE_SERIAL -> certificateSerial = BigInteger(value.blob) - Tag.CERTIFICATE_NOT_BEFORE -> certificateNotBefore = Date(value.dateTime) - Tag.CERTIFICATE_NOT_AFTER -> certificateNotAfter = Date(value.dateTime) - Tag.CERTIFICATE_SUBJECT -> certificateSubject = X500Name(X500Principal(value.blob).name) - Tag.RSA_PUBLIC_EXPONENT -> rsaPublicExponent = BigInteger(value.blob) - Tag.EC_CURVE -> { - ecCurve = value.ecCurve - ecCurveName = getEcCurveName(ecCurve) - } - Tag.PURPOSE -> purpose.add(value.keyPurpose) - Tag.DIGEST -> digest.add(value.digest) - Tag.ATTESTATION_CHALLENGE -> attestationChallenge = value.blob - Tag.ATTESTATION_ID_BRAND -> brand = value.blob - Tag.ATTESTATION_ID_DEVICE -> device = value.blob - Tag.ATTESTATION_ID_PRODUCT -> product = value.blob - Tag.ATTESTATION_ID_MANUFACTURER -> manufacturer = value.blob - Tag.ATTESTATION_ID_MODEL -> model = value.blob - Tag.ATTESTATION_ID_IMEI -> imei1 = value.blob - Tag.ATTESTATION_ID_SECOND_IMEI -> imei2 = value.blob - Tag.ATTESTATION_ID_MEID -> meid = value.blob - } - } - } - - fun setEcCurveName(curveSize: Int) { - ecCurveName = when (curveSize) { - 224 -> "secp224r1" - 256 -> "secp256r1" - 384 -> "secp384r1" - 521 -> "secp521r1" - else -> "secp256r1" - } - } - - companion object { - private fun getEcCurveName(curve: Int): String = when (curve) { - EcCurve.CURVE_25519 -> "CURVE_25519" - EcCurve.P_224 -> "secp224r1" - EcCurve.P_256 -> "secp256r1" - EcCurve.P_384 -> "secp384r1" - EcCurve.P_521 -> "secp521r1" - else -> throw IllegalArgumentException("Unknown EC curve: $curve") - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as KeyGenParameters - - return keySize == other.keySize && - algorithm == other.algorithm && - certificateSerial == other.certificateSerial && - certificateNotBefore == other.certificateNotBefore && - certificateNotAfter == other.certificateNotAfter && - certificateSubject == other.certificateSubject && - rsaPublicExponent == other.rsaPublicExponent && - ecCurve == other.ecCurve && - ecCurveName == other.ecCurveName && - purpose == other.purpose && - digest == other.digest && - attestationChallenge.contentEquals(other.attestationChallenge) && - brand.contentEquals(other.brand) && - device.contentEquals(other.device) && - product.contentEquals(other.product) && - manufacturer.contentEquals(other.manufacturer) && - model.contentEquals(other.model) && - imei1.contentEquals(other.imei1) && - imei2.contentEquals(other.imei2) && - meid.contentEquals(other.meid) && - serialno.contentEquals(other.serialno) - } - - override fun hashCode(): Int { - var result = keySize - result = 31 * result + algorithm - result = 31 * result + (certificateSerial?.hashCode() ?: 0) - result = 31 * result + (certificateNotBefore?.hashCode() ?: 0) - result = 31 * result + (certificateNotAfter?.hashCode() ?: 0) - result = 31 * result + (certificateSubject?.hashCode() ?: 0) - result = 31 * result + (rsaPublicExponent?.hashCode() ?: 0) - result = 31 * result + ecCurve - result = 31 * result + (ecCurveName?.hashCode() ?: 0) - result = 31 * result + purpose.hashCode() - result = 31 * result + digest.hashCode() - result = 31 * result + (attestationChallenge?.contentHashCode() ?: 0) - result = 31 * result + (brand?.contentHashCode() ?: 0) - result = 31 * result + (device?.contentHashCode() ?: 0) - result = 31 * result + (product?.contentHashCode() ?: 0) - result = 31 * result + (manufacturer?.contentHashCode() ?: 0) - result = 31 * result + (model?.contentHashCode() ?: 0) - result = 31 * result + (imei1?.contentHashCode() ?: 0) - result = 31 * result + (imei2?.contentHashCode() ?: 0) - result = 31 * result + (meid?.contentHashCode() ?: 0) - result = 31 * result + (serialno?.contentHashCode() ?: 0) - return result - } - } - - private val keyboxes = ConcurrentHashMap() - private val leafAlgorithm = ConcurrentHashMap() - - private const val ATTESTATION_APPLICATION_ID_PACKAGE_INFOS_INDEX = 0 - private const val ATTESTATION_APPLICATION_ID_SIGNATURE_DIGESTS_INDEX = 1 - private const val ATTESTATION_PACKAGE_INFO_PACKAGE_NAME_INDEX = 0 - private const val ATTESTATION_PACKAGE_INFO_VERSION_INDEX = 1 - - fun canHack(): Boolean = keyboxes.isNotEmpty() - - private data class Digest(val digest: ByteArray) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - other as Digest - return digest.contentEquals(other.digest) - } - - override fun hashCode(): Int = digest.contentHashCode() - } - - private fun parseKeyPair(keyContent: String): ParseResult { - return try { - PEMParser(StringReader(keyContent.trimLine())).use { parser -> - val pemObject = parser.readObject() - if (pemObject is PEMKeyPair) { - ParseResult.Success(pemObject) - } else { - ParseResult.Error("Invalid PEM key pair format") - } - } - } catch (t: Throwable) { - ParseResult.Error("Failed to parse PEM key pair", t) - } - } - - private fun parseCertificate(certContent: String): ParseResult { - return try { - PemReader(StringReader(certContent.trimLine())).use { reader -> - val pemObject = reader.readPemObject() - val certificate = certificateFactory.generateCertificate( - ByteArrayInputStream(pemObject.content) - ) - ParseResult.Success(certificate) - } - } catch (t: Throwable) { - ParseResult.Error("Failed to parse certificate", t) - } - } - - @Throws(CertificateParsingException::class) - private fun getByteArrayFromAsn1(asn1Encodable: ASN1Encodable): ByteArray { - return when (asn1Encodable) { - is DEROctetString -> asn1Encodable.octets - else -> throw CertificateParsingException("Expected DEROctetString, got ${asn1Encodable::class.simpleName}") - } - } - - fun readFromXml(xmlData: String?) { - keyboxes.clear() - leafAlgorithm.clear() - - if (xmlData == null) { - Logger.i("Clearing all keyboxes") - return - } - - try { - val xmlParser = XmlParser(xmlData) - - val numberOfKeyboxesResult = xmlParser.obtainPath("AndroidAttestation.NumberOfKeyboxes") - val numberOfKeyboxes = when (numberOfKeyboxesResult) { - is XmlParser.ParseResult.Success -> numberOfKeyboxesResult.attributes["text"]?.toIntOrNull() - ?: throw IllegalArgumentException("Invalid number of keyboxes") - is XmlParser.ParseResult.Error -> throw Exception(numberOfKeyboxesResult.message, numberOfKeyboxesResult.cause) - } - - repeat(numberOfKeyboxes) { i -> - processKeybox(xmlParser, i) - } - - Logger.i("Successfully updated $numberOfKeyboxes keyboxes") - } catch (t: Throwable) { - Logger.e("Error loading XML file (keyboxes cleared)", t) - } - } - - private fun processKeybox(xmlParser: XmlParser, index: Int) { - try { - val algorithmResult = xmlParser.obtainPath("AndroidAttestation.Keybox.Key[$index]") - val keyboxAlgorithm = when (algorithmResult) { - is XmlParser.ParseResult.Success -> algorithmResult.attributes["algorithm"] - ?: throw IllegalArgumentException("Missing algorithm attribute") - is XmlParser.ParseResult.Error -> throw Exception(algorithmResult.message, algorithmResult.cause) - } - - val privateKeyResult = xmlParser.obtainPath("AndroidAttestation.Keybox.Key[$index].PrivateKey") - val privateKeyContent = when (privateKeyResult) { - is XmlParser.ParseResult.Success -> privateKeyResult.attributes["text"] - ?: throw IllegalArgumentException("Missing private key text") - is XmlParser.ParseResult.Error -> throw Exception(privateKeyResult.message, privateKeyResult.cause) - } - - val numberOfCertificatesResult = xmlParser.obtainPath( - "AndroidAttestation.Keybox.Key[$index].CertificateChain.NumberOfCertificates" - ) - val numberOfCertificates = when (numberOfCertificatesResult) { - is XmlParser.ParseResult.Success -> numberOfCertificatesResult.attributes["text"]?.toIntOrNull() - ?: throw IllegalArgumentException("Invalid number of certificates") - is XmlParser.ParseResult.Error -> throw Exception(numberOfCertificatesResult.message, numberOfCertificatesResult.cause) - } - - val certificateChain = mutableListOf() - repeat(numberOfCertificates) { j -> - val certResult = xmlParser.obtainPath( - "AndroidAttestation.Keybox.Key[$index].CertificateChain.Certificate[$j]" - ) - val certContent = when (certResult) { - is XmlParser.ParseResult.Success -> certResult.attributes["text"] - ?: throw IllegalArgumentException("Missing certificate text") - is XmlParser.ParseResult.Error -> throw Exception(certResult.message, certResult.cause) - } - - when (val certParseResult = parseCertificate(certContent)) { - is ParseResult.Success -> certificateChain.add(certParseResult.data) - is ParseResult.Error -> throw Exception(certParseResult.message, certParseResult.cause) - } - } - - val pemKeyPair = when (val keyParseResult = parseKeyPair(privateKeyContent)) { - is ParseResult.Success -> keyParseResult.data - is ParseResult.Error -> throw Exception(keyParseResult.message, keyParseResult.cause) - } - - val keyPair = JcaPEMKeyConverter().getKeyPair(pemKeyPair) - - val algorithmName = when (keyboxAlgorithm.lowercase()) { - "ecdsa" -> KeyProperties.KEY_ALGORITHM_EC - "rsa" -> KeyProperties.KEY_ALGORITHM_RSA - else -> keyboxAlgorithm - } - - keyboxes[algorithmName] = KeyBox(pemKeyPair, keyPair, certificateChain) - - } catch (t: Throwable) { - Logger.e("Error processing keybox $index", t) - throw t - } - } - - fun hackCertificateChain(certificateChain: Array?): Array { - if (certificateChain == null) { - throw UnsupportedOperationException("Certificate chain is null!") - } - - return try { - val leaf = certificateFactory.generateCertificate( - ByteArrayInputStream(certificateChain[0].encoded) - ) as X509Certificate - - val extensionBytes = leaf.getExtensionValue(ATTESTATION_OID.id) - ?: return certificateChain // No attestation extension, return original - - hackCertificateWithAttestation(leaf, certificateChain) - } catch (t: Throwable) { - Logger.e("Failed to hack certificate chain", t) - certificateChain - } - } - - fun hackCertificateChainCA(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 = leafAlgorithm.remove(key) - ?: throw UnsupportedOperationException("No algorithm found for key $key") - - val keybox = keyboxes[algorithm] - ?: throw UnsupportedOperationException("Unsupported algorithm: $algorithm") - - CertificateUtils.run { keybox.certificates.toByteArray() } ?: caList - } catch (t: Throwable) { - Logger.e("Failed to hack CA certificate chain", t) - caList - } - } - - fun hackCertificateChainUSR(certificate: ByteArray?, alias: String, uid: Int): ByteArray { - if (certificate == null) { - throw UnsupportedOperationException("Leaf certificate is null!") - } - - return try { - val leaf = certificateFactory.generateCertificate( - ByteArrayInputStream(certificate) - ) as X509Certificate - - val extensionBytes = leaf.getExtensionValue(ATTESTATION_OID.id) - ?: return certificate // No attestation extension, return original - - val keyIdentifier = KeyIdentifier(alias, uid) - leafAlgorithm[keyIdentifier] = leaf.publicKey.algorithm - - hackSingleCertificate(leaf)?.encoded ?: certificate - } catch (t: Throwable) { - Logger.e("Failed to hack user certificate", t) - certificate - } - } - - fun generateKeyPair(params: KeyGenParameters): KeyPair? { - return try { - when (params.algorithm) { - Algorithm.EC -> { - Logger.d("Generating EC keypair of size ${params.keySize}") - buildECKeyPair(params) - } - Algorithm.RSA -> { - Logger.d("Generating RSA keypair of size ${params.keySize}") - buildRSAKeyPair(params) - } - else -> { - Logger.e("Unsupported algorithm: ${params.algorithm}") - null - } - } - } catch (t: Throwable) { - Logger.e("Failed to generate key pair", t) - null - } - } - - fun generateChain(uid: Int, params: KeyGenParameters, keyPair: KeyPair): List? { - return try { - val keybox = getKeyboxForAlgorithm(params.algorithm) - ?: return null - - val issuer = X509CertificateHolder(keybox.certificates[0].encoded).subject - val leaf = buildCertificate(keyPair, keybox, params, issuer, uid) - - val chain = mutableListOf().apply { - add(leaf) - addAll(keybox.certificates) - } - - CertificateUtils.run { chain.toByteArrayList() } - } catch (t: Throwable) { - Logger.e("Failed to generate certificate chain", t) - null - } - } - - fun generateKeyPair( - uid: Int, - descriptor: KeyDescriptor, - attestKeyDescriptor: KeyDescriptor?, - params: KeyGenParameters - ): Pair>? { - Logger.i("Requested KeyPair with alias: ${descriptor.alias}") - - val isAttestPurpose = attestKeyDescriptor != null - if (isAttestPurpose) { - Logger.i("Requested KeyPair with attestKey: ${attestKeyDescriptor?.alias}") - } - - return try { - val keyPair = generateKeyPair(params) ?: return null - val keybox = getKeyboxForAlgorithm(params.algorithm) ?: return null - - val (rootKeyPair, issuer) = if (isAttestPurpose) { - val attestInfo = getAttestationKeyInfo(uid, attestKeyDescriptor!!) - if (attestInfo != null) { - attestInfo.first to attestInfo.second - } else { - 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, rootKeyPair) - val chain = if (isAttestPurpose) mutableListOf() else mutableListOf().apply { addAll(keybox.certificates) } - chain.add(0, leaf) - - Logger.d("Successfully generated certificate for alias: ${descriptor.alias}") - Pair(keyPair, chain) - } catch (t: Throwable) { - Logger.e("Failed to generate key pair with certificates", t) - null - } - } - - private fun buildECKeyPair(params: KeyGenParameters): KeyPair { - Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) - Security.addProvider(BouncyCastleProvider()) - - val spec = ECGenParameterSpec(params.ecCurveName) - val keyPairGenerator = KeyPairGenerator.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME) - keyPairGenerator.initialize(spec) - return keyPairGenerator.generateKeyPair() - } - - private fun buildRSAKeyPair(params: KeyGenParameters): KeyPair { - Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) - Security.addProvider(BouncyCastleProvider()) - - val spec = RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent) - val keyPairGenerator = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME) - keyPairGenerator.initialize(spec) - return keyPairGenerator.generateKeyPair() - } - - private fun getKeyboxForAlgorithm(algorithm: Int): KeyBox? { - val algorithmName = when (algorithm) { - Algorithm.EC -> KeyProperties.KEY_ALGORITHM_EC - Algorithm.RSA -> KeyProperties.KEY_ALGORITHM_RSA - else -> { - Logger.e("Unsupported algorithm: $algorithm") - return null - } - } - return keyboxes[algorithmName] - } - - private fun getAttestationKeyInfo(uid: Int, attestKeyDescriptor: KeyDescriptor): Pair? { - Logger.d("Looking for attestation key: uid=$uid alias=${attestKeyDescriptor.alias}") - - val keyInfo = SecurityLevelInterceptor.getKeyPairs(uid, attestKeyDescriptor.alias) - return if (keyInfo != null) { - val issuer = X509CertificateHolder(keyInfo.second[0].encoded).subject - Pair(keyInfo.first, issuer) - } else { - Logger.e("Attestation key info not found, falling back to default keybox") - null - } - } - - private fun hackCertificateWithAttestation(leaf: X509Certificate, originalChain: Array): Array { - val leafHolder = X509CertificateHolder(leaf.encoded) - val extension = leafHolder.getExtension(ATTESTATION_OID) - val sequence = ASN1Sequence.getInstance(extension.extnValue.octets) - val encodables = sequence.toArray() - val teeEnforced = encodables[7] as ASN1Sequence - - val vector = ASN1EncodableVector() - var rootOfTrust: ASN1Encodable? = null - - teeEnforced.forEach { element -> - val taggedObject = element as ASN1TaggedObject - if (taggedObject.tagNo == 704) { - rootOfTrust = taggedObject.baseObject.toASN1Primitive() - } else { - vector.add(taggedObject) - } - } - - val keybox = keyboxes[leaf.publicKey.algorithm] - ?: throw UnsupportedOperationException("Unsupported algorithm: ${leaf.publicKey.algorithm}") - - val certificates = LinkedList(keybox.certificates) - val builder = X509v3CertificateBuilder( - X509CertificateHolder(certificates[0].encoded).subject, - leafHolder.serialNumber, - leafHolder.notBefore, - leafHolder.notAfter, - leafHolder.subject, - leafHolder.subjectPublicKeyInfo - ) - - val signer = JcaContentSignerBuilder(leaf.sigAlgName).build(keybox.keyPair.private) - - val hackedExtension = createHackedAttestationExtension(rootOfTrust, vector, encodables) - builder.addExtension(hackedExtension) - - leafHolder.extensions.extensionOIDs.forEach { oid -> - if (oid.id != ATTESTATION_OID.id) { - builder.addExtension(leafHolder.getExtension(oid)) - } - } - - certificates.addFirst(JcaX509CertificateConverter().getCertificate(builder.build(signer))) - return certificates.toTypedArray() - } - - private fun hackSingleCertificate(leaf: X509Certificate): Certificate? { - return try { - val leafHolder = X509CertificateHolder(leaf.encoded) - val extension = leafHolder.getExtension(ATTESTATION_OID) - val sequence = ASN1Sequence.getInstance(extension.extnValue.octets) - val encodables = sequence.toArray() - val teeEnforced = encodables[7] as ASN1Sequence - - val vector = ASN1EncodableVector() - var rootOfTrust: ASN1Encodable? = null - - teeEnforced.forEach { element -> - val taggedObject = element as ASN1TaggedObject - if (taggedObject.tagNo == 704) { - rootOfTrust = taggedObject.baseObject.toASN1Primitive() - } else { - vector.add(taggedObject) - } - } - - val keybox = keyboxes[leaf.publicKey.algorithm] - ?: throw UnsupportedOperationException("Unsupported algorithm: ${leaf.publicKey.algorithm}") - - val builder = X509v3CertificateBuilder( - X509CertificateHolder(keybox.certificates[0].encoded).subject, - leafHolder.serialNumber, - leafHolder.notBefore, - leafHolder.notAfter, - leafHolder.subject, - leafHolder.subjectPublicKeyInfo - ) - - val signer = JcaContentSignerBuilder(leaf.sigAlgName).build(keybox.keyPair.private) - - val hackedExtension = createHackedAttestationExtension(rootOfTrust, vector, encodables) - builder.addExtension(hackedExtension) - - leafHolder.extensions.extensionOIDs.forEach { oid -> - if (oid.id != ATTESTATION_OID.id) { - builder.addExtension(leafHolder.getExtension(oid)) - } - } - - JcaX509CertificateConverter().getCertificate(builder.build(signer)) - } catch (t: Throwable) { - Logger.e("Failed to hack single certificate", t) - null - } - } - - private fun createHackedAttestationExtension( - originalRootOfTrust: ASN1Encodable?, - vector: ASN1EncodableVector, - originalEncodables: Array - ): Extension { - val verifiedBootKey = bootKey - var verifiedBootHash: ByteArray? = null - - try { - if (originalRootOfTrust is ASN1Sequence) { - verifiedBootHash = getByteArrayFromAsn1(originalRootOfTrust.getObjectAt(3)) - } - } catch (t: Throwable) { - Logger.e("Failed to get verified boot hash from original, using generated", t) - } - - if (verifiedBootHash == null) { - verifiedBootHash = bootHash - } - - val rootOfTrustElements = arrayOf( - DEROctetString(verifiedBootKey), - ASN1Boolean.TRUE, - ASN1Enumerated(0), - DEROctetString(verifiedBootHash) - ) - val hackedRootOfTrust = DERSequence(rootOfTrustElements) - - vector.add(DERTaggedObject(true, 718, ASN1Integer(vendorPatchLevelLong.toLong()))) - vector.add(DERTaggedObject(true, 719, ASN1Integer(bootPatchLevelLong.toLong()))) - vector.add(DERTaggedObject(true, 706, ASN1Integer(patchLevel.toLong()))) - vector.add(DERTaggedObject(true, 705, ASN1Integer(osVersion.toLong()))) - vector.add(DERTaggedObject(704, hackedRootOfTrust)) - - val hackEnforced = DERSequence(vector) - originalEncodables[7] = hackEnforced - val hackedSequence = DERSequence(originalEncodables) - val hackedSequenceOctets = DEROctetString(hackedSequence) - - return Extension(ATTESTATION_OID, false, hackedSequenceOctets) - } - - private fun buildCertificate( - keyPair: KeyPair, - keybox: KeyBox, - params: KeyGenParameters, - issuer: X500Name, - uid: Int, - signingKeyPair: KeyPair = keybox.keyPair - ): Certificate { - val builder = JcaX509v3CertificateBuilder( - issuer, - params.certificateSerial ?: BigInteger.ONE, - params.certificateNotBefore ?: Date(), - params.certificateNotAfter ?: (keybox.certificates[0] as X509Certificate).notAfter, - params.certificateSubject ?: X500Name("CN=Android KeyStore Key"), - keyPair.public - ) - - builder.addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.keyCertSign)) - builder.addExtension(createAttestationExtension(params, uid)) - - val contentSigner = when (params.algorithm) { - Algorithm.EC -> JcaContentSignerBuilder("SHA256withECDSA").build(signingKeyPair.private) - Algorithm.RSA -> JcaContentSignerBuilder("SHA256withRSA").build(signingKeyPair.private) - else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}") - } - - return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner)) - } - - private fun createAttestationExtension(params: KeyGenParameters, uid: Int): Extension { - try { - val key = bootKey - val hash = bootHash - - val rootOfTrustEncodables = arrayOf( - DEROctetString(key), - ASN1Boolean.TRUE, - ASN1Enumerated(0), - DEROctetString(hash) - ) - val rootOfTrustSeq = DERSequence(rootOfTrustEncodables) - - val purpose = DERSet(fromIntList(params.purpose)) - val algorithm = ASN1Integer(params.algorithm.toLong()) - val keySize = ASN1Integer(params.keySize.toLong()) - val digest = DERSet(fromIntList(params.digest)) - val ecCurve = ASN1Integer(params.ecCurve.toLong()) - val noAuthRequired = DERNull.INSTANCE - - val osVersion = ASN1Integer(io.github.beakthoven.TrickyStoreOSS.osVersion.toLong()) - val osPatchLevel = ASN1Integer(io.github.beakthoven.TrickyStoreOSS.patchLevel.toLong()) - val applicationID = createApplicationId(uid) - val bootPatchLevel = ASN1Integer(bootPatchLevelLong.toLong()) - val vendorPatchLevel = ASN1Integer(vendorPatchLevelLong.toLong()) - val creationDateTime = ASN1Integer(System.currentTimeMillis()) - val origin = ASN1Integer(0L) - val moduleHash = DEROctetString(io.github.beakthoven.TrickyStoreOSS.moduleHash) - - val teeEnforcedObjects = mutableListOf( - DERTaggedObject(true, 1, purpose), - DERTaggedObject(true, 2, algorithm), - DERTaggedObject(true, 3, keySize), - DERTaggedObject(true, 5, digest), - DERTaggedObject(true, 10, ecCurve), - DERTaggedObject(true, 503, noAuthRequired), - DERTaggedObject(true, 702, origin), - DERTaggedObject(true, 704, rootOfTrustSeq), - DERTaggedObject(true, 705, osVersion), - DERTaggedObject(true, 706, osPatchLevel), - DERTaggedObject(true, 718, vendorPatchLevel), - DERTaggedObject(true, 719, bootPatchLevel), - 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.imei2?.let { teeEnforcedObjects.add(DERTaggedObject(true, 715, DEROctetString(it))) } - params.meid?.let { teeEnforcedObjects.add(DERTaggedObject(true, 723, DEROctetString(it))) } - - teeEnforcedObjects.sortBy { it.tagNo } - - val softwareEnforcedObjects = arrayOf( - DERTaggedObject(true, 709, applicationID), - DERTaggedObject(true, 701, creationDateTime) - ) - - return Extension( - ATTESTATION_OID, - false, - getAsn1OctetString(teeEnforcedObjects.toTypedArray(), softwareEnforcedObjects, params) - ) - } catch (t: Throwable) { - Logger.e("Failed to create attestation extension", t) - throw t - } - } - - private fun fromIntList(list: List): Array { - return list.map { ASN1Integer(it.toLong()) }.toTypedArray() - } - - private fun getAsn1OctetString( - teeEnforcedEncodables: Array, - softwareEnforcedEncodables: Array, - params: KeyGenParameters - ): ASN1OctetString { - val attestationVersion = ASN1Integer(400L) - val attestationSecurityLevel = ASN1Enumerated(1) - val keymasterVersion = ASN1Integer(400L) - val keymasterSecurityLevel = ASN1Enumerated(1) - val attestationChallenge = DEROctetString(params.attestationChallenge ?: ByteArray(0)) - val uniqueId = DEROctetString(ByteArray(0)) - val softwareEnforced = DERSequence(softwareEnforcedEncodables) - val teeEnforced = DERSequence(teeEnforcedEncodables) - - val keyDescriptionEncodables = arrayOf( - attestationVersion, - attestationSecurityLevel, - keymasterVersion, - keymasterSecurityLevel, - attestationChallenge, - uniqueId, - softwareEnforced, - teeEnforced - ) - - val keyDescriptionSeq = DERSequence(keyDescriptionEncodables) - return DEROctetString(keyDescriptionSeq.encoded) - } - - @Throws(Throwable::class) - private fun createApplicationId(uid: Int): DEROctetString { - val pm = Config.getPm() ?: throw IllegalStateException("PackageManager not found!") - val packages = pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid") - - val packageInfoArray = Array(packages.size) { i -> - val packageName = packages[i] - val packageInfo = pm.getPackageInfoCompat(packageName, PackageManager.GET_SIGNING_CERTIFICATES.toLong(), uid / 100000) - - DERSequence(arrayOf( - DEROctetString(packageName.toByteArray(StandardCharsets.UTF_8)), - ASN1Integer(packageInfo.longVersionCode) - )) - } - - val signatures = mutableSetOf() - val messageDigest = MessageDigest.getInstance("SHA-256") - - packages.forEach { packageName -> - val packageInfo = pm.getPackageInfoCompat(packageName, PackageManager.GET_SIGNING_CERTIFICATES.toLong(), uid / 100000) - packageInfo.signingInfo?.apkContentsSigners?.forEach { signature -> - signatures.add(Digest(messageDigest.digest(signature.toByteArray()))) - } - } - - val signaturesArray = signatures.map { DEROctetString(it.digest) }.toTypedArray() - - val applicationIdArray = arrayOf( - DERSet(packageInfoArray), - DERSet(signaturesArray) - ) - - return DEROctetString(DERSequence(applicationIdArray).encoded) - } +/* + * Copyright 2025 Dakkshesh + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package io.github.beakthoven.TrickyStoreOSS + +import android.content.pm.PackageManager +import android.hardware.security.keymint.Algorithm +import android.hardware.security.keymint.EcCurve +import android.hardware.security.keymint.KeyParameter +import android.hardware.security.keymint.Tag +import android.security.keystore.KeyProperties +import android.system.keystore2.KeyDescriptor +import android.util.Pair +import io.github.beakthoven.TrickyStoreOSS.* +import io.github.beakthoven.TrickyStoreOSS.core.config.Config +import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger +import io.github.beakthoven.TrickyStoreOSS.interceptors.SecurityLevelInterceptor +import org.bouncycastle.asn1.* +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.KeyUsage +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.openssl.PEMKeyPair +import org.bouncycastle.openssl.PEMParser +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import org.bouncycastle.util.io.pem.PemReader +import java.io.ByteArrayInputStream +import java.io.StringReader +import java.math.BigInteger +import java.nio.charset.StandardCharsets +import java.security.* +import java.security.cert.Certificate +import java.security.cert.CertificateFactory +import java.security.cert.CertificateParsingException +import java.security.cert.X509Certificate +import java.security.spec.ECGenParameterSpec +import java.security.spec.RSAKeyGenParameterSpec +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import javax.security.auth.x500.X500Principal + +object CertificateHacker { + + private val ATTESTATION_OID = ASN1ObjectIdentifier("1.3.6.1.4.1.11129.2.1.17") + + private val certificateFactory: CertificateFactory by lazy { + try { + CertificateFactory.getInstance("X.509") + } catch (t: Throwable) { + Logger.e("Failed to initialize certificate factory", t) + throw RuntimeException("Cannot initialize certificate factory", t) + } + } + + data class KeyBox( + val pemKeyPair: PEMKeyPair, + val keyPair: KeyPair, + val certificates: List + ) + + data class KeyIdentifier( + val alias: String, + val uid: Int + ) + + sealed class ParseResult { + data class Success(val data: T) : ParseResult() + data class Error(val message: String, val cause: Throwable? = null) : ParseResult() + } + + sealed class HackResult { + data class Success(val data: T) : HackResult() + data class Error(val message: String, val cause: Throwable? = null) : HackResult() + } + + data class KeyGenParameters( + var keySize: Int = 0, + var algorithm: Int = 0, + var certificateSerial: BigInteger? = null, + var certificateNotBefore: Date? = null, + var certificateNotAfter: Date? = null, + var certificateSubject: X500Name? = null, + var rsaPublicExponent: BigInteger? = null, + var ecCurve: Int = 0, + var ecCurveName: String? = null, + var purpose: MutableList = mutableListOf(), + var digest: MutableList = mutableListOf(), + var attestationChallenge: ByteArray? = null, + var brand: ByteArray? = null, + var device: ByteArray? = null, + var product: ByteArray? = null, + var manufacturer: ByteArray? = null, + var model: ByteArray? = null, + var imei1: ByteArray? = null, + var imei2: ByteArray? = null, + var meid: ByteArray? = null, + var serialno: ByteArray? = null + ) { + + constructor(params: Array) : this() { + parseKeyParameters(params) + } + + private fun parseKeyParameters(params: Array) { + params.forEach { param -> + Logger.d("Processing key parameter: ${param.tag}") + val value = param.value + + when (param.tag) { + Tag.KEY_SIZE -> keySize = value.integer + Tag.ALGORITHM -> algorithm = value.algorithm + Tag.CERTIFICATE_SERIAL -> certificateSerial = BigInteger(value.blob) + Tag.CERTIFICATE_NOT_BEFORE -> certificateNotBefore = Date(value.dateTime) + Tag.CERTIFICATE_NOT_AFTER -> certificateNotAfter = Date(value.dateTime) + Tag.CERTIFICATE_SUBJECT -> certificateSubject = X500Name(X500Principal(value.blob).name) + Tag.RSA_PUBLIC_EXPONENT -> rsaPublicExponent = BigInteger(value.blob) + Tag.EC_CURVE -> { + ecCurve = value.ecCurve + ecCurveName = getEcCurveName(ecCurve) + } + Tag.PURPOSE -> purpose.add(value.keyPurpose) + Tag.DIGEST -> digest.add(value.digest) + Tag.ATTESTATION_CHALLENGE -> attestationChallenge = value.blob + Tag.ATTESTATION_ID_BRAND -> brand = value.blob + Tag.ATTESTATION_ID_DEVICE -> device = value.blob + Tag.ATTESTATION_ID_PRODUCT -> product = value.blob + Tag.ATTESTATION_ID_MANUFACTURER -> manufacturer = value.blob + Tag.ATTESTATION_ID_MODEL -> model = value.blob + Tag.ATTESTATION_ID_IMEI -> imei1 = value.blob + Tag.ATTESTATION_ID_SECOND_IMEI -> imei2 = value.blob + Tag.ATTESTATION_ID_MEID -> meid = value.blob + } + } + } + + fun setEcCurveName(curveSize: Int) { + ecCurveName = when (curveSize) { + 224 -> "secp224r1" + 256 -> "secp256r1" + 384 -> "secp384r1" + 521 -> "secp521r1" + else -> "secp256r1" + } + } + + companion object { + private fun getEcCurveName(curve: Int): String = when (curve) { + EcCurve.CURVE_25519 -> "CURVE_25519" + EcCurve.P_224 -> "secp224r1" + EcCurve.P_256 -> "secp256r1" + EcCurve.P_384 -> "secp384r1" + EcCurve.P_521 -> "secp521r1" + else -> throw IllegalArgumentException("Unknown EC curve: $curve") + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as KeyGenParameters + + return keySize == other.keySize && + algorithm == other.algorithm && + certificateSerial == other.certificateSerial && + certificateNotBefore == other.certificateNotBefore && + certificateNotAfter == other.certificateNotAfter && + certificateSubject == other.certificateSubject && + rsaPublicExponent == other.rsaPublicExponent && + ecCurve == other.ecCurve && + ecCurveName == other.ecCurveName && + purpose == other.purpose && + digest == other.digest && + attestationChallenge.contentEquals(other.attestationChallenge) && + brand.contentEquals(other.brand) && + device.contentEquals(other.device) && + product.contentEquals(other.product) && + manufacturer.contentEquals(other.manufacturer) && + model.contentEquals(other.model) && + imei1.contentEquals(other.imei1) && + imei2.contentEquals(other.imei2) && + meid.contentEquals(other.meid) && + serialno.contentEquals(other.serialno) + } + + override fun hashCode(): Int { + var result = keySize + result = 31 * result + algorithm + result = 31 * result + (certificateSerial?.hashCode() ?: 0) + result = 31 * result + (certificateNotBefore?.hashCode() ?: 0) + result = 31 * result + (certificateNotAfter?.hashCode() ?: 0) + result = 31 * result + (certificateSubject?.hashCode() ?: 0) + result = 31 * result + (rsaPublicExponent?.hashCode() ?: 0) + result = 31 * result + ecCurve + result = 31 * result + (ecCurveName?.hashCode() ?: 0) + result = 31 * result + purpose.hashCode() + result = 31 * result + digest.hashCode() + result = 31 * result + (attestationChallenge?.contentHashCode() ?: 0) + result = 31 * result + (brand?.contentHashCode() ?: 0) + result = 31 * result + (device?.contentHashCode() ?: 0) + result = 31 * result + (product?.contentHashCode() ?: 0) + result = 31 * result + (manufacturer?.contentHashCode() ?: 0) + result = 31 * result + (model?.contentHashCode() ?: 0) + result = 31 * result + (imei1?.contentHashCode() ?: 0) + result = 31 * result + (imei2?.contentHashCode() ?: 0) + result = 31 * result + (meid?.contentHashCode() ?: 0) + result = 31 * result + (serialno?.contentHashCode() ?: 0) + return result + } + } + + private val keyboxes = ConcurrentHashMap() + private val leafAlgorithm = ConcurrentHashMap() + + private const val ATTESTATION_APPLICATION_ID_PACKAGE_INFOS_INDEX = 0 + private const val ATTESTATION_APPLICATION_ID_SIGNATURE_DIGESTS_INDEX = 1 + private const val ATTESTATION_PACKAGE_INFO_PACKAGE_NAME_INDEX = 0 + private const val ATTESTATION_PACKAGE_INFO_VERSION_INDEX = 1 + + fun canHack(): Boolean = keyboxes.isNotEmpty() + + private data class Digest(val digest: ByteArray) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Digest + return digest.contentEquals(other.digest) + } + + override fun hashCode(): Int = digest.contentHashCode() + } + + private fun parseKeyPair(keyContent: String): ParseResult { + return try { + PEMParser(StringReader(keyContent.trimLine())).use { parser -> + val pemObject = parser.readObject() + if (pemObject is PEMKeyPair) { + ParseResult.Success(pemObject) + } else { + ParseResult.Error("Invalid PEM key pair format") + } + } + } catch (t: Throwable) { + ParseResult.Error("Failed to parse PEM key pair", t) + } + } + + private fun parseCertificate(certContent: String): ParseResult { + return try { + PemReader(StringReader(certContent.trimLine())).use { reader -> + val pemObject = reader.readPemObject() + val certificate = certificateFactory.generateCertificate( + ByteArrayInputStream(pemObject.content) + ) + ParseResult.Success(certificate) + } + } catch (t: Throwable) { + ParseResult.Error("Failed to parse certificate", t) + } + } + + @Throws(CertificateParsingException::class) + private fun getByteArrayFromAsn1(asn1Encodable: ASN1Encodable): ByteArray { + return when (asn1Encodable) { + is DEROctetString -> asn1Encodable.octets + else -> throw CertificateParsingException("Expected DEROctetString, got ${asn1Encodable::class.simpleName}") + } + } + + fun readFromXml(xmlData: String?) { + keyboxes.clear() + leafAlgorithm.clear() + + if (xmlData == null) { + Logger.i("Clearing all keyboxes") + return + } + + try { + val xmlParser = XmlParser(xmlData) + + val numberOfKeyboxesResult = xmlParser.obtainPath("AndroidAttestation.NumberOfKeyboxes") + val numberOfKeyboxes = when (numberOfKeyboxesResult) { + is XmlParser.ParseResult.Success -> numberOfKeyboxesResult.attributes["text"]?.toIntOrNull() + ?: throw IllegalArgumentException("Invalid number of keyboxes") + is XmlParser.ParseResult.Error -> throw Exception(numberOfKeyboxesResult.message, numberOfKeyboxesResult.cause) + } + + repeat(numberOfKeyboxes) { i -> + processKeybox(xmlParser, i) + } + + Logger.i("Successfully updated $numberOfKeyboxes keyboxes") + } catch (t: Throwable) { + Logger.e("Error loading XML file (keyboxes cleared)", t) + } + } + + private fun processKeybox(xmlParser: XmlParser, index: Int) { + try { + val algorithmResult = xmlParser.obtainPath("AndroidAttestation.Keybox.Key[$index]") + val keyboxAlgorithm = when (algorithmResult) { + is XmlParser.ParseResult.Success -> algorithmResult.attributes["algorithm"] + ?: throw IllegalArgumentException("Missing algorithm attribute") + is XmlParser.ParseResult.Error -> throw Exception(algorithmResult.message, algorithmResult.cause) + } + + val privateKeyResult = xmlParser.obtainPath("AndroidAttestation.Keybox.Key[$index].PrivateKey") + val privateKeyContent = when (privateKeyResult) { + is XmlParser.ParseResult.Success -> privateKeyResult.attributes["text"] + ?: throw IllegalArgumentException("Missing private key text") + is XmlParser.ParseResult.Error -> throw Exception(privateKeyResult.message, privateKeyResult.cause) + } + + val numberOfCertificatesResult = xmlParser.obtainPath( + "AndroidAttestation.Keybox.Key[$index].CertificateChain.NumberOfCertificates" + ) + val numberOfCertificates = when (numberOfCertificatesResult) { + is XmlParser.ParseResult.Success -> numberOfCertificatesResult.attributes["text"]?.toIntOrNull() + ?: throw IllegalArgumentException("Invalid number of certificates") + is XmlParser.ParseResult.Error -> throw Exception(numberOfCertificatesResult.message, numberOfCertificatesResult.cause) + } + + val certificateChain = mutableListOf() + repeat(numberOfCertificates) { j -> + val certResult = xmlParser.obtainPath( + "AndroidAttestation.Keybox.Key[$index].CertificateChain.Certificate[$j]" + ) + val certContent = when (certResult) { + is XmlParser.ParseResult.Success -> certResult.attributes["text"] + ?: throw IllegalArgumentException("Missing certificate text") + is XmlParser.ParseResult.Error -> throw Exception(certResult.message, certResult.cause) + } + + when (val certParseResult = parseCertificate(certContent)) { + is ParseResult.Success -> certificateChain.add(certParseResult.data) + is ParseResult.Error -> throw Exception(certParseResult.message, certParseResult.cause) + } + } + + val pemKeyPair = when (val keyParseResult = parseKeyPair(privateKeyContent)) { + is ParseResult.Success -> keyParseResult.data + is ParseResult.Error -> throw Exception(keyParseResult.message, keyParseResult.cause) + } + + val keyPair = JcaPEMKeyConverter().getKeyPair(pemKeyPair) + + val algorithmName = when (keyboxAlgorithm.lowercase()) { + "ecdsa" -> KeyProperties.KEY_ALGORITHM_EC + "rsa" -> KeyProperties.KEY_ALGORITHM_RSA + else -> keyboxAlgorithm + } + + keyboxes[algorithmName] = KeyBox(pemKeyPair, keyPair, certificateChain) + + } catch (t: Throwable) { + Logger.e("Error processing keybox $index", t) + throw t + } + } + + fun hackCertificateChain(certificateChain: Array?): Array { + if (certificateChain == null) { + throw UnsupportedOperationException("Certificate chain is null!") + } + + return try { + val leaf = certificateFactory.generateCertificate( + ByteArrayInputStream(certificateChain[0].encoded) + ) as X509Certificate + + val extensionBytes = leaf.getExtensionValue(ATTESTATION_OID.id) + ?: return certificateChain // No attestation extension, return original + + hackCertificateWithAttestation(leaf, certificateChain) + } catch (t: Throwable) { + Logger.e("Failed to hack certificate chain", t) + certificateChain + } + } + + fun hackCertificateChainCA(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 = leafAlgorithm.remove(key) + ?: throw UnsupportedOperationException("No algorithm found for key $key") + + val keybox = keyboxes[algorithm] + ?: throw UnsupportedOperationException("Unsupported algorithm: $algorithm") + + CertificateUtils.run { keybox.certificates.toByteArray() } ?: caList + } catch (t: Throwable) { + Logger.e("Failed to hack CA certificate chain", t) + caList + } + } + + fun hackCertificateChainUSR(certificate: ByteArray?, alias: String, uid: Int): ByteArray { + if (certificate == null) { + throw UnsupportedOperationException("Leaf certificate is null!") + } + + return try { + val leaf = certificateFactory.generateCertificate( + ByteArrayInputStream(certificate) + ) as X509Certificate + + val extensionBytes = leaf.getExtensionValue(ATTESTATION_OID.id) + ?: return certificate // No attestation extension, return original + + val keyIdentifier = KeyIdentifier(alias, uid) + leafAlgorithm[keyIdentifier] = leaf.publicKey.algorithm + + hackSingleCertificate(leaf)?.encoded ?: certificate + } catch (t: Throwable) { + Logger.e("Failed to hack user certificate", t) + certificate + } + } + + fun generateKeyPair(params: KeyGenParameters): KeyPair? { + return try { + when (params.algorithm) { + Algorithm.EC -> { + Logger.d("Generating EC keypair of size ${params.keySize}") + buildECKeyPair(params) + } + Algorithm.RSA -> { + Logger.d("Generating RSA keypair of size ${params.keySize}") + buildRSAKeyPair(params) + } + else -> { + Logger.e("Unsupported algorithm: ${params.algorithm}") + null + } + } + } catch (t: Throwable) { + Logger.e("Failed to generate key pair", t) + null + } + } + + fun generateChain(uid: Int, params: KeyGenParameters, keyPair: KeyPair): List? { + return try { + val keybox = getKeyboxForAlgorithm(params.algorithm) + ?: return null + + val issuer = X509CertificateHolder(keybox.certificates[0].encoded).subject + val leaf = buildCertificate(keyPair, keybox, params, issuer, uid) + + val chain = mutableListOf().apply { + add(leaf) + addAll(keybox.certificates) + } + + CertificateUtils.run { chain.toByteArrayList() } + } catch (t: Throwable) { + Logger.e("Failed to generate certificate chain", t) + null + } + } + + fun generateKeyPair( + uid: Int, + descriptor: KeyDescriptor, + attestKeyDescriptor: KeyDescriptor?, + params: KeyGenParameters + ): Pair>? { + Logger.i("Requested KeyPair with alias: ${descriptor.alias}") + + val isAttestPurpose = attestKeyDescriptor != null + if (isAttestPurpose) { + Logger.i("Requested KeyPair with attestKey: ${attestKeyDescriptor?.alias}") + } + + return try { + val keyPair = generateKeyPair(params) ?: return null + val keybox = getKeyboxForAlgorithm(params.algorithm) ?: return null + + val (rootKeyPair, issuer) = if (isAttestPurpose) { + val attestInfo = getAttestationKeyInfo(uid, attestKeyDescriptor!!) + if (attestInfo != null) { + attestInfo.first to attestInfo.second + } else { + 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, rootKeyPair) + val chain = if (isAttestPurpose) mutableListOf() else mutableListOf().apply { addAll(keybox.certificates) } + chain.add(0, leaf) + + Logger.d("Successfully generated certificate for alias: ${descriptor.alias}") + Pair(keyPair, chain) + } catch (t: Throwable) { + Logger.e("Failed to generate key pair with certificates", t) + null + } + } + + private fun buildECKeyPair(params: KeyGenParameters): KeyPair { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) + Security.addProvider(BouncyCastleProvider()) + + val spec = ECGenParameterSpec(params.ecCurveName) + val keyPairGenerator = KeyPairGenerator.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME) + keyPairGenerator.initialize(spec) + return keyPairGenerator.generateKeyPair() + } + + private fun buildRSAKeyPair(params: KeyGenParameters): KeyPair { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) + Security.addProvider(BouncyCastleProvider()) + + val spec = RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent) + val keyPairGenerator = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME) + keyPairGenerator.initialize(spec) + return keyPairGenerator.generateKeyPair() + } + + private fun getKeyboxForAlgorithm(algorithm: Int): KeyBox? { + val algorithmName = when (algorithm) { + Algorithm.EC -> KeyProperties.KEY_ALGORITHM_EC + Algorithm.RSA -> KeyProperties.KEY_ALGORITHM_RSA + else -> { + Logger.e("Unsupported algorithm: $algorithm") + return null + } + } + return keyboxes[algorithmName] + } + + private fun getAttestationKeyInfo(uid: Int, attestKeyDescriptor: KeyDescriptor): Pair? { + Logger.d("Looking for attestation key: uid=$uid alias=${attestKeyDescriptor.alias}") + + val keyInfo = SecurityLevelInterceptor.getKeyPairs(uid, attestKeyDescriptor.alias) + return if (keyInfo != null) { + val issuer = X509CertificateHolder(keyInfo.second[0].encoded).subject + Pair(keyInfo.first, issuer) + } else { + Logger.e("Attestation key info not found, falling back to default keybox") + null + } + } + + private fun hackCertificateWithAttestation(leaf: X509Certificate, originalChain: Array): Array { + val leafHolder = X509CertificateHolder(leaf.encoded) + val extension = leafHolder.getExtension(ATTESTATION_OID) + val sequence = ASN1Sequence.getInstance(extension.extnValue.octets) + val encodables = sequence.toArray() + val teeEnforced = encodables[7] as ASN1Sequence + + val vector = ASN1EncodableVector() + var rootOfTrust: ASN1Encodable? = null + + teeEnforced.forEach { element -> + val taggedObject = element as ASN1TaggedObject + if (taggedObject.tagNo == 704) { + rootOfTrust = taggedObject.baseObject.toASN1Primitive() + } else { + vector.add(taggedObject) + } + } + + val keybox = keyboxes[leaf.publicKey.algorithm] + ?: throw UnsupportedOperationException("Unsupported algorithm: ${leaf.publicKey.algorithm}") + + val certificates = LinkedList(keybox.certificates) + val builder = X509v3CertificateBuilder( + X509CertificateHolder(certificates[0].encoded).subject, + leafHolder.serialNumber, + leafHolder.notBefore, + leafHolder.notAfter, + leafHolder.subject, + leafHolder.subjectPublicKeyInfo + ) + + val signer = JcaContentSignerBuilder(leaf.sigAlgName).build(keybox.keyPair.private) + + val hackedExtension = createHackedAttestationExtension(rootOfTrust, vector, encodables) + builder.addExtension(hackedExtension) + + leafHolder.extensions.extensionOIDs.forEach { oid -> + if (oid.id != ATTESTATION_OID.id) { + builder.addExtension(leafHolder.getExtension(oid)) + } + } + + certificates.addFirst(JcaX509CertificateConverter().getCertificate(builder.build(signer))) + return certificates.toTypedArray() + } + + private fun hackSingleCertificate(leaf: X509Certificate): Certificate? { + return try { + val leafHolder = X509CertificateHolder(leaf.encoded) + val extension = leafHolder.getExtension(ATTESTATION_OID) + val sequence = ASN1Sequence.getInstance(extension.extnValue.octets) + val encodables = sequence.toArray() + val teeEnforced = encodables[7] as ASN1Sequence + + val vector = ASN1EncodableVector() + var rootOfTrust: ASN1Encodable? = null + + teeEnforced.forEach { element -> + val taggedObject = element as ASN1TaggedObject + if (taggedObject.tagNo == 704) { + rootOfTrust = taggedObject.baseObject.toASN1Primitive() + } else { + vector.add(taggedObject) + } + } + + val keybox = keyboxes[leaf.publicKey.algorithm] + ?: throw UnsupportedOperationException("Unsupported algorithm: ${leaf.publicKey.algorithm}") + + val builder = X509v3CertificateBuilder( + X509CertificateHolder(keybox.certificates[0].encoded).subject, + leafHolder.serialNumber, + leafHolder.notBefore, + leafHolder.notAfter, + leafHolder.subject, + leafHolder.subjectPublicKeyInfo + ) + + val signer = JcaContentSignerBuilder(leaf.sigAlgName).build(keybox.keyPair.private) + + val hackedExtension = createHackedAttestationExtension(rootOfTrust, vector, encodables) + builder.addExtension(hackedExtension) + + leafHolder.extensions.extensionOIDs.forEach { oid -> + if (oid.id != ATTESTATION_OID.id) { + builder.addExtension(leafHolder.getExtension(oid)) + } + } + + JcaX509CertificateConverter().getCertificate(builder.build(signer)) + } catch (t: Throwable) { + Logger.e("Failed to hack single certificate", t) + null + } + } + + private fun createHackedAttestationExtension( + originalRootOfTrust: ASN1Encodable?, + vector: ASN1EncodableVector, + originalEncodables: Array + ): Extension { + val verifiedBootKey = bootKey + var verifiedBootHash: ByteArray? = null + + try { + if (originalRootOfTrust is ASN1Sequence) { + verifiedBootHash = getByteArrayFromAsn1(originalRootOfTrust.getObjectAt(3)) + } + } catch (t: Throwable) { + Logger.e("Failed to get verified boot hash from original, using generated", t) + } + + if (verifiedBootHash == null) { + verifiedBootHash = bootHash + } + + val rootOfTrustElements = arrayOf( + DEROctetString(verifiedBootKey), + ASN1Boolean.TRUE, + ASN1Enumerated(0), + DEROctetString(verifiedBootHash) + ) + val hackedRootOfTrust = DERSequence(rootOfTrustElements) + + vector.add(DERTaggedObject(true, 718, ASN1Integer(vendorPatchLevelLong.toLong()))) + vector.add(DERTaggedObject(true, 719, ASN1Integer(bootPatchLevelLong.toLong()))) + vector.add(DERTaggedObject(true, 706, ASN1Integer(patchLevel.toLong()))) + vector.add(DERTaggedObject(true, 705, ASN1Integer(osVersion.toLong()))) + vector.add(DERTaggedObject(704, hackedRootOfTrust)) + + val hackEnforced = DERSequence(vector) + originalEncodables[7] = hackEnforced + val hackedSequence = DERSequence(originalEncodables) + val hackedSequenceOctets = DEROctetString(hackedSequence) + + return Extension(ATTESTATION_OID, false, hackedSequenceOctets) + } + + private fun buildCertificate( + keyPair: KeyPair, + keybox: KeyBox, + params: KeyGenParameters, + issuer: X500Name, + uid: Int, + signingKeyPair: KeyPair = keybox.keyPair + ): Certificate { + val builder = JcaX509v3CertificateBuilder( + issuer, + params.certificateSerial ?: BigInteger.ONE, + params.certificateNotBefore ?: Date(), + params.certificateNotAfter ?: (keybox.certificates[0] as X509Certificate).notAfter, + params.certificateSubject ?: X500Name("CN=Android KeyStore Key"), + keyPair.public + ) + + builder.addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.keyCertSign)) + builder.addExtension(createAttestationExtension(params, uid)) + + val contentSigner = when (params.algorithm) { + Algorithm.EC -> JcaContentSignerBuilder("SHA256withECDSA").build(signingKeyPair.private) + Algorithm.RSA -> JcaContentSignerBuilder("SHA256withRSA").build(signingKeyPair.private) + else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}") + } + + return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner)) + } + + private fun createAttestationExtension(params: KeyGenParameters, uid: Int): Extension { + try { + val key = bootKey + val hash = bootHash + + val rootOfTrustEncodables = arrayOf( + DEROctetString(key), + ASN1Boolean.TRUE, + ASN1Enumerated(0), + DEROctetString(hash) + ) + val rootOfTrustSeq = DERSequence(rootOfTrustEncodables) + + val purpose = DERSet(fromIntList(params.purpose)) + val algorithm = ASN1Integer(params.algorithm.toLong()) + val keySize = ASN1Integer(params.keySize.toLong()) + val digest = DERSet(fromIntList(params.digest)) + val ecCurve = ASN1Integer(params.ecCurve.toLong()) + val noAuthRequired = DERNull.INSTANCE + + val osVersion = ASN1Integer(io.github.beakthoven.TrickyStoreOSS.osVersion.toLong()) + val osPatchLevel = ASN1Integer(io.github.beakthoven.TrickyStoreOSS.patchLevel.toLong()) + val applicationID = createApplicationId(uid) + val bootPatchLevel = ASN1Integer(bootPatchLevelLong.toLong()) + val vendorPatchLevel = ASN1Integer(vendorPatchLevelLong.toLong()) + val creationDateTime = ASN1Integer(System.currentTimeMillis()) + val origin = ASN1Integer(0L) + val moduleHash = DEROctetString(io.github.beakthoven.TrickyStoreOSS.moduleHash) + + val teeEnforcedObjects = mutableListOf( + DERTaggedObject(true, 1, purpose), + DERTaggedObject(true, 2, algorithm), + DERTaggedObject(true, 3, keySize), + DERTaggedObject(true, 5, digest), + DERTaggedObject(true, 10, ecCurve), + DERTaggedObject(true, 503, noAuthRequired), + DERTaggedObject(true, 702, origin), + DERTaggedObject(true, 704, rootOfTrustSeq), + DERTaggedObject(true, 705, osVersion), + DERTaggedObject(true, 706, osPatchLevel), + DERTaggedObject(true, 718, vendorPatchLevel), + DERTaggedObject(true, 719, bootPatchLevel), + 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.imei2?.let { teeEnforcedObjects.add(DERTaggedObject(true, 715, DEROctetString(it))) } + params.meid?.let { teeEnforcedObjects.add(DERTaggedObject(true, 723, DEROctetString(it))) } + + teeEnforcedObjects.sortBy { it.tagNo } + + val softwareEnforcedObjects = arrayOf( + DERTaggedObject(true, 709, applicationID), + DERTaggedObject(true, 701, creationDateTime) + ) + + return Extension( + ATTESTATION_OID, + false, + getAsn1OctetString(teeEnforcedObjects.toTypedArray(), softwareEnforcedObjects, params) + ) + } catch (t: Throwable) { + Logger.e("Failed to create attestation extension", t) + throw t + } + } + + private fun fromIntList(list: List): Array { + return list.map { ASN1Integer(it.toLong()) }.toTypedArray() + } + + private fun getAsn1OctetString( + teeEnforcedEncodables: Array, + softwareEnforcedEncodables: Array, + params: KeyGenParameters + ): ASN1OctetString { + val attestationVersion = ASN1Integer(400L) + val attestationSecurityLevel = ASN1Enumerated(1) + val keymasterVersion = ASN1Integer(400L) + val keymasterSecurityLevel = ASN1Enumerated(1) + val attestationChallenge = DEROctetString(params.attestationChallenge ?: ByteArray(0)) + val uniqueId = DEROctetString(ByteArray(0)) + val softwareEnforced = DERSequence(softwareEnforcedEncodables) + val teeEnforced = DERSequence(teeEnforcedEncodables) + + val keyDescriptionEncodables = arrayOf( + attestationVersion, + attestationSecurityLevel, + keymasterVersion, + keymasterSecurityLevel, + attestationChallenge, + uniqueId, + softwareEnforced, + teeEnforced + ) + + val keyDescriptionSeq = DERSequence(keyDescriptionEncodables) + return DEROctetString(keyDescriptionSeq.encoded) + } + + @Throws(Throwable::class) + private fun createApplicationId(uid: Int): DEROctetString { + val pm = Config.getPm() ?: throw IllegalStateException("PackageManager not found!") + val packages = pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid") + + val packageInfoArray = Array(packages.size) { i -> + val packageName = packages[i] + val packageInfo = pm.getPackageInfoCompat(packageName, PackageManager.GET_SIGNING_CERTIFICATES.toLong(), uid / 100000) + + DERSequence(arrayOf( + DEROctetString(packageName.toByteArray(StandardCharsets.UTF_8)), + ASN1Integer(packageInfo.longVersionCode) + )) + } + + val signatures = mutableSetOf() + val messageDigest = MessageDigest.getInstance("SHA-256") + + packages.forEach { packageName -> + val packageInfo = pm.getPackageInfoCompat(packageName, PackageManager.GET_SIGNING_CERTIFICATES.toLong(), uid / 100000) + packageInfo.signingInfo?.apkContentsSigners?.forEach { signature -> + signatures.add(Digest(messageDigest.digest(signature.toByteArray()))) + } + } + + val signaturesArray = signatures.map { DEROctetString(it.digest) }.toTypedArray() + + val applicationIdArray = arrayOf( + DERSet(packageInfoArray), + DERSet(signaturesArray) + ) + + return DEROctetString(DERSequence(applicationIdArray).encoded) + } } \ No newline at end of file diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateUtils.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateUtils.kt index e797df1..2fbece9 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateUtils.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/CertificateUtils.kt @@ -1,160 +1,160 @@ -/* - * Copyright 2025 Dakkshesh - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package io.github.beakthoven.TrickyStoreOSS - -import android.system.keystore2.KeyEntryResponse -import android.system.keystore2.KeyMetadata -import android.util.Log -import io.github.beakthoven.TrickyStoreOSS.CertificateUtils.putCertificateChain -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.security.cert.Certificate -import java.security.cert.CertificateException -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate - -object CertificateUtils { - private const val TAG = "CertificateUtils" - - 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 - } - } - - fun ByteArray?.toCertificate(): X509Certificate? { - return this?.let { bytes -> - try { - val certFactory = CertificateFactory.getInstance("X.509") - certFactory.generateCertificate(ByteArrayInputStream(bytes)) as? X509Certificate - } catch (e: CertificateException) { - Log.w(TAG, "Couldn't parse certificate in keystore", e) - null - } - } - } - - fun ByteArray.toCertificateResult(): CertificateResult { - return try { - val certFactory = CertificateFactory.getInstance("X.509") - 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 - } 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 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() - } - } - } - - fun KeyEntryResponse.putCertificateChain(chain: Array): Result { - 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) { - output.write(chain[i].encoded) - } - certificateChain = output.toByteArray() - } - } else { - certificateChain = null - } - } - } -} - -fun ByteArray?.toX509Certificate(): X509Certificate? = CertificateUtils.run { this@toX509Certificate.toCertificate() } - -fun ByteArray?.toX509Certificates(): Collection = CertificateUtils.run { this@toX509Certificates.toCertificates() } - -fun Collection.encodedBytes(): ByteArray? = CertificateUtils.run { this@encodedBytes.toByteArray() } - -fun Collection.encodedBytesList(): List? = CertificateUtils.run { this@encodedBytesList.toByteArrayList() } - -fun KeyEntryResponse.putCertificateChain(chain: Array): Result { - 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) { - output.write(chain[i].encoded) - } - certificateChain = output.toByteArray() - } - } else { - certificateChain = null - } - } +/* + * Copyright 2025 Dakkshesh + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package io.github.beakthoven.TrickyStoreOSS + +import android.system.keystore2.KeyEntryResponse +import android.system.keystore2.KeyMetadata +import android.util.Log +import io.github.beakthoven.TrickyStoreOSS.CertificateUtils.putCertificateChain +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.security.cert.Certificate +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate + +object CertificateUtils { + private const val TAG = "CertificateUtils" + + 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 + } + } + + fun ByteArray?.toCertificate(): X509Certificate? { + return this?.let { bytes -> + try { + val certFactory = CertificateFactory.getInstance("X.509") + certFactory.generateCertificate(ByteArrayInputStream(bytes)) as? X509Certificate + } catch (e: CertificateException) { + Log.w(TAG, "Couldn't parse certificate in keystore", e) + null + } + } + } + + fun ByteArray.toCertificateResult(): CertificateResult { + return try { + val certFactory = CertificateFactory.getInstance("X.509") + 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 + } 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 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() + } + } + } + + fun KeyEntryResponse.putCertificateChain(chain: Array): Result { + 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) { + output.write(chain[i].encoded) + } + certificateChain = output.toByteArray() + } + } else { + certificateChain = null + } + } + } +} + +fun ByteArray?.toX509Certificate(): X509Certificate? = CertificateUtils.run { this@toX509Certificate.toCertificate() } + +fun ByteArray?.toX509Certificates(): Collection = CertificateUtils.run { this@toX509Certificates.toCertificates() } + +fun Collection.encodedBytes(): ByteArray? = CertificateUtils.run { this@encodedBytes.toByteArray() } + +fun Collection.encodedBytesList(): List? = CertificateUtils.run { this@encodedBytesList.toByteArrayList() } + +fun KeyEntryResponse.putCertificateChain(chain: Array): Result { + 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) { + output.write(chain[i].encoded) + } + certificateChain = output.toByteArray() + } + } else { + certificateChain = null + } + } } \ No newline at end of file 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 4d2a542..c093a75 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/XmlParser.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/XmlParser.kt @@ -1,155 +1,155 @@ -/* - * Copyright 2025 Dakkshesh - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package io.github.beakthoven.TrickyStoreOSS - -import org.xmlpull.v1.XmlPullParser -import org.xmlpull.v1.XmlPullParserException -import org.xmlpull.v1.XmlPullParserFactory -import java.io.IOException -import java.io.StringReader - -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) - } catch (e: XmlPullParserException) { - ParseResult.Error("XML parsing error: ${e.message}", e) - } catch (e: IOException) { - ParseResult.Error("IO error while parsing XML: ${e.message}", e) - } catch (e: Exception) { - ParseResult.Error("Unexpected error: ${e.message}", e) - } - } - - @Throws(Exception::class) - fun obtainPathLegacy(path: String): Map { - when (val result = obtainPath(path)) { - is ParseResult.Success -> return result.attributes - 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 - ): 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]) - } else { - handleRegularTag(parser, tags, index) - } - } else { - skipCurrentElement(parser) - } - } - - throw XmlPullParserException("Path not found: ${tags.joinToString(".")}") - } - - @Throws(IOException::class, XmlPullParserException::class) - private fun handleIndexedTag( - parser: XmlPullParser, - tags: Array, - index: Int, - tagCounts: MutableMap, - currentTag: String, - indexPart: String - ): Map { - 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) - } else { - if (index == tags.size - 1) { - readAttributes(parser) - } else { - readData(parser, tags, index + 1, tagCounts) - } - } - } - - @Throws(IOException::class, XmlPullParserException::class) - private fun handleRegularTag( - parser: XmlPullParser, - tags: Array, - index: Int - ): Map { - return if (index == tags.size - 1) { - readAttributes(parser) - } else { - 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) - if (name != null && value != null) { - attributes[name] = value - } - } - - if (parser.next() == XmlPullParser.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()) { - XmlPullParser.END_TAG -> depth-- - XmlPullParser.START_TAG -> depth++ - } - } - } -} - +/* + * Copyright 2025 Dakkshesh + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package io.github.beakthoven.TrickyStoreOSS + +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import org.xmlpull.v1.XmlPullParserFactory +import java.io.IOException +import java.io.StringReader + +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) + } catch (e: XmlPullParserException) { + ParseResult.Error("XML parsing error: ${e.message}", e) + } catch (e: IOException) { + ParseResult.Error("IO error while parsing XML: ${e.message}", e) + } catch (e: Exception) { + ParseResult.Error("Unexpected error: ${e.message}", e) + } + } + + @Throws(Exception::class) + fun obtainPathLegacy(path: String): Map { + when (val result = obtainPath(path)) { + is ParseResult.Success -> return result.attributes + 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 + ): 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]) + } else { + handleRegularTag(parser, tags, index) + } + } else { + skipCurrentElement(parser) + } + } + + throw XmlPullParserException("Path not found: ${tags.joinToString(".")}") + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun handleIndexedTag( + parser: XmlPullParser, + tags: Array, + index: Int, + tagCounts: MutableMap, + currentTag: String, + indexPart: String + ): Map { + 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) + } else { + if (index == tags.size - 1) { + readAttributes(parser) + } else { + readData(parser, tags, index + 1, tagCounts) + } + } + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun handleRegularTag( + parser: XmlPullParser, + tags: Array, + index: Int + ): Map { + return if (index == tags.size - 1) { + readAttributes(parser) + } else { + 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) + if (name != null && value != null) { + attributes[name] = value + } + } + + if (parser.next() == XmlPullParser.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()) { + XmlPullParser.END_TAG -> depth-- + XmlPullParser.START_TAG -> depth++ + } + } + } +} + fun String.toXmlParser(): XmlParser = XmlParser(this) \ No newline at end of file diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/core/config/Config.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/core/config/Config.kt index dff9f8c..ae66967 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/core/config/Config.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/core/config/Config.kt @@ -1,267 +1,267 @@ -/* - * Copyright 2025 Dakkshesh - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package io.github.beakthoven.TrickyStoreOSS.core.config - -import android.content.pm.IPackageManager -import android.os.Build -import android.os.FileObserver -import android.os.ServiceManager -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyProperties -import io.github.beakthoven.TrickyStoreOSS.CertificateHacker -import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger -import java.io.File -import java.security.KeyPairGenerator -import java.security.KeyStore -import java.security.SecureRandom -import java.security.spec.ECGenParameterSpec - -object Config { - private val hackPackages = mutableSetOf() - private val generatePackages = mutableSetOf() - private val packageModes = mutableMapOf() - - enum class Mode { - AUTO, LEAF_HACK, GENERATE - } - - private fun updateTargetPackages(f: File?) = runCatching { - hackPackages.clear() - generatePackages.clear() - packageModes.clear() - // Default: always generate for these - listOf("com.google.android.gsf", "com.google.android.gms", "com.android.vending").forEach { - generatePackages.add(it) - packageModes[it] = Mode.GENERATE - } - f?.readLines()?.forEach { - if (it.isNotBlank() && !it.startsWith("#")) { - val n = it.trim() - when { - n.endsWith("!") -> { - val pkg = n.removeSuffix("!").trim() - generatePackages.add(pkg) - packageModes[pkg] = Mode.GENERATE - } - n.endsWith("?") -> { - val pkg = n.removeSuffix("?").trim() - hackPackages.add(pkg) - packageModes[pkg] = Mode.LEAF_HACK - } - else -> { - // Auto mode - packageModes[n] = Mode.AUTO - } - } - } - } - Logger.i("update hack packages: $hackPackages, generate packages=$generatePackages, packageModes=$packageModes") - }.onFailure { - Logger.e("failed to update target files", it) - } - - private fun updateKeyBox(f: File?) = runCatching { - CertificateHacker.readFromXml(f?.readText()) - }.onFailure { - Logger.e("failed to update keybox", it) - } - - private const val CONFIG_PATH = "/data/adb/tricky_store" - private const val TARGET_FILE = "target.txt" - private const val KEYBOX_FILE = "keybox.xml" - private const val TEE_STATUS_FILE = "tee_status" - private const val PATCHLEVEL_FILE = "security_patch.txt" - private val root = File(CONFIG_PATH) - - @Volatile - private var teeBroken: Boolean? = null - - private fun isTEEWorking(): Boolean { - val alias = "tee_attest_test_key" - return try { - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - android.app.ActivityThread.initializeMainlineModules(); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - android.security.keystore2.AndroidKeyStoreProvider.install(); - } else { - android.security.keystore.AndroidKeyStoreProvider.install(); - } - - val keyStore = KeyStore.getInstance("AndroidKeyStore") - keyStore.load(null) - - val keyPairGenerator = KeyPairGenerator.getInstance( - KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore") - - val challenge = ByteArray(16).apply { - SecureRandom().nextBytes(this) - } - - val parameterSpec = KeyGenParameterSpec.Builder( - alias, - KeyProperties.PURPOSE_SIGN - ) - .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) - .setDigests(KeyProperties.DIGEST_SHA256) - .setAttestationChallenge(challenge) - .setIsStrongBoxBacked(false) - .build() - - keyPairGenerator.initialize(parameterSpec) - keyPairGenerator.generateKeyPair() - - keyStore.deleteEntry(alias) - true - } catch (e: Exception) { - Logger.e("TEE check failure: ${e.message}") - false - } - } - - - private fun storeTEEStatus(root: File) { - val statusFile = File(root, TEE_STATUS_FILE) - val status = isTEEWorking() - teeBroken = !status - try { - statusFile.writeText("teeBroken=${!status}") - } catch (e: Exception) { - Logger.e("Failed to write TEE status: ${e.message}") - } - } - - private fun loadTEEStatus(root: File) { - val statusFile = File(root, TEE_STATUS_FILE) - if (statusFile.exists()) { - val line = statusFile.readText().trim() - teeBroken = line == "teeBroken=true" - } else { - teeBroken = null - } - } - - 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 - } - when (path) { - TARGET_FILE -> updateTargetPackages(f) - KEYBOX_FILE -> updateKeyBox(f) - PATCHLEVEL_FILE -> updatePatchLevel(f) - } - } - } - - fun initialize() { - root.mkdirs() - val scope = File(root, TARGET_FILE) - if (scope.exists()) { - updateTargetPackages(scope) - } else { - Logger.e("target.txt file not found, please put it to $scope !") - } - val keybox = File(root, KEYBOX_FILE) - if (!keybox.exists()) { - Logger.e("keybox file not found, please put it to $keybox !") - } else { - updateKeyBox(keybox) - } - storeTEEStatus(root) - val patchFile = File(root, PATCHLEVEL_FILE) - updatePatchLevel(if (patchFile.exists()) patchFile else null) - ConfigObserver.startWatching() - } - - private var iPm: IPackageManager? = null - - fun getPm(): IPackageManager? { - if (iPm == null) { - iPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package")) - } - 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 - } - else -> {} - } - } - return 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 - } - else -> {} - } - } - return false - }.onFailure { Logger.e("failed to get packages", it) }.getOrNull() ?: false - - @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 - } - } - 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) - } -} - -data class CustomPatchLevel( - val system: String? = null, - val vendor: String? = null, - val boot: String? = null, - val all: String? = null +/* + * Copyright 2025 Dakkshesh + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package io.github.beakthoven.TrickyStoreOSS.core.config + +import android.content.pm.IPackageManager +import android.os.Build +import android.os.FileObserver +import android.os.ServiceManager +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import io.github.beakthoven.TrickyStoreOSS.CertificateHacker +import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger +import java.io.File +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.SecureRandom +import java.security.spec.ECGenParameterSpec + +object Config { + private val hackPackages = mutableSetOf() + private val generatePackages = mutableSetOf() + private val packageModes = mutableMapOf() + + enum class Mode { + AUTO, LEAF_HACK, GENERATE + } + + private fun updateTargetPackages(f: File?) = runCatching { + hackPackages.clear() + generatePackages.clear() + packageModes.clear() + // Default: always generate for these + listOf("com.google.android.gsf", "com.google.android.gms", "com.android.vending").forEach { + generatePackages.add(it) + packageModes[it] = Mode.GENERATE + } + f?.readLines()?.forEach { + if (it.isNotBlank() && !it.startsWith("#")) { + val n = it.trim() + when { + n.endsWith("!") -> { + val pkg = n.removeSuffix("!").trim() + generatePackages.add(pkg) + packageModes[pkg] = Mode.GENERATE + } + n.endsWith("?") -> { + val pkg = n.removeSuffix("?").trim() + hackPackages.add(pkg) + packageModes[pkg] = Mode.LEAF_HACK + } + else -> { + // Auto mode + packageModes[n] = Mode.AUTO + } + } + } + } + Logger.i("update hack packages: $hackPackages, generate packages=$generatePackages, packageModes=$packageModes") + }.onFailure { + Logger.e("failed to update target files", it) + } + + private fun updateKeyBox(f: File?) = runCatching { + CertificateHacker.readFromXml(f?.readText()) + }.onFailure { + Logger.e("failed to update keybox", it) + } + + private const val CONFIG_PATH = "/data/adb/tricky_store" + private const val TARGET_FILE = "target.txt" + private const val KEYBOX_FILE = "keybox.xml" + private const val TEE_STATUS_FILE = "tee_status" + private const val PATCHLEVEL_FILE = "security_patch.txt" + private val root = File(CONFIG_PATH) + + @Volatile + private var teeBroken: Boolean? = null + + private fun isTEEWorking(): Boolean { + val alias = "tee_attest_test_key" + return try { + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + android.app.ActivityThread.initializeMainlineModules(); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + android.security.keystore2.AndroidKeyStoreProvider.install(); + } else { + android.security.keystore.AndroidKeyStoreProvider.install(); + } + + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + + val keyPairGenerator = KeyPairGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore") + + val challenge = ByteArray(16).apply { + SecureRandom().nextBytes(this) + } + + val parameterSpec = KeyGenParameterSpec.Builder( + alias, + KeyProperties.PURPOSE_SIGN + ) + .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) + .setDigests(KeyProperties.DIGEST_SHA256) + .setAttestationChallenge(challenge) + .setIsStrongBoxBacked(false) + .build() + + keyPairGenerator.initialize(parameterSpec) + keyPairGenerator.generateKeyPair() + + keyStore.deleteEntry(alias) + true + } catch (e: Exception) { + Logger.e("TEE check failure: ${e.message}") + false + } + } + + + private fun storeTEEStatus(root: File) { + val statusFile = File(root, TEE_STATUS_FILE) + val status = isTEEWorking() + teeBroken = !status + try { + statusFile.writeText("teeBroken=${!status}") + } catch (e: Exception) { + Logger.e("Failed to write TEE status: ${e.message}") + } + } + + private fun loadTEEStatus(root: File) { + val statusFile = File(root, TEE_STATUS_FILE) + if (statusFile.exists()) { + val line = statusFile.readText().trim() + teeBroken = line == "teeBroken=true" + } else { + teeBroken = null + } + } + + 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 + } + when (path) { + TARGET_FILE -> updateTargetPackages(f) + KEYBOX_FILE -> updateKeyBox(f) + PATCHLEVEL_FILE -> updatePatchLevel(f) + } + } + } + + fun initialize() { + root.mkdirs() + val scope = File(root, TARGET_FILE) + if (scope.exists()) { + updateTargetPackages(scope) + } else { + Logger.e("target.txt file not found, please put it to $scope !") + } + val keybox = File(root, KEYBOX_FILE) + if (!keybox.exists()) { + Logger.e("keybox file not found, please put it to $keybox !") + } else { + updateKeyBox(keybox) + } + storeTEEStatus(root) + val patchFile = File(root, PATCHLEVEL_FILE) + updatePatchLevel(if (patchFile.exists()) patchFile else null) + ConfigObserver.startWatching() + } + + private var iPm: IPackageManager? = null + + fun getPm(): IPackageManager? { + if (iPm == null) { + iPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package")) + } + 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 + } + else -> {} + } + } + return 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 + } + else -> {} + } + } + return false + }.onFailure { Logger.e("failed to get packages", it) }.getOrNull() ?: false + + @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 + } + } + 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) + } +} + +data class CustomPatchLevel( + val system: String? = null, + val vendor: String? = null, + val boot: String? = null, + val all: String? = null ) \ No newline at end of file diff --git a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/core/logging/Logger.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/core/logging/Logger.kt index 3fbc53b..2fbd94c 100644 --- a/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/core/logging/Logger.kt +++ b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/core/logging/Logger.kt @@ -1,64 +1,64 @@ -/* - * Copyright 2025 Dakkshesh - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package io.github.beakthoven.TrickyStoreOSS.core.logging - -import android.util.Log - -object Logger { - const val TAG = "TrickyStore" - - 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, "wtf: $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) - } - } - - fun logIf(level: LogLevel, condition: Boolean = true, messageProvider: () -> String) { - if (condition && Log.isLoggable(TAG, level.priority)) { - log(level, messageProvider()) - } - } +/* + * Copyright 2025 Dakkshesh + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package io.github.beakthoven.TrickyStoreOSS.core.logging + +import android.util.Log + +object Logger { + const val TAG = "TrickyStore" + + 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, "wtf: $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) + } + } + + 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/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/BinderInterceptor.kt b/app/src/main/java/io/github/beakthoven/TrickyStoreOSS/interceptors/BinderInterceptor.kt index fc7d910..d241b49 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 @@ -1,181 +1,181 @@ -/* - * Copyright 2025 Dakkshesh - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package io.github.beakthoven.TrickyStoreOSS.interceptors - -import android.os.Binder -import android.os.IBinder -import android.os.Parcel -import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger - -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) { - Logger.d("Backdoor access granted for binder: $binder") - reply.readStrongBinder() - } else { - Logger.d("Backdoor access denied for binder: $binder") - null - } - } catch (e: Exception) { - Logger.e("Failed to access binder backdoor", e) - null - } finally { - data.recycle() - reply.recycle() - } - } - - fun registerBinderInterceptor( - backdoor: IBinder, - target: IBinder, - interceptor: BinderInterceptor - ) { - val data = Parcel.obtain() - val reply = Parcel.obtain() - - try { - data.writeStrongBinder(target) - data.writeStrongBinder(interceptor) - backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0) - Logger.d("Registered interceptor for target: $target") - } catch (e: Exception) { - Logger.e("Failed to register binder interceptor", e) - } finally { - data.recycle() - reply.recycle() - } - } - } - - open fun onPreTransact( - 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 - ): 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) - } - - writeResultToReply(result, reply!!) - return true - } - - private fun handlePreTransact(data: Parcel): Result { - val target = data.readStrongBinder() - val transactionCode = data.readInt() - val transactionFlags = data.readInt() - 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) - } finally { - transactionData.recycle() - } - } - - private fun handlePostTransact(data: Parcel): Result { - val target = data.readStrongBinder() - val transactionCode = data.readInt() - val transactionFlags = data.readInt() - 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) - } finally { - transactionData.recycle() - transactionReply.recycle() - } - } - - private fun writeResultToReply(result: Result, reply: Parcel) { - when (result) { - Skip -> reply.writeInt(RESULT_SKIP) - Continue -> reply.writeInt(RESULT_CONTINUE) - is OverrideReply -> { - reply.writeInt(RESULT_OVERRIDE_REPLY) - reply.writeInt(result.code) - reply.writeLong(result.reply.dataSize().toLong()) - reply.appendFrom(result.reply, 0, result.reply.dataSize()) - result.reply.recycle() - } - is OverrideData -> { - reply.writeInt(RESULT_OVERRIDE_DATA) - reply.writeLong(result.data.dataSize().toLong()) - reply.appendFrom(result.data, 0, result.data.dataSize()) - result.data.recycle() - } - } - } +/* + * Copyright 2025 Dakkshesh + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package io.github.beakthoven.TrickyStoreOSS.interceptors + +import android.os.Binder +import android.os.IBinder +import android.os.Parcel +import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger + +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) { + Logger.d("Backdoor access granted for binder: $binder") + reply.readStrongBinder() + } else { + Logger.d("Backdoor access denied for binder: $binder") + null + } + } catch (e: Exception) { + Logger.e("Failed to access binder backdoor", e) + null + } finally { + data.recycle() + reply.recycle() + } + } + + fun registerBinderInterceptor( + backdoor: IBinder, + target: IBinder, + interceptor: BinderInterceptor + ) { + val data = Parcel.obtain() + val reply = Parcel.obtain() + + try { + data.writeStrongBinder(target) + data.writeStrongBinder(interceptor) + backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0) + Logger.d("Registered interceptor for target: $target") + } catch (e: Exception) { + Logger.e("Failed to register binder interceptor", e) + } finally { + data.recycle() + reply.recycle() + } + } + } + + open fun onPreTransact( + 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 + ): 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) + } + + writeResultToReply(result, reply!!) + return true + } + + private fun handlePreTransact(data: Parcel): Result { + val target = data.readStrongBinder() + val transactionCode = data.readInt() + val transactionFlags = data.readInt() + 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) + } finally { + transactionData.recycle() + } + } + + private fun handlePostTransact(data: Parcel): Result { + val target = data.readStrongBinder() + val transactionCode = data.readInt() + val transactionFlags = data.readInt() + 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) + } finally { + transactionData.recycle() + transactionReply.recycle() + } + } + + private fun writeResultToReply(result: Result, reply: Parcel) { + when (result) { + Skip -> reply.writeInt(RESULT_SKIP) + Continue -> reply.writeInt(RESULT_CONTINUE) + is OverrideReply -> { + reply.writeInt(RESULT_OVERRIDE_REPLY) + reply.writeInt(result.code) + reply.writeLong(result.reply.dataSize().toLong()) + reply.appendFrom(result.reply, 0, result.reply.dataSize()) + result.reply.recycle() + } + is OverrideData -> { + reply.writeInt(RESULT_OVERRIDE_DATA) + reply.writeLong(result.data.dataSize().toLong()) + reply.appendFrom(result.data, 0, result.data.dataSize()) + result.data.recycle() + } + } + } } \ 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 d0600b2..e07299b 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 @@ -1,142 +1,142 @@ -/* - * Copyright 2025 Dakkshesh - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package io.github.beakthoven.TrickyStoreOSS.interceptors - -import android.os.IBinder -import android.os.Parcel -import android.os.Parcelable -import android.os.ServiceManager -import android.security.KeyStore -import android.security.keystore.KeystoreResponse -import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger -import 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 onInterceptorSetup(service: IBinder, backdoor: IBinder) { - // Default implementation does nothing - } -} - -object InterceptorUtils { - - fun createSuccessKeystoreResponse(): KeystoreResponse { - val parcel = Parcel.obtain() - try { - parcel.writeInt(KeyStore.NO_ERROR) - parcel.writeString("") - parcel.setDataPosition(0) - return KeystoreResponse.CREATOR.createFromParcel(parcel) - } finally { - 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 { - 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 { - val parcel = Parcel.obtain() - parcel.writeNoException() - parcel.writeTypedObject(obj, flags) - return BinderInterceptor.OverrideReply(resultCode, parcel) - } - - fun String.extractAlias(): String { - return when { - contains("_") -> split("_")[1] - else -> this - } - } - - fun Parcel.hasException(): Boolean { - return kotlin.runCatching { readException() }.exceptionOrNull() != null - } +/* + * Copyright 2025 Dakkshesh + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package io.github.beakthoven.TrickyStoreOSS.interceptors + +import android.os.IBinder +import android.os.Parcel +import android.os.Parcelable +import android.os.ServiceManager +import android.security.KeyStore +import android.security.keystore.KeystoreResponse +import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger +import 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 onInterceptorSetup(service: IBinder, backdoor: IBinder) { + // Default implementation does nothing + } +} + +object InterceptorUtils { + + fun createSuccessKeystoreResponse(): KeystoreResponse { + val parcel = Parcel.obtain() + try { + parcel.writeInt(KeyStore.NO_ERROR) + parcel.writeString("") + parcel.setDataPosition(0) + return KeystoreResponse.CREATOR.createFromParcel(parcel) + } finally { + 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 { + 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 { + val parcel = Parcel.obtain() + parcel.writeNoException() + parcel.writeTypedObject(obj, flags) + return BinderInterceptor.OverrideReply(resultCode, parcel) + } + + fun String.extractAlias(): String { + return when { + 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 1d7aead..a21016f 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 @@ -1,158 +1,158 @@ -/* - * Copyright 2025 Dakkshesh - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package io.github.beakthoven.TrickyStoreOSS.interceptors - -import android.annotation.SuppressLint -import android.hardware.security.keymint.SecurityLevel -import android.os.IBinder -import android.os.Parcel -import android.system.keystore2.IKeystoreService -import android.system.keystore2.KeyDescriptor -import android.system.keystore2.KeyEntryResponse -import io.github.beakthoven.TrickyStoreOSS.CertificateHacker -import io.github.beakthoven.TrickyStoreOSS.CertificateUtils -import io.github.beakthoven.TrickyStoreOSS.core.config.Config -import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger -import io.github.beakthoven.TrickyStoreOSS.getTransactCode -import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createTypedObjectReply -import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.hasException -import io.github.beakthoven.TrickyStoreOSS.putCertificateChain - -@SuppressLint("BlockedPrivateApi") -object Keystore2Interceptor : BaseKeystoreInterceptor() { - private val getKeyEntryTransaction = - 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() - if (tee != null) { - Logger.i("Registering for TEE SecurityLevel: $tee") - val interceptor = SecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT) - registerBinderInterceptor(backdoor, tee.asBinder(), interceptor) - teeInterceptor = interceptor - } else { - Logger.i("No TEE SecurityLevel found") - } - - 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) - registerBinderInterceptor(backdoor, strongBox.asBinder(), interceptor) - strongBoxInterceptor = interceptor - } else { - Logger.i("No StrongBox SecurityLevel found") - } - } - - override fun onPreTransact( - target: IBinder, - code: Int, - flags: Int, - callingUid: Int, - callingPid: Int, - data: Parcel - ): Result { - if (code == getKeyEntryTransaction) { - if (CertificateHacker.canHack()) { - Logger.d("intercept pre $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()}") - kotlin.runCatching { - data.enforceInterface(IKeystoreService.DESCRIPTOR) - val descriptor = data.readTypedObject(KeyDescriptor.CREATOR) ?: return@runCatching - if (Config.needGenerate(callingUid)) { - val response = SecurityLevelInterceptor.getKeyResponse(callingUid, descriptor.alias) - ?: return@runCatching - Logger.i("Generate key for uid=$callingUid alias=${descriptor.alias}") - return createTypedObjectReply(response) - } else if (Config.needHack(callingUid)) { - if (SecurityLevelInterceptor.shouldSkipLeafHack(callingUid, descriptor.alias)) { - Logger.i("skip leaf hack for uid=$callingUid alias=${descriptor.alias}") - val response = SecurityLevelInterceptor.getKeyResponse(callingUid, descriptor.alias) - if (response != null) { - 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}") - return@runCatching - } - } else { - Logger.i("proceeding with leaf hack for uid=$callingUid alias=${descriptor.alias}") - return Continue - } - } - return Skip - } - } - } - return Skip - } - - override fun onPostTransact( - target: IBinder, - code: Int, - flags: Int, - callingUid: Int, - callingPid: Int, - data: Parcel, - reply: Parcel?, - 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()}") - - if (code == deleteKeyTransaction && resultCode == 0) { - data.enforceInterface("android.system.keystore2.IKeystoreService") - - val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR) - if (keyDescriptor == null || keyDescriptor.domain == 0) return Skip - - SecurityLevelInterceptor.keys.remove(SecurityLevelInterceptor.Key(callingUid, keyDescriptor.alias)) - - return Skip - } else if (code == getKeyEntryTransaction) { - try { - data.enforceInterface("android.system.keystore2.IKeystoreService") - val response = reply.readTypedObject(KeyEntryResponse.CREATOR) - if (response != null) { - val chain = CertificateUtils.run { response.getCertificateChain() } - if (chain != null) { - val newChain = CertificateHacker.hackCertificateChain(chain) - response.putCertificateChain(newChain).getOrThrow() - Logger.i("Hacked certificate for uid=$callingUid") - return createTypedObjectReply(response) - } else { - p.recycle() - } - } else { - p.recycle() - } - } catch (t: Throwable) { - Logger.e("failed to hack certificate chain of uid=$callingUid pid=$callingPid!", t) - p.recycle() - } - } - return Skip - } +/* + * Copyright 2025 Dakkshesh + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package io.github.beakthoven.TrickyStoreOSS.interceptors + +import android.annotation.SuppressLint +import android.hardware.security.keymint.SecurityLevel +import android.os.IBinder +import android.os.Parcel +import android.system.keystore2.IKeystoreService +import android.system.keystore2.KeyDescriptor +import android.system.keystore2.KeyEntryResponse +import io.github.beakthoven.TrickyStoreOSS.CertificateHacker +import io.github.beakthoven.TrickyStoreOSS.CertificateUtils +import io.github.beakthoven.TrickyStoreOSS.core.config.Config +import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger +import io.github.beakthoven.TrickyStoreOSS.getTransactCode +import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createTypedObjectReply +import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.hasException +import io.github.beakthoven.TrickyStoreOSS.putCertificateChain + +@SuppressLint("BlockedPrivateApi") +object Keystore2Interceptor : BaseKeystoreInterceptor() { + private val getKeyEntryTransaction = + 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() + if (tee != null) { + Logger.i("Registering for TEE SecurityLevel: $tee") + val interceptor = SecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT) + registerBinderInterceptor(backdoor, tee.asBinder(), interceptor) + teeInterceptor = interceptor + } else { + Logger.i("No TEE SecurityLevel found") + } + + 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) + registerBinderInterceptor(backdoor, strongBox.asBinder(), interceptor) + strongBoxInterceptor = interceptor + } else { + Logger.i("No StrongBox SecurityLevel found") + } + } + + override fun onPreTransact( + target: IBinder, + code: Int, + flags: Int, + callingUid: Int, + callingPid: Int, + data: Parcel + ): Result { + if (code == getKeyEntryTransaction) { + if (CertificateHacker.canHack()) { + Logger.d("intercept pre $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()}") + kotlin.runCatching { + data.enforceInterface(IKeystoreService.DESCRIPTOR) + val descriptor = data.readTypedObject(KeyDescriptor.CREATOR) ?: return@runCatching + if (Config.needGenerate(callingUid)) { + val response = SecurityLevelInterceptor.getKeyResponse(callingUid, descriptor.alias) + ?: return@runCatching + Logger.i("Generate key for uid=$callingUid alias=${descriptor.alias}") + return createTypedObjectReply(response) + } else if (Config.needHack(callingUid)) { + if (SecurityLevelInterceptor.shouldSkipLeafHack(callingUid, descriptor.alias)) { + Logger.i("skip leaf hack for uid=$callingUid alias=${descriptor.alias}") + val response = SecurityLevelInterceptor.getKeyResponse(callingUid, descriptor.alias) + if (response != null) { + 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}") + return@runCatching + } + } else { + Logger.i("proceeding with leaf hack for uid=$callingUid alias=${descriptor.alias}") + return Continue + } + } + return Skip + } + } + } + return Skip + } + + override fun onPostTransact( + target: IBinder, + code: Int, + flags: Int, + callingUid: Int, + callingPid: Int, + data: Parcel, + reply: Parcel?, + 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()}") + + if (code == deleteKeyTransaction && resultCode == 0) { + data.enforceInterface("android.system.keystore2.IKeystoreService") + + val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR) + if (keyDescriptor == null || keyDescriptor.domain == 0) return Skip + + SecurityLevelInterceptor.keys.remove(SecurityLevelInterceptor.Key(callingUid, keyDescriptor.alias)) + + return Skip + } else if (code == getKeyEntryTransaction) { + try { + data.enforceInterface("android.system.keystore2.IKeystoreService") + val response = reply.readTypedObject(KeyEntryResponse.CREATOR) + if (response != null) { + val chain = CertificateUtils.run { response.getCertificateChain() } + if (chain != null) { + val newChain = CertificateHacker.hackCertificateChain(chain) + response.putCertificateChain(newChain).getOrThrow() + Logger.i("Hacked certificate for uid=$callingUid") + return createTypedObjectReply(response) + } else { + p.recycle() + } + } else { + p.recycle() + } + } catch (t: Throwable) { + Logger.e("failed to hack certificate chain of uid=$callingUid pid=$callingPid!", t) + p.recycle() + } + } + return Skip + } } \ No newline at end of file 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 45e3e33..0aa15c7 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 @@ -1,238 +1,238 @@ -/* - * Copyright 2025 Dakkshesh - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package io.github.beakthoven.TrickyStoreOSS.interceptors - -import android.annotation.SuppressLint -import android.os.IBinder -import android.os.Parcel -import android.security.Credentials -import android.security.KeyStore -import android.security.keymaster.ExportResult -import android.security.keymaster.KeyCharacteristics -import android.security.keymaster.KeymasterArguments -import android.security.keymaster.KeymasterCertificateChain -import android.security.keymaster.KeymasterDefs -import android.security.keystore.IKeystoreCertificateChainCallback -import android.security.keystore.IKeystoreExportKeyCallback -import android.security.keystore.IKeystoreKeyCharacteristicsCallback -import android.security.keystore.IKeystoreService -import io.github.beakthoven.TrickyStoreOSS.CertificateHacker -import io.github.beakthoven.TrickyStoreOSS.core.config.Config -import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger -import io.github.beakthoven.TrickyStoreOSS.getTransactCode -import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createByteArrayReply -import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createSuccessKeystoreResponse -import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createSuccessReply -import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.extractAlias -import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.hasException -import java.math.BigInteger -import java.security.KeyPair -import java.util.Date - -@SuppressLint("BlockedPrivateApi") -object KeystoreInterceptor : BaseKeystoreInterceptor() { - private val getTransaction = - getTransactCode(IKeystoreService.Stub::class.java, "get") - private val generateKeyTransaction = - getTransactCode(IKeystoreService.Stub::class.java, "generateKey") - private val getKeyCharacteristicsTransaction = - getTransactCode(IKeystoreService.Stub::class.java, "getKeyCharacteristics") - private val exportKeyTransaction = - 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" - - private const val DESCRIPTOR = "android.security.keystore.IKeystoreService" - - private val keyArguments = HashMap() - private val keyPairs = HashMap() - - data class Key(val uid: Int, val alias: String) - - override fun onPreTransact( - target: IBinder, - code: Int, - flags: Int, - callingUid: Int, - callingPid: Int, - data: Parcel - ): Result { - if (CertificateHacker.canHack()) { - if (code == getTransaction) { - if (Config.needHack(callingUid)) { - return Continue - } else if (Config.needGenerate(callingUid)) { - return Skip - } - } else if (Config.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 = CertificateHacker.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) - } - } - keyArguments[Key(callingUid, alias)] = kgp - } - - val kc = KeyCharacteristics() - kc.swEnforced = KeymasterArguments() - kc.hwEnforced = kma - - val ksr = createSuccessKeystoreResponse() - callback.onFinished(ksr, kc) - - return createSuccessReply() - }.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 - - val ksr = createSuccessKeystoreResponse() - callback.onFinished(ksr, kc) - - 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 = CertificateHacker.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() - - callback.onFinished(er) - - 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)) - - val ksr = createSuccessKeystoreResponse() - - val key = Key(callingUid, alias) - val ka = keyArguments[key]!! - ka.attestationChallenge = attestationChallenge - val chain = CertificateHacker.generateChain(callingUid, ka, keyPairs[key]!!) - - val kcc = KeymasterCertificateChain(chain) - callback.onFinished(ksr, kcc) - } - - return createSuccessReply() - }.onFailure { - Logger.e("attestKeyTransaction error", it) - } - } - } - } - } - return Skip - } - - override fun onPostTransact( - target: IBinder, - code: Int, - flags: Int, - callingUid: Int, - callingPid: Int, - data: Parcel, - reply: Parcel?, - 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()}") - try { - data.enforceInterface(DESCRIPTOR) - val alias = data.readString() ?: "" - var response = reply.createByteArray() - when { - alias.startsWith(Credentials.USER_CERTIFICATE) -> { - response = CertificateHacker.hackCertificateChainUSR(response!!, alias.extractAlias(), callingUid) - Logger.i("Hacked leaf certificate for uid=$callingUid") - return createByteArrayReply(response) - } - alias.startsWith(Credentials.CA_CERTIFICATE) -> { - response = CertificateHacker.hackCertificateChainCA(response!!, alias.extractAlias(), callingUid) - Logger.i("Hacked CA certificate chain for uid=$callingUid") - return createByteArrayReply(response) - } - else -> p.recycle() - } - } catch (t: Throwable) { - Logger.e("failed to hack certificate chain of uid=$callingUid pid=$callingPid!", t) - p.recycle() - } - return Skip - } +/* + * Copyright 2025 Dakkshesh + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package io.github.beakthoven.TrickyStoreOSS.interceptors + +import android.annotation.SuppressLint +import android.os.IBinder +import android.os.Parcel +import android.security.Credentials +import android.security.KeyStore +import android.security.keymaster.ExportResult +import android.security.keymaster.KeyCharacteristics +import android.security.keymaster.KeymasterArguments +import android.security.keymaster.KeymasterCertificateChain +import android.security.keymaster.KeymasterDefs +import android.security.keystore.IKeystoreCertificateChainCallback +import android.security.keystore.IKeystoreExportKeyCallback +import android.security.keystore.IKeystoreKeyCharacteristicsCallback +import android.security.keystore.IKeystoreService +import io.github.beakthoven.TrickyStoreOSS.CertificateHacker +import io.github.beakthoven.TrickyStoreOSS.core.config.Config +import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger +import io.github.beakthoven.TrickyStoreOSS.getTransactCode +import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createByteArrayReply +import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createSuccessKeystoreResponse +import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createSuccessReply +import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.extractAlias +import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.hasException +import java.math.BigInteger +import java.security.KeyPair +import java.util.Date + +@SuppressLint("BlockedPrivateApi") +object KeystoreInterceptor : BaseKeystoreInterceptor() { + private val getTransaction = + getTransactCode(IKeystoreService.Stub::class.java, "get") + private val generateKeyTransaction = + getTransactCode(IKeystoreService.Stub::class.java, "generateKey") + private val getKeyCharacteristicsTransaction = + getTransactCode(IKeystoreService.Stub::class.java, "getKeyCharacteristics") + private val exportKeyTransaction = + 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" + + private const val DESCRIPTOR = "android.security.keystore.IKeystoreService" + + private val keyArguments = HashMap() + private val keyPairs = HashMap() + + data class Key(val uid: Int, val alias: String) + + override fun onPreTransact( + target: IBinder, + code: Int, + flags: Int, + callingUid: Int, + callingPid: Int, + data: Parcel + ): Result { + if (CertificateHacker.canHack()) { + if (code == getTransaction) { + if (Config.needHack(callingUid)) { + return Continue + } else if (Config.needGenerate(callingUid)) { + return Skip + } + } else if (Config.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 = CertificateHacker.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) + } + } + keyArguments[Key(callingUid, alias)] = kgp + } + + val kc = KeyCharacteristics() + kc.swEnforced = KeymasterArguments() + kc.hwEnforced = kma + + val ksr = createSuccessKeystoreResponse() + callback.onFinished(ksr, kc) + + return createSuccessReply() + }.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 + + val ksr = createSuccessKeystoreResponse() + callback.onFinished(ksr, kc) + + 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 = CertificateHacker.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() + + callback.onFinished(er) + + 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)) + + val ksr = createSuccessKeystoreResponse() + + val key = Key(callingUid, alias) + val ka = keyArguments[key]!! + ka.attestationChallenge = attestationChallenge + val chain = CertificateHacker.generateChain(callingUid, ka, keyPairs[key]!!) + + val kcc = KeymasterCertificateChain(chain) + callback.onFinished(ksr, kcc) + } + + return createSuccessReply() + }.onFailure { + Logger.e("attestKeyTransaction error", it) + } + } + } + } + } + return Skip + } + + override fun onPostTransact( + target: IBinder, + code: Int, + flags: Int, + callingUid: Int, + callingPid: Int, + data: Parcel, + reply: Parcel?, + 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()}") + try { + data.enforceInterface(DESCRIPTOR) + val alias = data.readString() ?: "" + var response = reply.createByteArray() + when { + alias.startsWith(Credentials.USER_CERTIFICATE) -> { + response = CertificateHacker.hackCertificateChainUSR(response!!, alias.extractAlias(), callingUid) + Logger.i("Hacked leaf certificate for uid=$callingUid") + return createByteArrayReply(response) + } + alias.startsWith(Credentials.CA_CERTIFICATE) -> { + response = CertificateHacker.hackCertificateChainCA(response!!, alias.extractAlias(), callingUid) + Logger.i("Hacked CA certificate chain for uid=$callingUid") + return createByteArrayReply(response) + } + else -> p.recycle() + } + } catch (t: Throwable) { + Logger.e("failed to hack certificate chain of uid=$callingUid pid=$callingPid!", t) + p.recycle() + } + 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 2a5494b..862fc52 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 @@ -1,180 +1,180 @@ -/* - * Copyright 2025 Dakkshesh - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package io.github.beakthoven.TrickyStoreOSS.interceptors - -import android.hardware.security.keymint.KeyParameter -import android.hardware.security.keymint.KeyParameterValue -import android.hardware.security.keymint.Tag -import android.os.IBinder -import android.os.Parcel -import android.system.keystore2.Authorization -import android.system.keystore2.IKeystoreSecurityLevel -import android.system.keystore2.KeyDescriptor -import android.system.keystore2.KeyEntryResponse -import android.system.keystore2.KeyMetadata -import androidx.annotation.Keep -import io.github.beakthoven.TrickyStoreOSS.CertificateHacker -import io.github.beakthoven.TrickyStoreOSS.core.config.Config -import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger -import io.github.beakthoven.TrickyStoreOSS.getTransactCode -import io.github.beakthoven.TrickyStoreOSS.putCertificateChain -import java.security.KeyPair -import java.security.cert.Certificate -import java.util.concurrent.ConcurrentHashMap - -class SecurityLevelInterceptor( - private val original: IKeystoreSecurityLevel, - private val level: Int -) : BinderInterceptor() { - companion object { - private val generateKeyTransaction = - getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey") - private val deleteKeyTransaction = - getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "deleteKey") - private val createOperationTransaction = - getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "createOperation") - - @Keep - val keys = ConcurrentHashMap() - - @Keep - val keyPairs = ConcurrentHashMap>>() - - @Keep - val skipLeafHacks = ConcurrentHashMap() - - @Keep - fun getKeyResponse(uid: Int, alias: String): KeyEntryResponse? = - keys[Key(uid, alias)]?.response - - @Keep - fun getKeyPairs(uid: Int, alias: String): Pair>? = - keyPairs[Key(uid, alias)] - - @Keep - fun shouldSkipLeafHack(uid: Int, alias: String): Boolean = - skipLeafHacks[Key(uid, alias)] ?: false - } - - data class Key(val uid: Int, val alias: String) - data class Info(val keyPair: KeyPair, val response: KeyEntryResponse) - - override fun onPreTransact( - target: IBinder, - code: Int, - flags: Int, - callingUid: Int, - callingPid: Int, - 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 = CertificateHacker.KeyGenParameters(params) - if (Config.needGenerate(callingUid)) { - val pair = CertificateHacker.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp) - ?: 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 (Config.needHack(callingUid)) { - if ((kgp.purpose.contains(7)) || (attestationKeyDescriptor != null)) { - Logger.i("Generating key in generation mode for attestation: uid=$callingUid alias=${keyDescriptor.alias}") - val pair = CertificateHacker.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp) - ?: 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) - } - } - return Skip - } - - private fun buildResponse( - chain: List, - params: CertificateHacker.KeyGenParameters, - descriptor: KeyDescriptor - ): KeyEntryResponse { - val response = KeyEntryResponse() - val metadata = KeyMetadata() - metadata.keySecurityLevel = level - metadata.putCertificateChain(chain.toTypedArray()).getOrThrow() - val d = KeyDescriptor() - d.domain = descriptor.domain - d.nspace = descriptor.nspace - metadata.key = d - val authorizations = ArrayList() - var a: Authorization - for (i in params.purpose.toList()) { - a = Authorization() - a.keyParameter = KeyParameter() - a.keyParameter.tag = Tag.PURPOSE - a.keyParameter.value = KeyParameterValue.keyPurpose(i) - a.securityLevel = level - authorizations.add(a) - } - for (i in params.digest.toList()) { - a = Authorization() - a.keyParameter = KeyParameter() - a.keyParameter.tag = Tag.DIGEST - a.keyParameter.value = KeyParameterValue.digest(i) - a.securityLevel = level - authorizations.add(a) - } - a = Authorization() - a.keyParameter = KeyParameter() - a.keyParameter.tag = Tag.ALGORITHM - a.keyParameter.value = KeyParameterValue.algorithm(params.algorithm) - a.securityLevel = level - authorizations.add(a) - a = Authorization() - a.keyParameter = KeyParameter() - a.keyParameter.tag = Tag.KEY_SIZE - a.keyParameter.value = KeyParameterValue.integer(params.keySize) - a.securityLevel = level - authorizations.add(a) - a = Authorization() - a.keyParameter = KeyParameter() - a.keyParameter.tag = Tag.EC_CURVE - a.keyParameter.value = KeyParameterValue.ecCurve(params.ecCurve) - a.securityLevel = level - authorizations.add(a) - a = Authorization() - a.keyParameter = KeyParameter() - a.keyParameter.tag = Tag.NO_AUTH_REQUIRED - a.keyParameter.value = KeyParameterValue.boolValue(true) - a.securityLevel = level - authorizations.add(a) - metadata.authorizations = authorizations.toTypedArray() - response.metadata = metadata - response.iSecurityLevel = original - return response - } +/* + * Copyright 2025 Dakkshesh + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package io.github.beakthoven.TrickyStoreOSS.interceptors + +import android.hardware.security.keymint.KeyParameter +import android.hardware.security.keymint.KeyParameterValue +import android.hardware.security.keymint.Tag +import android.os.IBinder +import android.os.Parcel +import android.system.keystore2.Authorization +import android.system.keystore2.IKeystoreSecurityLevel +import android.system.keystore2.KeyDescriptor +import android.system.keystore2.KeyEntryResponse +import android.system.keystore2.KeyMetadata +import androidx.annotation.Keep +import io.github.beakthoven.TrickyStoreOSS.CertificateHacker +import io.github.beakthoven.TrickyStoreOSS.core.config.Config +import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger +import io.github.beakthoven.TrickyStoreOSS.getTransactCode +import io.github.beakthoven.TrickyStoreOSS.putCertificateChain +import java.security.KeyPair +import java.security.cert.Certificate +import java.util.concurrent.ConcurrentHashMap + +class SecurityLevelInterceptor( + private val original: IKeystoreSecurityLevel, + private val level: Int +) : BinderInterceptor() { + companion object { + private val generateKeyTransaction = + getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey") + private val deleteKeyTransaction = + getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "deleteKey") + private val createOperationTransaction = + getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "createOperation") + + @Keep + val keys = ConcurrentHashMap() + + @Keep + val keyPairs = ConcurrentHashMap>>() + + @Keep + val skipLeafHacks = ConcurrentHashMap() + + @Keep + fun getKeyResponse(uid: Int, alias: String): KeyEntryResponse? = + keys[Key(uid, alias)]?.response + + @Keep + fun getKeyPairs(uid: Int, alias: String): Pair>? = + keyPairs[Key(uid, alias)] + + @Keep + fun shouldSkipLeafHack(uid: Int, alias: String): Boolean = + skipLeafHacks[Key(uid, alias)] ?: false + } + + data class Key(val uid: Int, val alias: String) + data class Info(val keyPair: KeyPair, val response: KeyEntryResponse) + + override fun onPreTransact( + target: IBinder, + code: Int, + flags: Int, + callingUid: Int, + callingPid: Int, + 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 = CertificateHacker.KeyGenParameters(params) + if (Config.needGenerate(callingUid)) { + val pair = CertificateHacker.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp) + ?: 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 (Config.needHack(callingUid)) { + if ((kgp.purpose.contains(7)) || (attestationKeyDescriptor != null)) { + Logger.i("Generating key in generation mode for attestation: uid=$callingUid alias=${keyDescriptor.alias}") + val pair = CertificateHacker.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp) + ?: 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) + } + } + return Skip + } + + private fun buildResponse( + chain: List, + params: CertificateHacker.KeyGenParameters, + descriptor: KeyDescriptor + ): KeyEntryResponse { + val response = KeyEntryResponse() + val metadata = KeyMetadata() + metadata.keySecurityLevel = level + metadata.putCertificateChain(chain.toTypedArray()).getOrThrow() + val d = KeyDescriptor() + d.domain = descriptor.domain + d.nspace = descriptor.nspace + metadata.key = d + val authorizations = ArrayList() + var a: Authorization + for (i in params.purpose.toList()) { + a = Authorization() + a.keyParameter = KeyParameter() + a.keyParameter.tag = Tag.PURPOSE + a.keyParameter.value = KeyParameterValue.keyPurpose(i) + a.securityLevel = level + authorizations.add(a) + } + for (i in params.digest.toList()) { + a = Authorization() + a.keyParameter = KeyParameter() + a.keyParameter.tag = Tag.DIGEST + a.keyParameter.value = KeyParameterValue.digest(i) + a.securityLevel = level + authorizations.add(a) + } + a = Authorization() + a.keyParameter = KeyParameter() + a.keyParameter.tag = Tag.ALGORITHM + a.keyParameter.value = KeyParameterValue.algorithm(params.algorithm) + a.securityLevel = level + authorizations.add(a) + a = Authorization() + a.keyParameter = KeyParameter() + a.keyParameter.tag = Tag.KEY_SIZE + a.keyParameter.value = KeyParameterValue.integer(params.keySize) + a.securityLevel = level + authorizations.add(a) + a = Authorization() + a.keyParameter = KeyParameter() + a.keyParameter.tag = Tag.EC_CURVE + a.keyParameter.value = KeyParameterValue.ecCurve(params.ecCurve) + a.securityLevel = level + authorizations.add(a) + a = Authorization() + a.keyParameter = KeyParameter() + a.keyParameter.tag = Tag.NO_AUTH_REQUIRED + a.keyParameter.value = KeyParameterValue.boolValue(true) + a.securityLevel = level + authorizations.add(a) + metadata.authorizations = authorizations.toTypedArray() + response.metadata = metadata + response.iSecurityLevel = original + return response + } } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 11db7c9..de08903 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Aug 04 09:32:06 IST 2025 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists +#Mon Aug 04 09:32:06 IST 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew.bat b/gradlew.bat index ac1b06f..107acd3 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,89 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/module/keybox.xml b/module/keybox.xml index 9c02996..fd21ae4 100644 --- a/module/keybox.xml +++ b/module/keybox.xml @@ -1,114 +1,114 @@ - - - 1 - - - - -----BEGIN EC PRIVATE KEY----- - MHcCAQEEICHghkMqFRmEWc82OlD8FMnarfk19SfC39ceTW28QuVEoAoGCCqGSM49 - AwEHoUQDQgAE6555+EJjWazLKpFMiYbMcK2QZpOCqXMmE/6sy/ghJ0whdJdKKv6l - uU1/ZtTgZRBmNbxTt6CjpnFYPts+Ea4QFA== - -----END EC PRIVATE KEY----- - - - 2 - - -----BEGIN CERTIFICATE----- - MIICeDCCAh6gAwIBAgICEAEwCgYIKoZIzj0EAwIwgZgxCzAJBgNVBAYTAlVTMRMw - EQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBWaWV3MRUwEwYD - VQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJvaWQxMzAxBgNVBAMMKkFu - ZHJvaWQgS2V5c3RvcmUgU29mdHdhcmUgQXR0ZXN0YXRpb24gUm9vdDAeFw0xNjAx - MTEwMDQ2MDlaFw0yNjAxMDgwMDQ2MDlaMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE - CAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdB - bmRyb2lkMTswOQYDVQQDDDJBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVz - dGF0aW9uIEludGVybWVkaWF0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOue - efhCY1msyyqRTImGzHCtkGaTgqlzJhP+rMv4ISdMIXSXSir+pblNf2bU4GUQZjW8 - U7ego6ZxWD7bPhGuEBSjZjBkMB0GA1UdDgQWBBQ//KzWGrE6noEguNUlHMVlux6R - qTAfBgNVHSMEGDAWgBTIrel3TEXDo88NFhDkeUM6IVowzzASBgNVHRMBAf8ECDAG - AQH/AgEAMA4GA1UdDwEB/wQEAwIChDAKBggqhkjOPQQDAgNIADBFAiBLipt77oK8 - wDOHri/AiZi03cONqycqRZ9pDMfDktQPjgIhAO7aAV229DLp1IQ7YkyUBO86fMy9 - Xvsiu+f+uXc/WT/7 - -----END CERTIFICATE----- - - - -----BEGIN CERTIFICATE----- - MIICizCCAjKgAwIBAgIJAKIFntEOQ1tXMAoGCCqGSM49BAMCMIGYMQswCQYDVQQG - EwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmll - dzEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMTMwMQYD - VQQDDCpBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIFJvb3Qw - HhcNMTYwMTExMDA0MzUwWhcNMzYwMTA2MDA0MzUwWjCBmDELMAkGA1UEBhMCVVMx - EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTAT - BgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDEzMDEGA1UEAwwq - QW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBSb290MFkwEwYH - KoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamguD/9/SQ59 - dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpKNjMGEwHQYDVR0O - BBYEFMit6XdMRcOjzw0WEOR5QzohWjDPMB8GA1UdIwQYMBaAFMit6XdMRcOjzw0W - EOR5QzohWjDPMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKEMAoGCCqG - SM49BAMCA0cAMEQCIDUho++LNEYenNVg8x1YiSBq3KNlQfYNns6KGYxmSGB7AiBN - C/NR2TB8fVvaNTQdqEcbY6WFZTytTySn502vQX3xvw== - -----END CERTIFICATE----- - - - - - - -----BEGIN RSA PRIVATE KEY----- - MIICXQIBAAKBgQDAgyPcVogbuDAgafWwhWHG7r5/BeL1qEIEir6LR752/q7yXPKb - KvoyABQWAUKZiaFfz8aBXrNjWDwv0vIL5Jgyg92BSxbX4YVBeuVKvClqOm21wAQI - O2jFVsHwIzmRZBmGTVC3TUCuykhMdzVsiVoMJ1q/rEmdXX0jYvKcXgLocQIDAQAB - AoGBAL6GCwuZqAKm+xpZQ4p7txUGWwmjbcbpysxr88AsNNfXnpTGYGQo2Ix7f2V3 - wc3qZAdKvo5yht8fCBHclygmCGjeldMu/Ja20IT/JxpfYN78xwPno45uKbqaPF/C - woB2tqiWrx0014gozpvdsfNPnJQEQweBKY4gExZyW728mTpBAkEA4cbZJ2RsCRbs - NoJtWUmDdAwh8bB0xKGlmGfGaXlchdPcRkxbkp6Uv7NODcxQFLEPEzQat/3V9gQU - 0qMmytQcxQJBANpIWZd4XNVjD7D9jFJU+Y5TjhiYOq6ea35qWntdNDdVuSGOvUAy - DSg4fXifdvohi8wti2il9kGPu+ylF5qzr70CQFD+/DJklVlhbtZTThVFCTKdk6PY - ENvlvbmCKSz3i9i624Agro1X9LcdBThv/p6dsnHKNHejSZnbdvjl7OnA1J0CQBW3 - TPJ8zv+Ls2vwTZ2DRrCaL3DS9EObDyasfgP36dH3fUuRX9KbKCPwOstdUgDghX/y - qAPpPu6W1iNc6VRCvCECQQCQp0XaiXCyzWSWYDJCKMX4KFb/1mW6moXI1g8bi+5x - fs0scurgHa2GunZU1M9FrbXx8rMdn4Eiz6XxpVcPmy0l - -----END RSA PRIVATE KEY----- - - - 2 - - -----BEGIN CERTIFICATE----- - MIICtjCCAh+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwYzELMAkGA1UEBhMCVVMx - EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTAT - BgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDAeFw0xNjAxMDQx - MjQwNTNaFw0zNTEyMzAxMjQwNTNaMHYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApD - YWxpZm9ybmlhMRUwEwYDVQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJv - aWQxKTAnBgNVBAMMIEFuZHJvaWQgU29mdHdhcmUgQXR0ZXN0YXRpb24gS2V5MIGf - MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDAgyPcVogbuDAgafWwhWHG7r5/BeL1 - qEIEir6LR752/q7yXPKbKvoyABQWAUKZiaFfz8aBXrNjWDwv0vIL5Jgyg92BSxbX - 4YVBeuVKvClqOm21wAQIO2jFVsHwIzmRZBmGTVC3TUCuykhMdzVsiVoMJ1q/rEmd - XX0jYvKcXgLocQIDAQABo2YwZDAdBgNVHQ4EFgQU1AwQG/jNY7n3OVK1DhNcpteZ - k4YwHwYDVR0jBBgwFoAUKfrxrMxN0kyWQCd1trDpMuUH/i4wEgYDVR0TAQH/BAgw - BgEB/wIBADAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADgYEAni1IX4xn - M9waha2Z11Aj6hTsQ7DhnerCI0YecrUZ3GAi5KVoMWwLVcTmnKItnzpPk2sxixZ4 - Fg2Iy9mLzICdhPDCJ+NrOPH90ecXcjFZNX2W88V/q52PlmEmT7K+gbsNSQQiis6f - 9/VCLiVE+iEHElqDtVWtGIL4QBSbnCBjBH8= - -----END CERTIFICATE----- - - - -----BEGIN CERTIFICATE----- - MIICpzCCAhCgAwIBAgIJAP+U2d2fB8gMMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNV - BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW - aWV3MRUwEwYDVQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJvaWQwHhcN - MTYwMTA0MTIzMTA4WhcNMzUxMjMwMTIzMTA4WjBjMQswCQYDVQQGEwJVUzETMBEG - A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEVMBMGA1UE - CgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMIGfMA0GCSqGSIb3DQEB - AQUAA4GNADCBiQKBgQCia63rbi5EYe/VDoLmt5TRdSMfd5tjkWP/96r/C3JHTsAs - Q+wzfNes7UA+jCigZtX3hwszl94OuE4TQKuvpSe/lWmgMdsGUmX4RFlXYfC78hdL - t0GAZMAoDo9Sd47b0ke2RekZyOmLw9vCkT/X11DEHTVm+Vfkl5YLCazOkjWFmwID - AQABo2MwYTAdBgNVHQ4EFgQUKfrxrMxN0kyWQCd1trDpMuUH/i4wHwYDVR0jBBgw - FoAUKfrxrMxN0kyWQCd1trDpMuUH/i4wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B - Af8EBAMCAoQwDQYJKoZIhvcNAQELBQADgYEAT3LzNlmNDsG5dFsxWfbwjSVJMJ6j - HBwp0kUtILlNX2S06IDHeHqcOd6os/W/L3BfRxBcxebrTQaZYdKumgf/93y4q+uc - DyQHXrF/unlx/U1bnt8Uqf7f7XzAiF343ZtkMlbVNZriE/mPzsF83O+kqrJVw4Op - Lvtc9mL1J1IXvmM= - -----END CERTIFICATE----- - - - - - + + + 1 + + + + -----BEGIN EC PRIVATE KEY----- + MHcCAQEEICHghkMqFRmEWc82OlD8FMnarfk19SfC39ceTW28QuVEoAoGCCqGSM49 + AwEHoUQDQgAE6555+EJjWazLKpFMiYbMcK2QZpOCqXMmE/6sy/ghJ0whdJdKKv6l + uU1/ZtTgZRBmNbxTt6CjpnFYPts+Ea4QFA== + -----END EC PRIVATE KEY----- + + + 2 + + -----BEGIN CERTIFICATE----- + MIICeDCCAh6gAwIBAgICEAEwCgYIKoZIzj0EAwIwgZgxCzAJBgNVBAYTAlVTMRMw + EQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBWaWV3MRUwEwYD + VQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJvaWQxMzAxBgNVBAMMKkFu + ZHJvaWQgS2V5c3RvcmUgU29mdHdhcmUgQXR0ZXN0YXRpb24gUm9vdDAeFw0xNjAx + MTEwMDQ2MDlaFw0yNjAxMDgwMDQ2MDlaMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE + CAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdB + bmRyb2lkMTswOQYDVQQDDDJBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVz + dGF0aW9uIEludGVybWVkaWF0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOue + efhCY1msyyqRTImGzHCtkGaTgqlzJhP+rMv4ISdMIXSXSir+pblNf2bU4GUQZjW8 + U7ego6ZxWD7bPhGuEBSjZjBkMB0GA1UdDgQWBBQ//KzWGrE6noEguNUlHMVlux6R + qTAfBgNVHSMEGDAWgBTIrel3TEXDo88NFhDkeUM6IVowzzASBgNVHRMBAf8ECDAG + AQH/AgEAMA4GA1UdDwEB/wQEAwIChDAKBggqhkjOPQQDAgNIADBFAiBLipt77oK8 + wDOHri/AiZi03cONqycqRZ9pDMfDktQPjgIhAO7aAV229DLp1IQ7YkyUBO86fMy9 + Xvsiu+f+uXc/WT/7 + -----END CERTIFICATE----- + + + -----BEGIN CERTIFICATE----- + MIICizCCAjKgAwIBAgIJAKIFntEOQ1tXMAoGCCqGSM49BAMCMIGYMQswCQYDVQQG + EwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmll + dzEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMTMwMQYD + VQQDDCpBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIFJvb3Qw + HhcNMTYwMTExMDA0MzUwWhcNMzYwMTA2MDA0MzUwWjCBmDELMAkGA1UEBhMCVVMx + EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTAT + BgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDEzMDEGA1UEAwwq + QW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBSb290MFkwEwYH + KoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamguD/9/SQ59 + dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpKNjMGEwHQYDVR0O + BBYEFMit6XdMRcOjzw0WEOR5QzohWjDPMB8GA1UdIwQYMBaAFMit6XdMRcOjzw0W + EOR5QzohWjDPMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKEMAoGCCqG + SM49BAMCA0cAMEQCIDUho++LNEYenNVg8x1YiSBq3KNlQfYNns6KGYxmSGB7AiBN + C/NR2TB8fVvaNTQdqEcbY6WFZTytTySn502vQX3xvw== + -----END CERTIFICATE----- + + + + + + -----BEGIN RSA PRIVATE KEY----- + MIICXQIBAAKBgQDAgyPcVogbuDAgafWwhWHG7r5/BeL1qEIEir6LR752/q7yXPKb + KvoyABQWAUKZiaFfz8aBXrNjWDwv0vIL5Jgyg92BSxbX4YVBeuVKvClqOm21wAQI + O2jFVsHwIzmRZBmGTVC3TUCuykhMdzVsiVoMJ1q/rEmdXX0jYvKcXgLocQIDAQAB + AoGBAL6GCwuZqAKm+xpZQ4p7txUGWwmjbcbpysxr88AsNNfXnpTGYGQo2Ix7f2V3 + wc3qZAdKvo5yht8fCBHclygmCGjeldMu/Ja20IT/JxpfYN78xwPno45uKbqaPF/C + woB2tqiWrx0014gozpvdsfNPnJQEQweBKY4gExZyW728mTpBAkEA4cbZJ2RsCRbs + NoJtWUmDdAwh8bB0xKGlmGfGaXlchdPcRkxbkp6Uv7NODcxQFLEPEzQat/3V9gQU + 0qMmytQcxQJBANpIWZd4XNVjD7D9jFJU+Y5TjhiYOq6ea35qWntdNDdVuSGOvUAy + DSg4fXifdvohi8wti2il9kGPu+ylF5qzr70CQFD+/DJklVlhbtZTThVFCTKdk6PY + ENvlvbmCKSz3i9i624Agro1X9LcdBThv/p6dsnHKNHejSZnbdvjl7OnA1J0CQBW3 + TPJ8zv+Ls2vwTZ2DRrCaL3DS9EObDyasfgP36dH3fUuRX9KbKCPwOstdUgDghX/y + qAPpPu6W1iNc6VRCvCECQQCQp0XaiXCyzWSWYDJCKMX4KFb/1mW6moXI1g8bi+5x + fs0scurgHa2GunZU1M9FrbXx8rMdn4Eiz6XxpVcPmy0l + -----END RSA PRIVATE KEY----- + + + 2 + + -----BEGIN CERTIFICATE----- + MIICtjCCAh+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwYzELMAkGA1UEBhMCVVMx + EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTAT + BgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDAeFw0xNjAxMDQx + MjQwNTNaFw0zNTEyMzAxMjQwNTNaMHYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApD + YWxpZm9ybmlhMRUwEwYDVQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJv + aWQxKTAnBgNVBAMMIEFuZHJvaWQgU29mdHdhcmUgQXR0ZXN0YXRpb24gS2V5MIGf + MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDAgyPcVogbuDAgafWwhWHG7r5/BeL1 + qEIEir6LR752/q7yXPKbKvoyABQWAUKZiaFfz8aBXrNjWDwv0vIL5Jgyg92BSxbX + 4YVBeuVKvClqOm21wAQIO2jFVsHwIzmRZBmGTVC3TUCuykhMdzVsiVoMJ1q/rEmd + XX0jYvKcXgLocQIDAQABo2YwZDAdBgNVHQ4EFgQU1AwQG/jNY7n3OVK1DhNcpteZ + k4YwHwYDVR0jBBgwFoAUKfrxrMxN0kyWQCd1trDpMuUH/i4wEgYDVR0TAQH/BAgw + BgEB/wIBADAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADgYEAni1IX4xn + M9waha2Z11Aj6hTsQ7DhnerCI0YecrUZ3GAi5KVoMWwLVcTmnKItnzpPk2sxixZ4 + Fg2Iy9mLzICdhPDCJ+NrOPH90ecXcjFZNX2W88V/q52PlmEmT7K+gbsNSQQiis6f + 9/VCLiVE+iEHElqDtVWtGIL4QBSbnCBjBH8= + -----END CERTIFICATE----- + + + -----BEGIN CERTIFICATE----- + MIICpzCCAhCgAwIBAgIJAP+U2d2fB8gMMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNV + BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW + aWV3MRUwEwYDVQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJvaWQwHhcN + MTYwMTA0MTIzMTA4WhcNMzUxMjMwMTIzMTA4WjBjMQswCQYDVQQGEwJVUzETMBEG + A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEVMBMGA1UE + CgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMIGfMA0GCSqGSIb3DQEB + AQUAA4GNADCBiQKBgQCia63rbi5EYe/VDoLmt5TRdSMfd5tjkWP/96r/C3JHTsAs + Q+wzfNes7UA+jCigZtX3hwszl94OuE4TQKuvpSe/lWmgMdsGUmX4RFlXYfC78hdL + t0GAZMAoDo9Sd47b0ke2RekZyOmLw9vCkT/X11DEHTVm+Vfkl5YLCazOkjWFmwID + AQABo2MwYTAdBgNVHQ4EFgQUKfrxrMxN0kyWQCd1trDpMuUH/i4wHwYDVR0jBBgw + FoAUKfrxrMxN0kyWQCd1trDpMuUH/i4wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B + Af8EBAMCAoQwDQYJKoZIhvcNAQELBQADgYEAT3LzNlmNDsG5dFsxWfbwjSVJMJ6j + HBwp0kUtILlNX2S06IDHeHqcOd6os/W/L3BfRxBcxebrTQaZYdKumgf/93y4q+uc + DyQHXrF/unlx/U1bnt8Uqf7f7XzAiF343ZtkMlbVNZriE/mPzsF83O+kqrJVw4Op + Lvtc9mL1J1IXvmM= + -----END CERTIFICATE----- + + + + +