Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/src/main/java/to/bitkit/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import to.bitkit.appwidget.AppWidgetRefreshScheduler
import to.bitkit.env.Env
import to.bitkit.services.BluetoothInit
import to.bitkit.services.PubkyAuthHandlerRegistrar
import to.bitkit.utils.Crypto
import to.bitkit.utils.Logger
import to.bitkit.utils.SubscriptionClockOffsetSync
import javax.inject.Inject
Expand Down Expand Up @@ -42,6 +43,8 @@ internal open class App : Application(), Configuration.Provider {
.build()

override fun onCreate() {
// Runs before super.onCreate(), where Hilt starts building services that open TLS connections
Crypto.installSecurityProvider()
Comment thread
Jasonvdb marked this conversation as resolved.
super.onCreate()
Env.initAppStoragePath(filesDir.absolutePath)
installUncaughtExceptionLogger()
Expand Down
44 changes: 30 additions & 14 deletions app/src/main/java/to/bitkit/utils/Crypto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,35 @@ import javax.inject.Singleton
@Suppress("SwallowedException", "MagicNumber", "TooGenericExceptionCaught")
@Singleton
class Crypto @Inject constructor() {
companion object {
/**
* Puts the bundled BouncyCastle in place of the outdated "BC" provider that Android registers.
*
* `App.onCreate` calls this before anything can open a TLS connection. While the swap runs no
* provider offers the "BKS" keystore, and a native TLS verifier that loads its classes in that
* window fails for the rest of the process. Later calls do nothing.
*/
@Synchronized
fun installSecurityProvider() {
// TODO show setup failure on UI? It throws from App.onCreate and stops start-up
try {
val provider = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME)
when {
provider == null -> Security.addProvider(BouncyCastleProvider())
provider::class.java != BouncyCastleProvider::class.java -> {
// We substitute the outdated BC provider registered in Android.
// Build the replacement first so the gap without a "BC" provider stays short.
val replacement = BouncyCastleProvider()
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.insertProviderAt(replacement, 1)
}
}
} catch (e: Exception) {
throw CryptoError.SecurityProviderSetupFailed()
}
}
}

@Suppress("ArrayInDataClass")
data class KeyPair(
val privateKey: ByteArray,
Expand All @@ -50,20 +79,7 @@ class Crypto @Inject constructor() {
private val transformation = "AES/GCM/NoPadding"

init {
// TODO move init to VM (to enable error handling on UI)?
try {
val provider = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME)
when {
provider == null -> Security.addProvider(BouncyCastleProvider())
provider::class.java != BouncyCastleProvider::class.java -> {
// We substitute the outdated BC provider registered in Android
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.insertProviderAt(BouncyCastleProvider(), 1)
}
}
} catch (e: Exception) {
throw CryptoError.SecurityProviderSetupFailed()
}
installSecurityProvider()
Comment thread
Jasonvdb marked this conversation as resolved.
}

fun generateKeyPair(): KeyPair {
Expand Down
59 changes: 59 additions & 0 deletions app/src/test/java/to/bitkit/AppTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package to.bitkit

import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.After
import org.junit.Before
import org.junit.Test
import to.bitkit.utils.AppError
import java.security.Provider
import java.security.Security
import kotlin.test.assertFailsWith
import kotlin.test.assertIs

class AppTest {
private companion object {
const val BC = BouncyCastleProvider.PROVIDER_NAME
}

private var baselineProvider: Provider? = null
private var baselinePosition = 0

@Before
fun setUp() {
baselineProvider = Security.getProvider(BC)
baselinePosition = Security.getProviders().indexOfFirst { it === baselineProvider } + 1
}

@After
fun tearDown() {
// The provider list is shared by the whole JVM, so restore BC as it was before this test started
Security.removeProvider(BC)
baselineProvider?.let { Security.insertProviderAt(it, baselinePosition) }
}

@Test
fun `onCreate installs the security provider before Hilt injects the app`() {
Security.removeProvider(BC)
Security.addProvider(OutdatedBcProvider())
val app = InjectionProbeApp()

// The Hilt Gradle plugin rewrites App to extend the generated Hilt_App, whose onCreate() injects App through
// hiltInternalInject(). The probe's method of that name overrides it at runtime and stops onCreate() there.
assertFailsWith<InjectionReached> { app.onCreate() }

assertIs<BouncyCastleProvider>(app.providerAtInjection)
}

private class InjectionProbeApp : App() {
var providerAtInjection: Provider? = null

fun hiltInternalInject() {
providerAtInjection = Security.getProvider(BC)
throw InjectionReached()
}
}

private class InjectionReached : AppError("Reached Hilt injection")

private class OutdatedBcProvider : Provider(BC, 1.0, "Stub for the BC provider that Android registers")
}
66 changes: 66 additions & 0 deletions app/src/test/java/to/bitkit/utils/CryptoTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package to.bitkit.utils

import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.After
import org.junit.Before
import org.junit.Test
import to.bitkit.env.Env.derivationName
Expand All @@ -8,17 +10,37 @@ import to.bitkit.ext.fromHex
import to.bitkit.ext.toBase64
import to.bitkit.ext.toHex
import to.bitkit.fcm.EncryptedNotification
import java.security.Provider
import java.security.Security
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertSame
import kotlin.test.assertTrue

class CryptoTest {
private companion object {
const val BC = BouncyCastleProvider.PROVIDER_NAME
}

private lateinit var sut: Crypto
private var baselineProvider: Provider? = null
private var baselinePosition = 0

@Before
fun setUp() {
baselineProvider = Security.getProvider(BC)
baselinePosition = positionOf(baselineProvider)
Comment thread
Jasonvdb marked this conversation as resolved.
sut = Crypto()
}

@After
fun tearDown() {
// The provider list is shared by the whole JVM, so restore BC as it was before this test started
Security.removeProvider(BC)
baselineProvider?.let { Security.insertProviderAt(it, baselinePosition) }
}

@Test
fun `it should generate valid shared secret from keypair`() {
val (privateKey, publicKey) = sut.generateKeyPair()
Expand Down Expand Up @@ -107,4 +129,48 @@ class CryptoTest {

assertEquals(decryptedPayload, value.decodeToString())
}

@Test
fun `installSecurityProvider adds BouncyCastle when no BC provider is registered`() {
Security.removeProvider(BC)

Crypto.installSecurityProvider()

assertIs<BouncyCastleProvider>(Security.getProvider(BC))
}

@Test
fun `installSecurityProvider replaces an outdated BC provider at position 1`() {
val outdated = OutdatedBcProvider()
Security.removeProvider(BC)
Security.addProvider(outdated)

Crypto.installSecurityProvider()

val installed = assertIs<BouncyCastleProvider>(Security.getProviders().first())
assertSame(installed, Security.getProvider(BC))
assertTrue(Security.getProviders().none { it === outdated })
}

@Test
fun `installSecurityProvider keeps the installed provider on later calls`() {
Security.removeProvider(BC)
Security.addProvider(OutdatedBcProvider())
Crypto.installSecurityProvider()
val installed = Security.getProviders().toList()

Crypto.installSecurityProvider()
Crypto()

assertSameProviders(installed, Security.getProviders().toList())
}

private fun positionOf(provider: Provider?) = Security.getProviders().indexOfFirst { it === provider } + 1

private fun assertSameProviders(expected: List<Provider>, actual: List<Provider>) {
assertEquals(expected.size, actual.size)
expected.zip(actual).forEach { (want, got) -> assertSame(want, got) }
}

private class OutdatedBcProvider : Provider(BC, 1.0, "Stub for the BC provider that Android registers")
}
1 change: 1 addition & 0 deletions changelog.d/next/1416.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Pubky profile, contact and payment features no longer fail after a cold start until the app is restarted.
Loading