java: Initial implementation of high-level module payload

R8 minification is essential for this, because otherwise the Kotlin
standard library is too big and results in the app compiling to multiple
dex files. It's not impossible to load multiple dex buffers, but let's
keep it simple here.
This commit is contained in:
Danny Lin 2021-08-22 20:36:09 -07:00
parent 9a63924813
commit b0416cd3aa
No known key found for this signature in database
GPG Key ID: 1988FAA1797EE5AC
7 changed files with 177 additions and 5 deletions

View File

@ -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'
}

View File

@ -18,4 +18,12 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile
-keep class dev.kdrag0n.safetynetriru.EntryPoint {
public static void init();
}
-keepclassmembers class dev.kdrag0n.safetynetriru.ProxyKeyStoreSpi {
public <init>(...);
}

View File

@ -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)
}
}
}

View File

@ -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<Certificate>? {
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<out Certificate>?) = orig.engineSetKeyEntry(alias, key, password, chain)
override fun engineSetKeyEntry(alias: String?, key: ByteArray?, chain: Array<out Certificate>?) = 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<String>? = 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
}
}

View File

@ -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<Service>? {
logDebug("Get services")
return super.getServices()
}
}

View File

@ -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>("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")
}
}

View File

@ -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 <T> 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)
}
}