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
8 changes: 4 additions & 4 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ val bitkitAndroidTestAnnotation = bitkitAndroidTestAnnotationName?.let {

android {
namespace = "to.bitkit"
compileSdk = 36
compileSdk = 37
requestedNdkVersion?.let { ndkVersion = it }
defaultConfig {
applicationId = "to.bitkit"
Expand Down Expand Up @@ -508,7 +508,7 @@ composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
}

val nativeDebugSymbols by configurations.creating {
val nativeDebugSymbols = configurations.create("nativeDebugSymbols") {
isCanBeConsumed = false
isCanBeResolved = true
}
Expand All @@ -525,7 +525,7 @@ fun Provider<MinimalExternalModuleDependency>.nativeDebugSymbolsArtifact(): Stri
return "${dependency.module.group}:${dependency.module.name}:$version:native-debug-symbols@zip"
}

val syncNativeDebugSymbolArtifacts by tasks.registering(Sync::class) {
tasks.register<Sync>("syncNativeDebugSymbolArtifacts") {
group = "build"
description = "Downloads native debug symbol archives for release native dependencies."

Expand Down Expand Up @@ -560,7 +560,7 @@ dependencies {
val localBitkitCoreAar = providers.gradleProperty("bitkitCoreAar").orNull
if (localBitkitCoreAar != null) {
debugImplementation(files(localBitkitCoreAar))
debugImplementation("org.jetbrains.kotlinx:atomicfu:0.23.1")
debugImplementation("org.jetbrains.kotlinx:atomicfu:0.33.0")
releaseImplementation(libs.bitkit.core)
} else {
implementation(libs.bitkit.core)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.junit4.v2.createEmptyComposeRule
import androidx.core.net.toUri
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.compose.ComposeNavigator
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.testing.TestNavHostController
import androidx.navigation.compose.rememberNavController
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
Expand All @@ -38,7 +36,7 @@ class ScreenDeepLinkDetachmentTest {
@get:Rule
val composeTestRule = createEmptyComposeRule()

private lateinit var navController: TestNavHostController
private lateinit var navController: NavHostController

@Test
fun testAttachedScreenUriIsHandledByGraphWithoutGate() {
Expand Down Expand Up @@ -117,12 +115,7 @@ class ScreenDeepLinkDetachmentTest {

@Composable
private fun TestGraph() {
val context = LocalContext.current
val controller = remember {
TestNavHostController(context).apply {
navigatorProvider.addNavigator(ComposeNavigator())
}
}
val controller = rememberNavController()
navController = controller

NavHost(navController = controller, startDestination = Routes.Home) {
Expand Down
3 changes: 1 addition & 2 deletions app/src/debug/java/to/bitkit/dev/DevToolsProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import android.net.Uri
import android.os.Binder
import android.os.Bundle
import android.os.Process
import androidx.core.os.bundleOf
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
Expand Down Expand Up @@ -273,7 +272,7 @@ private sealed interface DevResult {

@Serializable data class Error(val message: String? = null) : DevResult

fun toBundle() = bundleOf(KEY_RESULT to DEV_JSON.encodeToString(this))
fun toBundle() = Bundle().apply { putString(KEY_RESULT, DEV_JSON.encodeToString(this@DevResult)) }
}

private fun ProbeOutcome.toDevResult(paymentIds: Set<String>): DevResult = when (this) {
Expand Down
38 changes: 38 additions & 0 deletions app/src/main/java/to/bitkit/ext/Bluetooth.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package to.bitkit.ext

import android.Manifest
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattConnectionSettings
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresPermission
import java.util.concurrent.Executor

private val directExecutor = Executor(Runnable::run)

/**
* Opens a direct GATT client connection to this device.
*
* From API 37 this uses [BluetoothGattConnectionSettings], with automatic MTU exchange turned off because the
* hardware wallet transports request their own MTU once connected. Older releases have only the legacy
* overload, which already behaves that way.
*/
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
fun BluetoothDevice.connectGattDirect(
context: Context,
callback: BluetoothGattCallback,
transport: Int,
): BluetoothGatt? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN) {
val settings = BluetoothGattConnectionSettings.Builder()
.setAutoConnectEnabled(false)
.setAutomaticMtuEnabled(false)
.setTransport(transport)
.build()
return connectGatt(settings, directExecutor, callback)
}
@Suppress("DEPRECATION") // The replacement above does not exist before API 37.
return connectGatt(context, false, callback, transport)
}
10 changes: 7 additions & 3 deletions app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -303,12 +303,16 @@ class PubkyRepo @Inject constructor(
private val labelOnly = contacts.filter { it.showsLabelOnly }.associateBy { it.profile.publicKey }

val job = SupervisorJob(scope.coroutineContext.job)

val refreshScope = CoroutineScope(scope.coroutineContext + job)
val pendingResults = mutableMapOf<String, RefreshedContact>()
var scheduledApply: Job? = null
private val bulkLookups = contacts.associate {
it.profile.publicKey to scope.launch(job, CoroutineStart.LAZY) { lookUp(this@ContactProfileRefresh, it) }
it.profile.publicKey to refreshScope.launch(start = CoroutineStart.LAZY) {
lookUp(this@ContactProfileRefresh, it)
}
}
private val finish = scope.launch(job, CoroutineStart.LAZY) {
private val finish = refreshScope.launch(start = CoroutineStart.LAZY) {
bulkLookups.values.joinAll()
onFinished(this@ContactProfileRefresh)
}
Expand Down Expand Up @@ -1926,7 +1930,7 @@ class PubkyRepo @Inject constructor(
private fun queueRefreshedContact(refresh: ContactProfileRefresh, refreshed: RefreshedContact) {
refresh.pendingResults[refreshed.contact.profile.publicKey] = refreshed
if (refresh.scheduledApply != null) return
refresh.scheduledApply = scope.launch(refresh.job) {
refresh.scheduledApply = refresh.refreshScope.launch {
delay(CONTACT_REFRESH_BATCH_WINDOW)
synchronized(contactsLock) { applyRefreshResults(refresh) }
}
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/to/bitkit/services/JadeTransport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import to.bitkit.ext.bluetoothManager
import to.bitkit.ext.connectGattDirect
import to.bitkit.ext.usbManager
import to.bitkit.models.HwWalletVendor
import to.bitkit.models.TransportType
Expand Down Expand Up @@ -661,7 +662,8 @@ class JadeTransport @Inject constructor(
waitForBonding(device, address)?.let { return it }

val connectionLatch = CountDownLatch(1)
val gatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
val gatt = device.connectGattDirect(context, gattCallback, BluetoothDevice.TRANSPORT_LE)
?: return fail("BLE connection could not be opened", JadeTransportErrorCode.NOT_CONNECTED)
bleConnections[path] = BleConnection(gatt = gatt, connectionLatch = connectionLatch)

if (!connectionLatch.await(BLE_CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
Expand Down
8 changes: 7 additions & 1 deletion app/src/main/java/to/bitkit/services/TrezorTransport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import to.bitkit.ext.bluetoothManager
import to.bitkit.ext.connectGattDirect
import to.bitkit.ext.nowMs
import to.bitkit.ext.usbManager
import to.bitkit.models.TransportType
Expand Down Expand Up @@ -927,7 +928,12 @@ class TrezorTransport @Inject constructor(
if (bondError != null) return bondError

val connectionLatch = CountDownLatch(1)
val gatt = device.connectGatt(context, false, bleGattCallback)
val gatt = device.connectGattDirect(context, bleGattCallback, BluetoothDevice.TRANSPORT_AUTO)
?: return TrezorTransportWriteResult(
success = false,
error = "BLE connection could not be opened: $path",
errorCode = null,
)

val connection = BleConnection(
gatt = gatt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ class ContactDetailViewModel @Inject constructor(

override fun onCleared() {
paymentScanJob?.cancel()
super.onCleared()
}

private fun cancelPendingPayment() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,6 @@ class TrezorViewModel @Inject constructor(
override fun onCleared() {
isCleared = true
_uiState.value.activeWatcherId?.let { trezorRepo.stopWatcherOnCleared(it) }
super.onCleared()
}

fun clearError() {
Expand Down
1 change: 0 additions & 1 deletion app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7895,7 +7895,6 @@ class AppViewModel @Inject constructor(

override fun onCleared() {
isSubmittingPaymentRequest = false
super.onCleared()
}

fun beginHardwarePaymentSubmission(): () -> Unit = beginPaymentSubmission()
Expand Down
1 change: 0 additions & 1 deletion app/src/main/java/to/bitkit/viewmodels/BackupsViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ class BackupsViewModel @Inject constructor(
}

override fun onCleared() {
super.onCleared()
backupRepo.stopObservingBackups()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ class LdkDebugViewModel @Inject constructor(
}

override fun onCleared() {
super.onCleared()
File(context.cacheDir, DIR_EXPORTS).deleteRecursively()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ class QuickPayViewModel @Inject constructor(

override fun onCleared() {
session?.let { quickPayRepo.detach(it) }
super.onCleared()
}

private fun QuickPaySessionEvent.toUiResult(): QuickPayResult = when (this) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,6 @@ class VssDebugViewModel @Inject constructor(
}

override fun onCleared() {
super.onCleared()
File(context.cacheDir, DIR_EXPORTS).deleteRecursively()
}
}
Expand Down
33 changes: 33 additions & 0 deletions app/src/test/java/to/bitkit/services/ElectrumProbeServiceTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import org.bouncycastle.asn1.x509.GeneralNames
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import org.junit.After
import org.junit.AfterClass
import org.junit.BeforeClass
import org.junit.Test
import org.lightningdevkit.ldknode.Network
import to.bitkit.ext.nowMillis
Expand All @@ -29,6 +31,8 @@ import java.net.SocketTimeoutException
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.Provider
import java.security.Security
import java.security.cert.CertPathValidatorException
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
Expand Down Expand Up @@ -71,8 +75,37 @@ private val KEY_PASSWORD = "probe".toCharArray()
*/
private const val JSSE_PROVIDER = "SunJSSE"

private const val CONSCRYPT_PROVIDER = "Conscrypt"

@OptIn(ExperimentalCoroutinesApi::class)
class ElectrumProbeServiceTest : BaseUnitTest() {
companion object {
private var detachedConscrypt: Pair<Provider, Int>? = null

/**
* Naming SunJSSE is not enough: its first server socket initialises `NamedGroup`, which asks the
* provider list for EC parameters, and Conscrypt 2.6 (Robolectric 4.17) answers and then throws on
* binary-field curves. The class fails to initialise for the rest of the JVM, so Conscrypt is taken
* out of the list while these tests run and put back where it was afterwards.
*/
@JvmStatic
@BeforeClass
fun detachConscrypt() {
val providers = Security.getProviders()
val index = providers.indexOfFirst { it.name == CONSCRYPT_PROVIDER }
if (index < 0) return
detachedConscrypt = providers[index] to index + 1
Security.removeProvider(CONSCRYPT_PROVIDER)
}

@JvmStatic
@AfterClass
fun restoreConscrypt() {
detachedConscrypt?.let { (provider, position) -> Security.insertProviderAt(provider, position) }
detachedConscrypt = null
}
}

private var socketFactory: SSLSocketFactory =
SSLContext.getInstance("TLS", JSSE_PROVIDER).apply { init(null, null, null) }.socketFactory

Expand Down
1 change: 1 addition & 0 deletions changelog.d/next/1787.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update BouncyCastle to 1.86, which fixes certificate-validation and ASN.1 parsing vulnerabilities in the crypto provider the app installs.
4 changes: 2 additions & 2 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
androidSdk = pkgs.androidenv.composeAndroidPackages {
cmdLineToolsVersion = "latest";
platformToolsVersion = "latest";
platformVersions = [ "36" ]; # compileSdk/targetSdk
platformVersions = [ "37.0" ]; # compileSdk
buildToolsVersions = [ buildToolsVersion ];
includeNDK = true;
ndkVersions = [ ndkVersion ];
Expand All @@ -68,7 +68,7 @@
{
default = pkgs.mkShell {
packages = with pkgs; [
jdk17 # AGP 9.3 / Gradle 9.5 require JDK 17+
jdk17 # AGP 9.3 / Gradle 9.8 require JDK 17+
just
git
];
Expand Down
Loading
Loading