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[]);
|
||||
}
|
||||
|
||||
-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 dd(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.ServiceManager
|
||||
import android.os.SystemProperties
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.CustomPatchLevel
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.AttestUtils.CachedAttestData
|
||||
import io.github.beakthoven.TrickyStoreOSS.config.CustomPatchLevel
|
||||
import io.github.beakthoven.TrickyStoreOSS.config.PkgConfig
|
||||
import io.github.beakthoven.TrickyStoreOSS.logging.Logger
|
||||
import org.bouncycastle.asn1.ASN1Integer
|
||||
import org.bouncycastle.asn1.DEROctetString
|
||||
import org.bouncycastle.asn1.DERSequence
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
|
||||
fun getTransactCode(clazz: Class<*>, method: String): Int =
|
||||
clazz.getDeclaredField("TRANSACTION_$method").apply { isAccessible = true }
|
||||
.getInt(null)
|
||||
object AndroidUtils {
|
||||
|
||||
// cache attest data to avoid running attestation multiple times
|
||||
private val cachedAttestData: AttestationData? by lazy {
|
||||
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
|
||||
}
|
||||
|
||||
private fun getBootHashFromAttestation(): ByteArray? {
|
||||
return try {
|
||||
cachedAttestData?.verifiedBootHash
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Failed to get boot hash from attestation: ${e.message}")
|
||||
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() = 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
|
||||
val bootKey: ByteArray by lazy {
|
||||
randomBytes()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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)
|
||||
}
|
||||
} 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)
|
||||
@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
|
||||
}
|
||||
|
||||
packages.list
|
||||
.map { it.packageName to it.longVersionCode }
|
||||
.sortedBy { it.first }
|
||||
}.getOrElse {
|
||||
Logger.e("Failed to get APEX package information")
|
||||
emptyList()
|
||||
return if (digest.length == 64) digest.hexToByteArray() else null
|
||||
}
|
||||
}
|
||||
|
||||
val moduleHash: ByteArray by lazy {
|
||||
runCatching {
|
||||
val encodables = apexInfos.flatMap { (packageName, versionCode) ->
|
||||
listOf(
|
||||
DEROctetString(packageName.toByteArray()),
|
||||
ASN1Integer(versionCode)
|
||||
)
|
||||
private fun getBootHashFromAttestation(): ByteArray? {
|
||||
return try {
|
||||
CachedAttestData?.verifiedBootHash
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Failed to get boot hash from attestation: ${e.message}")
|
||||
null
|
||||
}
|
||||
|
||||
val sequence = DERSequence(encodables.toTypedArray())
|
||||
MessageDigest.getInstance("SHA-256").digest(sequence.encoded)
|
||||
}.getOrElse {
|
||||
Logger.e("Failed to compute module hash", it)
|
||||
ByteArray(32)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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(
|
||||
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() }
|
||||
|
||||
@@ -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
|
||||
|
||||
import android.os.Build
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.config.PkgConfig
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.Keystore2Interceptor
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.KeystoreInterceptor
|
||||
import io.github.beakthoven.TrickyStoreOSS.logging.Logger
|
||||
|
||||
private const val RETRY_DELAY_MS = 1000L
|
||||
private const val SERVICE_SLEEP_MS = 1000000L
|
||||
@@ -18,7 +18,7 @@ fun main(args: Array<String>) {
|
||||
Logger.i("Welcome to TrickyStoreOSS!")
|
||||
|
||||
try {
|
||||
setupBootHash()
|
||||
AndroidUtils.setupBootHash()
|
||||
initializeInterceptors()
|
||||
maintainService()
|
||||
} catch (e: Exception) {
|
||||
@@ -35,7 +35,7 @@ private fun initializeInterceptors() {
|
||||
Thread.sleep(RETRY_DELAY_MS)
|
||||
}
|
||||
|
||||
Config.initialize()
|
||||
PkgConfig.initialize()
|
||||
Logger.i("Interceptors initialized successfully")
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,17 @@
|
||||
|
||||
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.XmlPullParserException
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import java.io.IOException
|
||||
import java.io.StringReader
|
||||
import java.security.cert.Certificate
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.core.config
|
||||
package io.github.beakthoven.TrickyStoreOSS.config
|
||||
|
||||
import android.content.pm.IPackageManager
|
||||
import android.os.FileObserver
|
||||
import android.os.IBinder
|
||||
import android.os.IInterface
|
||||
import android.os.ServiceManager
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHacker
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.teeStatus
|
||||
import io.github.beakthoven.TrickyStoreOSS.AttestUtils.TEEStatus
|
||||
import io.github.beakthoven.TrickyStoreOSS.KeyBoxUtils
|
||||
import io.github.beakthoven.TrickyStoreOSS.logging.Logger
|
||||
import java.io.File
|
||||
|
||||
object Config {
|
||||
object PkgConfig {
|
||||
private val hackPackages = mutableSetOf<String>()
|
||||
private val generatePackages = mutableSetOf<String>()
|
||||
private val packageModes = mutableMapOf<String, Mode>()
|
||||
@@ -55,7 +55,7 @@ object Config {
|
||||
}
|
||||
|
||||
private fun updateKeyBox(f: File?) = runCatching {
|
||||
CertificateHacker.readFromXml(f?.readText())
|
||||
KeyBoxUtils.readFromXml(f?.readText())
|
||||
}.onFailure {
|
||||
Logger.e("failed to update keybox", it)
|
||||
}
|
||||
@@ -72,7 +72,7 @@ object Config {
|
||||
|
||||
private fun storeTEEStatus(root: File) {
|
||||
val statusFile = File(root, TEE_STATUS_FILE)
|
||||
teeBroken = !teeStatus
|
||||
teeBroken = !TEEStatus
|
||||
try {
|
||||
statusFile.writeText("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.IBinder
|
||||
import android.os.Parcel
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.logging.Logger
|
||||
|
||||
open class BinderInterceptor : Binder() {
|
||||
|
||||
|
||||
+5
-1
@@ -11,7 +11,7 @@ import android.os.Parcelable
|
||||
import android.os.ServiceManager
|
||||
import android.security.KeyStore
|
||||
import android.security.keystore.KeystoreResponse
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.logging.Logger
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
abstract class BaseKeystoreInterceptor : BinderInterceptor() {
|
||||
@@ -96,6 +96,10 @@ abstract class BaseKeystoreInterceptor : BinderInterceptor() {
|
||||
|
||||
object InterceptorUtils {
|
||||
|
||||
fun getTransactCode(clazz: Class<*>, method: String): Int =
|
||||
clazz.getDeclaredField("TRANSACTION_$method").apply { isAccessible = true }
|
||||
.getInt(null)
|
||||
|
||||
fun createSuccessKeystoreResponse(): KeystoreResponse {
|
||||
val parcel = Parcel.obtain()
|
||||
try {
|
||||
|
||||
+9
-8
@@ -12,13 +12,14 @@ import android.os.Parcel
|
||||
import android.system.keystore2.IKeystoreService
|
||||
import android.system.keystore2.KeyDescriptor
|
||||
import android.system.keystore2.KeyEntryResponse
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHacker
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHack
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateUtils
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.getTransactCode
|
||||
import io.github.beakthoven.TrickyStoreOSS.KeyBoxUtils
|
||||
import io.github.beakthoven.TrickyStoreOSS.config.PkgConfig
|
||||
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.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.putCertificateChain
|
||||
|
||||
@SuppressLint("BlockedPrivateApi")
|
||||
@@ -74,17 +75,17 @@ object Keystore2Interceptor : BaseKeystoreInterceptor() {
|
||||
data: Parcel
|
||||
): Result {
|
||||
if (code == getKeyEntryTransaction) {
|
||||
if (CertificateHacker.hasKeyboxes()) {
|
||||
if (KeyBoxUtils.hasKeyboxes()) {
|
||||
Logger.d("intercept pre $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()}")
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR) ?: return@runCatching
|
||||
if (Config.needGenerate(callingUid)) {
|
||||
if (PkgConfig.needGenerate(callingUid)) {
|
||||
val response = SecurityLevelInterceptor.getKeyResponse(callingUid, descriptor.alias)
|
||||
?: return@runCatching
|
||||
Logger.i("Generate key for uid=$callingUid alias=${descriptor.alias}")
|
||||
return createTypedObjectReply(response)
|
||||
} else if (Config.needHack(callingUid)) {
|
||||
} else if (PkgConfig.needHack(callingUid)) {
|
||||
if (SecurityLevelInterceptor.shouldSkipLeafHack(callingUid, descriptor.alias)) {
|
||||
Logger.i("skip leaf hack for uid=$callingUid alias=${descriptor.alias}")
|
||||
val response = SecurityLevelInterceptor.getKeyResponse(callingUid, descriptor.alias)
|
||||
@@ -138,7 +139,7 @@ object Keystore2Interceptor : BaseKeystoreInterceptor() {
|
||||
if (response != null) {
|
||||
val chain = CertificateUtils.run { response.getCertificateChain() }
|
||||
if (chain != null) {
|
||||
val newChain = CertificateHacker.hackCertificateChain(chain)
|
||||
val newChain = CertificateHack.hackCertificateChain(chain)
|
||||
response.putCertificateChain(newChain).getOrThrow()
|
||||
Logger.i("Hacked certificate for uid=$callingUid")
|
||||
return createTypedObjectReply(response)
|
||||
|
||||
+17
-15
@@ -19,15 +19,17 @@ import android.security.keystore.IKeystoreCertificateChainCallback
|
||||
import android.security.keystore.IKeystoreExportKeyCallback
|
||||
import android.security.keystore.IKeystoreKeyCharacteristicsCallback
|
||||
import android.security.keystore.IKeystoreService
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHacker
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.getTransactCode
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateGen
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHack
|
||||
import io.github.beakthoven.TrickyStoreOSS.KeyBoxUtils
|
||||
import io.github.beakthoven.TrickyStoreOSS.config.PkgConfig
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createByteArrayReply
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createSuccessKeystoreResponse
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createSuccessReply
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.extractAlias
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.getTransactCode
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.hasException
|
||||
import io.github.beakthoven.TrickyStoreOSS.logging.Logger
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyPair
|
||||
import java.util.Date
|
||||
@@ -51,7 +53,7 @@ object KeystoreInterceptor : BaseKeystoreInterceptor() {
|
||||
|
||||
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>()
|
||||
|
||||
data class Key(val uid: Int, val alias: String)
|
||||
@@ -64,14 +66,14 @@ object KeystoreInterceptor : BaseKeystoreInterceptor() {
|
||||
callingPid: Int,
|
||||
data: Parcel
|
||||
): Result {
|
||||
if (CertificateHacker.hasKeyboxes()) {
|
||||
if (KeyBoxUtils.hasKeyboxes()) {
|
||||
if (code == getTransaction) {
|
||||
if (Config.needHack(callingUid)) {
|
||||
if (PkgConfig.needHack(callingUid)) {
|
||||
return Continue
|
||||
} else if (Config.needGenerate(callingUid)) {
|
||||
} else if (PkgConfig.needGenerate(callingUid)) {
|
||||
return Skip
|
||||
}
|
||||
} else if (Config.needGenerate(callingUid)) {
|
||||
} else if (PkgConfig.needGenerate(callingUid)) {
|
||||
when (code) {
|
||||
generateKeyTransaction -> {
|
||||
kotlin.runCatching {
|
||||
@@ -81,12 +83,12 @@ object KeystoreInterceptor : BaseKeystoreInterceptor() {
|
||||
Logger.i("generateKeyTransaction uid $callingUid alias $alias")
|
||||
val check = data.readInt()
|
||||
val kma = KeymasterArguments()
|
||||
val kgp = CertificateHacker.KeyGenParameters()
|
||||
val kgp = CertificateGen.KeyGenParameters()
|
||||
if (check == 1) {
|
||||
kma.readFromParcel(data)
|
||||
kgp.algorithm = kma.getEnum(KeymasterDefs.KM_TAG_ALGORITHM, 0)
|
||||
kgp.keySize = kma.getUnsignedInt(KeymasterDefs.KM_TAG_KEY_SIZE, 0).toInt()
|
||||
kgp.setEcCurveName(kgp.keySize)
|
||||
//kgp.setEcCurveName(kgp.keySize)
|
||||
kgp.purpose = kma.getEnums(KeymasterDefs.KM_TAG_PURPOSE)
|
||||
kgp.digest = kma.getEnums(KeymasterDefs.KM_TAG_DIGEST)
|
||||
kgp.certificateNotBefore = kma.getDate(KeymasterDefs.KM_TAG_ACTIVE_DATETIME, Date())
|
||||
@@ -146,7 +148,7 @@ object KeystoreInterceptor : BaseKeystoreInterceptor() {
|
||||
val callback = IKeystoreExportKeyCallback.Stub.asInterface(data.readStrongBinder())
|
||||
val alias = data.readString()!!.extractAlias()
|
||||
Logger.i("exportKeyTransaction uid $callingUid alias $alias")
|
||||
val kp = CertificateHacker.generateKeyPair(keyArguments[Key(callingUid, alias)]!!)
|
||||
val kp = CertificateGen.generateKeyPair(keyArguments[Key(callingUid, alias)]!!)
|
||||
keyPairs[Key(callingUid, alias)] = kp!!
|
||||
|
||||
val erP = Parcel.obtain()
|
||||
@@ -181,7 +183,7 @@ object KeystoreInterceptor : BaseKeystoreInterceptor() {
|
||||
val key = Key(callingUid, alias)
|
||||
val ka = keyArguments[key]!!
|
||||
ka.attestationChallenge = attestationChallenge
|
||||
val chain = CertificateHacker.generateChain(callingUid, ka, keyPairs[key]!!)
|
||||
val chain = CertificateGen.generateChain(callingUid, ka, keyPairs[key]!!)
|
||||
|
||||
val kcc = KeymasterCertificateChain(chain)
|
||||
callback.onFinished(ksr, kcc)
|
||||
@@ -218,12 +220,12 @@ object KeystoreInterceptor : BaseKeystoreInterceptor() {
|
||||
var response = reply.createByteArray()
|
||||
when {
|
||||
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")
|
||||
return createByteArrayReply(response)
|
||||
}
|
||||
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")
|
||||
return createByteArrayReply(response)
|
||||
}
|
||||
|
||||
+10
-10
@@ -16,10 +16,10 @@ import android.system.keystore2.KeyDescriptor
|
||||
import android.system.keystore2.KeyEntryResponse
|
||||
import android.system.keystore2.KeyMetadata
|
||||
import androidx.annotation.Keep
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHacker
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.getTransactCode
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateGen
|
||||
import io.github.beakthoven.TrickyStoreOSS.config.PkgConfig
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.getTransactCode
|
||||
import io.github.beakthoven.TrickyStoreOSS.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.putCertificateChain
|
||||
import java.security.KeyPair
|
||||
import java.security.cert.Certificate
|
||||
@@ -80,9 +80,9 @@ class SecurityLevelInterceptor(
|
||||
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
||||
val aFlags = data.readInt()
|
||||
val entropy = data.createByteArray()
|
||||
val kgp = CertificateHacker.KeyGenParameters(params)
|
||||
if (Config.needGenerate(callingUid)) {
|
||||
val pair = CertificateHacker.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp, level)
|
||||
val kgp = CertificateGen.KeyGenParameters(params)
|
||||
if (PkgConfig.needGenerate(callingUid)) {
|
||||
val pair = CertificateGen.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp, level)
|
||||
?: return@runCatching
|
||||
keyPairs[Key(callingUid, keyDescriptor.alias)] = Pair(pair.first, pair.second)
|
||||
val response = buildResponse(pair.second, kgp, attestationKeyDescriptor ?: keyDescriptor)
|
||||
@@ -91,10 +91,10 @@ class SecurityLevelInterceptor(
|
||||
p.writeNoException()
|
||||
p.writeTypedObject(response.metadata, 0)
|
||||
return OverrideReply(0, p)
|
||||
} else if (Config.needHack(callingUid)) {
|
||||
} else if (PkgConfig.needHack(callingUid)) {
|
||||
if ((kgp.purpose.contains(7)) || (attestationKeyDescriptor != null)) {
|
||||
Logger.i("Generating key in generation mode for attestation: uid=$callingUid alias=${keyDescriptor.alias}")
|
||||
val pair = CertificateHacker.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp, level)
|
||||
val pair = CertificateGen.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp, level)
|
||||
?: return@runCatching
|
||||
keyPairs[Key(callingUid, keyDescriptor.alias)] = Pair(pair.first, pair.second)
|
||||
val response = buildResponse(pair.second, kgp, attestationKeyDescriptor ?: keyDescriptor)
|
||||
@@ -119,7 +119,7 @@ class SecurityLevelInterceptor(
|
||||
|
||||
private fun buildResponse(
|
||||
chain: List<Certificate>,
|
||||
params: CertificateHacker.KeyGenParameters,
|
||||
params: CertificateGen.KeyGenParameters,
|
||||
descriptor: KeyDescriptor
|
||||
): KeyEntryResponse {
|
||||
val response = KeyEntryResponse()
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
* 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
|
||||
|
||||
@@ -27,7 +27,7 @@ object Logger {
|
||||
}
|
||||
|
||||
fun e(message: String, throwable: Throwable) {
|
||||
Log.e(TAG, "wtf: $message", throwable)
|
||||
Log.e(TAG, "fatal: $message", throwable)
|
||||
}
|
||||
|
||||
fun i(message: String) {
|
||||
Reference in New Issue
Block a user