diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/CertProvider.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/CertProvider.kt index a51c543..33c37a3 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/CertProvider.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/CertProvider.kt @@ -47,7 +47,10 @@ object CertProvider { val kind: Kind = Kind.SELF_SIGNED, ) - /** Loaded fresh each call, so a newly imported or pushed cert takes effect on the next connect. */ + /** + * The identity for the next connection. A newly imported or pushed cert takes effect on the + * next connect: see [loadFromFiles] for how the cache notices. + */ fun load(context: Context): Identity = loadFromFiles(context) ?: run { val generated = SelfSignedCert.generate() Identity( @@ -65,7 +68,38 @@ object CertProvider { */ fun loadSupplied(context: Context): Identity? = loadFromFiles(context) + /** + * Unlocking a PKCS#12 file takes about a second on a Pixel 6, which a head unit waiting for + * its first reply may not wait out. So the unlocked identity is kept, keyed on the files it + * could come from: importing, downloading, removing or pushing a cert changes the key. + */ + @Volatile private var cached: Pair? = null + + private fun sourcesKey(context: Context): String { + val dir = context.getExternalFilesDir(null) + val files = listOf( + importedFile(context, P12_NAME), + downloadedFile(context, P12_NAME), + dir?.let { File(it, P12_NAME) }, + dir?.let { File(it, PASS_NAME) }, + ) + return files.joinToString("|") { f -> f?.let { "${it.path}:${it.lastModified()}:${it.length()}" } ?: "-" } + } + + /** Unlocks the certificate ahead of time, so the first connection doesn't wait on it. */ + fun warm(context: Context) { + runCatching { loadFromFiles(context) } + } + private fun loadFromFiles(context: Context): Identity? { + val key = sourcesKey(context) + cached?.let { (k, identity) -> if (k == key) return identity } + val identity = unlockFromFiles(context) ?: return null + cached = key to identity + return identity + } + + private fun unlockFromFiles(context: Context): Identity? { val imported = importedFile(context, P12_NAME) if (imported.isFile) { runCatching { diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt index e1dedcf..69b3c6b 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt @@ -10,6 +10,7 @@ import app.seb3thehacker.gearslip.car.CarServices import app.seb3thehacker.gearslip.car.CarSettings import java.io.InputStream import java.io.OutputStream +import java.util.concurrent.FutureTask /** * Drives the phone side of the control-channel handshake far enough to answer one question: @@ -38,7 +39,10 @@ class GearslipRunner( private val parser = Frames.Parser() private val assembler = Frames.Assembler() - private lateinit var tls: PhoneTls + private lateinit var identityTask: FutureTask + + /** Waits for the certificate only when TLS is first needed; see [run]. */ + private val tls: PhoneTls by lazy { identityTask.get() } @Volatile private var state = State.WAIT_VERSION @Volatile private var lastProgress = System.currentTimeMillis() @@ -76,14 +80,19 @@ class GearslipRunner( SessionStatus.connecting("Connected to the head unit") log.i("device=${Build.MANUFACTURER} ${Build.MODEL} android=${Build.VERSION.RELEASE} (sdk ${Build.VERSION.SDK_INT})") - val identity = identityProvider() - val cert = identity.certificate - log.i("phone certificate source: ${identity.source}") - SessionReport.certificate(identity.source.toString(), cert.issuerX500Principal.toString()) - log.i(" subject = ${cert.subjectX500Principal}") - log.i(" issuer = ${cert.issuerX500Principal}") - log.i(" serial = ${cert.serialNumber} valid ${cert.notBefore}..${cert.notAfter}") - tls = PhoneTls(identity.keyStore, identity.password) + // The head unit speaks first, and some give up if the phone doesn't read within about a + // second. The version exchange needs no certificate, so read at once and unlock the + // certificate alongside; the TLS handshake waits for it. + identityTask = FutureTask { + val identity = identityProvider() + val cert = identity.certificate + log.i("phone certificate source: ${identity.source}") + SessionReport.certificate(identity.source.toString(), cert.issuerX500Principal.toString()) + log.i(" subject = ${cert.subjectX500Principal}") + log.i(" issuer = ${cert.issuerX500Principal}") + log.i(" serial = ${cert.serialNumber} valid ${cert.notBefore}..${cert.notAfter}") + PhoneTls(identity.keyStore, identity.password) + }.also { Thread(it, "gearslip-identity").start() } startWatchdog() diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/Prefetch.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/Prefetch.kt index a29f7be..d1404cf 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/Prefetch.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/Prefetch.kt @@ -1,13 +1,15 @@ package app.seb3thehacker.gearslip.car import android.content.Context +import app.seb3thehacker.gearslip.CertProvider import app.seb3thehacker.gearslip.GearslipLog import app.seb3thehacker.gearslip.mirror.PhoneApps import java.util.concurrent.Executors /** * Loads what the car screens show before anyone opens them: the car and media apps with their - * icons, the phone's launcher apps, the weather and the calendar. Each screen paints from these + * icons, the phone's launcher apps, the weather and the calendar. Also unlocks the certificate, + * so a head unit's first message is answered at once. Each screen paints from these * results at once and refreshes behind them, so nothing waits on the package manager, the * network or the calendar provider. * @@ -28,6 +30,7 @@ object Prefetch { val app = context.applicationContext worker.execute { val start = System.currentTimeMillis() + step("certificate") { CertProvider.warm(app) } step("launcher apps") { warmLauncherApps(app) } step("phone apps") { PhoneApps.warm(app) } step("weather and calendar") { warmDashboard(app) }