Add ktfmt-gradle as formatter

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