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