+17
-17
@@ -1,17 +1,17 @@
|
||||
BasedOnStyle: LLVM
|
||||
|
||||
Language: Cpp
|
||||
Standard: c++20
|
||||
|
||||
ColumnLimit: 135
|
||||
|
||||
AlignEscapedNewlines: Left
|
||||
AllowShortFunctionsOnASingleLine: Empty
|
||||
AllowShortLambdasOnASingleLine: Empty
|
||||
AlwaysBreakTemplateDeclarations: true
|
||||
IndentPPDirectives: AfterHash
|
||||
|
||||
AccessModifierOffset: -4
|
||||
IndentWidth: 4
|
||||
UseTab: Never
|
||||
|
||||
BasedOnStyle: LLVM
|
||||
|
||||
Language: Cpp
|
||||
Standard: c++20
|
||||
|
||||
ColumnLimit: 135
|
||||
|
||||
AlignEscapedNewlines: Left
|
||||
AllowShortFunctionsOnASingleLine: Empty
|
||||
AllowShortLambdasOnASingleLine: Empty
|
||||
AlwaysBreakTemplateDeclarations: true
|
||||
IndentPPDirectives: AfterHash
|
||||
|
||||
AccessModifierOffset: -4
|
||||
IndentWidth: 4
|
||||
UseTab: Never
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<manifest/>
|
||||
@@ -1,188 +1,188 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS
|
||||
|
||||
import android.content.pm.IPackageManager
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.ServiceManager
|
||||
import android.os.SystemProperties
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.CustomPatchLevel
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import org.bouncycastle.asn1.ASN1Integer
|
||||
import org.bouncycastle.asn1.DEROctetString
|
||||
import org.bouncycastle.asn1.DERSequence
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
|
||||
fun getTransactCode(clazz: Class<*>, method: String): Int =
|
||||
clazz.getDeclaredField("TRANSACTION_$method").apply { isAccessible = true }
|
||||
.getInt(null)
|
||||
|
||||
val bootHash: ByteArray by lazy {
|
||||
getBootHashFromProp() ?: randomBytes()
|
||||
}
|
||||
|
||||
val bootKey: ByteArray by lazy {
|
||||
randomBytes()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
private fun getBootHashFromProp(): ByteArray? {
|
||||
val digest = SystemProperties.get("ro.boot.vbmeta.digest", null) ?: return null
|
||||
return if (digest.length == 64) digest.hexToByteArray() else null
|
||||
}
|
||||
|
||||
private fun randomBytes(): ByteArray = ByteArray(32).also {
|
||||
ThreadLocalRandom.current().nextBytes(it)
|
||||
}
|
||||
|
||||
val patchLevel: Int
|
||||
get() = getCustomPatchLevel("system", false)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false)
|
||||
|
||||
val patchLevelLong: Int
|
||||
get() = getCustomPatchLevel("system", true)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true)
|
||||
|
||||
val vendorPatchLevel: Int
|
||||
get() = getCustomPatchLevel("vendor", false)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false)
|
||||
|
||||
val vendorPatchLevelLong: Int
|
||||
get() = getCustomPatchLevel("vendor", true)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true)
|
||||
|
||||
val bootPatchLevel: Int
|
||||
get() = getCustomPatchLevel("boot", false)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false)
|
||||
|
||||
val bootPatchLevelLong: Int
|
||||
get() = getCustomPatchLevel("boot", true)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true)
|
||||
|
||||
private val customPatchLevel: CustomPatchLevel?
|
||||
get() = Config._customPatchLevel
|
||||
|
||||
private fun getCustomPatchLevel(component: String, isLong: Boolean): Int? {
|
||||
val config = customPatchLevel ?: return null
|
||||
val value = when (component) {
|
||||
"system" -> config.system ?: config.all
|
||||
"vendor" -> config.vendor ?: config.all
|
||||
"boot" -> config.boot ?: config.all
|
||||
else -> config.all
|
||||
} ?: return null
|
||||
|
||||
when {
|
||||
value.equals("no", ignoreCase = true) -> return null
|
||||
value.equals("prop", ignoreCase = true) -> return null
|
||||
}
|
||||
|
||||
return parsePatchLevelValue(value, component, isLong)
|
||||
}
|
||||
|
||||
private fun parsePatchLevelValue(value: String, component: String, isLong: Boolean): Int? {
|
||||
val normalized = value.replace("-", "")
|
||||
|
||||
return try {
|
||||
when (normalized.length) {
|
||||
8 -> {
|
||||
val year = normalized.substring(0, 4).toInt()
|
||||
val month = normalized.substring(4, 6).toInt()
|
||||
val day = normalized.substring(6, 8).toInt()
|
||||
if (isLong) year * 10000 + month * 100 + day
|
||||
else year * 100 + month
|
||||
}
|
||||
6 -> {
|
||||
val year = normalized.substring(0, 4).toInt()
|
||||
val month = normalized.substring(4, 6).toInt()
|
||||
if (isLong) year * 10000 + month * 100
|
||||
else year * 100 + month
|
||||
}
|
||||
else -> {
|
||||
Logger.e("Invalid patch level length for $component: $normalized")
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: NumberFormatException) {
|
||||
Logger.e("Patch level parse error for $component=$value", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val osVersion: Int
|
||||
get() = getOsVersion(Build.VERSION.SDK_INT)
|
||||
|
||||
private val osVersionMap = mapOf(
|
||||
Build.VERSION_CODES.BAKLAVA to 160000,
|
||||
Build.VERSION_CODES.VANILLA_ICE_CREAM to 150000,
|
||||
Build.VERSION_CODES.UPSIDE_DOWN_CAKE to 140000,
|
||||
Build.VERSION_CODES.TIRAMISU to 130000,
|
||||
Build.VERSION_CODES.S_V2 to 120100,
|
||||
Build.VERSION_CODES.S to 120000,
|
||||
Build.VERSION_CODES.R to 110000,
|
||||
Build.VERSION_CODES.Q to 100000
|
||||
)
|
||||
|
||||
private fun getOsVersion(sdkVersion: Int): Int = osVersionMap[sdkVersion] ?: 160000
|
||||
|
||||
fun String.convertPatchLevel(isLong: Boolean): Int = runCatching {
|
||||
val parts = split("-")
|
||||
when {
|
||||
isLong && parts.size >= 3 -> parts[0].toInt() * 10000 + parts[1].toInt() * 100 + parts[2].toInt()
|
||||
parts.size >= 2 -> parts[0].toInt() * 100 + parts[1].toInt()
|
||||
else -> throw IllegalArgumentException("Invalid patch level format: $this")
|
||||
}
|
||||
}.onFailure {
|
||||
Logger.e("Invalid patch level format: $this", it)
|
||||
}.getOrDefault(202404)
|
||||
|
||||
fun IPackageManager.getPackageInfoCompat(name: String, flags: Long, userId: Int) =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getPackageInfo(name, flags, userId)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getPackageInfo(name, flags.toInt(), userId)
|
||||
}
|
||||
|
||||
val apexInfos: List<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)
|
||||
}
|
||||
|
||||
packages.list
|
||||
.map { it.packageName to it.longVersionCode }
|
||||
.sortedBy { it.first }
|
||||
}.getOrElse {
|
||||
Logger.e("Failed to get APEX package information")
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
val moduleHash: ByteArray by lazy {
|
||||
runCatching {
|
||||
val encodables = apexInfos.flatMap { (packageName, versionCode) ->
|
||||
listOf(
|
||||
DEROctetString(packageName.toByteArray()),
|
||||
ASN1Integer(versionCode)
|
||||
)
|
||||
}
|
||||
|
||||
val sequence = DERSequence(encodables.toTypedArray())
|
||||
MessageDigest.getInstance("SHA-256").digest(sequence.encoded)
|
||||
}.getOrElse {
|
||||
Logger.e("Failed to compute module hash", it)
|
||||
ByteArray(32)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS
|
||||
|
||||
import android.content.pm.IPackageManager
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.ServiceManager
|
||||
import android.os.SystemProperties
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.CustomPatchLevel
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import org.bouncycastle.asn1.ASN1Integer
|
||||
import org.bouncycastle.asn1.DEROctetString
|
||||
import org.bouncycastle.asn1.DERSequence
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
|
||||
fun getTransactCode(clazz: Class<*>, method: String): Int =
|
||||
clazz.getDeclaredField("TRANSACTION_$method").apply { isAccessible = true }
|
||||
.getInt(null)
|
||||
|
||||
val bootHash: ByteArray by lazy {
|
||||
getBootHashFromProp() ?: randomBytes()
|
||||
}
|
||||
|
||||
val bootKey: ByteArray by lazy {
|
||||
randomBytes()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
private fun getBootHashFromProp(): ByteArray? {
|
||||
val digest = SystemProperties.get("ro.boot.vbmeta.digest", null) ?: return null
|
||||
return if (digest.length == 64) digest.hexToByteArray() else null
|
||||
}
|
||||
|
||||
private fun randomBytes(): ByteArray = ByteArray(32).also {
|
||||
ThreadLocalRandom.current().nextBytes(it)
|
||||
}
|
||||
|
||||
val patchLevel: Int
|
||||
get() = getCustomPatchLevel("system", false)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false)
|
||||
|
||||
val patchLevelLong: Int
|
||||
get() = getCustomPatchLevel("system", true)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true)
|
||||
|
||||
val vendorPatchLevel: Int
|
||||
get() = getCustomPatchLevel("vendor", false)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false)
|
||||
|
||||
val vendorPatchLevelLong: Int
|
||||
get() = getCustomPatchLevel("vendor", true)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true)
|
||||
|
||||
val bootPatchLevel: Int
|
||||
get() = getCustomPatchLevel("boot", false)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false)
|
||||
|
||||
val bootPatchLevelLong: Int
|
||||
get() = getCustomPatchLevel("boot", true)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true)
|
||||
|
||||
private val customPatchLevel: CustomPatchLevel?
|
||||
get() = Config._customPatchLevel
|
||||
|
||||
private fun getCustomPatchLevel(component: String, isLong: Boolean): Int? {
|
||||
val config = customPatchLevel ?: return null
|
||||
val value = when (component) {
|
||||
"system" -> config.system ?: config.all
|
||||
"vendor" -> config.vendor ?: config.all
|
||||
"boot" -> config.boot ?: config.all
|
||||
else -> config.all
|
||||
} ?: return null
|
||||
|
||||
when {
|
||||
value.equals("no", ignoreCase = true) -> return null
|
||||
value.equals("prop", ignoreCase = true) -> return null
|
||||
}
|
||||
|
||||
return parsePatchLevelValue(value, component, isLong)
|
||||
}
|
||||
|
||||
private fun parsePatchLevelValue(value: String, component: String, isLong: Boolean): Int? {
|
||||
val normalized = value.replace("-", "")
|
||||
|
||||
return try {
|
||||
when (normalized.length) {
|
||||
8 -> {
|
||||
val year = normalized.substring(0, 4).toInt()
|
||||
val month = normalized.substring(4, 6).toInt()
|
||||
val day = normalized.substring(6, 8).toInt()
|
||||
if (isLong) year * 10000 + month * 100 + day
|
||||
else year * 100 + month
|
||||
}
|
||||
6 -> {
|
||||
val year = normalized.substring(0, 4).toInt()
|
||||
val month = normalized.substring(4, 6).toInt()
|
||||
if (isLong) year * 10000 + month * 100
|
||||
else year * 100 + month
|
||||
}
|
||||
else -> {
|
||||
Logger.e("Invalid patch level length for $component: $normalized")
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: NumberFormatException) {
|
||||
Logger.e("Patch level parse error for $component=$value", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val osVersion: Int
|
||||
get() = getOsVersion(Build.VERSION.SDK_INT)
|
||||
|
||||
private val osVersionMap = mapOf(
|
||||
Build.VERSION_CODES.BAKLAVA to 160000,
|
||||
Build.VERSION_CODES.VANILLA_ICE_CREAM to 150000,
|
||||
Build.VERSION_CODES.UPSIDE_DOWN_CAKE to 140000,
|
||||
Build.VERSION_CODES.TIRAMISU to 130000,
|
||||
Build.VERSION_CODES.S_V2 to 120100,
|
||||
Build.VERSION_CODES.S to 120000,
|
||||
Build.VERSION_CODES.R to 110000,
|
||||
Build.VERSION_CODES.Q to 100000
|
||||
)
|
||||
|
||||
private fun getOsVersion(sdkVersion: Int): Int = osVersionMap[sdkVersion] ?: 160000
|
||||
|
||||
fun String.convertPatchLevel(isLong: Boolean): Int = runCatching {
|
||||
val parts = split("-")
|
||||
when {
|
||||
isLong && parts.size >= 3 -> parts[0].toInt() * 10000 + parts[1].toInt() * 100 + parts[2].toInt()
|
||||
parts.size >= 2 -> parts[0].toInt() * 100 + parts[1].toInt()
|
||||
else -> throw IllegalArgumentException("Invalid patch level format: $this")
|
||||
}
|
||||
}.onFailure {
|
||||
Logger.e("Invalid patch level format: $this", it)
|
||||
}.getOrDefault(202404)
|
||||
|
||||
fun IPackageManager.getPackageInfoCompat(name: String, flags: Long, userId: Int) =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getPackageInfo(name, flags, userId)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getPackageInfo(name, flags.toInt(), userId)
|
||||
}
|
||||
|
||||
val apexInfos: List<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)
|
||||
}
|
||||
|
||||
packages.list
|
||||
.map { it.packageName to it.longVersionCode }
|
||||
.sortedBy { it.first }
|
||||
}.getOrElse {
|
||||
Logger.e("Failed to get APEX package information")
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
val moduleHash: ByteArray by lazy {
|
||||
runCatching {
|
||||
val encodables = apexInfos.flatMap { (packageName, versionCode) ->
|
||||
listOf(
|
||||
DEROctetString(packageName.toByteArray()),
|
||||
ASN1Integer(versionCode)
|
||||
)
|
||||
}
|
||||
|
||||
val sequence = DERSequence(encodables.toTypedArray())
|
||||
MessageDigest.getInstance("SHA-256").digest(sequence.encoded)
|
||||
}.getOrElse {
|
||||
Logger.e("Failed to compute module hash", it)
|
||||
ByteArray(32)
|
||||
}
|
||||
}
|
||||
|
||||
fun String.trimLine(): String = trim().split("\n").joinToString("\n") { it.trim() }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,160 +1,160 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS
|
||||
|
||||
import android.system.keystore2.KeyEntryResponse
|
||||
import android.system.keystore2.KeyMetadata
|
||||
import android.util.Log
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateUtils.putCertificateChain
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
object CertificateUtils {
|
||||
private const val TAG = "CertificateUtils"
|
||||
|
||||
sealed class CertificateResult<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
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray?.toCertificate(): X509Certificate? {
|
||||
return this?.let { bytes ->
|
||||
try {
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
certFactory.generateCertificate(ByteArrayInputStream(bytes)) as? X509Certificate
|
||||
} catch (e: CertificateException) {
|
||||
Log.w(TAG, "Couldn't parse certificate in keystore", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray.toCertificateResult(): CertificateResult<X509Certificate> {
|
||||
return try {
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
val certificate = certFactory.generateCertificate(ByteArrayInputStream(this)) as X509Certificate
|
||||
CertificateResult.Success(certificate)
|
||||
} catch (e: CertificateException) {
|
||||
CertificateResult.Error("Failed to parse certificate", e)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun ByteArray?.toCertificates(): Collection<X509Certificate> {
|
||||
return this?.let { bytes ->
|
||||
try {
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun KeyEntryResponse.putCertificateChain(chain: Array<Certificate>): Result<Unit> {
|
||||
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) {
|
||||
output.write(chain[i].encoded)
|
||||
}
|
||||
certificateChain = output.toByteArray()
|
||||
}
|
||||
} else {
|
||||
certificateChain = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray?.toX509Certificate(): X509Certificate? = CertificateUtils.run { this@toX509Certificate.toCertificate() }
|
||||
|
||||
fun ByteArray?.toX509Certificates(): Collection<X509Certificate> = CertificateUtils.run { this@toX509Certificates.toCertificates() }
|
||||
|
||||
fun Collection<Certificate>.encodedBytes(): ByteArray? = CertificateUtils.run { this@encodedBytes.toByteArray() }
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
output.write(chain[i].encoded)
|
||||
}
|
||||
certificateChain = output.toByteArray()
|
||||
}
|
||||
} else {
|
||||
certificateChain = null
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS
|
||||
|
||||
import android.system.keystore2.KeyEntryResponse
|
||||
import android.system.keystore2.KeyMetadata
|
||||
import android.util.Log
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateUtils.putCertificateChain
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
object CertificateUtils {
|
||||
private const val TAG = "CertificateUtils"
|
||||
|
||||
sealed class CertificateResult<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
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray?.toCertificate(): X509Certificate? {
|
||||
return this?.let { bytes ->
|
||||
try {
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
certFactory.generateCertificate(ByteArrayInputStream(bytes)) as? X509Certificate
|
||||
} catch (e: CertificateException) {
|
||||
Log.w(TAG, "Couldn't parse certificate in keystore", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray.toCertificateResult(): CertificateResult<X509Certificate> {
|
||||
return try {
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
val certificate = certFactory.generateCertificate(ByteArrayInputStream(this)) as X509Certificate
|
||||
CertificateResult.Success(certificate)
|
||||
} catch (e: CertificateException) {
|
||||
CertificateResult.Error("Failed to parse certificate", e)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun ByteArray?.toCertificates(): Collection<X509Certificate> {
|
||||
return this?.let { bytes ->
|
||||
try {
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun KeyEntryResponse.putCertificateChain(chain: Array<Certificate>): Result<Unit> {
|
||||
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) {
|
||||
output.write(chain[i].encoded)
|
||||
}
|
||||
certificateChain = output.toByteArray()
|
||||
}
|
||||
} else {
|
||||
certificateChain = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray?.toX509Certificate(): X509Certificate? = CertificateUtils.run { this@toX509Certificate.toCertificate() }
|
||||
|
||||
fun ByteArray?.toX509Certificates(): Collection<X509Certificate> = CertificateUtils.run { this@toX509Certificates.toCertificates() }
|
||||
|
||||
fun Collection<Certificate>.encodedBytes(): ByteArray? = CertificateUtils.run { this@encodedBytes.toByteArray() }
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
output.write(chain[i].encoded)
|
||||
}
|
||||
certificateChain = output.toByteArray()
|
||||
}
|
||||
} else {
|
||||
certificateChain = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,155 +1,155 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import java.io.IOException
|
||||
import java.io.StringReader
|
||||
|
||||
class XmlParser(private val xmlContent: String) {
|
||||
|
||||
sealed class ParseResult {
|
||||
data class Success(val attributes: Map<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)
|
||||
} catch (e: XmlPullParserException) {
|
||||
ParseResult.Error("XML parsing error: ${e.message}", e)
|
||||
} catch (e: IOException) {
|
||||
ParseResult.Error("IO error while parsing XML: ${e.message}", e)
|
||||
} catch (e: Exception) {
|
||||
ParseResult.Error("Unexpected error: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun obtainPathLegacy(path: String): Map<String, String> {
|
||||
when (val result = obtainPath(path)) {
|
||||
is ParseResult.Success -> return result.attributes
|
||||
is ParseResult.Error -> throw result.cause ?: Exception(result.message)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun readData(
|
||||
parser: XmlPullParser,
|
||||
tags: Array<String>,
|
||||
index: 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])
|
||||
} else {
|
||||
handleRegularTag(parser, tags, index)
|
||||
}
|
||||
} else {
|
||||
skipCurrentElement(parser)
|
||||
}
|
||||
}
|
||||
|
||||
throw XmlPullParserException("Path not found: ${tags.joinToString(".")}")
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun handleIndexedTag(
|
||||
parser: XmlPullParser,
|
||||
tags: Array<String>,
|
||||
index: Int,
|
||||
tagCounts: MutableMap<String, Int>,
|
||||
currentTag: String,
|
||||
indexPart: String
|
||||
): Map<String, String> {
|
||||
val targetIndex = indexPart.replace("]", "").toIntOrNull()
|
||||
?: throw XmlPullParserException("Invalid index in tag: $indexPart")
|
||||
|
||||
val currentCount = tagCounts.getOrDefault(currentTag, 0)
|
||||
|
||||
return if (currentCount < targetIndex) {
|
||||
tagCounts[currentTag] = currentCount + 1
|
||||
readData(parser, tags, index, tagCounts)
|
||||
} else {
|
||||
if (index == tags.size - 1) {
|
||||
readAttributes(parser)
|
||||
} else {
|
||||
readData(parser, tags, index + 1, tagCounts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun handleRegularTag(
|
||||
parser: XmlPullParser,
|
||||
tags: Array<String>,
|
||||
index: Int
|
||||
): Map<String, String> {
|
||||
return if (index == tags.size - 1) {
|
||||
readAttributes(parser)
|
||||
} else {
|
||||
readData(parser, tags, index + 1, mutableMapOf())
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun readAttributes(parser: XmlPullParser): Map<String, String> {
|
||||
val attributes = mutableMapOf<String, String>()
|
||||
|
||||
for (i in 0 until parser.attributeCount) {
|
||||
val name = parser.getAttributeName(i)
|
||||
val value = parser.getAttributeValue(i)
|
||||
if (name != null && value != null) {
|
||||
attributes[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (parser.next() == XmlPullParser.TEXT) {
|
||||
parser.text?.let { text ->
|
||||
attributes["text"] = text
|
||||
}
|
||||
}
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
@Throws(XmlPullParserException::class, IOException::class)
|
||||
private fun skipCurrentElement(parser: XmlPullParser) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG) {
|
||||
throw IllegalStateException("Parser must be positioned at START_TAG")
|
||||
}
|
||||
|
||||
var depth = 1
|
||||
while (depth != 0) {
|
||||
when (parser.next()) {
|
||||
XmlPullParser.END_TAG -> depth--
|
||||
XmlPullParser.START_TAG -> depth++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import java.io.IOException
|
||||
import java.io.StringReader
|
||||
|
||||
class XmlParser(private val xmlContent: String) {
|
||||
|
||||
sealed class ParseResult {
|
||||
data class Success(val attributes: Map<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)
|
||||
} catch (e: XmlPullParserException) {
|
||||
ParseResult.Error("XML parsing error: ${e.message}", e)
|
||||
} catch (e: IOException) {
|
||||
ParseResult.Error("IO error while parsing XML: ${e.message}", e)
|
||||
} catch (e: Exception) {
|
||||
ParseResult.Error("Unexpected error: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun obtainPathLegacy(path: String): Map<String, String> {
|
||||
when (val result = obtainPath(path)) {
|
||||
is ParseResult.Success -> return result.attributes
|
||||
is ParseResult.Error -> throw result.cause ?: Exception(result.message)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun readData(
|
||||
parser: XmlPullParser,
|
||||
tags: Array<String>,
|
||||
index: 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])
|
||||
} else {
|
||||
handleRegularTag(parser, tags, index)
|
||||
}
|
||||
} else {
|
||||
skipCurrentElement(parser)
|
||||
}
|
||||
}
|
||||
|
||||
throw XmlPullParserException("Path not found: ${tags.joinToString(".")}")
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun handleIndexedTag(
|
||||
parser: XmlPullParser,
|
||||
tags: Array<String>,
|
||||
index: Int,
|
||||
tagCounts: MutableMap<String, Int>,
|
||||
currentTag: String,
|
||||
indexPart: String
|
||||
): Map<String, String> {
|
||||
val targetIndex = indexPart.replace("]", "").toIntOrNull()
|
||||
?: throw XmlPullParserException("Invalid index in tag: $indexPart")
|
||||
|
||||
val currentCount = tagCounts.getOrDefault(currentTag, 0)
|
||||
|
||||
return if (currentCount < targetIndex) {
|
||||
tagCounts[currentTag] = currentCount + 1
|
||||
readData(parser, tags, index, tagCounts)
|
||||
} else {
|
||||
if (index == tags.size - 1) {
|
||||
readAttributes(parser)
|
||||
} else {
|
||||
readData(parser, tags, index + 1, tagCounts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun handleRegularTag(
|
||||
parser: XmlPullParser,
|
||||
tags: Array<String>,
|
||||
index: Int
|
||||
): Map<String, String> {
|
||||
return if (index == tags.size - 1) {
|
||||
readAttributes(parser)
|
||||
} else {
|
||||
readData(parser, tags, index + 1, mutableMapOf())
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun readAttributes(parser: XmlPullParser): Map<String, String> {
|
||||
val attributes = mutableMapOf<String, String>()
|
||||
|
||||
for (i in 0 until parser.attributeCount) {
|
||||
val name = parser.getAttributeName(i)
|
||||
val value = parser.getAttributeValue(i)
|
||||
if (name != null && value != null) {
|
||||
attributes[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (parser.next() == XmlPullParser.TEXT) {
|
||||
parser.text?.let { text ->
|
||||
attributes["text"] = text
|
||||
}
|
||||
}
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
@Throws(XmlPullParserException::class, IOException::class)
|
||||
private fun skipCurrentElement(parser: XmlPullParser) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG) {
|
||||
throw IllegalStateException("Parser must be positioned at START_TAG")
|
||||
}
|
||||
|
||||
var depth = 1
|
||||
while (depth != 0) {
|
||||
when (parser.next()) {
|
||||
XmlPullParser.END_TAG -> depth--
|
||||
XmlPullParser.START_TAG -> depth++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.toXmlParser(): XmlParser = XmlParser(this)
|
||||
@@ -1,267 +1,267 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.core.config
|
||||
|
||||
import android.content.pm.IPackageManager
|
||||
import android.os.Build
|
||||
import android.os.FileObserver
|
||||
import android.os.ServiceManager
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHacker
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import java.io.File
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.SecureRandom
|
||||
import java.security.spec.ECGenParameterSpec
|
||||
|
||||
object Config {
|
||||
private val hackPackages = mutableSetOf<String>()
|
||||
private val generatePackages = mutableSetOf<String>()
|
||||
private val packageModes = mutableMapOf<String, Mode>()
|
||||
|
||||
enum class Mode {
|
||||
AUTO, LEAF_HACK, GENERATE
|
||||
}
|
||||
|
||||
private fun updateTargetPackages(f: File?) = runCatching {
|
||||
hackPackages.clear()
|
||||
generatePackages.clear()
|
||||
packageModes.clear()
|
||||
// Default: always generate for these
|
||||
listOf("com.google.android.gsf", "com.google.android.gms", "com.android.vending").forEach {
|
||||
generatePackages.add(it)
|
||||
packageModes[it] = Mode.GENERATE
|
||||
}
|
||||
f?.readLines()?.forEach {
|
||||
if (it.isNotBlank() && !it.startsWith("#")) {
|
||||
val n = it.trim()
|
||||
when {
|
||||
n.endsWith("!") -> {
|
||||
val pkg = n.removeSuffix("!").trim()
|
||||
generatePackages.add(pkg)
|
||||
packageModes[pkg] = Mode.GENERATE
|
||||
}
|
||||
n.endsWith("?") -> {
|
||||
val pkg = n.removeSuffix("?").trim()
|
||||
hackPackages.add(pkg)
|
||||
packageModes[pkg] = Mode.LEAF_HACK
|
||||
}
|
||||
else -> {
|
||||
// Auto mode
|
||||
packageModes[n] = Mode.AUTO
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Logger.i("update hack packages: $hackPackages, generate packages=$generatePackages, packageModes=$packageModes")
|
||||
}.onFailure {
|
||||
Logger.e("failed to update target files", it)
|
||||
}
|
||||
|
||||
private fun updateKeyBox(f: File?) = runCatching {
|
||||
CertificateHacker.readFromXml(f?.readText())
|
||||
}.onFailure {
|
||||
Logger.e("failed to update keybox", it)
|
||||
}
|
||||
|
||||
private const val CONFIG_PATH = "/data/adb/tricky_store"
|
||||
private const val TARGET_FILE = "target.txt"
|
||||
private const val KEYBOX_FILE = "keybox.xml"
|
||||
private const val TEE_STATUS_FILE = "tee_status"
|
||||
private const val PATCHLEVEL_FILE = "security_patch.txt"
|
||||
private val root = File(CONFIG_PATH)
|
||||
|
||||
@Volatile
|
||||
private var teeBroken: Boolean? = null
|
||||
|
||||
private fun isTEEWorking(): Boolean {
|
||||
val alias = "tee_attest_test_key"
|
||||
return try {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
android.app.ActivityThread.initializeMainlineModules();
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
android.security.keystore2.AndroidKeyStoreProvider.install();
|
||||
} else {
|
||||
android.security.keystore.AndroidKeyStoreProvider.install();
|
||||
}
|
||||
|
||||
val keyStore = KeyStore.getInstance("AndroidKeyStore")
|
||||
keyStore.load(null)
|
||||
|
||||
val keyPairGenerator = KeyPairGenerator.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
|
||||
|
||||
val challenge = ByteArray(16).apply {
|
||||
SecureRandom().nextBytes(this)
|
||||
}
|
||||
|
||||
val parameterSpec = KeyGenParameterSpec.Builder(
|
||||
alias,
|
||||
KeyProperties.PURPOSE_SIGN
|
||||
)
|
||||
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
|
||||
.setDigests(KeyProperties.DIGEST_SHA256)
|
||||
.setAttestationChallenge(challenge)
|
||||
.setIsStrongBoxBacked(false)
|
||||
.build()
|
||||
|
||||
keyPairGenerator.initialize(parameterSpec)
|
||||
keyPairGenerator.generateKeyPair()
|
||||
|
||||
keyStore.deleteEntry(alias)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Logger.e("TEE check failure: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun storeTEEStatus(root: File) {
|
||||
val statusFile = File(root, TEE_STATUS_FILE)
|
||||
val status = isTEEWorking()
|
||||
teeBroken = !status
|
||||
try {
|
||||
statusFile.writeText("teeBroken=${!status}")
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Failed to write TEE status: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadTEEStatus(root: File) {
|
||||
val statusFile = File(root, TEE_STATUS_FILE)
|
||||
if (statusFile.exists()) {
|
||||
val line = statusFile.readText().trim()
|
||||
teeBroken = line == "teeBroken=true"
|
||||
} else {
|
||||
teeBroken = null
|
||||
}
|
||||
}
|
||||
|
||||
object ConfigObserver : FileObserver(root, CLOSE_WRITE or DELETE or MOVED_FROM or MOVED_TO) {
|
||||
override fun onEvent(event: Int, path: String?) {
|
||||
path ?: return
|
||||
val f = when (event) {
|
||||
CLOSE_WRITE, MOVED_TO -> File(root, path)
|
||||
DELETE, MOVED_FROM -> null
|
||||
else -> return
|
||||
}
|
||||
when (path) {
|
||||
TARGET_FILE -> updateTargetPackages(f)
|
||||
KEYBOX_FILE -> updateKeyBox(f)
|
||||
PATCHLEVEL_FILE -> updatePatchLevel(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun initialize() {
|
||||
root.mkdirs()
|
||||
val scope = File(root, TARGET_FILE)
|
||||
if (scope.exists()) {
|
||||
updateTargetPackages(scope)
|
||||
} else {
|
||||
Logger.e("target.txt file not found, please put it to $scope !")
|
||||
}
|
||||
val keybox = File(root, KEYBOX_FILE)
|
||||
if (!keybox.exists()) {
|
||||
Logger.e("keybox file not found, please put it to $keybox !")
|
||||
} else {
|
||||
updateKeyBox(keybox)
|
||||
}
|
||||
storeTEEStatus(root)
|
||||
val patchFile = File(root, PATCHLEVEL_FILE)
|
||||
updatePatchLevel(if (patchFile.exists()) patchFile else null)
|
||||
ConfigObserver.startWatching()
|
||||
}
|
||||
|
||||
private var iPm: IPackageManager? = null
|
||||
|
||||
fun getPm(): IPackageManager? {
|
||||
if (iPm == null) {
|
||||
iPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"))
|
||||
}
|
||||
return iPm
|
||||
}
|
||||
|
||||
fun needHack(callingUid: Int): Boolean = kotlin.runCatching {
|
||||
val ps = getPm()?.getPackagesForUid(callingUid) ?: return false
|
||||
if (teeBroken == null) loadTEEStatus(root)
|
||||
for (pkg in ps) {
|
||||
when (packageModes[pkg]) {
|
||||
Mode.LEAF_HACK -> return true
|
||||
Mode.AUTO -> {
|
||||
if (teeBroken == false) return true
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}.onFailure { Logger.e("failed to get packages", it) }.getOrNull() ?: false
|
||||
|
||||
fun needGenerate(callingUid: Int): Boolean = kotlin.runCatching {
|
||||
val ps = getPm()?.getPackagesForUid(callingUid) ?: return false
|
||||
if (teeBroken == null) loadTEEStatus(root)
|
||||
for (pkg in ps) {
|
||||
when (packageModes[pkg]) {
|
||||
Mode.GENERATE -> return true
|
||||
Mode.AUTO -> {
|
||||
if (teeBroken == true) return true
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}.onFailure { Logger.e("failed to get packages", it) }.getOrNull() ?: false
|
||||
|
||||
@Volatile
|
||||
var _customPatchLevel: CustomPatchLevel? = null
|
||||
|
||||
fun updatePatchLevel(f: File?) = runCatching {
|
||||
if (f == null || !f.exists()) {
|
||||
_customPatchLevel = null
|
||||
return@runCatching
|
||||
}
|
||||
val lines = f.readLines().map { it.trim() }.filter { it.isNotEmpty() && !it.startsWith("#") }
|
||||
if (lines.isEmpty()) {
|
||||
_customPatchLevel = null
|
||||
return@runCatching
|
||||
}
|
||||
if (lines.size == 1 && !lines[0].contains("=")) {
|
||||
_customPatchLevel = CustomPatchLevel(all = lines[0])
|
||||
return@runCatching
|
||||
}
|
||||
val map = mutableMapOf<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
|
||||
)
|
||||
}.onFailure {
|
||||
Logger.e("failed to update patch level", it)
|
||||
}
|
||||
}
|
||||
|
||||
data class CustomPatchLevel(
|
||||
val system: String? = null,
|
||||
val vendor: String? = null,
|
||||
val boot: String? = null,
|
||||
val all: String? = null
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.core.config
|
||||
|
||||
import android.content.pm.IPackageManager
|
||||
import android.os.Build
|
||||
import android.os.FileObserver
|
||||
import android.os.ServiceManager
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHacker
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import java.io.File
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.SecureRandom
|
||||
import java.security.spec.ECGenParameterSpec
|
||||
|
||||
object Config {
|
||||
private val hackPackages = mutableSetOf<String>()
|
||||
private val generatePackages = mutableSetOf<String>()
|
||||
private val packageModes = mutableMapOf<String, Mode>()
|
||||
|
||||
enum class Mode {
|
||||
AUTO, LEAF_HACK, GENERATE
|
||||
}
|
||||
|
||||
private fun updateTargetPackages(f: File?) = runCatching {
|
||||
hackPackages.clear()
|
||||
generatePackages.clear()
|
||||
packageModes.clear()
|
||||
// Default: always generate for these
|
||||
listOf("com.google.android.gsf", "com.google.android.gms", "com.android.vending").forEach {
|
||||
generatePackages.add(it)
|
||||
packageModes[it] = Mode.GENERATE
|
||||
}
|
||||
f?.readLines()?.forEach {
|
||||
if (it.isNotBlank() && !it.startsWith("#")) {
|
||||
val n = it.trim()
|
||||
when {
|
||||
n.endsWith("!") -> {
|
||||
val pkg = n.removeSuffix("!").trim()
|
||||
generatePackages.add(pkg)
|
||||
packageModes[pkg] = Mode.GENERATE
|
||||
}
|
||||
n.endsWith("?") -> {
|
||||
val pkg = n.removeSuffix("?").trim()
|
||||
hackPackages.add(pkg)
|
||||
packageModes[pkg] = Mode.LEAF_HACK
|
||||
}
|
||||
else -> {
|
||||
// Auto mode
|
||||
packageModes[n] = Mode.AUTO
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Logger.i("update hack packages: $hackPackages, generate packages=$generatePackages, packageModes=$packageModes")
|
||||
}.onFailure {
|
||||
Logger.e("failed to update target files", it)
|
||||
}
|
||||
|
||||
private fun updateKeyBox(f: File?) = runCatching {
|
||||
CertificateHacker.readFromXml(f?.readText())
|
||||
}.onFailure {
|
||||
Logger.e("failed to update keybox", it)
|
||||
}
|
||||
|
||||
private const val CONFIG_PATH = "/data/adb/tricky_store"
|
||||
private const val TARGET_FILE = "target.txt"
|
||||
private const val KEYBOX_FILE = "keybox.xml"
|
||||
private const val TEE_STATUS_FILE = "tee_status"
|
||||
private const val PATCHLEVEL_FILE = "security_patch.txt"
|
||||
private val root = File(CONFIG_PATH)
|
||||
|
||||
@Volatile
|
||||
private var teeBroken: Boolean? = null
|
||||
|
||||
private fun isTEEWorking(): Boolean {
|
||||
val alias = "tee_attest_test_key"
|
||||
return try {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
android.app.ActivityThread.initializeMainlineModules();
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
android.security.keystore2.AndroidKeyStoreProvider.install();
|
||||
} else {
|
||||
android.security.keystore.AndroidKeyStoreProvider.install();
|
||||
}
|
||||
|
||||
val keyStore = KeyStore.getInstance("AndroidKeyStore")
|
||||
keyStore.load(null)
|
||||
|
||||
val keyPairGenerator = KeyPairGenerator.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
|
||||
|
||||
val challenge = ByteArray(16).apply {
|
||||
SecureRandom().nextBytes(this)
|
||||
}
|
||||
|
||||
val parameterSpec = KeyGenParameterSpec.Builder(
|
||||
alias,
|
||||
KeyProperties.PURPOSE_SIGN
|
||||
)
|
||||
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
|
||||
.setDigests(KeyProperties.DIGEST_SHA256)
|
||||
.setAttestationChallenge(challenge)
|
||||
.setIsStrongBoxBacked(false)
|
||||
.build()
|
||||
|
||||
keyPairGenerator.initialize(parameterSpec)
|
||||
keyPairGenerator.generateKeyPair()
|
||||
|
||||
keyStore.deleteEntry(alias)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Logger.e("TEE check failure: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun storeTEEStatus(root: File) {
|
||||
val statusFile = File(root, TEE_STATUS_FILE)
|
||||
val status = isTEEWorking()
|
||||
teeBroken = !status
|
||||
try {
|
||||
statusFile.writeText("teeBroken=${!status}")
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Failed to write TEE status: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadTEEStatus(root: File) {
|
||||
val statusFile = File(root, TEE_STATUS_FILE)
|
||||
if (statusFile.exists()) {
|
||||
val line = statusFile.readText().trim()
|
||||
teeBroken = line == "teeBroken=true"
|
||||
} else {
|
||||
teeBroken = null
|
||||
}
|
||||
}
|
||||
|
||||
object ConfigObserver : FileObserver(root, CLOSE_WRITE or DELETE or MOVED_FROM or MOVED_TO) {
|
||||
override fun onEvent(event: Int, path: String?) {
|
||||
path ?: return
|
||||
val f = when (event) {
|
||||
CLOSE_WRITE, MOVED_TO -> File(root, path)
|
||||
DELETE, MOVED_FROM -> null
|
||||
else -> return
|
||||
}
|
||||
when (path) {
|
||||
TARGET_FILE -> updateTargetPackages(f)
|
||||
KEYBOX_FILE -> updateKeyBox(f)
|
||||
PATCHLEVEL_FILE -> updatePatchLevel(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun initialize() {
|
||||
root.mkdirs()
|
||||
val scope = File(root, TARGET_FILE)
|
||||
if (scope.exists()) {
|
||||
updateTargetPackages(scope)
|
||||
} else {
|
||||
Logger.e("target.txt file not found, please put it to $scope !")
|
||||
}
|
||||
val keybox = File(root, KEYBOX_FILE)
|
||||
if (!keybox.exists()) {
|
||||
Logger.e("keybox file not found, please put it to $keybox !")
|
||||
} else {
|
||||
updateKeyBox(keybox)
|
||||
}
|
||||
storeTEEStatus(root)
|
||||
val patchFile = File(root, PATCHLEVEL_FILE)
|
||||
updatePatchLevel(if (patchFile.exists()) patchFile else null)
|
||||
ConfigObserver.startWatching()
|
||||
}
|
||||
|
||||
private var iPm: IPackageManager? = null
|
||||
|
||||
fun getPm(): IPackageManager? {
|
||||
if (iPm == null) {
|
||||
iPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"))
|
||||
}
|
||||
return iPm
|
||||
}
|
||||
|
||||
fun needHack(callingUid: Int): Boolean = kotlin.runCatching {
|
||||
val ps = getPm()?.getPackagesForUid(callingUid) ?: return false
|
||||
if (teeBroken == null) loadTEEStatus(root)
|
||||
for (pkg in ps) {
|
||||
when (packageModes[pkg]) {
|
||||
Mode.LEAF_HACK -> return true
|
||||
Mode.AUTO -> {
|
||||
if (teeBroken == false) return true
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}.onFailure { Logger.e("failed to get packages", it) }.getOrNull() ?: false
|
||||
|
||||
fun needGenerate(callingUid: Int): Boolean = kotlin.runCatching {
|
||||
val ps = getPm()?.getPackagesForUid(callingUid) ?: return false
|
||||
if (teeBroken == null) loadTEEStatus(root)
|
||||
for (pkg in ps) {
|
||||
when (packageModes[pkg]) {
|
||||
Mode.GENERATE -> return true
|
||||
Mode.AUTO -> {
|
||||
if (teeBroken == true) return true
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}.onFailure { Logger.e("failed to get packages", it) }.getOrNull() ?: false
|
||||
|
||||
@Volatile
|
||||
var _customPatchLevel: CustomPatchLevel? = null
|
||||
|
||||
fun updatePatchLevel(f: File?) = runCatching {
|
||||
if (f == null || !f.exists()) {
|
||||
_customPatchLevel = null
|
||||
return@runCatching
|
||||
}
|
||||
val lines = f.readLines().map { it.trim() }.filter { it.isNotEmpty() && !it.startsWith("#") }
|
||||
if (lines.isEmpty()) {
|
||||
_customPatchLevel = null
|
||||
return@runCatching
|
||||
}
|
||||
if (lines.size == 1 && !lines[0].contains("=")) {
|
||||
_customPatchLevel = CustomPatchLevel(all = lines[0])
|
||||
return@runCatching
|
||||
}
|
||||
val map = mutableMapOf<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
|
||||
)
|
||||
}.onFailure {
|
||||
Logger.e("failed to update patch level", it)
|
||||
}
|
||||
}
|
||||
|
||||
data class CustomPatchLevel(
|
||||
val system: String? = null,
|
||||
val vendor: String? = null,
|
||||
val boot: String? = null,
|
||||
val all: String? = null
|
||||
)
|
||||
@@ -1,64 +1,64 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.core.logging
|
||||
|
||||
import android.util.Log
|
||||
|
||||
object Logger {
|
||||
const val TAG = "TrickyStore"
|
||||
|
||||
sealed class LogLevel(val priority: Int) {
|
||||
object Debug : LogLevel(Log.DEBUG)
|
||||
object Info : LogLevel(Log.INFO)
|
||||
object Warning : LogLevel(Log.WARN)
|
||||
object Error : LogLevel(Log.ERROR)
|
||||
object Verbose : LogLevel(Log.VERBOSE)
|
||||
}
|
||||
|
||||
fun d(message: String) {
|
||||
Log.d(TAG, message)
|
||||
}
|
||||
|
||||
fun e(message: String) {
|
||||
Log.e(TAG, message)
|
||||
}
|
||||
|
||||
fun e(message: String, throwable: Throwable) {
|
||||
Log.e(TAG, "wtf: $message", throwable)
|
||||
}
|
||||
|
||||
fun i(message: String) {
|
||||
Log.i(TAG, message)
|
||||
}
|
||||
|
||||
fun w(message: String) {
|
||||
Log.w(TAG, message)
|
||||
}
|
||||
|
||||
fun w(message: String, throwable: Throwable) {
|
||||
Log.w(TAG, message, throwable)
|
||||
}
|
||||
|
||||
fun v(message: String) {
|
||||
Log.v(TAG, message)
|
||||
}
|
||||
|
||||
fun log(level: LogLevel, message: String, throwable: Throwable? = null) {
|
||||
when (level) {
|
||||
is LogLevel.Debug -> if (throwable != null) Log.d(TAG, message, throwable) else Log.d(TAG, message)
|
||||
is LogLevel.Info -> if (throwable != null) Log.i(TAG, message, throwable) else Log.i(TAG, message)
|
||||
is LogLevel.Warning -> if (throwable != null) Log.w(TAG, message, throwable) else Log.w(TAG, message)
|
||||
is LogLevel.Error -> if (throwable != null) Log.e(TAG, message, throwable) else Log.e(TAG, message)
|
||||
is LogLevel.Verbose -> if (throwable != null) Log.v(TAG, message, throwable) else Log.v(TAG, message)
|
||||
}
|
||||
}
|
||||
|
||||
fun logIf(level: LogLevel, condition: Boolean = true, messageProvider: () -> String) {
|
||||
if (condition && Log.isLoggable(TAG, level.priority)) {
|
||||
log(level, messageProvider())
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.core.logging
|
||||
|
||||
import android.util.Log
|
||||
|
||||
object Logger {
|
||||
const val TAG = "TrickyStore"
|
||||
|
||||
sealed class LogLevel(val priority: Int) {
|
||||
object Debug : LogLevel(Log.DEBUG)
|
||||
object Info : LogLevel(Log.INFO)
|
||||
object Warning : LogLevel(Log.WARN)
|
||||
object Error : LogLevel(Log.ERROR)
|
||||
object Verbose : LogLevel(Log.VERBOSE)
|
||||
}
|
||||
|
||||
fun d(message: String) {
|
||||
Log.d(TAG, message)
|
||||
}
|
||||
|
||||
fun e(message: String) {
|
||||
Log.e(TAG, message)
|
||||
}
|
||||
|
||||
fun e(message: String, throwable: Throwable) {
|
||||
Log.e(TAG, "wtf: $message", throwable)
|
||||
}
|
||||
|
||||
fun i(message: String) {
|
||||
Log.i(TAG, message)
|
||||
}
|
||||
|
||||
fun w(message: String) {
|
||||
Log.w(TAG, message)
|
||||
}
|
||||
|
||||
fun w(message: String, throwable: Throwable) {
|
||||
Log.w(TAG, message, throwable)
|
||||
}
|
||||
|
||||
fun v(message: String) {
|
||||
Log.v(TAG, message)
|
||||
}
|
||||
|
||||
fun log(level: LogLevel, message: String, throwable: Throwable? = null) {
|
||||
when (level) {
|
||||
is LogLevel.Debug -> if (throwable != null) Log.d(TAG, message, throwable) else Log.d(TAG, message)
|
||||
is LogLevel.Info -> if (throwable != null) Log.i(TAG, message, throwable) else Log.i(TAG, message)
|
||||
is LogLevel.Warning -> if (throwable != null) Log.w(TAG, message, throwable) else Log.w(TAG, message)
|
||||
is LogLevel.Error -> if (throwable != null) Log.e(TAG, message, throwable) else Log.e(TAG, message)
|
||||
is LogLevel.Verbose -> if (throwable != null) Log.v(TAG, message, throwable) else Log.v(TAG, message)
|
||||
}
|
||||
}
|
||||
|
||||
fun logIf(level: LogLevel, condition: Boolean = true, messageProvider: () -> String) {
|
||||
if (condition && Log.isLoggable(TAG, level.priority)) {
|
||||
log(level, messageProvider())
|
||||
}
|
||||
}
|
||||
}
|
||||
+180
-180
@@ -1,181 +1,181 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.interceptors
|
||||
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
|
||||
open class BinderInterceptor : Binder() {
|
||||
|
||||
sealed class Result
|
||||
|
||||
data object Skip : Result()
|
||||
|
||||
data object Continue : Result()
|
||||
|
||||
data class OverrideData(val data: Parcel) : Result()
|
||||
|
||||
data class OverrideReply(val code: Int = 0, val reply: Parcel) : Result()
|
||||
|
||||
companion object {
|
||||
private const val BACKDOOR_TRANSACTION_CODE = 0xdeadbeef.toInt()
|
||||
|
||||
private const val REGISTER_INTERCEPTOR_CODE = 1
|
||||
|
||||
private const val PRE_TRANSACT_CODE = 1
|
||||
private const val POST_TRANSACT_CODE = 2
|
||||
|
||||
private const val RESULT_SKIP = 1
|
||||
private const val RESULT_CONTINUE = 2
|
||||
private const val RESULT_OVERRIDE_REPLY = 3
|
||||
private const val RESULT_OVERRIDE_DATA = 4
|
||||
|
||||
fun getBinderBackdoor(binder: IBinder): IBinder? {
|
||||
val data = Parcel.obtain()
|
||||
val reply = Parcel.obtain()
|
||||
|
||||
return try {
|
||||
val success = binder.transact(BACKDOOR_TRANSACTION_CODE, data, reply, 0)
|
||||
if (success) {
|
||||
Logger.d("Backdoor access granted for binder: $binder")
|
||||
reply.readStrongBinder()
|
||||
} else {
|
||||
Logger.d("Backdoor access denied for binder: $binder")
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Failed to access binder backdoor", e)
|
||||
null
|
||||
} finally {
|
||||
data.recycle()
|
||||
reply.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
fun registerBinderInterceptor(
|
||||
backdoor: IBinder,
|
||||
target: IBinder,
|
||||
interceptor: BinderInterceptor
|
||||
) {
|
||||
val data = Parcel.obtain()
|
||||
val reply = Parcel.obtain()
|
||||
|
||||
try {
|
||||
data.writeStrongBinder(target)
|
||||
data.writeStrongBinder(interceptor)
|
||||
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
|
||||
Logger.d("Registered interceptor for target: $target")
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Failed to register binder interceptor", e)
|
||||
} finally {
|
||||
data.recycle()
|
||||
reply.recycle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun onPreTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel
|
||||
): Result = Skip
|
||||
|
||||
open fun onPostTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel,
|
||||
reply: Parcel?,
|
||||
resultCode: Int
|
||||
): Result = Skip
|
||||
|
||||
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
|
||||
val result = when (code) {
|
||||
PRE_TRANSACT_CODE -> handlePreTransact(data)
|
||||
POST_TRANSACT_CODE -> handlePostTransact(data)
|
||||
else -> return super.onTransact(code, data, reply, flags)
|
||||
}
|
||||
|
||||
writeResultToReply(result, reply!!)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handlePreTransact(data: Parcel): Result {
|
||||
val target = data.readStrongBinder()
|
||||
val transactionCode = data.readInt()
|
||||
val transactionFlags = data.readInt()
|
||||
val callingUid = data.readInt()
|
||||
val callingPid = data.readInt()
|
||||
val dataSize = data.readLong()
|
||||
|
||||
val transactionData = Parcel.obtain()
|
||||
return try {
|
||||
transactionData.appendFrom(data, data.dataPosition(), dataSize.toInt())
|
||||
transactionData.setDataPosition(0)
|
||||
onPreTransact(target, transactionCode, transactionFlags, callingUid, callingPid, transactionData)
|
||||
} finally {
|
||||
transactionData.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePostTransact(data: Parcel): Result {
|
||||
val target = data.readStrongBinder()
|
||||
val transactionCode = data.readInt()
|
||||
val transactionFlags = data.readInt()
|
||||
val callingUid = data.readInt()
|
||||
val callingPid = data.readInt()
|
||||
val resultCode = data.readInt()
|
||||
|
||||
val transactionData = Parcel.obtain()
|
||||
val transactionReply = Parcel.obtain()
|
||||
|
||||
return try {
|
||||
val dataSize = data.readLong().toInt()
|
||||
transactionData.appendFrom(data, data.dataPosition(), dataSize)
|
||||
transactionData.setDataPosition(0)
|
||||
data.setDataPosition(data.dataPosition() + dataSize)
|
||||
|
||||
val replySize = data.readLong().toInt()
|
||||
val reply = if (replySize > 0) {
|
||||
transactionReply.appendFrom(data, data.dataPosition(), replySize)
|
||||
transactionReply.setDataPosition(0)
|
||||
transactionReply
|
||||
} else null
|
||||
|
||||
onPostTransact(target, transactionCode, transactionFlags, callingUid, callingPid, transactionData, reply, resultCode)
|
||||
} finally {
|
||||
transactionData.recycle()
|
||||
transactionReply.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeResultToReply(result: Result, reply: Parcel) {
|
||||
when (result) {
|
||||
Skip -> reply.writeInt(RESULT_SKIP)
|
||||
Continue -> reply.writeInt(RESULT_CONTINUE)
|
||||
is OverrideReply -> {
|
||||
reply.writeInt(RESULT_OVERRIDE_REPLY)
|
||||
reply.writeInt(result.code)
|
||||
reply.writeLong(result.reply.dataSize().toLong())
|
||||
reply.appendFrom(result.reply, 0, result.reply.dataSize())
|
||||
result.reply.recycle()
|
||||
}
|
||||
is OverrideData -> {
|
||||
reply.writeInt(RESULT_OVERRIDE_DATA)
|
||||
reply.writeLong(result.data.dataSize().toLong())
|
||||
reply.appendFrom(result.data, 0, result.data.dataSize())
|
||||
result.data.recycle()
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.interceptors
|
||||
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
|
||||
open class BinderInterceptor : Binder() {
|
||||
|
||||
sealed class Result
|
||||
|
||||
data object Skip : Result()
|
||||
|
||||
data object Continue : Result()
|
||||
|
||||
data class OverrideData(val data: Parcel) : Result()
|
||||
|
||||
data class OverrideReply(val code: Int = 0, val reply: Parcel) : Result()
|
||||
|
||||
companion object {
|
||||
private const val BACKDOOR_TRANSACTION_CODE = 0xdeadbeef.toInt()
|
||||
|
||||
private const val REGISTER_INTERCEPTOR_CODE = 1
|
||||
|
||||
private const val PRE_TRANSACT_CODE = 1
|
||||
private const val POST_TRANSACT_CODE = 2
|
||||
|
||||
private const val RESULT_SKIP = 1
|
||||
private const val RESULT_CONTINUE = 2
|
||||
private const val RESULT_OVERRIDE_REPLY = 3
|
||||
private const val RESULT_OVERRIDE_DATA = 4
|
||||
|
||||
fun getBinderBackdoor(binder: IBinder): IBinder? {
|
||||
val data = Parcel.obtain()
|
||||
val reply = Parcel.obtain()
|
||||
|
||||
return try {
|
||||
val success = binder.transact(BACKDOOR_TRANSACTION_CODE, data, reply, 0)
|
||||
if (success) {
|
||||
Logger.d("Backdoor access granted for binder: $binder")
|
||||
reply.readStrongBinder()
|
||||
} else {
|
||||
Logger.d("Backdoor access denied for binder: $binder")
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Failed to access binder backdoor", e)
|
||||
null
|
||||
} finally {
|
||||
data.recycle()
|
||||
reply.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
fun registerBinderInterceptor(
|
||||
backdoor: IBinder,
|
||||
target: IBinder,
|
||||
interceptor: BinderInterceptor
|
||||
) {
|
||||
val data = Parcel.obtain()
|
||||
val reply = Parcel.obtain()
|
||||
|
||||
try {
|
||||
data.writeStrongBinder(target)
|
||||
data.writeStrongBinder(interceptor)
|
||||
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
|
||||
Logger.d("Registered interceptor for target: $target")
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Failed to register binder interceptor", e)
|
||||
} finally {
|
||||
data.recycle()
|
||||
reply.recycle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun onPreTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel
|
||||
): Result = Skip
|
||||
|
||||
open fun onPostTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel,
|
||||
reply: Parcel?,
|
||||
resultCode: Int
|
||||
): Result = Skip
|
||||
|
||||
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
|
||||
val result = when (code) {
|
||||
PRE_TRANSACT_CODE -> handlePreTransact(data)
|
||||
POST_TRANSACT_CODE -> handlePostTransact(data)
|
||||
else -> return super.onTransact(code, data, reply, flags)
|
||||
}
|
||||
|
||||
writeResultToReply(result, reply!!)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handlePreTransact(data: Parcel): Result {
|
||||
val target = data.readStrongBinder()
|
||||
val transactionCode = data.readInt()
|
||||
val transactionFlags = data.readInt()
|
||||
val callingUid = data.readInt()
|
||||
val callingPid = data.readInt()
|
||||
val dataSize = data.readLong()
|
||||
|
||||
val transactionData = Parcel.obtain()
|
||||
return try {
|
||||
transactionData.appendFrom(data, data.dataPosition(), dataSize.toInt())
|
||||
transactionData.setDataPosition(0)
|
||||
onPreTransact(target, transactionCode, transactionFlags, callingUid, callingPid, transactionData)
|
||||
} finally {
|
||||
transactionData.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePostTransact(data: Parcel): Result {
|
||||
val target = data.readStrongBinder()
|
||||
val transactionCode = data.readInt()
|
||||
val transactionFlags = data.readInt()
|
||||
val callingUid = data.readInt()
|
||||
val callingPid = data.readInt()
|
||||
val resultCode = data.readInt()
|
||||
|
||||
val transactionData = Parcel.obtain()
|
||||
val transactionReply = Parcel.obtain()
|
||||
|
||||
return try {
|
||||
val dataSize = data.readLong().toInt()
|
||||
transactionData.appendFrom(data, data.dataPosition(), dataSize)
|
||||
transactionData.setDataPosition(0)
|
||||
data.setDataPosition(data.dataPosition() + dataSize)
|
||||
|
||||
val replySize = data.readLong().toInt()
|
||||
val reply = if (replySize > 0) {
|
||||
transactionReply.appendFrom(data, data.dataPosition(), replySize)
|
||||
transactionReply.setDataPosition(0)
|
||||
transactionReply
|
||||
} else null
|
||||
|
||||
onPostTransact(target, transactionCode, transactionFlags, callingUid, callingPid, transactionData, reply, resultCode)
|
||||
} finally {
|
||||
transactionData.recycle()
|
||||
transactionReply.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeResultToReply(result: Result, reply: Parcel) {
|
||||
when (result) {
|
||||
Skip -> reply.writeInt(RESULT_SKIP)
|
||||
Continue -> reply.writeInt(RESULT_CONTINUE)
|
||||
is OverrideReply -> {
|
||||
reply.writeInt(RESULT_OVERRIDE_REPLY)
|
||||
reply.writeInt(result.code)
|
||||
reply.writeLong(result.reply.dataSize().toLong())
|
||||
reply.appendFrom(result.reply, 0, result.reply.dataSize())
|
||||
result.reply.recycle()
|
||||
}
|
||||
is OverrideData -> {
|
||||
reply.writeInt(RESULT_OVERRIDE_DATA)
|
||||
reply.writeLong(result.data.dataSize().toLong())
|
||||
reply.appendFrom(result.data, 0, result.data.dataSize())
|
||||
result.data.recycle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+141
-141
@@ -1,142 +1,142 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.interceptors
|
||||
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.os.ServiceManager
|
||||
import android.security.KeyStore
|
||||
import android.security.keystore.KeystoreResponse
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
abstract class BaseKeystoreInterceptor : BinderInterceptor() {
|
||||
|
||||
protected lateinit var keystore: IBinder
|
||||
protected var triedCount = 0
|
||||
protected var injected = false
|
||||
protected open val maxRetries: Int = 3
|
||||
|
||||
protected abstract val serviceName: String
|
||||
protected abstract val injectionCommand: String
|
||||
protected abstract val processName: String
|
||||
|
||||
fun tryRunKeystoreInterceptor(): Boolean {
|
||||
Logger.i("Trying to register ${this::class.simpleName} (attempt $triedCount)...")
|
||||
|
||||
val service = getService() ?: return false
|
||||
val backdoor = getBinderBackdoor(service)
|
||||
|
||||
return if (backdoor != null) {
|
||||
setupInterceptor(service, backdoor)
|
||||
} else {
|
||||
handleMissingBackdoor()
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun getService(): IBinder? = ServiceManager.getService(serviceName)
|
||||
|
||||
protected open fun setupInterceptor(service: IBinder, backdoor: IBinder): Boolean {
|
||||
keystore = service
|
||||
Logger.i("Registering for $serviceName: $keystore")
|
||||
|
||||
registerBinderInterceptor(backdoor, service, this)
|
||||
service.linkToDeath(createDeathRecipient(), 0)
|
||||
onInterceptorSetup(service, backdoor)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handleMissingBackdoor(): Boolean {
|
||||
if (triedCount >= maxRetries) {
|
||||
Logger.e("Tried injection $maxRetries times but still no backdoor, exiting")
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
if (!injected) {
|
||||
performInjection()
|
||||
injected = true
|
||||
}
|
||||
|
||||
triedCount++
|
||||
return false
|
||||
}
|
||||
|
||||
protected open fun performInjection() {
|
||||
Logger.i("Attempting to inject into $processName...")
|
||||
|
||||
val command = arrayOf("/system/bin/sh", "-c", injectionCommand)
|
||||
Logger.d("Injection command: ${command.joinToString(" ")}")
|
||||
|
||||
val process = Runtime.getRuntime().exec(command)
|
||||
|
||||
if (process.waitFor() != 0) {
|
||||
Logger.e("Injection failed! Daemon will exit")
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
Logger.i("Injection completed successfully")
|
||||
}
|
||||
|
||||
protected open fun createDeathRecipient(): IBinder.DeathRecipient = object : IBinder.DeathRecipient {
|
||||
override fun binderDied() {
|
||||
Logger.d("$serviceName died, daemon restarting")
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onInterceptorSetup(service: IBinder, backdoor: IBinder) {
|
||||
// Default implementation does nothing
|
||||
}
|
||||
}
|
||||
|
||||
object InterceptorUtils {
|
||||
|
||||
fun createSuccessKeystoreResponse(): KeystoreResponse {
|
||||
val parcel = Parcel.obtain()
|
||||
try {
|
||||
parcel.writeInt(KeyStore.NO_ERROR)
|
||||
parcel.writeString("")
|
||||
parcel.setDataPosition(0)
|
||||
return KeystoreResponse.CREATOR.createFromParcel(parcel)
|
||||
} finally {
|
||||
parcel.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
fun createSuccessReply(resultCode: Int = KeyStore.NO_ERROR): BinderInterceptor.OverrideReply {
|
||||
val parcel = Parcel.obtain()
|
||||
parcel.writeNoException()
|
||||
parcel.writeInt(resultCode)
|
||||
return BinderInterceptor.OverrideReply(0, parcel)
|
||||
}
|
||||
|
||||
fun createByteArrayReply(data: ByteArray, resultCode: Int = KeyStore.NO_ERROR): BinderInterceptor.OverrideReply {
|
||||
val parcel = Parcel.obtain()
|
||||
parcel.writeNoException()
|
||||
parcel.writeByteArray(data)
|
||||
return BinderInterceptor.OverrideReply(resultCode, parcel)
|
||||
}
|
||||
|
||||
fun <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]
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
fun Parcel.hasException(): Boolean {
|
||||
return kotlin.runCatching { readException() }.exceptionOrNull() != null
|
||||
}
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.interceptors
|
||||
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.os.ServiceManager
|
||||
import android.security.KeyStore
|
||||
import android.security.keystore.KeystoreResponse
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
abstract class BaseKeystoreInterceptor : BinderInterceptor() {
|
||||
|
||||
protected lateinit var keystore: IBinder
|
||||
protected var triedCount = 0
|
||||
protected var injected = false
|
||||
protected open val maxRetries: Int = 3
|
||||
|
||||
protected abstract val serviceName: String
|
||||
protected abstract val injectionCommand: String
|
||||
protected abstract val processName: String
|
||||
|
||||
fun tryRunKeystoreInterceptor(): Boolean {
|
||||
Logger.i("Trying to register ${this::class.simpleName} (attempt $triedCount)...")
|
||||
|
||||
val service = getService() ?: return false
|
||||
val backdoor = getBinderBackdoor(service)
|
||||
|
||||
return if (backdoor != null) {
|
||||
setupInterceptor(service, backdoor)
|
||||
} else {
|
||||
handleMissingBackdoor()
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun getService(): IBinder? = ServiceManager.getService(serviceName)
|
||||
|
||||
protected open fun setupInterceptor(service: IBinder, backdoor: IBinder): Boolean {
|
||||
keystore = service
|
||||
Logger.i("Registering for $serviceName: $keystore")
|
||||
|
||||
registerBinderInterceptor(backdoor, service, this)
|
||||
service.linkToDeath(createDeathRecipient(), 0)
|
||||
onInterceptorSetup(service, backdoor)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handleMissingBackdoor(): Boolean {
|
||||
if (triedCount >= maxRetries) {
|
||||
Logger.e("Tried injection $maxRetries times but still no backdoor, exiting")
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
if (!injected) {
|
||||
performInjection()
|
||||
injected = true
|
||||
}
|
||||
|
||||
triedCount++
|
||||
return false
|
||||
}
|
||||
|
||||
protected open fun performInjection() {
|
||||
Logger.i("Attempting to inject into $processName...")
|
||||
|
||||
val command = arrayOf("/system/bin/sh", "-c", injectionCommand)
|
||||
Logger.d("Injection command: ${command.joinToString(" ")}")
|
||||
|
||||
val process = Runtime.getRuntime().exec(command)
|
||||
|
||||
if (process.waitFor() != 0) {
|
||||
Logger.e("Injection failed! Daemon will exit")
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
Logger.i("Injection completed successfully")
|
||||
}
|
||||
|
||||
protected open fun createDeathRecipient(): IBinder.DeathRecipient = object : IBinder.DeathRecipient {
|
||||
override fun binderDied() {
|
||||
Logger.d("$serviceName died, daemon restarting")
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onInterceptorSetup(service: IBinder, backdoor: IBinder) {
|
||||
// Default implementation does nothing
|
||||
}
|
||||
}
|
||||
|
||||
object InterceptorUtils {
|
||||
|
||||
fun createSuccessKeystoreResponse(): KeystoreResponse {
|
||||
val parcel = Parcel.obtain()
|
||||
try {
|
||||
parcel.writeInt(KeyStore.NO_ERROR)
|
||||
parcel.writeString("")
|
||||
parcel.setDataPosition(0)
|
||||
return KeystoreResponse.CREATOR.createFromParcel(parcel)
|
||||
} finally {
|
||||
parcel.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
fun createSuccessReply(resultCode: Int = KeyStore.NO_ERROR): BinderInterceptor.OverrideReply {
|
||||
val parcel = Parcel.obtain()
|
||||
parcel.writeNoException()
|
||||
parcel.writeInt(resultCode)
|
||||
return BinderInterceptor.OverrideReply(0, parcel)
|
||||
}
|
||||
|
||||
fun createByteArrayReply(data: ByteArray, resultCode: Int = KeyStore.NO_ERROR): BinderInterceptor.OverrideReply {
|
||||
val parcel = Parcel.obtain()
|
||||
parcel.writeNoException()
|
||||
parcel.writeByteArray(data)
|
||||
return BinderInterceptor.OverrideReply(resultCode, parcel)
|
||||
}
|
||||
|
||||
fun <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]
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
fun Parcel.hasException(): Boolean {
|
||||
return kotlin.runCatching { readException() }.exceptionOrNull() != null
|
||||
}
|
||||
}
|
||||
+157
-157
@@ -1,158 +1,158 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.interceptors
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.hardware.security.keymint.SecurityLevel
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.system.keystore2.IKeystoreService
|
||||
import android.system.keystore2.KeyDescriptor
|
||||
import android.system.keystore2.KeyEntryResponse
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHacker
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateUtils
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.getTransactCode
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createTypedObjectReply
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.hasException
|
||||
import io.github.beakthoven.TrickyStoreOSS.putCertificateChain
|
||||
|
||||
@SuppressLint("BlockedPrivateApi")
|
||||
object Keystore2Interceptor : BaseKeystoreInterceptor() {
|
||||
private val getKeyEntryTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "getKeyEntry")
|
||||
private val deleteKeyTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "deleteKey")
|
||||
|
||||
override val serviceName = "android.system.keystore2.IKeystoreService/default"
|
||||
override val processName = "keystore2"
|
||||
override val injectionCommand = "exec ./inject `pidof keystore2` libTrickyStoreOSS.so entry"
|
||||
|
||||
private var teeInterceptor: SecurityLevelInterceptor? = null
|
||||
private var strongBoxInterceptor: SecurityLevelInterceptor? = null
|
||||
|
||||
override fun onInterceptorSetup(service: IBinder, backdoor: IBinder) {
|
||||
setupSecurityLevelInterceptors(service, backdoor)
|
||||
}
|
||||
|
||||
private fun setupSecurityLevelInterceptors(service: IBinder, backdoor: IBinder) {
|
||||
val ks = IKeystoreService.Stub.asInterface(service)
|
||||
|
||||
val tee = kotlin.runCatching { ks.getSecurityLevel(SecurityLevel.TRUSTED_ENVIRONMENT) }
|
||||
.getOrNull()
|
||||
if (tee != null) {
|
||||
Logger.i("Registering for TEE SecurityLevel: $tee")
|
||||
val interceptor = SecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
|
||||
registerBinderInterceptor(backdoor, tee.asBinder(), interceptor)
|
||||
teeInterceptor = interceptor
|
||||
} else {
|
||||
Logger.i("No TEE SecurityLevel found")
|
||||
}
|
||||
|
||||
val strongBox = kotlin.runCatching { ks.getSecurityLevel(SecurityLevel.STRONGBOX) }
|
||||
.getOrNull()
|
||||
if (strongBox != null) {
|
||||
Logger.i("Registering for StrongBox SecurityLevel: $strongBox")
|
||||
val interceptor = SecurityLevelInterceptor(strongBox, SecurityLevel.STRONGBOX)
|
||||
registerBinderInterceptor(backdoor, strongBox.asBinder(), interceptor)
|
||||
strongBoxInterceptor = interceptor
|
||||
} else {
|
||||
Logger.i("No StrongBox SecurityLevel found")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel
|
||||
): Result {
|
||||
if (code == getKeyEntryTransaction) {
|
||||
if (CertificateHacker.canHack()) {
|
||||
Logger.d("intercept pre $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()}")
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR) ?: return@runCatching
|
||||
if (Config.needGenerate(callingUid)) {
|
||||
val response = SecurityLevelInterceptor.getKeyResponse(callingUid, descriptor.alias)
|
||||
?: return@runCatching
|
||||
Logger.i("Generate key for uid=$callingUid alias=${descriptor.alias}")
|
||||
return createTypedObjectReply(response)
|
||||
} else if (Config.needHack(callingUid)) {
|
||||
if (SecurityLevelInterceptor.shouldSkipLeafHack(callingUid, descriptor.alias)) {
|
||||
Logger.i("skip leaf hack for uid=$callingUid alias=${descriptor.alias}")
|
||||
val response = SecurityLevelInterceptor.getKeyResponse(callingUid, descriptor.alias)
|
||||
if (response != null) {
|
||||
Logger.i("Found generated response for uid=$callingUid alias=${descriptor.alias}")
|
||||
return createTypedObjectReply(response)
|
||||
} else {
|
||||
Logger.e("No generated response found for uid=$callingUid alias=${descriptor.alias}")
|
||||
return@runCatching
|
||||
}
|
||||
} else {
|
||||
Logger.i("proceeding with leaf hack for uid=$callingUid alias=${descriptor.alias}")
|
||||
return Continue
|
||||
}
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
}
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
|
||||
override fun onPostTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel,
|
||||
reply: Parcel?,
|
||||
resultCode: Int
|
||||
): Result {
|
||||
if (target != keystore || reply == null) return Skip
|
||||
if (reply.hasException()) return Skip
|
||||
val p = Parcel.obtain()
|
||||
Logger.d("intercept post $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()} replySz=${reply.dataSize()}")
|
||||
|
||||
if (code == deleteKeyTransaction && resultCode == 0) {
|
||||
data.enforceInterface("android.system.keystore2.IKeystoreService")
|
||||
|
||||
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||
if (keyDescriptor == null || keyDescriptor.domain == 0) return Skip
|
||||
|
||||
SecurityLevelInterceptor.keys.remove(SecurityLevelInterceptor.Key(callingUid, keyDescriptor.alias))
|
||||
|
||||
return Skip
|
||||
} else if (code == getKeyEntryTransaction) {
|
||||
try {
|
||||
data.enforceInterface("android.system.keystore2.IKeystoreService")
|
||||
val response = reply.readTypedObject(KeyEntryResponse.CREATOR)
|
||||
if (response != null) {
|
||||
val chain = CertificateUtils.run { response.getCertificateChain() }
|
||||
if (chain != null) {
|
||||
val newChain = CertificateHacker.hackCertificateChain(chain)
|
||||
response.putCertificateChain(newChain).getOrThrow()
|
||||
Logger.i("Hacked certificate for uid=$callingUid")
|
||||
return createTypedObjectReply(response)
|
||||
} else {
|
||||
p.recycle()
|
||||
}
|
||||
} else {
|
||||
p.recycle()
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("failed to hack certificate chain of uid=$callingUid pid=$callingPid!", t)
|
||||
p.recycle()
|
||||
}
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.interceptors
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.hardware.security.keymint.SecurityLevel
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.system.keystore2.IKeystoreService
|
||||
import android.system.keystore2.KeyDescriptor
|
||||
import android.system.keystore2.KeyEntryResponse
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHacker
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateUtils
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.getTransactCode
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createTypedObjectReply
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.hasException
|
||||
import io.github.beakthoven.TrickyStoreOSS.putCertificateChain
|
||||
|
||||
@SuppressLint("BlockedPrivateApi")
|
||||
object Keystore2Interceptor : BaseKeystoreInterceptor() {
|
||||
private val getKeyEntryTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "getKeyEntry")
|
||||
private val deleteKeyTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "deleteKey")
|
||||
|
||||
override val serviceName = "android.system.keystore2.IKeystoreService/default"
|
||||
override val processName = "keystore2"
|
||||
override val injectionCommand = "exec ./inject `pidof keystore2` libTrickyStoreOSS.so entry"
|
||||
|
||||
private var teeInterceptor: SecurityLevelInterceptor? = null
|
||||
private var strongBoxInterceptor: SecurityLevelInterceptor? = null
|
||||
|
||||
override fun onInterceptorSetup(service: IBinder, backdoor: IBinder) {
|
||||
setupSecurityLevelInterceptors(service, backdoor)
|
||||
}
|
||||
|
||||
private fun setupSecurityLevelInterceptors(service: IBinder, backdoor: IBinder) {
|
||||
val ks = IKeystoreService.Stub.asInterface(service)
|
||||
|
||||
val tee = kotlin.runCatching { ks.getSecurityLevel(SecurityLevel.TRUSTED_ENVIRONMENT) }
|
||||
.getOrNull()
|
||||
if (tee != null) {
|
||||
Logger.i("Registering for TEE SecurityLevel: $tee")
|
||||
val interceptor = SecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
|
||||
registerBinderInterceptor(backdoor, tee.asBinder(), interceptor)
|
||||
teeInterceptor = interceptor
|
||||
} else {
|
||||
Logger.i("No TEE SecurityLevel found")
|
||||
}
|
||||
|
||||
val strongBox = kotlin.runCatching { ks.getSecurityLevel(SecurityLevel.STRONGBOX) }
|
||||
.getOrNull()
|
||||
if (strongBox != null) {
|
||||
Logger.i("Registering for StrongBox SecurityLevel: $strongBox")
|
||||
val interceptor = SecurityLevelInterceptor(strongBox, SecurityLevel.STRONGBOX)
|
||||
registerBinderInterceptor(backdoor, strongBox.asBinder(), interceptor)
|
||||
strongBoxInterceptor = interceptor
|
||||
} else {
|
||||
Logger.i("No StrongBox SecurityLevel found")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel
|
||||
): Result {
|
||||
if (code == getKeyEntryTransaction) {
|
||||
if (CertificateHacker.canHack()) {
|
||||
Logger.d("intercept pre $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()}")
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR) ?: return@runCatching
|
||||
if (Config.needGenerate(callingUid)) {
|
||||
val response = SecurityLevelInterceptor.getKeyResponse(callingUid, descriptor.alias)
|
||||
?: return@runCatching
|
||||
Logger.i("Generate key for uid=$callingUid alias=${descriptor.alias}")
|
||||
return createTypedObjectReply(response)
|
||||
} else if (Config.needHack(callingUid)) {
|
||||
if (SecurityLevelInterceptor.shouldSkipLeafHack(callingUid, descriptor.alias)) {
|
||||
Logger.i("skip leaf hack for uid=$callingUid alias=${descriptor.alias}")
|
||||
val response = SecurityLevelInterceptor.getKeyResponse(callingUid, descriptor.alias)
|
||||
if (response != null) {
|
||||
Logger.i("Found generated response for uid=$callingUid alias=${descriptor.alias}")
|
||||
return createTypedObjectReply(response)
|
||||
} else {
|
||||
Logger.e("No generated response found for uid=$callingUid alias=${descriptor.alias}")
|
||||
return@runCatching
|
||||
}
|
||||
} else {
|
||||
Logger.i("proceeding with leaf hack for uid=$callingUid alias=${descriptor.alias}")
|
||||
return Continue
|
||||
}
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
}
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
|
||||
override fun onPostTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel,
|
||||
reply: Parcel?,
|
||||
resultCode: Int
|
||||
): Result {
|
||||
if (target != keystore || reply == null) return Skip
|
||||
if (reply.hasException()) return Skip
|
||||
val p = Parcel.obtain()
|
||||
Logger.d("intercept post $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()} replySz=${reply.dataSize()}")
|
||||
|
||||
if (code == deleteKeyTransaction && resultCode == 0) {
|
||||
data.enforceInterface("android.system.keystore2.IKeystoreService")
|
||||
|
||||
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||
if (keyDescriptor == null || keyDescriptor.domain == 0) return Skip
|
||||
|
||||
SecurityLevelInterceptor.keys.remove(SecurityLevelInterceptor.Key(callingUid, keyDescriptor.alias))
|
||||
|
||||
return Skip
|
||||
} else if (code == getKeyEntryTransaction) {
|
||||
try {
|
||||
data.enforceInterface("android.system.keystore2.IKeystoreService")
|
||||
val response = reply.readTypedObject(KeyEntryResponse.CREATOR)
|
||||
if (response != null) {
|
||||
val chain = CertificateUtils.run { response.getCertificateChain() }
|
||||
if (chain != null) {
|
||||
val newChain = CertificateHacker.hackCertificateChain(chain)
|
||||
response.putCertificateChain(newChain).getOrThrow()
|
||||
Logger.i("Hacked certificate for uid=$callingUid")
|
||||
return createTypedObjectReply(response)
|
||||
} else {
|
||||
p.recycle()
|
||||
}
|
||||
} else {
|
||||
p.recycle()
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("failed to hack certificate chain of uid=$callingUid pid=$callingPid!", t)
|
||||
p.recycle()
|
||||
}
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
}
|
||||
+237
-237
@@ -1,238 +1,238 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.interceptors
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.security.Credentials
|
||||
import android.security.KeyStore
|
||||
import android.security.keymaster.ExportResult
|
||||
import android.security.keymaster.KeyCharacteristics
|
||||
import android.security.keymaster.KeymasterArguments
|
||||
import android.security.keymaster.KeymasterCertificateChain
|
||||
import android.security.keymaster.KeymasterDefs
|
||||
import android.security.keystore.IKeystoreCertificateChainCallback
|
||||
import android.security.keystore.IKeystoreExportKeyCallback
|
||||
import android.security.keystore.IKeystoreKeyCharacteristicsCallback
|
||||
import android.security.keystore.IKeystoreService
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHacker
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.getTransactCode
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createByteArrayReply
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createSuccessKeystoreResponse
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createSuccessReply
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.extractAlias
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.hasException
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyPair
|
||||
import java.util.Date
|
||||
|
||||
@SuppressLint("BlockedPrivateApi")
|
||||
object KeystoreInterceptor : BaseKeystoreInterceptor() {
|
||||
private val getTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "get")
|
||||
private val generateKeyTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "generateKey")
|
||||
private val getKeyCharacteristicsTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "getKeyCharacteristics")
|
||||
private val exportKeyTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "exportKey")
|
||||
private val attestKeyTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "attestKey")
|
||||
|
||||
override val serviceName = "android.security.keystore"
|
||||
override val processName = "keystore"
|
||||
override val injectionCommand = "exec ./inject `pidof keystore` libTrickyStoreOSS.so entry"
|
||||
|
||||
private const val DESCRIPTOR = "android.security.keystore.IKeystoreService"
|
||||
|
||||
private val keyArguments = HashMap<Key, CertificateHacker.KeyGenParameters>()
|
||||
private val keyPairs = HashMap<Key, KeyPair>()
|
||||
|
||||
data class Key(val uid: Int, val alias: String)
|
||||
|
||||
override fun onPreTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel
|
||||
): Result {
|
||||
if (CertificateHacker.canHack()) {
|
||||
if (code == getTransaction) {
|
||||
if (Config.needHack(callingUid)) {
|
||||
return Continue
|
||||
} else if (Config.needGenerate(callingUid)) {
|
||||
return Skip
|
||||
}
|
||||
} else if (Config.needGenerate(callingUid)) {
|
||||
when (code) {
|
||||
generateKeyTransaction -> {
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(DESCRIPTOR)
|
||||
val callback = IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
|
||||
val alias = data.readString()!!.extractAlias()
|
||||
Logger.i("generateKeyTransaction uid $callingUid alias $alias")
|
||||
val check = data.readInt()
|
||||
val kma = KeymasterArguments()
|
||||
val kgp = CertificateHacker.KeyGenParameters()
|
||||
if (check == 1) {
|
||||
kma.readFromParcel(data)
|
||||
kgp.algorithm = kma.getEnum(KeymasterDefs.KM_TAG_ALGORITHM, 0)
|
||||
kgp.keySize = kma.getUnsignedInt(KeymasterDefs.KM_TAG_KEY_SIZE, 0).toInt()
|
||||
kgp.setEcCurveName(kgp.keySize)
|
||||
kgp.purpose = kma.getEnums(KeymasterDefs.KM_TAG_PURPOSE)
|
||||
kgp.digest = kma.getEnums(KeymasterDefs.KM_TAG_DIGEST)
|
||||
kgp.certificateNotBefore = kma.getDate(KeymasterDefs.KM_TAG_ACTIVE_DATETIME, Date())
|
||||
if (kgp.algorithm == KeymasterDefs.KM_ALGORITHM_RSA) {
|
||||
try {
|
||||
val getArgumentByTag = KeymasterArguments::class.java.getDeclaredMethods().first { it.name == "getArgumentByTag" }
|
||||
getArgumentByTag.isAccessible = true
|
||||
val rsaArgument = getArgumentByTag.invoke(kma, KeymasterDefs.KM_TAG_RSA_PUBLIC_EXPONENT)
|
||||
|
||||
val getLongTagValue = KeymasterArguments::class.java.getDeclaredMethods().first { it.name == "getLongTagValue" }
|
||||
getLongTagValue.isAccessible = true
|
||||
kgp.rsaPublicExponent = getLongTagValue.invoke(kma, rsaArgument) as BigInteger
|
||||
} catch (ex: Exception) {
|
||||
Logger.e("Read rsaPublicExponent error", ex)
|
||||
}
|
||||
}
|
||||
keyArguments[Key(callingUid, alias)] = kgp
|
||||
}
|
||||
|
||||
val kc = KeyCharacteristics()
|
||||
kc.swEnforced = KeymasterArguments()
|
||||
kc.hwEnforced = kma
|
||||
|
||||
val ksr = createSuccessKeystoreResponse()
|
||||
callback.onFinished(ksr, kc)
|
||||
|
||||
return createSuccessReply()
|
||||
}.onFailure {
|
||||
Logger.e("generateKeyTransaction error", it)
|
||||
}
|
||||
}
|
||||
|
||||
getKeyCharacteristicsTransaction -> {
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(DESCRIPTOR)
|
||||
val callback = IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
|
||||
val alias = data.readString()!!.extractAlias()
|
||||
Logger.i("getKeyCharacteristicsTransaction uid $callingUid alias $alias")
|
||||
val kc = KeyCharacteristics()
|
||||
val kma = KeymasterArguments()
|
||||
kma.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, keyArguments[Key(callingUid, alias)]!!.algorithm)
|
||||
kc.swEnforced = KeymasterArguments()
|
||||
kc.hwEnforced = kma
|
||||
|
||||
val ksr = createSuccessKeystoreResponse()
|
||||
callback.onFinished(ksr, kc)
|
||||
|
||||
return createSuccessReply()
|
||||
}.onFailure {
|
||||
Logger.e("getKeyCharacteristicsTransaction error", it)
|
||||
}
|
||||
}
|
||||
|
||||
exportKeyTransaction -> {
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(DESCRIPTOR)
|
||||
val callback = IKeystoreExportKeyCallback.Stub.asInterface(data.readStrongBinder())
|
||||
val alias = data.readString()!!.extractAlias()
|
||||
Logger.i("exportKeyTransaction uid $callingUid alias $alias")
|
||||
val kp = CertificateHacker.generateKeyPair(keyArguments[Key(callingUid, alias)]!!)
|
||||
keyPairs[Key(callingUid, alias)] = kp!!
|
||||
|
||||
val erP = Parcel.obtain()
|
||||
erP.writeInt(KeyStore.NO_ERROR)
|
||||
erP.writeByteArray(kp.public.encoded)
|
||||
erP.setDataPosition(0)
|
||||
val er = ExportResult.CREATOR.createFromParcel(erP)
|
||||
erP.recycle()
|
||||
|
||||
callback.onFinished(er)
|
||||
|
||||
return createSuccessReply()
|
||||
}.onFailure {
|
||||
Logger.e("exportKeyTransaction error", it)
|
||||
}
|
||||
}
|
||||
|
||||
attestKeyTransaction -> {
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(DESCRIPTOR)
|
||||
val callback = IKeystoreCertificateChainCallback.Stub.asInterface(data.readStrongBinder())
|
||||
val alias = data.readString()!!.extractAlias()
|
||||
Logger.i("attestKeyTransaction uid $callingUid alias $alias")
|
||||
val check = data.readInt()
|
||||
val kma = KeymasterArguments()
|
||||
if (check == 1) {
|
||||
kma.readFromParcel(data)
|
||||
val attestationChallenge = kma.getBytes(KeymasterDefs.KM_TAG_ATTESTATION_CHALLENGE, ByteArray(0))
|
||||
|
||||
val ksr = createSuccessKeystoreResponse()
|
||||
|
||||
val key = Key(callingUid, alias)
|
||||
val ka = keyArguments[key]!!
|
||||
ka.attestationChallenge = attestationChallenge
|
||||
val chain = CertificateHacker.generateChain(callingUid, ka, keyPairs[key]!!)
|
||||
|
||||
val kcc = KeymasterCertificateChain(chain)
|
||||
callback.onFinished(ksr, kcc)
|
||||
}
|
||||
|
||||
return createSuccessReply()
|
||||
}.onFailure {
|
||||
Logger.e("attestKeyTransaction error", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
|
||||
override fun onPostTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel,
|
||||
reply: Parcel?,
|
||||
resultCode: Int
|
||||
): Result {
|
||||
if (target != keystore || code != getTransaction || reply == null) return Skip
|
||||
if (reply.hasException()) return Skip
|
||||
val p = Parcel.obtain()
|
||||
Logger.d("intercept post $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()} replySz=${reply.dataSize()}")
|
||||
try {
|
||||
data.enforceInterface(DESCRIPTOR)
|
||||
val alias = data.readString() ?: ""
|
||||
var response = reply.createByteArray()
|
||||
when {
|
||||
alias.startsWith(Credentials.USER_CERTIFICATE) -> {
|
||||
response = CertificateHacker.hackCertificateChainUSR(response!!, alias.extractAlias(), callingUid)
|
||||
Logger.i("Hacked leaf certificate for uid=$callingUid")
|
||||
return createByteArrayReply(response)
|
||||
}
|
||||
alias.startsWith(Credentials.CA_CERTIFICATE) -> {
|
||||
response = CertificateHacker.hackCertificateChainCA(response!!, alias.extractAlias(), callingUid)
|
||||
Logger.i("Hacked CA certificate chain for uid=$callingUid")
|
||||
return createByteArrayReply(response)
|
||||
}
|
||||
else -> p.recycle()
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("failed to hack certificate chain of uid=$callingUid pid=$callingPid!", t)
|
||||
p.recycle()
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.interceptors
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.security.Credentials
|
||||
import android.security.KeyStore
|
||||
import android.security.keymaster.ExportResult
|
||||
import android.security.keymaster.KeyCharacteristics
|
||||
import android.security.keymaster.KeymasterArguments
|
||||
import android.security.keymaster.KeymasterCertificateChain
|
||||
import android.security.keymaster.KeymasterDefs
|
||||
import android.security.keystore.IKeystoreCertificateChainCallback
|
||||
import android.security.keystore.IKeystoreExportKeyCallback
|
||||
import android.security.keystore.IKeystoreKeyCharacteristicsCallback
|
||||
import android.security.keystore.IKeystoreService
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHacker
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.getTransactCode
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createByteArrayReply
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createSuccessKeystoreResponse
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createSuccessReply
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.extractAlias
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.hasException
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyPair
|
||||
import java.util.Date
|
||||
|
||||
@SuppressLint("BlockedPrivateApi")
|
||||
object KeystoreInterceptor : BaseKeystoreInterceptor() {
|
||||
private val getTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "get")
|
||||
private val generateKeyTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "generateKey")
|
||||
private val getKeyCharacteristicsTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "getKeyCharacteristics")
|
||||
private val exportKeyTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "exportKey")
|
||||
private val attestKeyTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "attestKey")
|
||||
|
||||
override val serviceName = "android.security.keystore"
|
||||
override val processName = "keystore"
|
||||
override val injectionCommand = "exec ./inject `pidof keystore` libTrickyStoreOSS.so entry"
|
||||
|
||||
private const val DESCRIPTOR = "android.security.keystore.IKeystoreService"
|
||||
|
||||
private val keyArguments = HashMap<Key, CertificateHacker.KeyGenParameters>()
|
||||
private val keyPairs = HashMap<Key, KeyPair>()
|
||||
|
||||
data class Key(val uid: Int, val alias: String)
|
||||
|
||||
override fun onPreTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel
|
||||
): Result {
|
||||
if (CertificateHacker.canHack()) {
|
||||
if (code == getTransaction) {
|
||||
if (Config.needHack(callingUid)) {
|
||||
return Continue
|
||||
} else if (Config.needGenerate(callingUid)) {
|
||||
return Skip
|
||||
}
|
||||
} else if (Config.needGenerate(callingUid)) {
|
||||
when (code) {
|
||||
generateKeyTransaction -> {
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(DESCRIPTOR)
|
||||
val callback = IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
|
||||
val alias = data.readString()!!.extractAlias()
|
||||
Logger.i("generateKeyTransaction uid $callingUid alias $alias")
|
||||
val check = data.readInt()
|
||||
val kma = KeymasterArguments()
|
||||
val kgp = CertificateHacker.KeyGenParameters()
|
||||
if (check == 1) {
|
||||
kma.readFromParcel(data)
|
||||
kgp.algorithm = kma.getEnum(KeymasterDefs.KM_TAG_ALGORITHM, 0)
|
||||
kgp.keySize = kma.getUnsignedInt(KeymasterDefs.KM_TAG_KEY_SIZE, 0).toInt()
|
||||
kgp.setEcCurveName(kgp.keySize)
|
||||
kgp.purpose = kma.getEnums(KeymasterDefs.KM_TAG_PURPOSE)
|
||||
kgp.digest = kma.getEnums(KeymasterDefs.KM_TAG_DIGEST)
|
||||
kgp.certificateNotBefore = kma.getDate(KeymasterDefs.KM_TAG_ACTIVE_DATETIME, Date())
|
||||
if (kgp.algorithm == KeymasterDefs.KM_ALGORITHM_RSA) {
|
||||
try {
|
||||
val getArgumentByTag = KeymasterArguments::class.java.getDeclaredMethods().first { it.name == "getArgumentByTag" }
|
||||
getArgumentByTag.isAccessible = true
|
||||
val rsaArgument = getArgumentByTag.invoke(kma, KeymasterDefs.KM_TAG_RSA_PUBLIC_EXPONENT)
|
||||
|
||||
val getLongTagValue = KeymasterArguments::class.java.getDeclaredMethods().first { it.name == "getLongTagValue" }
|
||||
getLongTagValue.isAccessible = true
|
||||
kgp.rsaPublicExponent = getLongTagValue.invoke(kma, rsaArgument) as BigInteger
|
||||
} catch (ex: Exception) {
|
||||
Logger.e("Read rsaPublicExponent error", ex)
|
||||
}
|
||||
}
|
||||
keyArguments[Key(callingUid, alias)] = kgp
|
||||
}
|
||||
|
||||
val kc = KeyCharacteristics()
|
||||
kc.swEnforced = KeymasterArguments()
|
||||
kc.hwEnforced = kma
|
||||
|
||||
val ksr = createSuccessKeystoreResponse()
|
||||
callback.onFinished(ksr, kc)
|
||||
|
||||
return createSuccessReply()
|
||||
}.onFailure {
|
||||
Logger.e("generateKeyTransaction error", it)
|
||||
}
|
||||
}
|
||||
|
||||
getKeyCharacteristicsTransaction -> {
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(DESCRIPTOR)
|
||||
val callback = IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
|
||||
val alias = data.readString()!!.extractAlias()
|
||||
Logger.i("getKeyCharacteristicsTransaction uid $callingUid alias $alias")
|
||||
val kc = KeyCharacteristics()
|
||||
val kma = KeymasterArguments()
|
||||
kma.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, keyArguments[Key(callingUid, alias)]!!.algorithm)
|
||||
kc.swEnforced = KeymasterArguments()
|
||||
kc.hwEnforced = kma
|
||||
|
||||
val ksr = createSuccessKeystoreResponse()
|
||||
callback.onFinished(ksr, kc)
|
||||
|
||||
return createSuccessReply()
|
||||
}.onFailure {
|
||||
Logger.e("getKeyCharacteristicsTransaction error", it)
|
||||
}
|
||||
}
|
||||
|
||||
exportKeyTransaction -> {
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(DESCRIPTOR)
|
||||
val callback = IKeystoreExportKeyCallback.Stub.asInterface(data.readStrongBinder())
|
||||
val alias = data.readString()!!.extractAlias()
|
||||
Logger.i("exportKeyTransaction uid $callingUid alias $alias")
|
||||
val kp = CertificateHacker.generateKeyPair(keyArguments[Key(callingUid, alias)]!!)
|
||||
keyPairs[Key(callingUid, alias)] = kp!!
|
||||
|
||||
val erP = Parcel.obtain()
|
||||
erP.writeInt(KeyStore.NO_ERROR)
|
||||
erP.writeByteArray(kp.public.encoded)
|
||||
erP.setDataPosition(0)
|
||||
val er = ExportResult.CREATOR.createFromParcel(erP)
|
||||
erP.recycle()
|
||||
|
||||
callback.onFinished(er)
|
||||
|
||||
return createSuccessReply()
|
||||
}.onFailure {
|
||||
Logger.e("exportKeyTransaction error", it)
|
||||
}
|
||||
}
|
||||
|
||||
attestKeyTransaction -> {
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(DESCRIPTOR)
|
||||
val callback = IKeystoreCertificateChainCallback.Stub.asInterface(data.readStrongBinder())
|
||||
val alias = data.readString()!!.extractAlias()
|
||||
Logger.i("attestKeyTransaction uid $callingUid alias $alias")
|
||||
val check = data.readInt()
|
||||
val kma = KeymasterArguments()
|
||||
if (check == 1) {
|
||||
kma.readFromParcel(data)
|
||||
val attestationChallenge = kma.getBytes(KeymasterDefs.KM_TAG_ATTESTATION_CHALLENGE, ByteArray(0))
|
||||
|
||||
val ksr = createSuccessKeystoreResponse()
|
||||
|
||||
val key = Key(callingUid, alias)
|
||||
val ka = keyArguments[key]!!
|
||||
ka.attestationChallenge = attestationChallenge
|
||||
val chain = CertificateHacker.generateChain(callingUid, ka, keyPairs[key]!!)
|
||||
|
||||
val kcc = KeymasterCertificateChain(chain)
|
||||
callback.onFinished(ksr, kcc)
|
||||
}
|
||||
|
||||
return createSuccessReply()
|
||||
}.onFailure {
|
||||
Logger.e("attestKeyTransaction error", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
|
||||
override fun onPostTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel,
|
||||
reply: Parcel?,
|
||||
resultCode: Int
|
||||
): Result {
|
||||
if (target != keystore || code != getTransaction || reply == null) return Skip
|
||||
if (reply.hasException()) return Skip
|
||||
val p = Parcel.obtain()
|
||||
Logger.d("intercept post $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()} replySz=${reply.dataSize()}")
|
||||
try {
|
||||
data.enforceInterface(DESCRIPTOR)
|
||||
val alias = data.readString() ?: ""
|
||||
var response = reply.createByteArray()
|
||||
when {
|
||||
alias.startsWith(Credentials.USER_CERTIFICATE) -> {
|
||||
response = CertificateHacker.hackCertificateChainUSR(response!!, alias.extractAlias(), callingUid)
|
||||
Logger.i("Hacked leaf certificate for uid=$callingUid")
|
||||
return createByteArrayReply(response)
|
||||
}
|
||||
alias.startsWith(Credentials.CA_CERTIFICATE) -> {
|
||||
response = CertificateHacker.hackCertificateChainCA(response!!, alias.extractAlias(), callingUid)
|
||||
Logger.i("Hacked CA certificate chain for uid=$callingUid")
|
||||
return createByteArrayReply(response)
|
||||
}
|
||||
else -> p.recycle()
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("failed to hack certificate chain of uid=$callingUid pid=$callingPid!", t)
|
||||
p.recycle()
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
}
|
||||
+179
-179
@@ -1,180 +1,180 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.interceptors
|
||||
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.KeyParameterValue
|
||||
import android.hardware.security.keymint.Tag
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.system.keystore2.Authorization
|
||||
import android.system.keystore2.IKeystoreSecurityLevel
|
||||
import android.system.keystore2.KeyDescriptor
|
||||
import android.system.keystore2.KeyEntryResponse
|
||||
import android.system.keystore2.KeyMetadata
|
||||
import androidx.annotation.Keep
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHacker
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.getTransactCode
|
||||
import io.github.beakthoven.TrickyStoreOSS.putCertificateChain
|
||||
import java.security.KeyPair
|
||||
import java.security.cert.Certificate
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class SecurityLevelInterceptor(
|
||||
private val original: IKeystoreSecurityLevel,
|
||||
private val level: Int
|
||||
) : BinderInterceptor() {
|
||||
companion object {
|
||||
private val generateKeyTransaction =
|
||||
getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
|
||||
private val deleteKeyTransaction =
|
||||
getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "deleteKey")
|
||||
private val createOperationTransaction =
|
||||
getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "createOperation")
|
||||
|
||||
@Keep
|
||||
val keys = ConcurrentHashMap<Key, Info>()
|
||||
|
||||
@Keep
|
||||
val keyPairs = ConcurrentHashMap<Key, Pair<KeyPair, List<Certificate>>>()
|
||||
|
||||
@Keep
|
||||
val skipLeafHacks = ConcurrentHashMap<Key, Boolean>()
|
||||
|
||||
@Keep
|
||||
fun getKeyResponse(uid: Int, alias: String): KeyEntryResponse? =
|
||||
keys[Key(uid, alias)]?.response
|
||||
|
||||
@Keep
|
||||
fun getKeyPairs(uid: Int, alias: String): Pair<KeyPair, List<Certificate>>? =
|
||||
keyPairs[Key(uid, alias)]
|
||||
|
||||
@Keep
|
||||
fun shouldSkipLeafHack(uid: Int, alias: String): Boolean =
|
||||
skipLeafHacks[Key(uid, alias)] ?: false
|
||||
}
|
||||
|
||||
data class Key(val uid: Int, val alias: String)
|
||||
data class Info(val keyPair: KeyPair, val response: KeyEntryResponse)
|
||||
|
||||
override fun onPreTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel
|
||||
): Result {
|
||||
if (code == generateKeyTransaction) {
|
||||
Logger.i("intercept key gen uid=$callingUid pid=$callingPid")
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||
val keyDescriptor =
|
||||
data.readTypedObject(KeyDescriptor.CREATOR) ?: return@runCatching
|
||||
val attestationKeyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
||||
val aFlags = data.readInt()
|
||||
val entropy = data.createByteArray()
|
||||
val kgp = CertificateHacker.KeyGenParameters(params)
|
||||
if (Config.needGenerate(callingUid)) {
|
||||
val pair = CertificateHacker.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp)
|
||||
?: return@runCatching
|
||||
keyPairs[Key(callingUid, keyDescriptor.alias)] = Pair(pair.first, pair.second)
|
||||
val response = buildResponse(pair.second, kgp, attestationKeyDescriptor ?: keyDescriptor)
|
||||
keys[Key(callingUid, keyDescriptor.alias)] = Info(pair.first, response)
|
||||
val p = Parcel.obtain()
|
||||
p.writeNoException()
|
||||
p.writeTypedObject(response.metadata, 0)
|
||||
return OverrideReply(0, p)
|
||||
} else if (Config.needHack(callingUid)) {
|
||||
if ((kgp.purpose.contains(7)) || (attestationKeyDescriptor != null)) {
|
||||
Logger.i("Generating key in generation mode for attestation: uid=$callingUid alias=${keyDescriptor.alias}")
|
||||
val pair = CertificateHacker.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp)
|
||||
?: return@runCatching
|
||||
keyPairs[Key(callingUid, keyDescriptor.alias)] = Pair(pair.first, pair.second)
|
||||
val response = buildResponse(pair.second, kgp, attestationKeyDescriptor ?: keyDescriptor)
|
||||
keys[Key(callingUid, keyDescriptor.alias)] = Info(pair.first, response)
|
||||
SecurityLevelInterceptor.skipLeafHacks[Key(callingUid, keyDescriptor.alias)] = true
|
||||
val p = Parcel.obtain()
|
||||
p.writeNoException()
|
||||
p.writeTypedObject(response.metadata, 0)
|
||||
return OverrideReply(0, p)
|
||||
} else {
|
||||
skipLeafHacks.remove(Key(callingUid, keyDescriptor.alias))
|
||||
Logger.i("Cleared skip flag for non-attestation key: uid=$callingUid alias=${keyDescriptor.alias}")
|
||||
return Skip
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
Logger.e("parse key gen request", it)
|
||||
}
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
|
||||
private fun buildResponse(
|
||||
chain: List<Certificate>,
|
||||
params: CertificateHacker.KeyGenParameters,
|
||||
descriptor: KeyDescriptor
|
||||
): KeyEntryResponse {
|
||||
val response = KeyEntryResponse()
|
||||
val metadata = KeyMetadata()
|
||||
metadata.keySecurityLevel = level
|
||||
metadata.putCertificateChain(chain.toTypedArray()).getOrThrow()
|
||||
val d = KeyDescriptor()
|
||||
d.domain = descriptor.domain
|
||||
d.nspace = descriptor.nspace
|
||||
metadata.key = d
|
||||
val authorizations = ArrayList<Authorization>()
|
||||
var a: Authorization
|
||||
for (i in params.purpose.toList()) {
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.PURPOSE
|
||||
a.keyParameter.value = KeyParameterValue.keyPurpose(i)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
}
|
||||
for (i in params.digest.toList()) {
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.DIGEST
|
||||
a.keyParameter.value = KeyParameterValue.digest(i)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
}
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.ALGORITHM
|
||||
a.keyParameter.value = KeyParameterValue.algorithm(params.algorithm)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.KEY_SIZE
|
||||
a.keyParameter.value = KeyParameterValue.integer(params.keySize)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.EC_CURVE
|
||||
a.keyParameter.value = KeyParameterValue.ecCurve(params.ecCurve)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.NO_AUTH_REQUIRED
|
||||
a.keyParameter.value = KeyParameterValue.boolValue(true)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
metadata.authorizations = authorizations.toTypedArray<Authorization>()
|
||||
response.metadata = metadata
|
||||
response.iSecurityLevel = original
|
||||
return response
|
||||
}
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.interceptors
|
||||
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.KeyParameterValue
|
||||
import android.hardware.security.keymint.Tag
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.system.keystore2.Authorization
|
||||
import android.system.keystore2.IKeystoreSecurityLevel
|
||||
import android.system.keystore2.KeyDescriptor
|
||||
import android.system.keystore2.KeyEntryResponse
|
||||
import android.system.keystore2.KeyMetadata
|
||||
import androidx.annotation.Keep
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHacker
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.getTransactCode
|
||||
import io.github.beakthoven.TrickyStoreOSS.putCertificateChain
|
||||
import java.security.KeyPair
|
||||
import java.security.cert.Certificate
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class SecurityLevelInterceptor(
|
||||
private val original: IKeystoreSecurityLevel,
|
||||
private val level: Int
|
||||
) : BinderInterceptor() {
|
||||
companion object {
|
||||
private val generateKeyTransaction =
|
||||
getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
|
||||
private val deleteKeyTransaction =
|
||||
getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "deleteKey")
|
||||
private val createOperationTransaction =
|
||||
getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "createOperation")
|
||||
|
||||
@Keep
|
||||
val keys = ConcurrentHashMap<Key, Info>()
|
||||
|
||||
@Keep
|
||||
val keyPairs = ConcurrentHashMap<Key, Pair<KeyPair, List<Certificate>>>()
|
||||
|
||||
@Keep
|
||||
val skipLeafHacks = ConcurrentHashMap<Key, Boolean>()
|
||||
|
||||
@Keep
|
||||
fun getKeyResponse(uid: Int, alias: String): KeyEntryResponse? =
|
||||
keys[Key(uid, alias)]?.response
|
||||
|
||||
@Keep
|
||||
fun getKeyPairs(uid: Int, alias: String): Pair<KeyPair, List<Certificate>>? =
|
||||
keyPairs[Key(uid, alias)]
|
||||
|
||||
@Keep
|
||||
fun shouldSkipLeafHack(uid: Int, alias: String): Boolean =
|
||||
skipLeafHacks[Key(uid, alias)] ?: false
|
||||
}
|
||||
|
||||
data class Key(val uid: Int, val alias: String)
|
||||
data class Info(val keyPair: KeyPair, val response: KeyEntryResponse)
|
||||
|
||||
override fun onPreTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel
|
||||
): Result {
|
||||
if (code == generateKeyTransaction) {
|
||||
Logger.i("intercept key gen uid=$callingUid pid=$callingPid")
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||
val keyDescriptor =
|
||||
data.readTypedObject(KeyDescriptor.CREATOR) ?: return@runCatching
|
||||
val attestationKeyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
||||
val aFlags = data.readInt()
|
||||
val entropy = data.createByteArray()
|
||||
val kgp = CertificateHacker.KeyGenParameters(params)
|
||||
if (Config.needGenerate(callingUid)) {
|
||||
val pair = CertificateHacker.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp)
|
||||
?: return@runCatching
|
||||
keyPairs[Key(callingUid, keyDescriptor.alias)] = Pair(pair.first, pair.second)
|
||||
val response = buildResponse(pair.second, kgp, attestationKeyDescriptor ?: keyDescriptor)
|
||||
keys[Key(callingUid, keyDescriptor.alias)] = Info(pair.first, response)
|
||||
val p = Parcel.obtain()
|
||||
p.writeNoException()
|
||||
p.writeTypedObject(response.metadata, 0)
|
||||
return OverrideReply(0, p)
|
||||
} else if (Config.needHack(callingUid)) {
|
||||
if ((kgp.purpose.contains(7)) || (attestationKeyDescriptor != null)) {
|
||||
Logger.i("Generating key in generation mode for attestation: uid=$callingUid alias=${keyDescriptor.alias}")
|
||||
val pair = CertificateHacker.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp)
|
||||
?: return@runCatching
|
||||
keyPairs[Key(callingUid, keyDescriptor.alias)] = Pair(pair.first, pair.second)
|
||||
val response = buildResponse(pair.second, kgp, attestationKeyDescriptor ?: keyDescriptor)
|
||||
keys[Key(callingUid, keyDescriptor.alias)] = Info(pair.first, response)
|
||||
SecurityLevelInterceptor.skipLeafHacks[Key(callingUid, keyDescriptor.alias)] = true
|
||||
val p = Parcel.obtain()
|
||||
p.writeNoException()
|
||||
p.writeTypedObject(response.metadata, 0)
|
||||
return OverrideReply(0, p)
|
||||
} else {
|
||||
skipLeafHacks.remove(Key(callingUid, keyDescriptor.alias))
|
||||
Logger.i("Cleared skip flag for non-attestation key: uid=$callingUid alias=${keyDescriptor.alias}")
|
||||
return Skip
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
Logger.e("parse key gen request", it)
|
||||
}
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
|
||||
private fun buildResponse(
|
||||
chain: List<Certificate>,
|
||||
params: CertificateHacker.KeyGenParameters,
|
||||
descriptor: KeyDescriptor
|
||||
): KeyEntryResponse {
|
||||
val response = KeyEntryResponse()
|
||||
val metadata = KeyMetadata()
|
||||
metadata.keySecurityLevel = level
|
||||
metadata.putCertificateChain(chain.toTypedArray()).getOrThrow()
|
||||
val d = KeyDescriptor()
|
||||
d.domain = descriptor.domain
|
||||
d.nspace = descriptor.nspace
|
||||
metadata.key = d
|
||||
val authorizations = ArrayList<Authorization>()
|
||||
var a: Authorization
|
||||
for (i in params.purpose.toList()) {
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.PURPOSE
|
||||
a.keyParameter.value = KeyParameterValue.keyPurpose(i)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
}
|
||||
for (i in params.digest.toList()) {
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.DIGEST
|
||||
a.keyParameter.value = KeyParameterValue.digest(i)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
}
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.ALGORITHM
|
||||
a.keyParameter.value = KeyParameterValue.algorithm(params.algorithm)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.KEY_SIZE
|
||||
a.keyParameter.value = KeyParameterValue.integer(params.keySize)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.EC_CURVE
|
||||
a.keyParameter.value = KeyParameterValue.ecCurve(params.ecCurve)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.NO_AUTH_REQUIRED
|
||||
a.keyParameter.value = KeyParameterValue.boolValue(true)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
metadata.authorizations = authorizations.toTypedArray<Authorization>()
|
||||
response.metadata = metadata
|
||||
response.iSecurityLevel = original
|
||||
return response
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
#Mon Aug 04 09:32:06 IST 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
#Mon Aug 04 09:32:06 IST 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
Vendored
+89
-89
@@ -1,89 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
||||
+114
-114
@@ -1,114 +1,114 @@
|
||||
<?xml version="1.0"?>
|
||||
<AndroidAttestation>
|
||||
<NumberOfKeyboxes>1</NumberOfKeyboxes>
|
||||
<Keybox DeviceID="sw">
|
||||
<Key algorithm="ecdsa">
|
||||
<PrivateKey format="pem">
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEICHghkMqFRmEWc82OlD8FMnarfk19SfC39ceTW28QuVEoAoGCCqGSM49
|
||||
AwEHoUQDQgAE6555+EJjWazLKpFMiYbMcK2QZpOCqXMmE/6sy/ghJ0whdJdKKv6l
|
||||
uU1/ZtTgZRBmNbxTt6CjpnFYPts+Ea4QFA==
|
||||
-----END EC PRIVATE KEY-----
|
||||
</PrivateKey>
|
||||
<CertificateChain>
|
||||
<NumberOfCertificates>2</NumberOfCertificates>
|
||||
<Certificate format="pem">
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICeDCCAh6gAwIBAgICEAEwCgYIKoZIzj0EAwIwgZgxCzAJBgNVBAYTAlVTMRMw
|
||||
EQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBWaWV3MRUwEwYD
|
||||
VQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJvaWQxMzAxBgNVBAMMKkFu
|
||||
ZHJvaWQgS2V5c3RvcmUgU29mdHdhcmUgQXR0ZXN0YXRpb24gUm9vdDAeFw0xNjAx
|
||||
MTEwMDQ2MDlaFw0yNjAxMDgwMDQ2MDlaMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE
|
||||
CAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdB
|
||||
bmRyb2lkMTswOQYDVQQDDDJBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVz
|
||||
dGF0aW9uIEludGVybWVkaWF0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOue
|
||||
efhCY1msyyqRTImGzHCtkGaTgqlzJhP+rMv4ISdMIXSXSir+pblNf2bU4GUQZjW8
|
||||
U7ego6ZxWD7bPhGuEBSjZjBkMB0GA1UdDgQWBBQ//KzWGrE6noEguNUlHMVlux6R
|
||||
qTAfBgNVHSMEGDAWgBTIrel3TEXDo88NFhDkeUM6IVowzzASBgNVHRMBAf8ECDAG
|
||||
AQH/AgEAMA4GA1UdDwEB/wQEAwIChDAKBggqhkjOPQQDAgNIADBFAiBLipt77oK8
|
||||
wDOHri/AiZi03cONqycqRZ9pDMfDktQPjgIhAO7aAV229DLp1IQ7YkyUBO86fMy9
|
||||
Xvsiu+f+uXc/WT/7
|
||||
-----END CERTIFICATE-----
|
||||
</Certificate>
|
||||
<Certificate format="pem">
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICizCCAjKgAwIBAgIJAKIFntEOQ1tXMAoGCCqGSM49BAMCMIGYMQswCQYDVQQG
|
||||
EwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmll
|
||||
dzEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMTMwMQYD
|
||||
VQQDDCpBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIFJvb3Qw
|
||||
HhcNMTYwMTExMDA0MzUwWhcNMzYwMTA2MDA0MzUwWjCBmDELMAkGA1UEBhMCVVMx
|
||||
EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTAT
|
||||
BgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDEzMDEGA1UEAwwq
|
||||
QW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBSb290MFkwEwYH
|
||||
KoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamguD/9/SQ59
|
||||
dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpKNjMGEwHQYDVR0O
|
||||
BBYEFMit6XdMRcOjzw0WEOR5QzohWjDPMB8GA1UdIwQYMBaAFMit6XdMRcOjzw0W
|
||||
EOR5QzohWjDPMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKEMAoGCCqG
|
||||
SM49BAMCA0cAMEQCIDUho++LNEYenNVg8x1YiSBq3KNlQfYNns6KGYxmSGB7AiBN
|
||||
C/NR2TB8fVvaNTQdqEcbY6WFZTytTySn502vQX3xvw==
|
||||
-----END CERTIFICATE-----
|
||||
</Certificate>
|
||||
</CertificateChain>
|
||||
</Key>
|
||||
<Key algorithm="rsa">
|
||||
<PrivateKey format="pem">
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXQIBAAKBgQDAgyPcVogbuDAgafWwhWHG7r5/BeL1qEIEir6LR752/q7yXPKb
|
||||
KvoyABQWAUKZiaFfz8aBXrNjWDwv0vIL5Jgyg92BSxbX4YVBeuVKvClqOm21wAQI
|
||||
O2jFVsHwIzmRZBmGTVC3TUCuykhMdzVsiVoMJ1q/rEmdXX0jYvKcXgLocQIDAQAB
|
||||
AoGBAL6GCwuZqAKm+xpZQ4p7txUGWwmjbcbpysxr88AsNNfXnpTGYGQo2Ix7f2V3
|
||||
wc3qZAdKvo5yht8fCBHclygmCGjeldMu/Ja20IT/JxpfYN78xwPno45uKbqaPF/C
|
||||
woB2tqiWrx0014gozpvdsfNPnJQEQweBKY4gExZyW728mTpBAkEA4cbZJ2RsCRbs
|
||||
NoJtWUmDdAwh8bB0xKGlmGfGaXlchdPcRkxbkp6Uv7NODcxQFLEPEzQat/3V9gQU
|
||||
0qMmytQcxQJBANpIWZd4XNVjD7D9jFJU+Y5TjhiYOq6ea35qWntdNDdVuSGOvUAy
|
||||
DSg4fXifdvohi8wti2il9kGPu+ylF5qzr70CQFD+/DJklVlhbtZTThVFCTKdk6PY
|
||||
ENvlvbmCKSz3i9i624Agro1X9LcdBThv/p6dsnHKNHejSZnbdvjl7OnA1J0CQBW3
|
||||
TPJ8zv+Ls2vwTZ2DRrCaL3DS9EObDyasfgP36dH3fUuRX9KbKCPwOstdUgDghX/y
|
||||
qAPpPu6W1iNc6VRCvCECQQCQp0XaiXCyzWSWYDJCKMX4KFb/1mW6moXI1g8bi+5x
|
||||
fs0scurgHa2GunZU1M9FrbXx8rMdn4Eiz6XxpVcPmy0l
|
||||
-----END RSA PRIVATE KEY-----
|
||||
</PrivateKey>
|
||||
<CertificateChain>
|
||||
<NumberOfCertificates>2</NumberOfCertificates>
|
||||
<Certificate format="pem">
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICtjCCAh+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwYzELMAkGA1UEBhMCVVMx
|
||||
EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTAT
|
||||
BgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDAeFw0xNjAxMDQx
|
||||
MjQwNTNaFw0zNTEyMzAxMjQwNTNaMHYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApD
|
||||
YWxpZm9ybmlhMRUwEwYDVQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJv
|
||||
aWQxKTAnBgNVBAMMIEFuZHJvaWQgU29mdHdhcmUgQXR0ZXN0YXRpb24gS2V5MIGf
|
||||
MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDAgyPcVogbuDAgafWwhWHG7r5/BeL1
|
||||
qEIEir6LR752/q7yXPKbKvoyABQWAUKZiaFfz8aBXrNjWDwv0vIL5Jgyg92BSxbX
|
||||
4YVBeuVKvClqOm21wAQIO2jFVsHwIzmRZBmGTVC3TUCuykhMdzVsiVoMJ1q/rEmd
|
||||
XX0jYvKcXgLocQIDAQABo2YwZDAdBgNVHQ4EFgQU1AwQG/jNY7n3OVK1DhNcpteZ
|
||||
k4YwHwYDVR0jBBgwFoAUKfrxrMxN0kyWQCd1trDpMuUH/i4wEgYDVR0TAQH/BAgw
|
||||
BgEB/wIBADAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADgYEAni1IX4xn
|
||||
M9waha2Z11Aj6hTsQ7DhnerCI0YecrUZ3GAi5KVoMWwLVcTmnKItnzpPk2sxixZ4
|
||||
Fg2Iy9mLzICdhPDCJ+NrOPH90ecXcjFZNX2W88V/q52PlmEmT7K+gbsNSQQiis6f
|
||||
9/VCLiVE+iEHElqDtVWtGIL4QBSbnCBjBH8=
|
||||
-----END CERTIFICATE-----
|
||||
</Certificate>
|
||||
<Certificate format="pem">
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICpzCCAhCgAwIBAgIJAP+U2d2fB8gMMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNV
|
||||
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW
|
||||
aWV3MRUwEwYDVQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJvaWQwHhcN
|
||||
MTYwMTA0MTIzMTA4WhcNMzUxMjMwMTIzMTA4WjBjMQswCQYDVQQGEwJVUzETMBEG
|
||||
A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEVMBMGA1UE
|
||||
CgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMIGfMA0GCSqGSIb3DQEB
|
||||
AQUAA4GNADCBiQKBgQCia63rbi5EYe/VDoLmt5TRdSMfd5tjkWP/96r/C3JHTsAs
|
||||
Q+wzfNes7UA+jCigZtX3hwszl94OuE4TQKuvpSe/lWmgMdsGUmX4RFlXYfC78hdL
|
||||
t0GAZMAoDo9Sd47b0ke2RekZyOmLw9vCkT/X11DEHTVm+Vfkl5YLCazOkjWFmwID
|
||||
AQABo2MwYTAdBgNVHQ4EFgQUKfrxrMxN0kyWQCd1trDpMuUH/i4wHwYDVR0jBBgw
|
||||
FoAUKfrxrMxN0kyWQCd1trDpMuUH/i4wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B
|
||||
Af8EBAMCAoQwDQYJKoZIhvcNAQELBQADgYEAT3LzNlmNDsG5dFsxWfbwjSVJMJ6j
|
||||
HBwp0kUtILlNX2S06IDHeHqcOd6os/W/L3BfRxBcxebrTQaZYdKumgf/93y4q+uc
|
||||
DyQHXrF/unlx/U1bnt8Uqf7f7XzAiF343ZtkMlbVNZriE/mPzsF83O+kqrJVw4Op
|
||||
Lvtc9mL1J1IXvmM=
|
||||
-----END CERTIFICATE-----
|
||||
</Certificate>
|
||||
</CertificateChain>
|
||||
</Key>
|
||||
</Keybox>
|
||||
</AndroidAttestation>
|
||||
<?xml version="1.0"?>
|
||||
<AndroidAttestation>
|
||||
<NumberOfKeyboxes>1</NumberOfKeyboxes>
|
||||
<Keybox DeviceID="sw">
|
||||
<Key algorithm="ecdsa">
|
||||
<PrivateKey format="pem">
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEICHghkMqFRmEWc82OlD8FMnarfk19SfC39ceTW28QuVEoAoGCCqGSM49
|
||||
AwEHoUQDQgAE6555+EJjWazLKpFMiYbMcK2QZpOCqXMmE/6sy/ghJ0whdJdKKv6l
|
||||
uU1/ZtTgZRBmNbxTt6CjpnFYPts+Ea4QFA==
|
||||
-----END EC PRIVATE KEY-----
|
||||
</PrivateKey>
|
||||
<CertificateChain>
|
||||
<NumberOfCertificates>2</NumberOfCertificates>
|
||||
<Certificate format="pem">
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICeDCCAh6gAwIBAgICEAEwCgYIKoZIzj0EAwIwgZgxCzAJBgNVBAYTAlVTMRMw
|
||||
EQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBWaWV3MRUwEwYD
|
||||
VQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJvaWQxMzAxBgNVBAMMKkFu
|
||||
ZHJvaWQgS2V5c3RvcmUgU29mdHdhcmUgQXR0ZXN0YXRpb24gUm9vdDAeFw0xNjAx
|
||||
MTEwMDQ2MDlaFw0yNjAxMDgwMDQ2MDlaMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE
|
||||
CAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdB
|
||||
bmRyb2lkMTswOQYDVQQDDDJBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVz
|
||||
dGF0aW9uIEludGVybWVkaWF0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOue
|
||||
efhCY1msyyqRTImGzHCtkGaTgqlzJhP+rMv4ISdMIXSXSir+pblNf2bU4GUQZjW8
|
||||
U7ego6ZxWD7bPhGuEBSjZjBkMB0GA1UdDgQWBBQ//KzWGrE6noEguNUlHMVlux6R
|
||||
qTAfBgNVHSMEGDAWgBTIrel3TEXDo88NFhDkeUM6IVowzzASBgNVHRMBAf8ECDAG
|
||||
AQH/AgEAMA4GA1UdDwEB/wQEAwIChDAKBggqhkjOPQQDAgNIADBFAiBLipt77oK8
|
||||
wDOHri/AiZi03cONqycqRZ9pDMfDktQPjgIhAO7aAV229DLp1IQ7YkyUBO86fMy9
|
||||
Xvsiu+f+uXc/WT/7
|
||||
-----END CERTIFICATE-----
|
||||
</Certificate>
|
||||
<Certificate format="pem">
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICizCCAjKgAwIBAgIJAKIFntEOQ1tXMAoGCCqGSM49BAMCMIGYMQswCQYDVQQG
|
||||
EwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmll
|
||||
dzEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMTMwMQYD
|
||||
VQQDDCpBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIFJvb3Qw
|
||||
HhcNMTYwMTExMDA0MzUwWhcNMzYwMTA2MDA0MzUwWjCBmDELMAkGA1UEBhMCVVMx
|
||||
EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTAT
|
||||
BgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDEzMDEGA1UEAwwq
|
||||
QW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBSb290MFkwEwYH
|
||||
KoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamguD/9/SQ59
|
||||
dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpKNjMGEwHQYDVR0O
|
||||
BBYEFMit6XdMRcOjzw0WEOR5QzohWjDPMB8GA1UdIwQYMBaAFMit6XdMRcOjzw0W
|
||||
EOR5QzohWjDPMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKEMAoGCCqG
|
||||
SM49BAMCA0cAMEQCIDUho++LNEYenNVg8x1YiSBq3KNlQfYNns6KGYxmSGB7AiBN
|
||||
C/NR2TB8fVvaNTQdqEcbY6WFZTytTySn502vQX3xvw==
|
||||
-----END CERTIFICATE-----
|
||||
</Certificate>
|
||||
</CertificateChain>
|
||||
</Key>
|
||||
<Key algorithm="rsa">
|
||||
<PrivateKey format="pem">
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXQIBAAKBgQDAgyPcVogbuDAgafWwhWHG7r5/BeL1qEIEir6LR752/q7yXPKb
|
||||
KvoyABQWAUKZiaFfz8aBXrNjWDwv0vIL5Jgyg92BSxbX4YVBeuVKvClqOm21wAQI
|
||||
O2jFVsHwIzmRZBmGTVC3TUCuykhMdzVsiVoMJ1q/rEmdXX0jYvKcXgLocQIDAQAB
|
||||
AoGBAL6GCwuZqAKm+xpZQ4p7txUGWwmjbcbpysxr88AsNNfXnpTGYGQo2Ix7f2V3
|
||||
wc3qZAdKvo5yht8fCBHclygmCGjeldMu/Ja20IT/JxpfYN78xwPno45uKbqaPF/C
|
||||
woB2tqiWrx0014gozpvdsfNPnJQEQweBKY4gExZyW728mTpBAkEA4cbZJ2RsCRbs
|
||||
NoJtWUmDdAwh8bB0xKGlmGfGaXlchdPcRkxbkp6Uv7NODcxQFLEPEzQat/3V9gQU
|
||||
0qMmytQcxQJBANpIWZd4XNVjD7D9jFJU+Y5TjhiYOq6ea35qWntdNDdVuSGOvUAy
|
||||
DSg4fXifdvohi8wti2il9kGPu+ylF5qzr70CQFD+/DJklVlhbtZTThVFCTKdk6PY
|
||||
ENvlvbmCKSz3i9i624Agro1X9LcdBThv/p6dsnHKNHejSZnbdvjl7OnA1J0CQBW3
|
||||
TPJ8zv+Ls2vwTZ2DRrCaL3DS9EObDyasfgP36dH3fUuRX9KbKCPwOstdUgDghX/y
|
||||
qAPpPu6W1iNc6VRCvCECQQCQp0XaiXCyzWSWYDJCKMX4KFb/1mW6moXI1g8bi+5x
|
||||
fs0scurgHa2GunZU1M9FrbXx8rMdn4Eiz6XxpVcPmy0l
|
||||
-----END RSA PRIVATE KEY-----
|
||||
</PrivateKey>
|
||||
<CertificateChain>
|
||||
<NumberOfCertificates>2</NumberOfCertificates>
|
||||
<Certificate format="pem">
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICtjCCAh+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwYzELMAkGA1UEBhMCVVMx
|
||||
EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTAT
|
||||
BgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDAeFw0xNjAxMDQx
|
||||
MjQwNTNaFw0zNTEyMzAxMjQwNTNaMHYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApD
|
||||
YWxpZm9ybmlhMRUwEwYDVQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJv
|
||||
aWQxKTAnBgNVBAMMIEFuZHJvaWQgU29mdHdhcmUgQXR0ZXN0YXRpb24gS2V5MIGf
|
||||
MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDAgyPcVogbuDAgafWwhWHG7r5/BeL1
|
||||
qEIEir6LR752/q7yXPKbKvoyABQWAUKZiaFfz8aBXrNjWDwv0vIL5Jgyg92BSxbX
|
||||
4YVBeuVKvClqOm21wAQIO2jFVsHwIzmRZBmGTVC3TUCuykhMdzVsiVoMJ1q/rEmd
|
||||
XX0jYvKcXgLocQIDAQABo2YwZDAdBgNVHQ4EFgQU1AwQG/jNY7n3OVK1DhNcpteZ
|
||||
k4YwHwYDVR0jBBgwFoAUKfrxrMxN0kyWQCd1trDpMuUH/i4wEgYDVR0TAQH/BAgw
|
||||
BgEB/wIBADAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADgYEAni1IX4xn
|
||||
M9waha2Z11Aj6hTsQ7DhnerCI0YecrUZ3GAi5KVoMWwLVcTmnKItnzpPk2sxixZ4
|
||||
Fg2Iy9mLzICdhPDCJ+NrOPH90ecXcjFZNX2W88V/q52PlmEmT7K+gbsNSQQiis6f
|
||||
9/VCLiVE+iEHElqDtVWtGIL4QBSbnCBjBH8=
|
||||
-----END CERTIFICATE-----
|
||||
</Certificate>
|
||||
<Certificate format="pem">
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICpzCCAhCgAwIBAgIJAP+U2d2fB8gMMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNV
|
||||
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW
|
||||
aWV3MRUwEwYDVQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJvaWQwHhcN
|
||||
MTYwMTA0MTIzMTA4WhcNMzUxMjMwMTIzMTA4WjBjMQswCQYDVQQGEwJVUzETMBEG
|
||||
A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEVMBMGA1UE
|
||||
CgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMIGfMA0GCSqGSIb3DQEB
|
||||
AQUAA4GNADCBiQKBgQCia63rbi5EYe/VDoLmt5TRdSMfd5tjkWP/96r/C3JHTsAs
|
||||
Q+wzfNes7UA+jCigZtX3hwszl94OuE4TQKuvpSe/lWmgMdsGUmX4RFlXYfC78hdL
|
||||
t0GAZMAoDo9Sd47b0ke2RekZyOmLw9vCkT/X11DEHTVm+Vfkl5YLCazOkjWFmwID
|
||||
AQABo2MwYTAdBgNVHQ4EFgQUKfrxrMxN0kyWQCd1trDpMuUH/i4wHwYDVR0jBBgw
|
||||
FoAUKfrxrMxN0kyWQCd1trDpMuUH/i4wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B
|
||||
Af8EBAMCAoQwDQYJKoZIhvcNAQELBQADgYEAT3LzNlmNDsG5dFsxWfbwjSVJMJ6j
|
||||
HBwp0kUtILlNX2S06IDHeHqcOd6os/W/L3BfRxBcxebrTQaZYdKumgf/93y4q+uc
|
||||
DyQHXrF/unlx/U1bnt8Uqf7f7XzAiF343ZtkMlbVNZriE/mPzsF83O+kqrJVw4Op
|
||||
Lvtc9mL1J1IXvmM=
|
||||
-----END CERTIFICATE-----
|
||||
</Certificate>
|
||||
</CertificateChain>
|
||||
</Key>
|
||||
</Keybox>
|
||||
</AndroidAttestation>
|
||||
|
||||
Reference in New Issue
Block a user