diff --git a/java_module/app/build.gradle b/java_module/app/build.gradle index 98da551..757c301 100644 --- a/java_module/app/build.gradle +++ b/java_module/app/build.gradle @@ -18,8 +18,9 @@ android { buildTypes { release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.debug } } compileOptions { @@ -32,7 +33,5 @@ android { } dependencies { - implementation 'androidx.core:core-ktx:1.6.0' - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation 'com.google.android.material:material:1.4.0' + implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.5.21' } diff --git a/java_module/app/proguard-rules.pro b/java_module/app/proguard-rules.pro index 481bb43..4667fda 100644 --- a/java_module/app/proguard-rules.pro +++ b/java_module/app/proguard-rules.pro @@ -18,4 +18,12 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile + +-keep class dev.kdrag0n.safetynetriru.EntryPoint { + public static void init(); +} + +-keepclassmembers class dev.kdrag0n.safetynetriru.ProxyKeyStoreSpi { + public (...); +} diff --git a/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/EntryPoint.kt b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/EntryPoint.kt new file mode 100644 index 0000000..625487a --- /dev/null +++ b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/EntryPoint.kt @@ -0,0 +1,16 @@ +package dev.kdrag0n.safetynetriru + +@Suppress("unused") +object EntryPoint { + @JvmStatic + fun init() { + runCatching { + logDebug("Entry point: Initializing SafetyNet patch") + SecurityBridge.init() + }.recoverCatching { e -> + // Throwing an exception would require the JNI code to handle exceptions, so just catch + // everything here. + logDebug("Error in entry point", e) + } + } +} diff --git a/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/ProxyKeyStoreSpi.kt b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/ProxyKeyStoreSpi.kt new file mode 100644 index 0000000..79e393e --- /dev/null +++ b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/ProxyKeyStoreSpi.kt @@ -0,0 +1,72 @@ +package dev.kdrag0n.safetynetriru + +import java.io.InputStream +import java.io.OutputStream +import java.security.Key +import java.security.KeyStoreSpi +import java.security.cert.Certificate +import java.util.* + +class ProxyKeyStoreSpi private constructor( + private val orig: KeyStoreSpi, +) : KeyStoreSpi() { + @Suppress("unused") + constructor() : this(androidImpl!!) + + init { + logDebug("Init proxy KeyStore SPI") + } + + // Avoid breaking other, legitimate uses of key attestation in Google Play Services, e.g. + // - com.google.android.gms.auth.cryptauth.register.ReEnrollmentChimeraService + // - tk_trace.129-RegisterForKeyPairOperation + private fun isCallerSafetyNet() = Thread.currentThread().stackTrace.any { + // a.a.engineGetCertificateChain(Unknown Source:15) + // java.security.KeyStore.getCertificateChain(KeyStore.java:1087) + // com.google.ccc.abuse.droidguard.DroidGuard.initNative(Native Method) + // com.google.ccc.abuse.droidguard.DroidGuard.init(DroidGuard.java:447) + // java.lang.reflect.Method.invoke(Native Method) + // xvq.b(:com.google.android.gms@212621053@21.26.21 (190400-387928701):1) + // xuc.a(:com.google.android.gms@212621053@21.26.21 (190400-387928701):5) + // xuc.eX(:com.google.android.gms@212621053@21.26.21 (190400-387928701):1) + // dzx.onTransact(:com.google.android.gms@212621053@21.26.21 (190400-387928701):8) + // android.os.Binder.execTransactInternal(Binder.java:1179) + // android.os.Binder.execTransact(Binder.java:1143) + logDebug("Stack trace element: $it") + it.className.contains("DroidGuard", ignoreCase = true) + } + + override fun engineGetCertificateChain(alias: String?): Array? { + logDebug("Proxy key store: get certificate chain") + + if (isCallerSafetyNet()) { + logDebug("Blocking call") + throw UnsupportedOperationException() + } else { + logDebug("Allowing call") + return orig.engineGetCertificateChain(alias) + } + } + + // Direct delegation. We have to do this manually because the Kotlin compiler can only do it + // for interfaces, not abstract classes. + override fun engineGetKey(alias: String?, password: CharArray?): Key? = orig.engineGetKey(alias, password) + override fun engineGetCertificate(alias: String?): Certificate? = orig.engineGetCertificate(alias) + override fun engineGetCreationDate(alias: String?): Date? = orig.engineGetCreationDate(alias) + override fun engineSetKeyEntry(alias: String?, key: Key?, password: CharArray?, chain: Array?) = orig.engineSetKeyEntry(alias, key, password, chain) + override fun engineSetKeyEntry(alias: String?, key: ByteArray?, chain: Array?) = orig.engineSetKeyEntry(alias, key, chain) + override fun engineSetCertificateEntry(alias: String?, cert: Certificate?) = orig.engineSetCertificateEntry(alias, cert) + override fun engineDeleteEntry(alias: String?) = orig.engineDeleteEntry(alias) + override fun engineAliases(): Enumeration? = orig.engineAliases() + override fun engineContainsAlias(alias: String?) = orig.engineContainsAlias(alias) + override fun engineSize() = orig.engineSize() + override fun engineIsKeyEntry(alias: String?) = orig.engineIsKeyEntry(alias) + override fun engineIsCertificateEntry(alias: String?) = orig.engineIsCertificateEntry(alias) + override fun engineGetCertificateAlias(cert: Certificate?): String? = orig.engineGetCertificateAlias(cert) + override fun engineStore(stream: OutputStream?, password: CharArray?) = orig.engineStore(stream, password) + override fun engineLoad(stream: InputStream?, password: CharArray?) = orig.engineLoad(stream, password) + + companion object { + @Volatile internal var androidImpl: KeyStoreSpi? = null + } +} diff --git a/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/ProxyProvider.kt b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/ProxyProvider.kt new file mode 100644 index 0000000..3644e0b --- /dev/null +++ b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/ProxyProvider.kt @@ -0,0 +1,27 @@ +package dev.kdrag0n.safetynetriru + +import java.security.Provider + +// This is mostly just a pass-through provider that exists to change the provider's ClassLoader. +// This works because Service looks up the class by name from the *provider* ClassLoader, not +// necessarily the bootstrap one. +class ProxyProvider( + orig: Provider, +) : Provider(orig.name, orig.version, orig.info) { + init { + logDebug("Init proxy provider - wrapping $orig") + + putAll(orig) + this["KeyStore.${SecurityBridge.PROVIDER_NAME}"] = ProxyKeyStoreSpi::class.java.name + } + + override fun getService(type: String?, algorithm: String?): Service? { + logDebug("Provider: get service - type=$type algorithm=$algorithm") + return super.getService(type, algorithm) + } + + override fun getServices(): MutableSet? { + logDebug("Get services") + return super.getServices() + } +} diff --git a/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/SecurityBridge.kt b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/SecurityBridge.kt new file mode 100644 index 0000000..b90f273 --- /dev/null +++ b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/SecurityBridge.kt @@ -0,0 +1,26 @@ +package dev.kdrag0n.safetynetriru + +import java.security.KeyStore +import java.security.KeyStoreSpi +import java.security.Security + +internal object SecurityBridge { + const val PROVIDER_NAME = "AndroidKeyStore" + + fun init() { + logDebug("Initializing SecurityBridge") + + val realProvider = Security.getProvider(PROVIDER_NAME) + val realKeystore = KeyStore.getInstance(PROVIDER_NAME) + val realSpi = realKeystore.get("keyStoreSpi") + logDebug("Real provider=$realProvider, keystore=$realKeystore, spi=$realSpi") + + val provider = ProxyProvider(realProvider) + logDebug("Removing real provider") + Security.removeProvider("AndroidKeyStore") + logDebug("Inserting provider $provider") + Security.insertProviderAt(provider, 1) + ProxyKeyStoreSpi.androidImpl = realSpi + logDebug("Security hooks installed") + } +} diff --git a/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/Utils.kt b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/Utils.kt new file mode 100644 index 0000000..d8600d5 --- /dev/null +++ b/java_module/app/src/main/java/dev/kdrag0n/safetynetriru/Utils.kt @@ -0,0 +1,24 @@ +package dev.kdrag0n.safetynetriru + +import android.util.Log + +private const val DEBUG = true +private const val TAG = "SafetyNetRiru/SARU" + +internal fun Any.get(name: String) = this::class.java.getDeclaredField(name).let { field -> + field.isAccessible = true + @Suppress("unchecked_cast") + field.get(this) as T +} + +internal fun logDebug(msg: String) { + if (DEBUG) { + Log.d(TAG, msg) + } +} + +internal fun logDebug(msg: String, e: Throwable) { + if (DEBUG) { + Log.d(TAG, msg, e) + } +}