@ledgerhq/device-transport-kit-react-native-hid 0.0.0-rn-hid-20250221112139 → 0.0.0-rnhid-transport-20250411151739
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/android/build.gradle +101 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/android/gradle.properties +1 -0
- package/android/gradlew +252 -0
- package/android/gradlew.bat +94 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/kotlin/com/ledger/androidtransporthid/BridgeEvents.kt +42 -0
- package/android/src/main/kotlin/com/ledger/androidtransporthid/TransportHidModule.kt +241 -0
- package/android/src/main/kotlin/com/ledger/androidtransporthid/TransportHidPackage.kt +25 -0
- package/android/src/main/kotlin/com/ledger/androidtransporthid/bridge/serialization.kt +124 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/AndroidUsbTransport.kt +16 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/DefaultAndroidUsbTransport.kt +298 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/UsbPermissionRequester.kt +18 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/connection/AndroidUsbApduSender.kt +133 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/controller/UsbAttachedReceiverController.kt +59 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/controller/UsbDetachedReceiverController.kt +58 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/controller/UsbPermissionReceiver.kt +92 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/LedgerUsbDevice.kt +16 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/ProductId.kt +11 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/UsbPermissionEvent.kt +14 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/UsbState.kt +16 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/VendorId.kt +11 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/utils/UsbDeviceMapper.kt +46 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/utils/UsbMapper.kt +56 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/UsbConst.android.kt +8 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceApduSender.kt +13 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnection.kt +95 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachine.kt +314 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/Apdu.kt +44 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduBuilder.kt +88 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduParser.kt +37 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduUtils.kt +37 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/SendApduResult.kt +47 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/connection/ConnectedDevice.kt +25 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/connection/ConnectionResult.kt +45 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/BleInformation.kt +8 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/LedgerDevice.kt +89 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/UsbInfo.kt +7 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/disconnection/DisconnectionResult.kt +10 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/ConnectivityType.kt +10 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/DiscoveryDevice.kt +18 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/DiscoveryResult.kt +28 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/utils/ByteArrayExtension.kt +116 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/utils/StringExtension.kt +21 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/connection/InternalConnectedDevice.kt +13 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/connection/InternalConnectionResult.kt +41 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/coroutine/SDKScope.kt +25 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/coroutine/SDKScopeHandler.kt +18 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/event/SdkEventDispatcher.kt +19 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/DisableLoggerService.kt +12 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LogInfo.kt +52 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LogLevel.kt +13 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LoggerService.kt +10 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/Transport.kt +21 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/TransportEvent.kt +18 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/FramerService.kt +210 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/FramerUtils.kt +35 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduConst.kt +9 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduFrame.kt +66 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduFramerHeader.kt +74 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/FramerConst.kt +14 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/utils/ByteExtension.kt +21 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/utils/InternalByteArrayExtension.kt +18 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/utils/Controller.kt +12 -0
- package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachineTest.kt +713 -0
- package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionTest.kt +218 -0
- package/lib/cjs/package.json +2 -1
- package/lib/esm/package.json +2 -1
- package/lib/types/tsconfig.prod.tsbuildinfo +1 -1
- package/package.json +6 -5
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: 2023 Ledger SAS
|
|
3
|
+
* SPDX-License-Identifier: LicenseRef-LEDGER
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
package com.ledger.devicesdk.shared.androidMain.transport.usb.model
|
|
7
|
+
|
|
8
|
+
internal sealed class UsbPermissionEvent {
|
|
9
|
+
data class PermissionGranted(
|
|
10
|
+
val ledgerUsbDevice: LedgerUsbDevice,
|
|
11
|
+
) : UsbPermissionEvent()
|
|
12
|
+
|
|
13
|
+
data object PermissionDenied : UsbPermissionEvent()
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: 2023 Ledger SAS
|
|
3
|
+
* SPDX-License-Identifier: LicenseRef-LEDGER
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
package com.ledger.devicesdk.shared.androidMain.transport.usb.model
|
|
7
|
+
|
|
8
|
+
internal sealed class UsbState {
|
|
9
|
+
data class Detached(
|
|
10
|
+
val ledgerUsbDevice: LedgerUsbDevice,
|
|
11
|
+
) : UsbState()
|
|
12
|
+
|
|
13
|
+
data class Attached(
|
|
14
|
+
val ledgerUsbDevice: LedgerUsbDevice,
|
|
15
|
+
): UsbState()
|
|
16
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: 2024 Ledger SAS
|
|
3
|
+
* SPDX-License-Identifier: LicenseRef-LEDGER
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
package com.ledger.devicesdk.shared.androidMain.transport.usb.utils
|
|
7
|
+
|
|
8
|
+
import com.ledger.devicesdk.shared.androidMain.transport.usb.model.ProductId
|
|
9
|
+
import com.ledger.devicesdk.shared.api.device.LedgerDevice
|
|
10
|
+
|
|
11
|
+
internal fun ProductId.toLedgerDevice(): LedgerDevice? =
|
|
12
|
+
when {
|
|
13
|
+
this.id.isLedgerDeviceProductId(LedgerDevice.NanoS) -> {
|
|
14
|
+
LedgerDevice.NanoS
|
|
15
|
+
}
|
|
16
|
+
this.id.isLedgerDeviceProductId(LedgerDevice.NanoSPlus) -> {
|
|
17
|
+
LedgerDevice.NanoSPlus
|
|
18
|
+
}
|
|
19
|
+
this.id.isLedgerDeviceProductId(LedgerDevice.NanoX) -> {
|
|
20
|
+
LedgerDevice.NanoX
|
|
21
|
+
}
|
|
22
|
+
this.id.isLedgerDeviceProductId(LedgerDevice.Stax) -> {
|
|
23
|
+
LedgerDevice.Stax
|
|
24
|
+
}
|
|
25
|
+
this.id.isLedgerDeviceProductId(LedgerDevice.Flex) -> {
|
|
26
|
+
LedgerDevice.Flex
|
|
27
|
+
}
|
|
28
|
+
else -> {
|
|
29
|
+
null
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private fun Int.isLedgerDeviceProductId(device: LedgerDevice): Boolean {
|
|
34
|
+
val productId = device.usbInfo.productIdMask.sdkHexToInt()
|
|
35
|
+
val bootloaderProductId = device.usbInfo.bootloaderProductId.sdkHexToInt()
|
|
36
|
+
val shiftedId = this shr 8
|
|
37
|
+
return shiftedId == productId || this == bootloaderProductId
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@OptIn(ExperimentalStdlibApi::class)
|
|
41
|
+
public fun String.sdkHexToInt(withPrefix: Boolean = true): Int =
|
|
42
|
+
if (withPrefix) {
|
|
43
|
+
this.substring(2).hexToInt()
|
|
44
|
+
} else {
|
|
45
|
+
this.hexToInt()
|
|
46
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: 2023 Ledger SAS
|
|
3
|
+
* SPDX-License-Identifier: LicenseRef-LEDGER
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
package com.ledger.devicesdk.shared.androidMain.transport.usb.utils
|
|
7
|
+
|
|
8
|
+
import android.content.Intent
|
|
9
|
+
import android.hardware.usb.UsbManager
|
|
10
|
+
import android.os.Build
|
|
11
|
+
import com.ledger.devicesdk.shared.api.device.LedgerDevice
|
|
12
|
+
import com.ledger.devicesdk.shared.api.discovery.ConnectivityType
|
|
13
|
+
import com.ledger.devicesdk.shared.api.discovery.DiscoveryDevice
|
|
14
|
+
import com.ledger.devicesdk.shared.androidMain.transport.usb.model.VendorId
|
|
15
|
+
import com.ledger.devicesdk.shared.androidMain.transport.usb.model.ProductId
|
|
16
|
+
import com.ledger.devicesdk.shared.androidMain.transport.usb.model.LedgerUsbDevice
|
|
17
|
+
|
|
18
|
+
internal fun LedgerUsbDevice.toScannedDevice() =
|
|
19
|
+
DiscoveryDevice(
|
|
20
|
+
uid = this.uid,
|
|
21
|
+
name = this.name,
|
|
22
|
+
ledgerDevice = this.ledgerDevice,
|
|
23
|
+
connectivityType = ConnectivityType.Usb,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
internal fun List<android.hardware.usb.UsbDevice>.toUsbDevices(): List<LedgerUsbDevice> = mapNotNull { it.toLedgerUsbDevice() }
|
|
27
|
+
|
|
28
|
+
internal fun android.hardware.usb.UsbDevice.toLedgerUsbDevice(): LedgerUsbDevice? {
|
|
29
|
+
val productId = ProductId(this.productId)
|
|
30
|
+
val vendorId = VendorId(this.vendorId)
|
|
31
|
+
|
|
32
|
+
val ledgerDevice = productId.toLedgerDevice()
|
|
33
|
+
return if (vendorId.id == LedgerDevice.LEDGER_USB_VENDOR_ID.toProductIdInt() && ledgerDevice != null) {
|
|
34
|
+
return LedgerUsbDevice(
|
|
35
|
+
uid = this.deviceId.toString(),
|
|
36
|
+
name = ledgerDevice.name,
|
|
37
|
+
vendorId = vendorId,
|
|
38
|
+
productId = productId,
|
|
39
|
+
ledgerDevice = ledgerDevice,
|
|
40
|
+
)
|
|
41
|
+
} else {
|
|
42
|
+
null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private fun String.toProductIdInt(): Int = this.substring(2).toInt(16)
|
|
47
|
+
|
|
48
|
+
internal fun Intent.getAndroidUsbDevice(): android.hardware.usb.UsbDevice {
|
|
49
|
+
val device =
|
|
50
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
51
|
+
getParcelableExtra(UsbManager.EXTRA_DEVICE, android.hardware.usb.UsbDevice::class.java)
|
|
52
|
+
} else {
|
|
53
|
+
getParcelableExtra(UsbManager.EXTRA_DEVICE)
|
|
54
|
+
}
|
|
55
|
+
return checkNotNull(device)
|
|
56
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: 2025 Ledger SAS
|
|
3
|
+
* SPDX-License-Identifier: LicenseRef-LEDGER
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
package com.ledger.devicesdk.shared.androidMainInternal.transport.deviceconnection
|
|
7
|
+
|
|
8
|
+
import com.ledger.devicesdk.shared.api.apdu.SendApduResult
|
|
9
|
+
|
|
10
|
+
internal interface DeviceApduSender<Dependencies> {
|
|
11
|
+
suspend fun send(apdu: ByteArray): SendApduResult
|
|
12
|
+
val dependencies: Dependencies
|
|
13
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
package com.ledger.devicesdk.shared.androidMainInternal.transport.deviceconnection
|
|
2
|
+
|
|
3
|
+
import com.ledger.devicesdk.shared.api.apdu.SendApduResult
|
|
4
|
+
import com.ledger.devicesdk.shared.internal.service.logger.LoggerService
|
|
5
|
+
import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleErrorLogInfo
|
|
6
|
+
import kotlinx.coroutines.CoroutineDispatcher
|
|
7
|
+
import kotlinx.coroutines.CoroutineScope
|
|
8
|
+
import kotlinx.coroutines.launch
|
|
9
|
+
import kotlin.coroutines.resume
|
|
10
|
+
import kotlin.coroutines.suspendCoroutine
|
|
11
|
+
import kotlin.time.Duration
|
|
12
|
+
|
|
13
|
+
internal class DeviceConnection<Dependencies>(
|
|
14
|
+
val sessionId: String,
|
|
15
|
+
private var deviceApduSender: DeviceApduSender<Dependencies>,
|
|
16
|
+
isFatalSendApduFailure: (SendApduResult.Failure) -> Boolean,
|
|
17
|
+
reconnectionTimeoutDuration: Duration,
|
|
18
|
+
coroutineDispatcher: CoroutineDispatcher,
|
|
19
|
+
private val onTerminated: (DeviceConnection<Dependencies>) -> Unit,
|
|
20
|
+
private val loggerService: LoggerService,
|
|
21
|
+
) {
|
|
22
|
+
private val stateMachine: DeviceConnectionStateMachine
|
|
23
|
+
private val coroutineScope = CoroutineScope(coroutineDispatcher)
|
|
24
|
+
|
|
25
|
+
init {
|
|
26
|
+
stateMachine = DeviceConnectionStateMachine(
|
|
27
|
+
sendApduFn = {
|
|
28
|
+
coroutineScope.launch {
|
|
29
|
+
val res = deviceApduSender.send(it)
|
|
30
|
+
handleApduResult(res)
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
onTerminated = {
|
|
34
|
+
onTerminated(this)
|
|
35
|
+
},
|
|
36
|
+
isFatalSendApduFailure = isFatalSendApduFailure,
|
|
37
|
+
reconnectionTimeoutDuration = reconnectionTimeoutDuration,
|
|
38
|
+
coroutineDispatcher = coroutineDispatcher,
|
|
39
|
+
onError = {
|
|
40
|
+
loggerService.log(
|
|
41
|
+
buildSimpleErrorLogInfo(
|
|
42
|
+
"DeviceConnection",
|
|
43
|
+
"State machine error $it"
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
},
|
|
47
|
+
loggerService = loggerService,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
private fun handleApduResult(result: SendApduResult) {
|
|
53
|
+
stateMachine.handleApduResult(result)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public fun getApduSender(): DeviceApduSender<Dependencies> {
|
|
57
|
+
return deviceApduSender
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public fun handleDeviceConnected(apduSender: DeviceApduSender<Dependencies>) {
|
|
61
|
+
deviceApduSender = apduSender
|
|
62
|
+
stateMachine.handleDeviceConnected()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public fun handleDeviceDisconnected() {
|
|
66
|
+
stateMachine.handleDeviceDisconnected()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public suspend fun requestSendApdu(apdu: ByteArray): SendApduResult =
|
|
70
|
+
suspendCoroutine { cont ->
|
|
71
|
+
stateMachine.requestSendApdu(
|
|
72
|
+
DeviceConnectionStateMachine.SendApduRequestContent(
|
|
73
|
+
apdu = apdu,
|
|
74
|
+
triggersDisconnection = apduTriggersDisconnection(apdu),
|
|
75
|
+
resultCallback = cont::resume
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public fun requestCloseConnection() {
|
|
81
|
+
stateMachine.requestCloseConnection()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private fun apduTriggersDisconnection(apdu: ByteArray): Boolean {
|
|
85
|
+
val apduMap = mapOf(
|
|
86
|
+
"openApp" to byteArrayOf(0xe0.toByte(), 0xd8.toByte(), 0x00, 0x00),
|
|
87
|
+
"closeApp" to byteArrayOf(0xb0.toByte(), 0xa7.toByte(), 0x00, 0x00)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return apduMap.values.any { known ->
|
|
91
|
+
(0 until 4).all { i -> known[i] == apdu[i] }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
package com.ledger.devicesdk.shared.androidMainInternal.transport.deviceconnection
|
|
2
|
+
|
|
3
|
+
import com.ledger.devicesdk.shared.api.apdu.SendApduFailureReason
|
|
4
|
+
import com.ledger.devicesdk.shared.api.apdu.SendApduResult
|
|
5
|
+
import com.ledger.devicesdk.shared.internal.service.logger.LoggerService
|
|
6
|
+
import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleDebugLogInfo
|
|
7
|
+
import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleInfoLogInfo
|
|
8
|
+
import kotlinx.coroutines.CoroutineDispatcher
|
|
9
|
+
import kotlinx.coroutines.CoroutineScope
|
|
10
|
+
import kotlinx.coroutines.Job
|
|
11
|
+
import kotlinx.coroutines.launch
|
|
12
|
+
import kotlin.time.Duration
|
|
13
|
+
|
|
14
|
+
internal class DeviceConnectionStateMachine(
|
|
15
|
+
private val sendApduFn: (apdu: ByteArray) -> Unit,
|
|
16
|
+
private val onTerminated: () -> Unit,
|
|
17
|
+
private val isFatalSendApduFailure: (SendApduResult.Failure) -> Boolean,
|
|
18
|
+
private val reconnectionTimeoutDuration: Duration,
|
|
19
|
+
private val onError: (Throwable) -> Unit,
|
|
20
|
+
private val loggerService: LoggerService,
|
|
21
|
+
coroutineDispatcher: CoroutineDispatcher,
|
|
22
|
+
) {
|
|
23
|
+
private val coroutineScope = CoroutineScope(coroutineDispatcher)
|
|
24
|
+
private var state: State = State.Connected
|
|
25
|
+
|
|
26
|
+
fun getState() = state
|
|
27
|
+
|
|
28
|
+
private fun pushState(newState: State) {
|
|
29
|
+
when (newState) {
|
|
30
|
+
is State.Connected -> {}
|
|
31
|
+
is State.SendingApdu -> {
|
|
32
|
+
sendApduFn(newState.requestContent.apdu)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
is State.WaitingForReconnection -> {
|
|
36
|
+
startReconnectionTimeout()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
is State.WaitingForReconnectionWithQueuedApdu -> {}
|
|
40
|
+
is State.Terminated -> {
|
|
41
|
+
onTerminated()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
this.state = newState
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private fun handleEvent(event: Event) {
|
|
48
|
+
val currentState = state
|
|
49
|
+
when (currentState) {
|
|
50
|
+
is State.Connected -> {
|
|
51
|
+
when (event) {
|
|
52
|
+
is Event.SendApduRequested -> {
|
|
53
|
+
pushState(State.SendingApdu(event.requestContent))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
is Event.CloseConnectionRequested -> {
|
|
57
|
+
pushState(State.Terminated)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
is Event.DeviceDisconnected -> {
|
|
61
|
+
pushState(State.WaitingForReconnection)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
else -> {
|
|
65
|
+
onError(Exception("Unhandled event: $event in state: $currentState"))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
is State.SendingApdu -> {
|
|
71
|
+
when (event) {
|
|
72
|
+
is Event.ApduResultReceived -> {
|
|
73
|
+
when (event.result) {
|
|
74
|
+
is SendApduResult.Failure -> {
|
|
75
|
+
if (isFatalSendApduFailure(event.result)) {
|
|
76
|
+
pushState(State.Terminated)
|
|
77
|
+
} else {
|
|
78
|
+
pushState(State.Connected)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
is SendApduResult.Success -> {
|
|
83
|
+
// check if last 2 bytes of APDU are [0x90,OxO0]
|
|
84
|
+
val apdu = event.result.apdu
|
|
85
|
+
val apduSize = apdu.size
|
|
86
|
+
val isSuccessApdu =
|
|
87
|
+
apdu.size >= 2 &&
|
|
88
|
+
apdu[apduSize - 2] == 0x90.toByte() &&
|
|
89
|
+
apdu[apduSize - 1] == 0x00.toByte()
|
|
90
|
+
|
|
91
|
+
if (isSuccessApdu && currentState.requestContent.triggersDisconnection) {
|
|
92
|
+
pushState(State.WaitingForReconnection)
|
|
93
|
+
} else {
|
|
94
|
+
pushState(State.Connected)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
currentState.requestContent.resultCallback(event.result)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
is Event.CloseConnectionRequested -> {
|
|
102
|
+
pushState(State.Terminated)
|
|
103
|
+
currentState.requestContent.resultCallback(
|
|
104
|
+
SendApduResult.Failure(
|
|
105
|
+
SendApduFailureReason.DeviceDisconnected
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
is Event.DeviceDisconnected -> {
|
|
111
|
+
pushState(State.WaitingForReconnection)
|
|
112
|
+
currentState.requestContent.resultCallback(
|
|
113
|
+
SendApduResult.Failure(
|
|
114
|
+
SendApduFailureReason.DeviceDisconnected
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
is Event.SendApduRequested -> {
|
|
120
|
+
event.requestContent.resultCallback(
|
|
121
|
+
SendApduResult.Failure(
|
|
122
|
+
SendApduFailureReason.DeviceBusy
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
else -> {
|
|
128
|
+
onError(Exception("Unhandled event: $event in state: $currentState"))
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
is State.WaitingForReconnection -> {
|
|
134
|
+
when (event) {
|
|
135
|
+
is Event.DeviceConnected -> {
|
|
136
|
+
pushState(State.Connected)
|
|
137
|
+
cancelReconnectionTimeout()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
is Event.SendApduRequested -> {
|
|
141
|
+
pushState(State.WaitingForReconnectionWithQueuedApdu(event.requestContent))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
is Event.WaitingForReconnectionTimedOut,
|
|
145
|
+
is Event.CloseConnectionRequested -> {
|
|
146
|
+
pushState(State.Terminated)
|
|
147
|
+
cancelReconnectionTimeout()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
is Event.DeviceDisconnected -> {
|
|
151
|
+
/**
|
|
152
|
+
* Do nothing, this will happen if we send an apdu that triggers a
|
|
153
|
+
* disconnection, because we will move to this state before the disconnection
|
|
154
|
+
* is detected:
|
|
155
|
+
*
|
|
156
|
+
* 1. APDU that triggers a disconnection is sent.
|
|
157
|
+
* 2. We receive a 0x9000 (success) response.
|
|
158
|
+
* -> We go to State.WaitingForReconnection.
|
|
159
|
+
* 3. Device disconnection is finally detected:
|
|
160
|
+
* -> Event.DeviceDisconnected is received here.
|
|
161
|
+
*/
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
else -> {
|
|
165
|
+
onError(Exception("Unhandled event: $event in state: $currentState"))
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
is State.WaitingForReconnectionWithQueuedApdu -> {
|
|
171
|
+
when (event) {
|
|
172
|
+
is Event.DeviceConnected -> {
|
|
173
|
+
pushState(State.SendingApdu(currentState.requestContent))
|
|
174
|
+
cancelReconnectionTimeout()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
is Event.CloseConnectionRequested,
|
|
178
|
+
is Event.WaitingForReconnectionTimedOut -> {
|
|
179
|
+
pushState(State.Terminated)
|
|
180
|
+
currentState.requestContent.resultCallback(
|
|
181
|
+
SendApduResult.Failure(
|
|
182
|
+
SendApduFailureReason.DeviceDisconnected
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
cancelReconnectionTimeout()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
is Event.SendApduRequested -> {
|
|
189
|
+
event.requestContent.resultCallback(
|
|
190
|
+
SendApduResult.Failure(
|
|
191
|
+
SendApduFailureReason.DeviceBusy
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
is Event.DeviceDisconnected -> {
|
|
197
|
+
/**
|
|
198
|
+
* Do nothing, this will happen if we send an apdu that triggers a
|
|
199
|
+
* disconnection, because we will move to this state before the disconnection
|
|
200
|
+
* is detected:
|
|
201
|
+
*
|
|
202
|
+
* 1. APDU that triggers a disconnection is sent.
|
|
203
|
+
* 2. We receive a 0x9000 (success) response
|
|
204
|
+
* -> We go to State.WaitingForReconnection in anticipation of the disconnection event.
|
|
205
|
+
* 3. We receive Event.SendApduRequested
|
|
206
|
+
* -> We go to WaitingForReconnectionWithQueuedApdu
|
|
207
|
+
* 4. Device disconnection is finally detected:
|
|
208
|
+
* -> Event.DeviceDisconnected is received here.
|
|
209
|
+
*
|
|
210
|
+
* It can also happen if the device is disconnected while we are sending an APDU.
|
|
211
|
+
* cf. description of event below.
|
|
212
|
+
*/
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
is Event.ApduResultReceived -> {
|
|
216
|
+
/**
|
|
217
|
+
* Do nothing, this will happen if while an APDU is being sent,
|
|
218
|
+
* the device disconnection is detected.
|
|
219
|
+
* 1. APDU is sent
|
|
220
|
+
* 2. Device disconnection is detected
|
|
221
|
+
* -> Event.DeviceDisconnected is received in SendingApdu state.
|
|
222
|
+
* -> We move to WaitingForReconnection state.
|
|
223
|
+
* 3. The function to send the APDU returns an error because the device is disconnected.
|
|
224
|
+
* -> Event.ApduResultReceived(result=Failure()) is received in the
|
|
225
|
+
* current state.
|
|
226
|
+
*
|
|
227
|
+
* It's a race condition between step 2 and 3.
|
|
228
|
+
*/
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
is State.Terminated -> {
|
|
234
|
+
onError(Exception("Unhandled event: $event in state: $currentState"))
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
val logMessage = """
|
|
238
|
+
Received event:
|
|
239
|
+
In state: $currentState
|
|
240
|
+
-> Event: $event
|
|
241
|
+
-> New state: $state
|
|
242
|
+
""".trimIndent()
|
|
243
|
+
loggerService.log(buildSimpleDebugLogInfo("DeviceConnectionStateMachine", logMessage))
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private var timeoutJob: Job? = null
|
|
247
|
+
private fun startReconnectionTimeout() {
|
|
248
|
+
// start a timeout and at the end, emit a WaitingForReconnectionTimedOut event
|
|
249
|
+
timeoutJob = coroutineScope.launch {
|
|
250
|
+
kotlinx.coroutines.delay(reconnectionTimeoutDuration)
|
|
251
|
+
handleEvent(Event.WaitingForReconnectionTimedOut)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private fun cancelReconnectionTimeout() {
|
|
256
|
+
timeoutJob?.cancel()
|
|
257
|
+
timeoutJob = null
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
public fun requestSendApdu(requestContent: SendApduRequestContent) {
|
|
261
|
+
handleEvent(Event.SendApduRequested(requestContent))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
public fun requestCloseConnection() {
|
|
265
|
+
handleEvent(Event.CloseConnectionRequested)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
public fun handleApduResult(result: SendApduResult) {
|
|
269
|
+
handleEvent(Event.ApduResultReceived(result))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
public fun handleDeviceConnected() {
|
|
273
|
+
handleEvent(Event.DeviceConnected)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
public fun handleDeviceDisconnected() {
|
|
277
|
+
handleEvent(Event.DeviceDisconnected)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
data class SendApduRequestContent(
|
|
281
|
+
val apdu: ByteArray,
|
|
282
|
+
val triggersDisconnection: Boolean,
|
|
283
|
+
val resultCallback: (SendApduResult) -> Unit
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
sealed class Event {
|
|
287
|
+
data object DeviceConnected : Event()
|
|
288
|
+
|
|
289
|
+
data object DeviceDisconnected : Event()
|
|
290
|
+
|
|
291
|
+
data class SendApduRequested(
|
|
292
|
+
val requestContent: SendApduRequestContent
|
|
293
|
+
) : Event()
|
|
294
|
+
|
|
295
|
+
data object CloseConnectionRequested : Event()
|
|
296
|
+
|
|
297
|
+
data class ApduResultReceived(val result: SendApduResult) : Event()
|
|
298
|
+
|
|
299
|
+
data object WaitingForReconnectionTimedOut : Event()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
sealed class State {
|
|
303
|
+
data object Connected : State()
|
|
304
|
+
|
|
305
|
+
data class SendingApdu(val requestContent: SendApduRequestContent) : State()
|
|
306
|
+
|
|
307
|
+
data object WaitingForReconnection : State()
|
|
308
|
+
|
|
309
|
+
data class WaitingForReconnectionWithQueuedApdu(val requestContent: SendApduRequestContent) :
|
|
310
|
+
State()
|
|
311
|
+
|
|
312
|
+
data object Terminated : State()
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: 2024 Ledger SAS
|
|
3
|
+
* SPDX-License-Identifier: LicenseRef-LEDGER
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
package com.ledger.devicesdk.shared.api.apdu
|
|
7
|
+
|
|
8
|
+
public data class Apdu internal constructor(
|
|
9
|
+
internal val classInstruction: Byte,
|
|
10
|
+
internal val instructionMethod: Byte,
|
|
11
|
+
internal val parameter1: Byte,
|
|
12
|
+
internal val parameter2: Byte,
|
|
13
|
+
internal val data: ByteArray?,
|
|
14
|
+
internal val dataLength: Int,
|
|
15
|
+
) {
|
|
16
|
+
override fun equals(other: Any?): Boolean {
|
|
17
|
+
if (this === other) return true
|
|
18
|
+
if (other == null || this::class != other::class) return false
|
|
19
|
+
|
|
20
|
+
other as Apdu
|
|
21
|
+
|
|
22
|
+
if (classInstruction != other.classInstruction) return false
|
|
23
|
+
if (instructionMethod != other.instructionMethod) return false
|
|
24
|
+
if (parameter1 != other.parameter1) return false
|
|
25
|
+
if (parameter2 != other.parameter2) return false
|
|
26
|
+
if (data != null) {
|
|
27
|
+
if (other.data == null) return false
|
|
28
|
+
if (!data.contentEquals(other.data)) return false
|
|
29
|
+
} else if (other.data != null) return false
|
|
30
|
+
if (dataLength != other.dataLength) return false
|
|
31
|
+
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
override fun hashCode(): Int {
|
|
36
|
+
var result = classInstruction.toInt()
|
|
37
|
+
result = 31 * result + instructionMethod
|
|
38
|
+
result = 31 * result + parameter1
|
|
39
|
+
result = 31 * result + parameter2
|
|
40
|
+
result = 31 * result + (data?.contentHashCode() ?: 0)
|
|
41
|
+
result = 31 * result + dataLength
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
44
|
+
}
|