diff --git a/.runDebug/GreenAndroid.run.xml b/.runDebug/GreenAndroid.run.xml
new file mode 100644
index 000000000..032e62a9b
--- /dev/null
+++ b/.runDebug/GreenAndroid.run.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/ActivityProvider.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/ActivityProvider.kt
new file mode 100644
index 000000000..2ca832674
--- /dev/null
+++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/ActivityProvider.kt
@@ -0,0 +1,7 @@
+package com.blockstream.common.devices
+
+import android.app.Activity
+
+interface ActivityProvider {
+ fun getCurrentActivity(): Activity?
+}
\ No newline at end of file
diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/AndroidActivityProvider.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/AndroidActivityProvider.kt
new file mode 100644
index 000000000..8692f772c
--- /dev/null
+++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/AndroidActivityProvider.kt
@@ -0,0 +1,21 @@
+package com.blockstream.common.devices
+
+import android.app.Activity
+import java.lang.ref.WeakReference
+
+// In Android module
+class AndroidActivityProvider : ActivityProvider {
+ private var weakActivity: WeakReference? = null
+
+ fun setActivity(activity: Activity) {
+ weakActivity = WeakReference(activity)
+ }
+
+ override fun getCurrentActivity(): Activity? {
+ return weakActivity?.get()
+ }
+
+ fun clearActivity() {
+ weakActivity = null
+ }
+}
\ No newline at end of file
diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/ApduCommand.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/ApduCommand.kt
new file mode 100644
index 000000000..63d0180ea
--- /dev/null
+++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/ApduCommand.kt
@@ -0,0 +1,92 @@
+package com.blockstream.common.devices
+
+import java.io.ByteArrayOutputStream
+import java.io.IOException
+
+/**
+ * ISO7816-4 APDU.
+ */
+class ApduCommand {
+ public val cla: Int
+ public val ins: Int
+ public val p1: Int
+ public val p2: Int
+ public val data: ByteArray
+ public val needsLE: Boolean
+
+ /**
+ * Constructs an APDU with no response data length field. The data field cannot be null, but can be a zero-length array.
+ *
+ * @param cla class byte
+ * @param ins instruction code
+ * @param p1 P1 parameter
+ * @param p2 P2 parameter
+ * @param data the APDU data
+ */
+ constructor(cla: Int, ins: Int, p1: Int, p2: Int, data: ByteArray) : this(cla, ins, p1, p2, data, false)
+
+ /**
+ * Constructs an APDU with an optional data length field. The data field cannot be null, but can be a zero-length array.
+ * The LE byte, if sent, is set to 0.
+ *
+ * @param cla class byte
+ * @param ins instruction code
+ * @param p1 P1 parameter
+ * @param p2 P2 parameter
+ * @param data the APDU data
+ * @param needsLE whether the LE byte should be sent or not
+ */
+ constructor(cla: Int, ins: Int, p1: Int, p2: Int, data: ByteArray, needsLE: Boolean) {
+ this.cla = cla and 0xff
+ this.ins = ins and 0xff
+ this.p1 = p1 and 0xff
+ this.p2 = p2 and 0xff
+ this.data = data
+ this.needsLE = needsLE
+ }
+
+ /**
+ * Serializes the APDU in order to send it to the card.
+ *
+ * @return the byte array representation of the APDU
+ */
+ @Throws(IOException::class)
+ fun serialize(): ByteArray {
+ val out = ByteArrayOutputStream()
+ out.write(cla)
+ out.write(ins)
+ out.write(p1)
+ out.write(p2)
+ out.write(data.size)
+ out.write(data)
+
+ if (needsLE) {
+ out.write(0) // Response length
+ }
+
+ return out.toByteArray()
+ }
+
+ /**
+ * Serializes the APDU to human readable hex string format
+ *
+ * @return the hex string representation of the APDU
+ */
+ fun toHexString(): String {
+ return try {
+ val raw = serialize()
+ StringBuilder(2 * raw.size).apply {
+ raw.forEach { b ->
+ append(HEXES[(b.toInt() and 0xF0) shr 4])
+ append(HEXES[b.toInt() and 0x0F])
+ }
+ }.toString()
+ } catch (e: Exception) {
+ "Exception in ApduCommand.toHexString()"
+ }
+ }
+
+ companion object {
+ const val HEXES = "0123456789ABCDEF"
+ }
+}
\ No newline at end of file
diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/ApduException.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/ApduException.kt
new file mode 100644
index 000000000..28c37920d
--- /dev/null
+++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/ApduException.kt
@@ -0,0 +1,27 @@
+package com.blockstream.common.devices
+
+/**
+ * Exception thrown when the response APDU from the card contains unexpected SW or data.
+ */
+class ApduException : Exception {
+ val sw: Int
+
+ /**
+ * Creates an exception with SW and message.
+ *
+ * @param sw the status word
+ * @param message a descriptive message of the error
+ */
+ constructor(sw: Int, message: String) : super("$message, 0x${String.format("%04X", sw)}") {
+ this.sw = sw
+ }
+
+ /**
+ * Creates an exception with a message.
+ *
+ * @param message a descriptive message of the error
+ */
+ constructor(message: String) : super(message) {
+ this.sw = 0
+ }
+}
\ No newline at end of file
diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/ApduResponse.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/ApduResponse.kt
new file mode 100644
index 000000000..84c5e2499
--- /dev/null
+++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/ApduResponse.kt
@@ -0,0 +1,158 @@
+package com.blockstream.common.devices
+
+/**
+ * ISO7816-4 APDU response.
+ */
+class ApduResponse {
+ private var apdu: ByteArray = byteArrayOf()
+ private var data: ByteArray = byteArrayOf()
+ private var sw: Int = 0
+ private var sw1: Int = 0
+ private var sw2: Int = 0
+
+ /**
+ * Creates an APDU object by parsing the raw response from the card.
+ *
+ * @param apdu the raw response from the card.
+ */
+ constructor(apdu: ByteArray) {
+ require(apdu.size >= 2) { "APDU response must be at least 2 bytes" }
+ this.apdu = apdu
+ parse()
+ }
+
+ constructor(data: ByteArray, sw1: Byte, sw2: Byte) {
+ val apduArray = ByteArray(data.size + 2)
+ System.arraycopy(data, 0, apduArray, 0, data.size)
+ apduArray[data.size] = sw1
+ apduArray[data.size + 1] = sw2
+ this.apdu = apduArray
+ parse()
+ }
+
+ /**
+ * Parses the APDU response, separating the response data from SW.
+ */
+ private fun parse() {
+ val length = apdu.size
+
+ sw1 = apdu[length - 2].toInt() and 0xff
+ sw2 = apdu[length - 1].toInt() and 0xff
+ sw = (sw1 shl 8) or sw2
+
+ data = ByteArray(length - 2)
+ System.arraycopy(apdu, 0, data, 0, length - 2)
+ }
+
+ /**
+ * Returns true if the SW is 0x9000.
+ *
+ * @return true if the SW is 0x9000.
+ */
+ fun isOK(): Boolean = sw == SW_OK
+
+ /**
+ * Asserts that the SW is 0x9000. Throws an exception if it isn't
+ *
+ * @return this object, to simplify chaining
+ * @throws ApduException if the SW is not 0x9000
+ */
+ @Throws(ApduException::class)
+ fun checkOK(): ApduResponse = checkSW(SW_OK)
+
+ /**
+ * Asserts that the SW is contained in the given list. Throws an exception if it isn't.
+ *
+ * @param codes the list of SWs to match.
+ * @return this object, to simplify chaining
+ * @throws ApduException if the SW is not 0x9000
+ */
+ @Throws(ApduException::class)
+ fun checkSW(vararg codes: Int): ApduResponse {
+ for (code in codes) {
+ if (sw == code) {
+ return this
+ }
+ }
+
+ when (sw) {
+ SW_SECURITY_CONDITION_NOT_SATISFIED ->
+ throw ApduException(sw, "security condition not satisfied")
+ SW_AUTHENTICATION_METHOD_BLOCKED ->
+ throw ApduException(sw, "authentication method blocked")
+ else ->
+ throw ApduException(sw, "Unexpected error SW")
+ }
+ }
+
+ /**
+ * Asserts that the SW is 0x9000. Throws an exception with the given message if it isn't
+ *
+ * @param message the error message
+ * @return this object, to simplify chaining
+ * @throws ApduException if the SW is not 0x9000
+ */
+ @Throws(ApduException::class)
+ fun checkOK(message: String): ApduResponse = checkSW(message, SW_OK)
+
+ /**
+ * Asserts that the SW is contained in the given list. Throws an exception with the given message if it isn't.
+ *
+ * @param message the error message
+ * @param codes the list of SWs to match.
+ * @return this object, to simplify chaining
+ * @throws ApduException if the SW is not 0x9000
+ */
+ @Throws(ApduException::class)
+ fun checkSW(message: String, vararg codes: Int): ApduResponse {
+ for (code in codes) {
+ if (sw == code) {
+ return this
+ }
+ }
+ throw ApduException(sw, message)
+ }
+
+ /**
+ * Serializes the APDU to human readable hex string format
+ *
+ * @return the hex string representation of the APDU
+ */
+ fun toHexString(): String {
+ return try {
+ if (apdu.isEmpty()) {
+ ""
+ } else {
+ StringBuilder(2 * apdu.size).apply {
+ apdu.forEach { b ->
+ append(HEXES[(b.toInt() and 0xF0) shr 4])
+ append(HEXES[b.toInt() and 0x0F])
+ }
+ }.toString()
+ }
+ } catch (e: Exception) {
+ "Exception in ApduResponse.toHexString()"
+ }
+ }
+
+ companion object {
+ const val SW_OK = 0x9000
+ const val SW_SECURITY_CONDITION_NOT_SATISFIED = 0x6982
+ const val SW_AUTHENTICATION_METHOD_BLOCKED = 0x6983
+ const val SW_CARD_LOCKED = 0x6283
+ const val SW_REFERENCED_DATA_NOT_FOUND = 0x6A88
+ const val SW_CONDITIONS_OF_USE_NOT_SATISFIED = 0x6985 // applet may be already installed
+ const val SW_WRONG_PIN_MASK = 0x63C0
+ const val SW_WRONG_PIN_LEGACY = 0x9C02
+ const val SW_BLOCKED_PIN = 0x9C0C
+ const val SW_FACTORY_RESET = 0xFF00
+ const val HEXES = "0123456789ABCDEF"
+ }
+
+ // Getters converted to Kotlin properties
+ fun getData(): ByteArray = data
+ fun getSw(): Int = sw
+ fun getSw1(): Int = sw1
+ fun getSw2(): Int = sw2
+ fun getBytes(): ByteArray = apdu
+}
\ No newline at end of file
diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/CardChannel.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/CardChannel.kt
new file mode 100644
index 000000000..c742d77d9
--- /dev/null
+++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/CardChannel.kt
@@ -0,0 +1,21 @@
+package com.blockstream.common.devices
+
+/**
+ * A channel to transcieve ISO7816-4 APDUs.
+ */
+interface CardChannel {
+ /**
+ * Sends the given C-APDU and returns an R-APDU.
+ *
+ * @param cmd the command to send
+ * @return the card response
+ * @throws IOException communication error
+ */
+ fun send(cmd: ApduCommand): ApduResponse
+
+ /**
+ * True if connected, false otherwise
+ * @return true if connected, false otherwise
+ */
+ fun isConnected(): Boolean
+}
\ No newline at end of file
diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/CardListener.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/CardListener.kt
new file mode 100644
index 000000000..e54eceb73
--- /dev/null
+++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/CardListener.kt
@@ -0,0 +1,18 @@
+package com.blockstream.common.devices
+
+/**
+ * Listener for card connection events.
+ */
+interface CardListener {
+ /**
+ * Executes when the card channel is connected.
+ *
+ * @param channel the connected card channel
+ */
+ fun onConnected(channel: CardChannel)
+
+ /**
+ * Executes when a previously connected card is disconnected.
+ */
+ fun onDisconnected()
+}
\ No newline at end of file
diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt
index a49500319..2e472b04d 100644
--- a/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt
+++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt
@@ -10,6 +10,7 @@ import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
+import android.nfc.NfcAdapter
import androidx.core.content.ContextCompat
import androidx.core.content.IntentCompat
import com.benasher44.uuid.Uuid
@@ -25,20 +26,26 @@ import com.juul.kable.PlatformAdvertisement
import com.juul.kable.peripheral
import java.lang.ref.WeakReference
-
class DeviceManagerAndroid constructor(
scope: ApplicationScope,
+ val activityProvider: ActivityProvider, // provide Activity reference needed by NfcAdapter
val context: Context,
sessionManager: SessionManager,
bluetoothManager: BluetoothManager,
val usbManager: UsbManager,
supportedBleDevices: List,
val deviceMapper: (
- deviceManager: DeviceManagerAndroid, usbDevice: UsbDevice?, bleService: Uuid?,
+ deviceManager: DeviceManagerAndroid,
+ usbDevice: UsbDevice?,
+ bleService: Uuid?,
peripheral: Peripheral?,
- isBonded: Boolean?
+ isBonded: Boolean?,
+ nfcDevice: NfcDevice?,
+ activityProvider: ActivityProvider?,
) -> AndroidDevice?
-): DeviceManager(scope, sessionManager, bluetoothManager, supportedBleDevices) {
+): CardListener, DeviceManager(scope, sessionManager, bluetoothManager, supportedBleDevices) {
+
+ private val nfcAdapter = NfcAdapter.getDefaultAdapter(context)
private var onPermissionSuccess: WeakReference<(() -> Unit)>? = null
private var onPermissionError: WeakReference<((throwable: Throwable?) -> Unit)>? = null
@@ -89,8 +96,9 @@ class DeviceManagerAndroid constructor(
ContextCompat.RECEIVER_EXPORTED
)
- scanUsbDevices()
+ scanNfcDevices()
+ scanUsbDevices()
}
override fun advertisedDevice(advertisement: PlatformAdvertisement) {
@@ -103,7 +111,7 @@ class DeviceManagerAndroid constructor(
val peripheral = scope.peripheral(advertisement)
val bleService = advertisement.uuids.firstOrNull()
- deviceMapper.invoke(this, null, bleService, peripheral, advertisement.isBonded())
+ deviceMapper.invoke(this, null, bleService, peripheral, advertisement.isBonded(), null,null)
?.also {
addBluetoothDevice(it)
}
@@ -124,7 +132,7 @@ class DeviceManagerAndroid constructor(
override fun refreshDevices(){
super.refreshDevices()
-
+ scanNfcDevices()
scanUsbDevices()
}
@@ -149,7 +157,7 @@ class DeviceManagerAndroid constructor(
// Jade or UsbDeviceMapper
(JadeUsbDevice.fromUsbDevice(deviceManager = this, usbDevice = usbDevice)
- ?: deviceMapper.invoke(this, usbDevice, null, null, null))?.let {
+ ?: deviceMapper.invoke(this, usbDevice, null, null, null, null,null))?.let {
newDevices += it
}
}
@@ -158,6 +166,87 @@ class DeviceManagerAndroid constructor(
usbDevices.value = oldDevices + newDevices
}
+ fun scanNfcDevices() {
+ logger.i { "Scan for NFC devices" }
+
+ val cardManager = NfcCardManager()
+ cardManager.setCardListener(this)
+ cardManager.start()
+
+ val activity = activityProvider.getCurrentActivity()
+
+ nfcAdapter?.enableReaderMode(
+ activity,
+ cardManager,
+ NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_NFC_B or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
+ null
+ )
+ }
+
+ override fun onConnected(channel: CardChannel) {
+ logger.i { "SATODEBUG DeviceManagerAndroid onConnected" }
+ for (nfcDeviceType in NfcDeviceType.entries) {
+ try {
+ val cmdSet = SatochipCommandSet(channel)
+
+ // try to select applet according to device candidate
+ cmdSet.cardSelect(nfcDeviceType).checkOK()
+
+ // device found, create new device
+ val nfcDevice = NfcDevice(nfcDeviceType)
+
+ // for Satochip devices, get card status
+ if (nfcDeviceType == NfcDeviceType.SATOCHIP){
+ val statusApdu = cmdSet.satochipGetStatus().checkOK()
+
+ val statusBytes = statusApdu.getData()
+ val statusSize = statusBytes.size
+ logger.i { "SATODEBUG DeviceManagerAndroid onConnected: statusSize: $statusSize" }
+
+ // check if card is seeded
+ val isSeeded = if ((statusBytes[9].toInt() == 0X00)) false else true
+ nfcDevice.isSeeded = isSeeded
+ logger.i { "SATODEBUG DeviceManagerAndroid onConnected: isSeeded: $isSeeded" }
+
+ // check that 2FA is not enabled
+ val needs2FA = if ((statusBytes[8].toInt() == 0X00)) false else true
+ logger.i { "SATODEBUG DeviceManagerAndroid onConnected: needs2FA: $needs2FA" }
+
+ // check if Liquid is supported
+ val supportsLiquid = if ((statusSize>15) && (statusBytes[15].toInt() == 0X00)) true else false
+ nfcDevice.supportsLiquid = supportsLiquid
+ logger.i { "SATODEBUG DeviceManagerAndroid onConnected: supportsLiquid: $supportsLiquid" }
+ }
+
+ // add device
+ val newDevices = mutableListOf()
+ deviceMapper.invoke(this, null, null, null, null, nfcDevice, activityProvider)
+ ?.let {
+ newDevices += it
+ }
+ logger.i { "SATODEBUG DeviceManagerAndroid onConnected newDevices: ${newDevices}" }
+ nfcDevices.value = newDevices
+
+ // disconnect card
+ onDisconnected()
+
+ // stop polling
+ val activity = activityProvider.getCurrentActivity()
+ nfcAdapter?.disableReaderMode(activity)
+
+ // should probably avoid to connect to multiple NFC devices at the same time?
+ return
+ } catch (e: Exception) {
+ logger.i { "SATODEBUG DeviceManagerAndroid onConnected: failed to connect to device: $nfcDeviceType" }
+ }
+ }
+ }
+
+ override fun onDisconnected() {
+ logger.i { "SATODEBUG DeviceManagerAndroid onDisconnected: Card disconnected!" }
+ }
+
+
companion object : Loggable() {
private const val ACTION_USB_PERMISSION = "com.blockstream.green.USB_PERMISSION"
}
diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardChannel.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardChannel.kt
new file mode 100644
index 000000000..49d220b8f
--- /dev/null
+++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardChannel.kt
@@ -0,0 +1,32 @@
+package com.blockstream.common.devices
+
+import android.nfc.tech.IsoDep
+import android.util.Log
+
+/**
+ * Implementation of the CardChannel interface using the Android NFC API.
+ */
+class NfcCardChannel(private val isoDep: IsoDep) : CardChannel {
+ companion object {
+ private const val TAG = "CardChannel"
+ }
+
+ override fun send(cmd: ApduCommand): ApduResponse {
+ val apdu = cmd.serialize()
+ Log.d(TAG, "COMMAND CLA: %02X INS: %02X P1: %02X P2: %02X LC: %02X".format(
+ cmd.cla, cmd.ins, cmd.p1, cmd.p2, cmd.data.size
+ ))
+
+ val resp = isoDep.transceive(apdu)
+ val response = ApduResponse(resp)
+ Log.d(TAG, "RESPONSE LEN: %02X, SW: %04X %n-----------------------".format(
+ response.getData().size, response.getSw()
+ ))
+
+ return response
+ }
+
+ override fun isConnected(): Boolean {
+ return isoDep.isConnected
+ }
+}
\ No newline at end of file
diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardManager.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardManager.kt
new file mode 100644
index 000000000..dc161c27a
--- /dev/null
+++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardManager.kt
@@ -0,0 +1,101 @@
+package com.blockstream.common.devices
+
+import android.nfc.NfcAdapter
+import android.nfc.Tag
+import android.nfc.tech.IsoDep
+import android.os.SystemClock
+import android.util.Log
+import kotlinx.io.IOException
+
+/**
+ * Manages connection of NFC-based cards. Extends Thread and must be started using the start() method. The thread has
+ * a runloop which monitors the connection and from which CardListener callbacks are called.
+ */
+class NfcCardManager @JvmOverloads constructor(
+ private val loopSleepMS: Long = DEFAULT_LOOP_SLEEP_MS
+) : Thread(), NfcAdapter.ReaderCallback {
+
+ private var isoDep: IsoDep? = null
+ private var isRunning = false
+ private var cardListener: CardListener? = null
+
+ /**
+ * True if connected, false otherwise.
+ * @return if connected, false otherwise
+ */
+ val isConnected: Boolean
+ get() = try {
+ isoDep?.isConnected == true
+ } catch (e: Exception) {
+ e.printStackTrace()
+ false
+ }
+
+ override fun onTagDiscovered(tag: Tag) {
+ isoDep = IsoDep.get(tag)?.apply {
+ try {
+ connect()
+ timeout = 120000
+ } catch (e: IOException) {
+ Log.e(TAG, "error connecting to tag")
+ }
+ }
+ }
+
+ /**
+ * Runloop. Do NOT invoke directly. Use start() instead.
+ */
+ override fun run() {
+ var connected = isConnected
+
+ while (true) {
+ val newConnected = isConnected
+ if (newConnected != connected) {
+ connected = newConnected
+ Log.i(TAG, "tag ${if (connected) "connected" else "disconnected"}")
+
+ if (connected && !isRunning) {
+ onCardConnected()
+ } else {
+ onCardDisconnected()
+ }
+ }
+
+ SystemClock.sleep(loopSleepMS)
+ }
+ }
+
+ /**
+ * Reacts on card connected by calling the callback of the registered listener.
+ */
+ private fun onCardConnected() {
+ isRunning = true
+
+ cardListener?.onConnected(NfcCardChannel(isoDep!!))
+
+ isRunning = false
+ }
+
+ /**
+ * Reacts on card disconnected by calling the callback of the registered listener.
+ */
+ private fun onCardDisconnected() {
+ isRunning = false
+ isoDep = null
+ cardListener?.onDisconnected()
+ }
+
+ /**
+ * Sets the card listener.
+ *
+ * @param listener the new listener
+ */
+ fun setCardListener(listener: CardListener) {
+ cardListener = listener
+ }
+
+ companion object {
+ private const val TAG = "NFCCardManager"
+ private const val DEFAULT_LOOP_SLEEP_MS: Long = 50
+ }
+}
\ No newline at end of file
diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcDevice.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcDevice.kt
new file mode 100644
index 000000000..1f4955d90
--- /dev/null
+++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcDevice.kt
@@ -0,0 +1,13 @@
+package com.blockstream.common.devices
+
+
+enum class NfcDeviceType {
+ SATOCHIP
+}
+
+class NfcDevice(val type: NfcDeviceType) {
+
+ var isSeeded = true
+ var supportsLiquid = true
+
+}
\ No newline at end of file
diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.kt
new file mode 100644
index 000000000..b5414b9eb
--- /dev/null
+++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.kt
@@ -0,0 +1,73 @@
+package com.blockstream.common.devices
+
+import java.io.IOException
+import java.util.logging.Level
+import java.util.logging.Logger
+
+/**
+ * This class is used to send APDU to the applet. Each method corresponds to an APDU as defined in the APPLICATION.md
+ * file. Some APDUs map to multiple methods for the sake of convenience since their payload or response require some
+ * pre/post processing.
+ */
+class SatochipCommandSet(private val apduChannel: CardChannel) {
+
+ companion object {
+ private val logger: Logger = Logger.getLogger("org.satochip.client")
+
+ val SATOCHIP_AID: ByteArray = hexToBytes("5361746f43686970") // SatoChip
+
+ /* s must be an even-length string. */
+ fun hexToBytes(s: String): ByteArray {
+ val len = s.length
+ val data = ByteArray(len / 2)
+ for (i in 0 until len step 2) {
+ data[i / 2] = ((Character.digit(s[i], 16) shl 4) +
+ Character.digit(s[i + 1], 16)).toByte()
+ }
+ return data
+ }
+ }
+
+ // Satochip or...
+ private var cardType: NfcDeviceType = NfcDeviceType.SATOCHIP
+
+ init {
+ logger.level = Level.WARNING
+ }
+
+ @Throws(IOException::class)
+ fun cardSelect(nfcDeviceType: NfcDeviceType): ApduResponse {
+ val selectApplet = when (nfcDeviceType) {
+ NfcDeviceType.SATOCHIP-> ApduCommand(0x00, 0xA4, 0x04, 0x00, SATOCHIP_AID)
+ }
+
+ logger.info("SATOCHIPLIB: C-APDU cardSelect:${selectApplet.toHexString()}")
+ val respApdu = apduChannel.send(selectApplet)
+ logger.info("SATOCHIPLIB: R-APDU cardSelect:${respApdu.toHexString()}")
+
+ if (respApdu.getSw() == 0x9000) {
+ this.cardType = cardType
+ logger.info("SATOCHIPLIB: Satochip-java: CardSelect: found a ${this.cardType}")
+ }
+ return respApdu
+ }
+
+ fun satochipGetStatus(): ApduResponse {
+
+ val plainApdu: ApduCommand = ApduCommand(
+ 0xB0,
+ 0x3C,
+ 0x00,
+ 0x00,
+ ByteArray(0)
+ )
+
+ logger.info("SATOCHIPLIB: C-APDU satochipGetStatus:" + plainApdu.toHexString())
+ val respApdu: ApduResponse = apduChannel.send(plainApdu)
+ logger.info("SATOCHIPLIB: R-APDU satochipGetStatus:" + respApdu.toHexString())
+
+ return respApdu
+ }
+
+
+}
\ No newline at end of file
diff --git a/common/src/commonMain/composeResources/drawable/nfc_scan.xml b/common/src/commonMain/composeResources/drawable/nfc_scan.xml
new file mode 100644
index 000000000..2a4ac0db6
--- /dev/null
+++ b/common/src/commonMain/composeResources/drawable/nfc_scan.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/common/src/commonMain/composeResources/values-cs/strings.xml b/common/src/commonMain/composeResources/values-cs/strings.xml
index 7a032616f..bd5444dfe 100644
--- a/common/src/commonMain/composeResources/values-cs/strings.xml
+++ b/common/src/commonMain/composeResources/values-cs/strings.xml
@@ -1092,6 +1092,7 @@
Skenovat QR pomocí zařízení Jade
Naskenujte QR kód pomocí autentikační aplikace
Skenovat k odeslat sem
+ Naskenujte svou kartu
Zámek obrazovky
Hledat
Hledat adresu
diff --git a/common/src/commonMain/composeResources/values-de/strings.xml b/common/src/commonMain/composeResources/values-de/strings.xml
index fc3532552..0d69510e7 100644
--- a/common/src/commonMain/composeResources/values-de/strings.xml
+++ b/common/src/commonMain/composeResources/values-de/strings.xml
@@ -1092,6 +1092,7 @@
Scan QR with Jade
Scanne den QR-Code mit einer Authenticator-App
Hier scannen und senden
+ Scannen Sie Ihre Karte
Bildschirmsperre
Suche
Adresse suchen
diff --git a/common/src/commonMain/composeResources/values-es/strings.xml b/common/src/commonMain/composeResources/values-es/strings.xml
index 00e01132f..a3ec3f1a9 100644
--- a/common/src/commonMain/composeResources/values-es/strings.xml
+++ b/common/src/commonMain/composeResources/values-es/strings.xml
@@ -1092,6 +1092,7 @@
Scan QR with Jade
Escanee el código QR con una app autenticadora
Escanear para enviar aquí
+ Escanea tu tarjeta
Bloqueo de pantalla
Buscar
Buscar una dirección
diff --git a/common/src/commonMain/composeResources/values-fr/strings.xml b/common/src/commonMain/composeResources/values-fr/strings.xml
index 5ebf12150..dab204199 100644
--- a/common/src/commonMain/composeResources/values-fr/strings.xml
+++ b/common/src/commonMain/composeResources/values-fr/strings.xml
@@ -1092,6 +1092,7 @@
Scan QR with Jade
Scannez le code QR avec une application Authenticator.
Scanner pour envoyer ici
+ Scannez votre carte
Verrouillage de l'écran
Recherche
Recherche d'adresse
diff --git a/common/src/commonMain/composeResources/values-he/strings.xml b/common/src/commonMain/composeResources/values-he/strings.xml
index c2c4adda5..4b1d08aad 100644
--- a/common/src/commonMain/composeResources/values-he/strings.xml
+++ b/common/src/commonMain/composeResources/values-he/strings.xml
@@ -1092,6 +1092,7 @@
Scan QR with Jade
Scan the QR Code with an Authenticator app
Scan to send here
+ Scan your card
Screen Lock
Search
Search address
diff --git a/common/src/commonMain/composeResources/values-it/strings.xml b/common/src/commonMain/composeResources/values-it/strings.xml
index f8961816e..a79909dc3 100644
--- a/common/src/commonMain/composeResources/values-it/strings.xml
+++ b/common/src/commonMain/composeResources/values-it/strings.xml
@@ -1092,6 +1092,7 @@
Scan QR with Jade
Scansiona il QR code con una app Authenticator
Scansiona per inviare qui
+ Scansiona la tua carta
Blocca Schermo
Cerca
Cerca indirizzo
diff --git a/common/src/commonMain/composeResources/values-ja/strings.xml b/common/src/commonMain/composeResources/values-ja/strings.xml
index 3b5c260c1..f3d9ebb89 100644
--- a/common/src/commonMain/composeResources/values-ja/strings.xml
+++ b/common/src/commonMain/composeResources/values-ja/strings.xml
@@ -1092,6 +1092,7 @@
Scan QR with Jade
AuthenticatorアプリでQRコードをスキャンする
スキャンしてここに送る
+ カードをスキャン
画面ロック
検索
アドレスを検索する
diff --git a/common/src/commonMain/composeResources/values-ko/strings.xml b/common/src/commonMain/composeResources/values-ko/strings.xml
index 297a75652..a3e875277 100644
--- a/common/src/commonMain/composeResources/values-ko/strings.xml
+++ b/common/src/commonMain/composeResources/values-ko/strings.xml
@@ -721,6 +721,7 @@
Scan QR code
Scan the QR Code with an Authenticator app
Scan to send here
+ 카드를 스캔하세요
화면 잠금
검색
Search address
diff --git a/common/src/commonMain/composeResources/values-nl/strings.xml b/common/src/commonMain/composeResources/values-nl/strings.xml
index 7603ea594..f77ba30c5 100644
--- a/common/src/commonMain/composeResources/values-nl/strings.xml
+++ b/common/src/commonMain/composeResources/values-nl/strings.xml
@@ -1092,6 +1092,7 @@
Scan QR with Jade
Scan de QR-code met een Authenticator-app
Scan hier om te sturen
+ Scan je kaart
Schermvergrendeling
Zoeken
Adres zoeken
diff --git a/common/src/commonMain/composeResources/values-pt-rBR/strings.xml b/common/src/commonMain/composeResources/values-pt-rBR/strings.xml
index da338a77c..47ae1ee11 100644
--- a/common/src/commonMain/composeResources/values-pt-rBR/strings.xml
+++ b/common/src/commonMain/composeResources/values-pt-rBR/strings.xml
@@ -1092,6 +1092,7 @@
Scan QR with Jade
Leia o código QR usando o app autenticador
Leia este código QR ou envie para o endereço abaixo
+ Escaneie seu cartão
Bloqueio de tela
Buscar
Procurar endereço
diff --git a/common/src/commonMain/composeResources/values-ro/strings.xml b/common/src/commonMain/composeResources/values-ro/strings.xml
index 4cdceff14..d44a8ce3a 100644
--- a/common/src/commonMain/composeResources/values-ro/strings.xml
+++ b/common/src/commonMain/composeResources/values-ro/strings.xml
@@ -1092,6 +1092,7 @@
Scan QR with Jade
Scanați un cod QR folosind aplicația Authenticator
Scanați pentru a trimite aici
+ Scanați cardul dumneavoastră
Blocare ecran
Căutare
Caută adresa
diff --git a/common/src/commonMain/composeResources/values-ru/strings.xml b/common/src/commonMain/composeResources/values-ru/strings.xml
index 54e994a68..f68599dae 100644
--- a/common/src/commonMain/composeResources/values-ru/strings.xml
+++ b/common/src/commonMain/composeResources/values-ru/strings.xml
@@ -1092,6 +1092,7 @@
Scan QR with Jade
Отсканируйте QR-код с помощью приложения Authenticator
Отсканируйте, чтобы отправить сюда
+ Сканируйте вашу карту
Блокировка экрана
Поиск
Адрес поиска
diff --git a/common/src/commonMain/composeResources/values-uk/strings.xml b/common/src/commonMain/composeResources/values-uk/strings.xml
index fa1889427..ff0a2cc5f 100644
--- a/common/src/commonMain/composeResources/values-uk/strings.xml
+++ b/common/src/commonMain/composeResources/values-uk/strings.xml
@@ -1092,6 +1092,7 @@
Scan QR with Jade
Відскануте QR-код за допомогою додатку Authenticator
Відскануйте, щоб відправити сюди
+ Скануйте вашу картку
Блокування екрану
Пошук
Адреса пошуку
diff --git a/common/src/commonMain/composeResources/values-vi/strings.xml b/common/src/commonMain/composeResources/values-vi/strings.xml
index adafc802c..afbae403d 100644
--- a/common/src/commonMain/composeResources/values-vi/strings.xml
+++ b/common/src/commonMain/composeResources/values-vi/strings.xml
@@ -1092,6 +1092,7 @@
Scan QR with Jade
Scan the QR Code with an Authenticator app
Scan to send here
+ Quét thẻ của bạn
Khóa màn hình
Tìm kiếm
Search address
diff --git a/common/src/commonMain/composeResources/values-zh/strings.xml b/common/src/commonMain/composeResources/values-zh/strings.xml
index e416d70de..0d69d581a 100644
--- a/common/src/commonMain/composeResources/values-zh/strings.xml
+++ b/common/src/commonMain/composeResources/values-zh/strings.xml
@@ -1092,6 +1092,7 @@
Scan QR with Jade
使用Authenticator app扫描二维码
扫描并发送到这里
+ 扫描您的卡片
锁屏
搜索
搜索地址
diff --git a/common/src/commonMain/composeResources/values/strings.xml b/common/src/commonMain/composeResources/values/strings.xml
index 403963e58..f7bc13a5d 100644
--- a/common/src/commonMain/composeResources/values/strings.xml
+++ b/common/src/commonMain/composeResources/values/strings.xml
@@ -1092,6 +1092,7 @@
Scan QR with Jade
Scan the QR Code with an Authenticator app
Scan to send here
+ Scan your card
Screen Lock
Search
Search address
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/devices/ConnectionType.kt b/common/src/commonMain/kotlin/com/blockstream/common/devices/ConnectionType.kt
index 199a2ee3c..ed9ea4bb7 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/devices/ConnectionType.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/devices/ConnectionType.kt
@@ -1,5 +1,5 @@
package com.blockstream.common.devices
enum class ConnectionType {
- USB, BLUETOOTH, QR
+ USB, BLUETOOTH, QR, NFC
}
\ No newline at end of file
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/devices/DeviceBrand.kt b/common/src/commonMain/kotlin/com/blockstream/common/devices/DeviceBrand.kt
index 7d53188af..07ea3619b 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/devices/DeviceBrand.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/devices/DeviceBrand.kt
@@ -1,7 +1,7 @@
package com.blockstream.common.devices
enum class DeviceBrand(val brand: String) {
- Blockstream("Blockstream"), Ledger("Ledger"), Trezor("Trezor"), Generic("Generic");
+ Blockstream("Blockstream"), Ledger("Ledger"), Trezor("Trezor"), Satochip("Satochip"), Generic("Generic");
val isTrezor
get() = this == Trezor
@@ -12,6 +12,9 @@ enum class DeviceBrand(val brand: String) {
val isJade
get() = this == Blockstream
+ val isSatochip
+ get() = this == Satochip
+
val isGeneric
get() = this == Generic
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/devices/DeviceModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/devices/DeviceModel.kt
index 1e842f806..be07d5186 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/devices/DeviceModel.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/devices/DeviceModel.kt
@@ -12,6 +12,7 @@ enum class DeviceModel(val deviceModel: String) {
LedgerGeneric("Ledger"),
LedgerNanoS("Ledger Nano S"),
LedgerNanoX("Ledger Nano X"),
+ SatochipGeneric("Satochip"),
Generic("Generic Hardware Wallet");
val deviceBrand: DeviceBrand
@@ -20,6 +21,7 @@ enum class DeviceModel(val deviceModel: String) {
TrezorGeneric, TrezorModelT, TrezorModelOne -> DeviceBrand.Trezor
LedgerGeneric, LedgerNanoS, LedgerNanoX -> DeviceBrand.Ledger
Generic -> DeviceBrand.Generic
+ SatochipGeneric -> DeviceBrand.Satochip
}
val isJade: Boolean
@@ -38,6 +40,7 @@ enum class DeviceModel(val deviceModel: String) {
LedgerNanoX -> "ledger_nano_x"
TrezorGeneric -> "trezor"
LedgerGeneric -> "ledger"
+ SatochipGeneric -> "satochip"
Generic -> "generic"
}
}
\ No newline at end of file
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/devices/GreenDevice.kt b/common/src/commonMain/kotlin/com/blockstream/common/devices/GreenDevice.kt
index da0dd2521..735e770e6 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/devices/GreenDevice.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/devices/GreenDevice.kt
@@ -40,6 +40,7 @@ interface GreenDevice: DeviceOperatingNetwork {
val isBonded: Boolean
val isUsb: Boolean
val isBle: Boolean
+ val isNfc: Boolean
val deviceState: StateFlow
val firmwareState: StateFlow
val name: String
@@ -47,6 +48,7 @@ interface GreenDevice: DeviceOperatingNetwork {
val isJade: Boolean
val isTrezor: Boolean
val isLedger: Boolean
+ val isSatochip: Boolean
val isOffline: Boolean
val isConnected: Boolean
val heartbeat: Long
@@ -105,6 +107,9 @@ abstract class GreenDeviceImpl constructor(
override val isBle
get() = type == ConnectionType.BLUETOOTH
+ override val isNfc
+ get() = type == ConnectionType.NFC
+
override val isOffline: Boolean
get() = deviceState.value == DeviceState.DISCONNECTED
@@ -120,6 +125,9 @@ abstract class GreenDeviceImpl constructor(
override val isLedger: Boolean
get() = deviceBrand.isLedger
+ override val isSatochip: Boolean
+ get() = deviceBrand.isSatochip
+
override var gdkHardwareWallet: GdkHardwareWallet? by Delegates.observable(null) { _, _, gdkHardwareWallet ->
logger.i { "Set GdkHardwareWallet" }
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt b/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt
index 49463d565..5341c21ea 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt
@@ -1346,6 +1346,7 @@ class GdkSession constructor(
DeviceBrand.Ledger -> DeviceModel.LedgerGeneric
DeviceBrand.Trezor -> DeviceModel.TrezorGeneric
DeviceBrand.Generic -> DeviceModel.Generic
+ DeviceBrand.Satochip -> DeviceModel.SatochipGeneric
}
}
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/gdk/data/Device.kt b/common/src/commonMain/kotlin/com/blockstream/common/gdk/data/Device.kt
index aae192bf6..068fa2bc8 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/gdk/data/Device.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/gdk/data/Device.kt
@@ -35,6 +35,9 @@ data class Device constructor(
val isLedger
get() = name.lowercase() == "ledger"
+ val isSatochip
+ get() = name.lowercase() == "satochip"
+
override fun kSerializer(): KSerializer {
return serializer()
}
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/gdk/device/GdkHardwareWallet.kt b/common/src/commonMain/kotlin/com/blockstream/common/gdk/device/GdkHardwareWallet.kt
index ffa7d1016..c4e3c5dec 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/gdk/device/GdkHardwareWallet.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/gdk/device/GdkHardwareWallet.kt
@@ -25,6 +25,7 @@ interface HardwareWalletInteraction{
fun interactionRequest(gdkHardwareWallet: GdkHardwareWallet, message: String?, isMasterBlindingKeyRequest: Boolean, completable: CompletableDeferred?)
fun requestPinMatrix(deviceBrand: DeviceBrand?): String?
fun requestPassphrase(deviceBrand: DeviceBrand?): String?
+ fun requestNfcToast(deviceBrand: DeviceBrand?, message: String?, completable: CompletableDeferred?)
}
abstract class GdkHardwareWallet {
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/managers/DeviceManager.kt b/common/src/commonMain/kotlin/com/blockstream/common/managers/DeviceManager.kt
index be8fc0b8c..96bd341eb 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/managers/DeviceManager.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/managers/DeviceManager.kt
@@ -55,6 +55,7 @@ open class DeviceManager constructor(
protected val usbDevices = MutableStateFlow>(listOf())
private val bleDevices = MutableStateFlow>(listOf())
+ protected val nfcDevices = MutableStateFlow>(listOf())
private val _status = MutableStateFlow(ScanStatus.Stopped)
val status = _status.asStateFlow()
@@ -71,8 +72,8 @@ open class DeviceManager constructor(
}
}
- val devices = combine(usbDevices, bleDevices, disconnectEvent) { usb, ble, _ ->
- ble.filter { it.deviceState.value == DeviceState.CONNECTED } + usb
+ val devices = combine(usbDevices, bleDevices, disconnectEvent, nfcDevices) { usb, ble, _, nfc->
+ ble.filter { it.deviceState.value == DeviceState.CONNECTED } + usb + nfc
}.stateIn(scope, SharingStarted.Eagerly, emptyList())
var savedDevice: GreenDevice? = null
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/GreenViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/GreenViewModel.kt
index c01c2a019..631dfbe1c 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/models/GreenViewModel.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/models/GreenViewModel.kt
@@ -236,7 +236,8 @@ open class GreenViewModel constructor(
val promo: MutableStateFlow = MutableStateFlow(null)
private var promoImpression: Boolean = false
- private var _deviceRequest: CompletableDeferred? = null
+ //private var _deviceRequest: CompletableDeferred? = null //satodebug use compagnon object
+
private var _bootstrapped: Boolean = false
open val isLoginRequired: Boolean = greenWalletOrNull != null
@@ -871,6 +872,10 @@ open class GreenViewModel constructor(
}
}
+ final override fun requestNfcToast(deviceBrand: DeviceBrand?, message: String?, completable: CompletableDeferred?) {
+ postSideEffect(SideEffects.DeviceRequestNfcToast(message, completable = completable))
+ }
+
protected open suspend fun denominatedValue(): DenominatedValue? = null
protected open fun setDenominatedValue(denominatedValue: DenominatedValue) { }
protected open fun errorReport(exception: Throwable): SupportData? { return null}
@@ -1019,5 +1024,6 @@ open class GreenViewModel constructor(
companion object: Loggable(){
fun preview() = object : GreenViewModel() { }
+ private var _deviceRequest: CompletableDeferred? = null
}
}
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/sideeffects/SideEffects.kt b/common/src/commonMain/kotlin/com/blockstream/common/sideeffects/SideEffects.kt
index d39510c0d..e4b2cea6b 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/sideeffects/SideEffects.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/sideeffects/SideEffects.kt
@@ -60,6 +60,7 @@ class SideEffects : SideEffect {
data object AppReview: SideEffect
data object DeviceRequestPassphrase: SideEffect
data object DeviceRequestPin: SideEffect
+ data class DeviceRequestNfcToast(val message: String?, val completable: CompletableDeferred?): SideEffect
data class DeviceInteraction(
val deviceId: String?,
val message: String?,
diff --git a/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt b/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt
new file mode 100644
index 000000000..1b40373ba
--- /dev/null
+++ b/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt
@@ -0,0 +1,73 @@
+package com.blockstream.compose.devices
+
+import android.content.Context
+import android.hardware.usb.UsbDevice
+import com.blockstream.common.devices.ActivityProvider
+import com.blockstream.common.devices.AndroidDevice
+import com.blockstream.common.devices.ConnectionType
+import com.blockstream.common.devices.DeviceBrand
+import com.blockstream.common.devices.DeviceManagerAndroid
+import com.blockstream.common.devices.GreenDevice
+import com.blockstream.common.devices.NfcDevice
+import com.blockstream.common.devices.NfcDeviceType
+import com.blockstream.common.gdk.Gdk
+import com.blockstream.common.gdk.data.Network
+import com.blockstream.common.gdk.device.HardwareConnectInteraction
+import com.blockstream.common.utils.Loggable
+import com.juul.kable.Peripheral
+
+class SatochipDevice constructor(
+ context: Context,
+ deviceManager: DeviceManagerAndroid,
+ usbDevice: UsbDevice? = null,
+ type: ConnectionType,
+ peripheral: Peripheral? = null,
+ isBonded: Boolean = false,
+ nfcDevice: NfcDevice? = null,
+ activityProvider: ActivityProvider? = null
+) : AndroidDevice(
+ context = context,
+ deviceManager = deviceManager,
+ usbDevice = usbDevice,
+ deviceBrand = DeviceBrand.Satochip,
+ type = type,
+ peripheral = peripheral,
+ isBonded = isBonded
+) {
+
+ val nfcDevice: NfcDevice? = nfcDevice
+ val activityProvider: ActivityProvider? = activityProvider
+
+ override suspend fun getOperatingNetworkForEnviroment(greenDevice: GreenDevice, gdk: Gdk, isTestnet: Boolean): Network =
+ if (isTestnet) gdk.networks().testnetBitcoinElectrum else gdk.networks().bitcoinElectrum
+
+ override suspend fun getOperatingNetwork(
+ greenDevice: GreenDevice,
+ gdk: Gdk,
+ interaction: HardwareConnectInteraction
+ ): Network? = interaction.requestNetwork()
+
+ companion object : Loggable() {
+
+ fun fromNfcDevice(
+ deviceManager: DeviceManagerAndroid,
+ nfcDevice: NfcDevice,
+ activityProvider: ActivityProvider?,
+ ): SatochipDevice? {
+ if (nfcDevice.type == NfcDeviceType.SATOCHIP) {
+ return SatochipDevice(
+ context = deviceManager.context,
+ deviceManager = deviceManager,
+ type = ConnectionType.NFC,
+ usbDevice = null,
+ nfcDevice = nfcDevice,
+ activityProvider = activityProvider,
+ )
+ } else {
+ return null
+ }
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt
index bf18e8680..cc700b490 100644
--- a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt
+++ b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt
@@ -1,5 +1,6 @@
package com.blockstream.compose.managers
+import android.app.Activity
import android.bluetooth.BluetoothAdapter
import com.blockstream.common.data.AppInfo
import com.blockstream.common.devices.AndroidDevice
@@ -14,6 +15,7 @@ import com.blockstream.common.gdk.data.Network
import com.blockstream.common.gdk.device.HardwareConnectInteraction
import com.blockstream.common.interfaces.ConnectionResult
import com.blockstream.compose.devices.LedgerDevice
+import com.blockstream.compose.devices.SatochipDevice
import com.blockstream.compose.devices.TrezorDevice
import com.blockstream.jade.HttpRequestHandler
import com.blockstream.jade.JadeAPI
@@ -26,6 +28,7 @@ import com.btchip.comm.BTChipTransport
import com.btchip.comm.android.BTChipTransportAndroid
import com.greenaddress.greenbits.wallets.BTChipHWWallet
import com.greenaddress.greenbits.wallets.LedgerBLEAdapter
+import com.greenaddress.greenbits.wallets.SatochipHWWallet
import com.greenaddress.greenbits.wallets.TrezorHWWallet
import com.satoshilabs.trezor.Trezor
import kotlinx.coroutines.CoroutineScope
@@ -58,6 +61,8 @@ class DeviceConnectionManagerAndroid constructor(
connectTrezorDevice(it, interaction)
} ?: (device as? LedgerDevice)?.let {
connectLedgerDevice(it, interaction)
+ } ?: (device as? SatochipDevice)?.let {
+ connectSatochipDevice(it, interaction)
} ?: super.connectDevice(device, httpRequestHandler, interaction))
}
@@ -146,6 +151,34 @@ class DeviceConnectionManagerAndroid constructor(
return ConnectionResult()
}
+ private suspend fun connectSatochipDevice(device: SatochipDevice, interaction: HardwareConnectInteraction): ConnectionResult {
+
+ if (device.nfcDevice?.isSeeded == false) {
+ interaction.showError("Satochip card is not setup with a seed! Please import a seed first.")
+ throw Exception("Satochip card is not setup with a seed! Please import a seed first.")
+ }
+
+ val satoDevice = com.blockstream.common.gdk.data.Device(
+ name = "Satochip",
+ supportsArbitraryScripts = true,
+ supportsLowR = false,
+ supportsHostUnblinding = true,
+ supportsExternalBlinding = false,
+ supportsLiquid = if (device.nfcDevice?.supportsLiquid == true) DeviceSupportsLiquid.Lite else DeviceSupportsLiquid.None,
+ supportsAntiExfilProtocol = DeviceSupportsAntiExfilProtocol.None
+ )
+
+ val pin: String? = null;
+ //val pin: String? = satochipInteraction?.requestPassphrase(DeviceBrand.Satochip)
+
+ // provide activity and context needed for NFC
+ val activity: Activity? = device.activityProvider?.getCurrentActivity()
+
+ device.gdkHardwareWallet = SatochipHWWallet(satoDevice, pin, activity, device.context)
+
+ return ConnectionResult()
+ }
+
private suspend fun connectLedgerDevice(
device: LedgerDevice,
interaction: HardwareConnectInteraction
diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/extensions/Resources.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/extensions/Resources.kt
index 113a46947..61849f6d2 100644
--- a/compose/src/commonMain/kotlin/com/blockstream/compose/extensions/Resources.kt
+++ b/compose/src/commonMain/kotlin/com/blockstream/compose/extensions/Resources.kt
@@ -45,6 +45,7 @@ import blockstream_green.common.generated.resources.ledger_device
import blockstream_green.common.generated.resources.lightning_fill
import blockstream_green.common.generated.resources.liquid
import blockstream_green.common.generated.resources.liquid_testnet
+import blockstream_green.common.generated.resources.nfc_scan
import blockstream_green.common.generated.resources.qr_code
import blockstream_green.common.generated.resources.spv_error
import blockstream_green.common.generated.resources.spv_in_progress
@@ -112,6 +113,7 @@ fun GreenDevice?.actionIcon(): DrawableResource = this?.deviceModel?.actionIcon(
fun DeviceBrand.deviceBrandIcon(): DrawableResource = when (this) {
DeviceBrand.Ledger -> Res.drawable.ledger_device
DeviceBrand.Trezor -> Res.drawable.trezor_device
+ DeviceBrand.Satochip -> Res.drawable.nfc_scan
DeviceBrand.Generic -> Res.drawable.generic_device
else -> Res.drawable.blockstream_devices
}
@@ -123,6 +125,7 @@ fun DeviceModel.icon(): DrawableResource = when (this) {
DeviceModel.TrezorGeneric, DeviceModel.TrezorModelT, DeviceModel.TrezorModelOne -> Res.drawable.trezor_device
DeviceModel.LedgerGeneric, DeviceModel.LedgerNanoS, DeviceModel.LedgerNanoX -> Res.drawable.ledger_device
DeviceModel.Generic -> Res.drawable.generic_device
+ DeviceModel.SatochipGeneric -> Res.drawable.nfc_scan
}
fun DeviceModel.actionIcon(): DrawableResource = when (this) {
diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/sheets/DeviceInteractionBottomSheet.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/sheets/DeviceInteractionBottomSheet.kt
index bb0271dca..91ee066bc 100644
--- a/compose/src/commonMain/kotlin/com/blockstream/compose/sheets/DeviceInteractionBottomSheet.kt
+++ b/compose/src/commonMain/kotlin/com/blockstream/compose/sheets/DeviceInteractionBottomSheet.kt
@@ -97,7 +97,7 @@ fun DeviceInteractionBottomSheet(
val title = when {
isMasterBlindingKeyRequest -> null
- transactionConfirmLook != null || verifyAddress != null -> stringResource(Res.string.id_confirm_on_your_device)
+ transactionConfirmLook != null || verifyAddress != null -> if (viewModel.device?.isNfc == true) "Scan your card" else stringResource(Res.string.id_confirm_on_your_device)
else -> {
message?.stringOrNull()
}
diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/sheets/NfcToastBottomSheet.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/sheets/NfcToastBottomSheet.kt
new file mode 100644
index 000000000..02b78fd87
--- /dev/null
+++ b/compose/src/commonMain/kotlin/com/blockstream/compose/sheets/NfcToastBottomSheet.kt
@@ -0,0 +1,128 @@
+package com.blockstream.compose.sheets
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import blockstream_green.common.generated.resources.Res
+import blockstream_green.common.generated.resources.blockstream_devices
+import blockstream_green.common.generated.resources.id_change
+import blockstream_green.common.generated.resources.id_confirm_on_your_device
+import blockstream_green.common.generated.resources.id_fee
+import blockstream_green.common.generated.resources.id_green_needs_the_master_blinding
+import blockstream_green.common.generated.resources.id_sent_to
+import blockstream_green.common.generated.resources.id_to_show_balances_and
+import blockstream_green.common.generated.resources.nfc_scan
+import blockstream_green.common.generated.resources.phone_keys
+import cafe.adriel.voyager.koin.koinScreenModel
+import com.blockstream.common.Parcelable
+import com.blockstream.common.Parcelize
+import com.blockstream.common.Urls
+import com.blockstream.common.data.GreenWallet
+import com.blockstream.common.events.Events
+import com.blockstream.common.looks.transaction.TransactionConfirmLook
+import com.blockstream.common.managers.DeviceManager
+import com.blockstream.common.models.SimpleGreenViewModel
+import com.blockstream.common.utils.StringHolder
+import com.blockstream.compose.components.GreenAddress
+import com.blockstream.compose.components.GreenAmount
+import com.blockstream.compose.components.GreenBottomSheet
+import com.blockstream.compose.components.GreenColumn
+import com.blockstream.compose.components.LearnMoreButton
+import com.blockstream.compose.extensions.actionIcon
+import com.blockstream.compose.extensions.icon
+import com.blockstream.compose.theme.bodyLarge
+import com.blockstream.compose.theme.titleSmall
+import com.blockstream.compose.theme.whiteMedium
+import org.jetbrains.compose.resources.painterResource
+import org.jetbrains.compose.resources.stringResource
+import org.koin.compose.koinInject
+import org.koin.core.parameter.parametersOf
+
+@Parcelize
+data class NfcToastBottomSheet(
+ val message: String? = null
+) : BottomScreen(), Parcelable {
+
+ @Composable
+ override fun Content() {
+
+
+ val viewModel = koinScreenModel {
+ parametersOf(null, null, "Scan card")
+ }
+
+ NfcToastBottomSheet(
+ viewModel = viewModel,
+ message = StringHolder.create(message),
+ onDismissRequest = onDismissRequest()
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun NfcToastBottomSheet(
+ viewModel: SimpleGreenViewModel,
+ message: StringHolder? = null,
+ onDismissRequest: () -> Unit,
+) {
+
+ val title = "Scan your card" //stringResource(Res.string.id_scan_your_card)
+
+ val deviceIcon = viewModel.device?.icon()
+
+ GreenBottomSheet(
+ title = title,
+ viewModel = viewModel,
+ sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
+ onDismissRequest = onDismissRequest
+ ) {
+
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+
+ Image(
+ //painter = painterResource(deviceIcon ?: Res.drawable.blockstream_devices),
+ //painter = painterResource(Res.drawable.phone_keys),
+ painter = painterResource(Res.drawable.nfc_scan),
+ contentDescription = null,
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .height(160.dp)
+ .padding(bottom = 16.dp)
+ )
+
+ GreenColumn(
+ padding = 0,
+ space = 16,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(
+ rememberScrollState()
+ )
+ ) {
+ if (message != null) {
+ Text(
+ text = message.string(),
+ color = whiteMedium,
+ textAlign = TextAlign.Center,
+ style = titleSmall,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/utils/SideEffects.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/utils/SideEffects.kt
index 6e063ddf0..d750e52b4 100644
--- a/compose/src/commonMain/kotlin/com/blockstream/compose/utils/SideEffects.kt
+++ b/compose/src/commonMain/kotlin/com/blockstream/compose/utils/SideEffects.kt
@@ -145,6 +145,7 @@ import com.blockstream.compose.sheets.JadeFirmwareUpdateBottomSheet
import com.blockstream.compose.sheets.LightningNodeBottomSheet
import com.blockstream.compose.sheets.LocalBottomSheetNavigatorM3
import com.blockstream.compose.sheets.NewJadeConnectedBottomSheet
+import com.blockstream.compose.sheets.NfcToastBottomSheet
import com.blockstream.compose.sheets.NoteBottomSheet
import com.blockstream.compose.sheets.PassphraseBottomSheet
import com.blockstream.compose.sheets.PinMatrixBottomSheet
@@ -545,6 +546,18 @@ fun HandleSideEffect(
bottomSheetNavigator?.show(PinMatrixBottomSheet)
}
+ is SideEffects.DeviceRequestNfcToast -> {
+ val screen = bottomSheetNavigator?.show(NfcToastBottomSheet(message = it.message) )
+
+ scope.launch (context = handleException()) {
+ it.completable?.also { completable ->
+ completable.await()
+ } ?: run { delay(3000L) }
+
+ bottomSheetNavigator?.hide(screen)
+ }
+ }
+
is SideEffects.DeviceInteraction -> {
val screen = bottomSheetNavigator?.show(
DeviceInteractionBottomSheet(
diff --git a/gradle.properties b/gradle.properties
index 894bfcec1..62d68bdfe 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -37,4 +37,7 @@ kapt.use.k2=true
org.gradle.caching=true
#org.gradle.configuration-cache=true
-#kotlin.incremental.native=true
\ No newline at end of file
+#kotlin.incremental.native=true
+
+# satochip debug
+org.gradle.dependency.verification=off
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index aa091488c..c373e5558 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -74,6 +74,7 @@ room = "2.5.2"
protobuf-java = "3.4.0"
media3 = "1.4.0"
+bouncycastle = "1.69"
[libraries]
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
@@ -174,6 +175,8 @@ voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voy
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3" }
+org-bouncycastle = { module = "org.bouncycastle:bcprov-jdk15to18", version.ref = "bouncycastle" }
+
[plugins]
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
diff --git a/green/build.gradle.kts b/green/build.gradle.kts
index f613ced79..14c182fcc 100644
--- a/green/build.gradle.kts
+++ b/green/build.gradle.kts
@@ -200,8 +200,8 @@ dependencies {
/** --- Koin ----------------------------------------------------------------------------- */
// For instrumentation tests
- androidTestImplementation(libs.koin.test)
- androidTestImplementation(libs.koin.test.junit4)
+ //androidTestImplementation(libs.koin.test)
+ //androidTestImplementation(libs.koin.test.junit4)
// For local unit tests
testImplementation(libs.koin.test)
diff --git a/green/src/main/java/com/blockstream/green/GreenActivity.kt b/green/src/main/java/com/blockstream/green/GreenActivity.kt
index 4a1e904ad..20d19a8f8 100644
--- a/green/src/main/java/com/blockstream/green/GreenActivity.kt
+++ b/green/src/main/java/com/blockstream/green/GreenActivity.kt
@@ -17,6 +17,7 @@ import com.blockstream.common.data.CredentialType
import com.blockstream.common.data.GreenWallet
import com.blockstream.common.database.Database
import com.blockstream.common.database.LoginCredentials
+import com.blockstream.common.devices.AndroidActivityProvider
import com.blockstream.common.events.Events
import com.blockstream.common.gdk.Gdk
import com.blockstream.common.gdk.data.Network
@@ -47,6 +48,7 @@ class GreenActivity : FragmentActivity() {
private val database: Database by inject()
private val sessionManager: SessionManager by inject()
private val settingsManager by inject()
+ private val activityProvider: AndroidActivityProvider by inject()
private val mainViewModel: MainViewModel by viewModel()
@@ -54,6 +56,7 @@ class GreenActivity : FragmentActivity() {
installSplashScreen()
super.onCreate(savedInstanceState)
+ activityProvider.setActivity(this)
enableEdgeToEdge()
setContent {
@@ -183,6 +186,11 @@ class GreenActivity : FragmentActivity() {
countly.onStop()
}
+ override fun onDestroy() {
+ super.onDestroy()
+ activityProvider.clearActivity()
+ }
+
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
countly.onConfigurationChanged(newConfig)
diff --git a/green/src/main/java/com/blockstream/green/di/GreenModules.kt b/green/src/main/java/com/blockstream/green/di/GreenModules.kt
index cd85a8fd9..1d4108a50 100644
--- a/green/src/main/java/com/blockstream/green/di/GreenModules.kt
+++ b/green/src/main/java/com/blockstream/green/di/GreenModules.kt
@@ -6,12 +6,16 @@ import android.content.Context
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import com.benasher44.uuid.Uuid
+import com.blockstream.common.devices.ActivityProvider
+import com.blockstream.common.devices.AndroidActivityProvider
import com.blockstream.common.devices.DeviceManagerAndroid
+import com.blockstream.common.devices.NfcDevice
import com.blockstream.common.fcm.FcmCommon
import com.blockstream.common.interfaces.DeviceConnectionInterface
import com.blockstream.common.managers.DeviceManager
import com.blockstream.common.managers.NotificationManager
import com.blockstream.compose.devices.LedgerDevice
+import com.blockstream.compose.devices.SatochipDevice
import com.blockstream.compose.devices.TrezorDevice
import com.blockstream.compose.managers.DeviceConnectionManager
import com.blockstream.compose.managers.DeviceConnectionManagerAndroid
@@ -36,15 +40,19 @@ val greenModules = module {
get()
)
} binds (arrayOf(NotificationManagerAndroid::class, NotificationManager::class))
+
+ single { AndroidActivityProvider() } binds arrayOf(ActivityProvider::class)
+
single {
DeviceManagerAndroid(
+ get(),
get(),
androidContext(),
get(),
get(),
get(),
listOf(LedgerDeviceBLE.SERVICE_UUID.toString(), JadeBleConnection.JADE_SERVICE)
- ) { deviceManagerAndroid: DeviceManagerAndroid, usbDevice: UsbDevice?, bleService: Uuid?, peripheral: Peripheral?, isBonded: Boolean? ->
+ ) { deviceManagerAndroid: DeviceManagerAndroid, usbDevice: UsbDevice?, bleService: Uuid?, peripheral: Peripheral?, isBonded: Boolean?, nfcDevice: NfcDevice?, activityProvider: ActivityProvider? ->
usbDevice?.let {
TrezorDevice.fromUsbDevice(deviceManager = deviceManagerAndroid, usbDevice = usbDevice)
?: LedgerDevice.fromUsbDevice(
@@ -53,6 +61,8 @@ val greenModules = module {
)
} ?: peripheral?.let {
LedgerDevice.fromScan(deviceManager = deviceManagerAndroid, bleService = bleService, peripheral = peripheral, isBonded = isBonded == true)
+ } ?: nfcDevice?.let {
+ SatochipDevice.fromNfcDevice(deviceManager = deviceManagerAndroid, nfcDevice= nfcDevice, activityProvider = activityProvider)
}
}
} binds (arrayOf(DeviceManager::class, DeviceManagerAndroid::class))
diff --git a/hardware/build.gradle.kts b/hardware/build.gradle.kts
index 714446cc7..b7bba24a0 100644
--- a/hardware/build.gradle.kts
+++ b/hardware/build.gradle.kts
@@ -42,6 +42,9 @@ dependencies {
implementation(libs.jackson.datatype.json.org)
/** ----------------------------------------------------------------------------------------- */
+ /** For satochip */
+ implementation(libs.org.bouncycastle)
+
testImplementation(libs.junit)
testImplementation(libs.androidx.core.testing)
testImplementation(libs.mockito.kotlin)
diff --git a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java
new file mode 100644
index 000000000..f3ba54ba1
--- /dev/null
+++ b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java
@@ -0,0 +1,763 @@
+package com.greenaddress.greenbits.wallets;
+
+import static io.ktor.util.CryptoKt.hex;
+
+import android.app.Activity;
+import android.content.Context;
+import android.nfc.NfcAdapter;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+import com.blockstream.common.devices.DeviceBrand;
+import com.blockstream.common.devices.DeviceModel;
+import com.blockstream.common.gdk.data.Account;
+import com.blockstream.common.gdk.data.Device;
+import com.blockstream.common.gdk.data.InputOutput;
+import com.blockstream.common.gdk.data.Network;
+import com.blockstream.common.gdk.device.BlindingFactorsResult;
+import com.blockstream.common.gdk.device.GdkHardwareWallet;
+import com.blockstream.common.gdk.device.HardwareWalletInteraction;
+import com.blockstream.common.gdk.device.SignMessageResult;
+import com.blockstream.common.gdk.device.SignTransactionResult;
+import com.blockstream.libwally.Wally;
+import com.btchip.utils.BufferUtils;
+import com.btchip.utils.VarintUtils;
+import com.google.common.base.Joiner;
+import com.satochip.ApduResponse;
+import com.satochip.ApplicationStatus;
+import com.satochip.Bip32Path;
+import com.satochip.BlockedPINException;
+import com.satochip.CardChannel;
+import com.satochip.CardListener;
+import com.satochip.NfcActionObject;
+import com.satochip.NfcActionStatus;
+import com.satochip.NfcActionType;
+import com.satochip.NfcCardManager;
+import com.satochip.SatochipCommandSet;
+import com.satochip.SatochipException;
+import com.satochip.SatochipParser;
+import com.satochip.WrongPINException;
+import com.satochip.WrongPINLegacyException;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import kotlinx.coroutines.CompletableDeferred;
+import kotlinx.coroutines.CompletableDeferredKt;
+import kotlinx.coroutines.flow.MutableStateFlow;
+
+
+public class SatochipHWWallet extends GdkHardwareWallet implements CardListener {
+
+ private static final String TAG = SatochipHWWallet.class.getSimpleName();
+
+ /** The string that prefixes all text messages signed using Bitcoin keys. */
+ private static final String BITCOIN_SIGNED_MESSAGE_HEADER = "Bitcoin Signed Message:\n";
+ private static final byte[] BITCOIN_SIGNED_MESSAGE_HEADER_BYTES = BITCOIN_SIGNED_MESSAGE_HEADER.getBytes(StandardCharsets.UTF_8);
+
+ private static final byte SIGHASH_ALL = 1;
+
+ private final Map mUserXPubs = new HashMap<>();
+
+ private final Device device;
+ private final DeviceModel model;
+ private final String firmwareVersion = "x.y-z.w"; //todo
+
+ private String pin;
+ private final Activity activity;
+ private final Context context;
+ private final CardChannel channel = null;
+ private NfcAdapter nfcAdapter = null;
+ private NfcActionObject actionObject = new NfcActionObject();
+
+
+ public SatochipHWWallet(Device device, String pin, Activity activity, Context context){
+ this.device = device;
+ this.model = DeviceModel.SatochipGeneric;
+ this.pin = pin;
+ this.activity = activity;
+ this.context = context;
+
+ NfcCardManager cardManager = new NfcCardManager();
+ cardManager.setCardListener(this);
+ cardManager.start();
+
+ nfcAdapter = NfcAdapter.getDefaultAdapter(this.context);
+ nfcAdapter.enableReaderMode(
+ activity,
+ cardManager,
+ NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_NFC_B | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
+ null
+ );
+
+ }
+
+ @Override
+ public synchronized void disconnect() {
+ Log.i(TAG, "SATODEBUG SatochipHWWallet disconnect start");
+ // No-op
+ }
+
+
+ public void onConnected(CardChannel channel) {
+
+ Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() Card is connected");
+ try {
+
+ if (this.actionObject.actionType == NfcActionType.none) {
+ this.actionObject.actionStatus = NfcActionStatus.none;
+ Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() nothing to do => disconnection!");
+ onDisconnected();
+ return;
+ }
+
+ SatochipCommandSet cmdSet = new SatochipCommandSet(channel);
+
+ ApduResponse rapduSelect = cmdSet.cardSelect("satochip").checkOK();
+ Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() applet selected");
+ // cardStatus
+ ApduResponse rapduStatus = cmdSet.cardGetStatus();//To update status if it's not the first reading
+ ApplicationStatus cardStatus = cmdSet.getApplicationStatus(); //applicationStatus ?: return
+ Log.i(TAG, "SATODEBUG SatochipHWWallet readCard cardStatus: $cardStatus");
+
+ // verify PIN
+ if (this.pin != null) {
+ byte[] pinBytes = this.pin.getBytes(Charset.forName("UTF-8"));
+ ApduResponse rapduPin = cmdSet.cardVerifyPIN(pinBytes);
+ } else {
+ if (actionObject.hwInteraction != null){
+ this.pin = actionObject.hwInteraction.requestPassphrase(DeviceBrand.Satochip);
+ byte[] pinBytes = this.pin.getBytes(Charset.forName("UTF-8"));
+ ApduResponse rapduPin = cmdSet.cardVerifyPIN(pinBytes);
+ } else {
+ throw new SatochipException(SatochipException.ExceptionReason.PIN_UNDEFINED);
+ }
+ }
+
+ // execute commands depending on actionType
+ if (this.actionObject.actionType == NfcActionType.getXpubs){
+ onConnectedGetXpubs(cmdSet);
+ } else if (this.actionObject.actionType == NfcActionType.signMessage) {
+ onConnectedSignMessage(cmdSet);
+ } else if (this.actionObject.actionType == NfcActionType.signTransaction){
+ onConnectedSignTransaction(cmdSet);
+ } else if (this.actionObject.actionType == NfcActionType.getMasterBlindingKey){
+ onConnectedGetMasterBlindingKey(cmdSet);
+ }
+
+ // disconnect
+ Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() trigger disconnection!");
+ onDisconnected();
+
+ } catch (SatochipException e) {
+ this.pin = null;
+ Log.e(TAG, "SATODEBUG onConnected: ERROR: "+ e);
+ onDisconnected();
+ if (actionObject.hwInteraction != null){
+ actionObject.hwInteraction.interactionRequest(this, "No PIN available", false, null);
+ }
+ } catch (WrongPINException e) {
+ this.pin = null;
+ Log.e(TAG, "SATODEBUG onConnected: WRONG PIN! "+ e);
+ onDisconnected();
+ if (actionObject.hwInteraction != null){
+ String message = "WRONG PIN!\n"+e.getRetryAttempts() + " tries remaining";
+ actionObject.hwInteraction.interactionRequest(this, message, false, null);
+ }
+ } catch (WrongPINLegacyException e) {
+ this.pin = null;
+ Log.e(TAG, "SATODEBUG onConnected: WRONG PIN LEGACY! "+ e);
+ onDisconnected();
+ if (actionObject.hwInteraction != null){
+ String message = "WRONG PIN!\n";
+ actionObject.hwInteraction.interactionRequest(this, message, false, null);
+ }
+ } catch (BlockedPINException e) {
+ this.pin = null;
+ Log.e(TAG, "SATODEBUG onConnected exception: "+ e);
+ onDisconnected();
+ if (actionObject.hwInteraction != null){
+ String message = "CARD BLOCKED!\nYou need to reset your card.";
+ actionObject.hwInteraction.interactionRequest(this, message, false, null);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "SATODEBUG onConnected exception: " + e);
+ e.printStackTrace();
+ onDisconnected();
+ }
+ }
+
+ public void onConnectedGetXpubs(SatochipCommandSet cmdSet) throws Exception {
+
+ // get paths
+ List extends List> paths = this.actionObject.pathsParam;
+ final List xpubs = new ArrayList<>(paths.size());
+
+ for (List path : paths) {
+ Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedGetXpubs() path: " + path);
+ final String key = Joiner.on("/").join(path);
+
+ if (!mUserXPubs.containsKey(key)) {
+
+ Bip32Path bip32path = pathToBip32Path(path);
+// Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedGetXpubs() depth: " + bip32path.getDepth()); //debug
+// Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedGetXpubs() bip32path: " + hex(bip32path.getBytes())); //debug
+
+ // get xpub from satochip
+ int xtype = this.actionObject.networkParam.getVerPublic();
+ String xpub = cmdSet.cardBip32GetXpub(bip32path, xtype);
+ Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedGetXpubs() xpub: " + xpub);
+
+ // cache xpub
+ mUserXPubs.put(key, xpub);
+
+ }{
+ Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedGetXpubs() recovered xpub from CACHE");
+ }
+ // update xpubs list
+ xpubs.add(mUserXPubs.get(key));
+ }
+
+ // action finished
+ this.actionObject.xpubsResult = xpubs;
+ this.actionObject.actionStatus = NfcActionStatus.finished;
+ }
+
+ public void onConnectedSignMessage(SatochipCommandSet cmdSet) throws Exception {
+
+ // get path
+ List path = this.actionObject.pathParam;
+ Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() path: " + path);
+
+ // derive key
+ Bip32Path bip32path = pathToBip32Path(path);
+ //Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() depth: " + bip32path.getDepth()); //debug
+ //Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() bip32path: " + hex(bip32path.getBytes())); //debug
+ byte[][] extendedKey = cmdSet.cardBip32GetExtendedKey(bip32path);
+ //Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() extendedKey_0: " + hex(extendedKey[0])); //debug
+ //Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() extendedKey_1: " + hex(extendedKey[1])); //debug
+
+ // compute message hash
+ String message = this.actionObject.messageParam;
+ //String message = "DEBUG TEST SATOCHIP";
+ Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() message: " + message); //debug
+ byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
+ Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() messageBytes: " + hex(messageBytes)); //debug
+ byte[] formatedMessageBytes;
+ try {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ bos.write(BITCOIN_SIGNED_MESSAGE_HEADER_BYTES.length);
+ bos.write(BITCOIN_SIGNED_MESSAGE_HEADER_BYTES);
+ VarintUtils.write(bos, messageBytes.length);
+ bos.write(messageBytes);
+ formatedMessageBytes = bos.toByteArray();
+ //Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() formatedMessageBytes: " + hex(formatedMessageBytes)); //debug
+ } catch (IOException e) {
+ throw new RuntimeException(e); // Cannot happen.
+ }
+ byte[] hashBytes = Wally.sha256d(formatedMessageBytes); // double hash
+ //Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() hashBytes: " + hex(hashBytes)); //debug
+
+// // debug
+// byte[] messageBytesWally = Wally.format_bitcoin_message(messageBytes, 0); // should be equal to formatedMessageBytes
+// Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() messageBytesWally: " + hex(messageBytesWally)); //debug
+// byte[] messageBytesWally2 = Wally.format_bitcoin_message(messageBytes, 1); // should be equal to hashBytes
+// Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() messageBytesWally2: " + hex(messageBytesWally2)); //debug
+
+ // sign hash
+ byte keynbr = (byte) 0xFF;
+ byte[] chalresponse = null;
+ ApduResponse rapdu = cmdSet.cardSignHash(keynbr, hashBytes, chalresponse);
+ byte[] sigBytes = rapdu.getData();
+ //Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() sigBytes: " + hex(sigBytes)); //debug
+
+ // verify sig
+ SatochipParser parser = new SatochipParser();
+ boolean isOk = parser.verifySig(Wally.sha256(formatedMessageBytes), sigBytes, extendedKey[0]);
+ Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() isOk: " + isOk); //debug
+
+ // format signature
+ final String sigHex = Wally.hex_from_bytes(sigBytes);
+ Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() sigHex: " + sigHex); //debug
+
+ // action finished
+ this.actionObject.signatureResult = sigHex;
+ this.actionObject.actionStatus = NfcActionStatus.finished;
+ }
+
+ public void onConnectedSignTransaction(SatochipCommandSet cmdSet) throws Exception {
+
+ // get paths, hashes & set sig result
+ final List extends List> paths = this.actionObject.pathsParam;
+ final List hashes = this.actionObject.hashesParam;
+ final int inputSize = paths.size();
+ List signaturesResult = new ArrayList<>(inputSize);
+ for (int i = 0; i < inputSize; ++i) {
+
+ // get path
+ List path = paths.get(i);
+ //Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignTransaction() path: " + path);
+
+ // derive key
+ Bip32Path bip32path = pathToBip32Path(path);
+ //Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignTransaction() depth: " + bip32path.getDepth()); //debug
+ //Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignTransaction() bip32path: " + hex(bip32path.getBytes())); //debug
+ byte[][] extendedKey = cmdSet.cardBip32GetExtendedKey(bip32path);
+ //Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignTransaction() extendedKey_0: " + hex(extendedKey[0])); //debug
+ //Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignTransaction() extendedKey_1: " + hex(extendedKey[1])); //debug
+
+ // compress pubkey
+ byte[] compressedPubkey = Arrays.copyOf(extendedKey[0], Wally.EC_PUBLIC_KEY_LEN);
+ if (compressedPubkey[Wally.EC_PUBLIC_KEY_LEN-1]%2 == 0){
+ compressedPubkey[0] = 0x02;
+ } else {
+ compressedPubkey[0] = 0x03;
+ }
+
+ // get & sign hash
+ byte[] hashBytes = hashes.get(i);
+ byte keynbr = (byte) 0xFF;
+ byte[] chalresponse = null;
+ ApduResponse rapdu = cmdSet.cardSignHash(keynbr, hashBytes, chalresponse);
+ byte[] sigBytes = rapdu.getData();
+ Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignTransaction() sigBytes: " + hex(sigBytes)); //debug
+
+ // verify sig
+// SatochipParser parser = new SatochipParser();
+// boolean isOk = parser.verifySig(hashBytes, sigBytes, extendedKey[0]);
+// Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignTransaction() isOk: " + isOk); //debug
+
+ // convert to compact
+ byte[] compactSigBytes = Wally.ec_sig_from_der(sigBytes);
+ Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignTransaction() compactSigBytes: " + hex(compactSigBytes)); //debug
+
+ // verify sig
+// int compactSigVerif = Wally.ec_sig_verify(compressedPubkey, hashBytes, Wally.EC_FLAG_ECDSA, compactSigBytes);
+// Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignTransaction() compactSigVerif: " + compactSigVerif); //debug
+
+ // sanitize sig
+ byte[] lowsCompactSigBytes = new byte[Wally.EC_SIGNATURE_LEN];
+ Wally.ec_sig_normalize(compactSigBytes, lowsCompactSigBytes);
+ Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignTransaction() lowsCompactSigBytes: " + hex(lowsCompactSigBytes)); //debug
+
+ // verify sig
+// int lowsCompactSigVerif = Wally.ec_sig_verify(compressedPubkey, hashBytes, Wally.EC_FLAG_ECDSA, lowsCompactSigBytes);
+// Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignTransaction() lowsCompactSigVerif: " + lowsCompactSigVerif); //debug
+
+ // convert back to der
+ final byte[] derSigBytes = new byte[Wally.EC_SIGNATURE_DER_MAX_LEN];
+ final int len = Wally.ec_sig_to_der(lowsCompactSigBytes, derSigBytes);
+ final String sigHex = Wally.hex_from_bytes(Arrays.copyOf(derSigBytes, len)) + "01";
+
+ Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignTransaction() sigHex: " + sigHex); //debug
+ signaturesResult.add(sigHex);
+ }
+
+ // action finished
+ this.actionObject.signaturesResult = signaturesResult;
+ this.actionObject.actionStatus = NfcActionStatus.finished;
+ }
+
+ public void onConnectedGetMasterBlindingKey(SatochipCommandSet cmdSet) throws Exception {
+
+ byte[] blindingKey = cmdSet.cardBip32GetLiquidMasterBlindingKey();
+
+ // action finished
+ this.actionObject.blindingKeyResult = blindingKey;
+ this.actionObject.actionStatus = NfcActionStatus.finished;
+ }
+
+
+ public void onDisconnected() {
+ Log.i(TAG, "SATODEBUG SatochipHWWallet onDisconnected: Card disconnected!");
+ }
+
+ @NonNull
+ @Override
+ public synchronized List getXpubs(@NonNull Network network, @NonNull List extends List> paths, @Nullable HardwareWalletInteraction hwInteraction) {
+ Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getXpubs start");
+ Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getXpubs paths: " + paths);
+
+
+ // first step: check if xpubs are all available in cache
+ boolean isCached = true;
+ final List cachedXpubs = new ArrayList<>(paths.size());
+ for (List path : paths) {
+ //Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() path: " + path);
+ final String key = Joiner.on("/").join(path);
+ if (mUserXPubs.containsKey(key)) {
+ cachedXpubs.add(mUserXPubs.get(key));
+ } else {
+ isCached = false;
+ }
+ }
+ if (isCached){
+ Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() XPUBS IN CACHE!");
+ return cachedXpubs;
+ }
+
+ CompletableDeferred completable = CompletableDeferredKt.CompletableDeferred(null);
+
+ // request to card if not cached already
+ try {
+ if(hwInteraction != null) {
+ hwInteraction.requestNfcToast(DeviceBrand.Satochip, "Exporting xpub...", completable);
+ }
+
+ this.actionObject.actionStatus = NfcActionStatus.busy;
+ this.actionObject.actionType = NfcActionType.getXpubs;
+ this.actionObject.networkParam = network;
+ this.actionObject.pathsParam = paths;
+ this.actionObject.hwInteraction = hwInteraction;
+
+ // poll for result from cardListener onConnected
+ while (this.actionObject.actionStatus == NfcActionStatus.busy) {
+ TimeUnit.MILLISECONDS.sleep(500);
+ Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() SLEEP");
+ }
+
+ // get result and reset action
+ this.actionObject.actionStatus = NfcActionStatus.none;
+ this.actionObject.actionType = NfcActionType.none;
+ final List xpubs = this.actionObject.xpubsResult;
+ return xpubs;
+
+ } catch (Exception e) {
+ Log.e("SatochipHWWallet", "getXpubs exception: " + e);
+ } finally {
+ completable.complete(true);
+ }
+
+ return null;
+ }
+
+ @NonNull
+ @Override
+ public SignMessageResult signMessage(@NonNull List path, @NonNull String message, boolean useAeProtocol, @Nullable String aeHostCommitment, @Nullable String aeHostEntropy, @Nullable HardwareWalletInteraction hwInteraction) {
+ Log.i("SatochipHWWallet", "signMessage start");
+ Log.i("SatochipHWWallet", "signMessage start path: " + path);
+ //Log.i("SatochipHWWallet", "signMessage start message: " + message);
+
+ if (useAeProtocol) {
+ throw new RuntimeException("Hardware Wallet does not support the Anti-Exfil protocol");
+ }
+
+ CompletableDeferred completable = CompletableDeferredKt.CompletableDeferred(null);
+
+ try {
+ if(hwInteraction != null) {
+ hwInteraction.requestNfcToast(DeviceBrand.Satochip, "Signing login message...", completable);
+ }
+
+ this.actionObject.actionStatus = NfcActionStatus.busy;
+ this.actionObject.actionType = NfcActionType.signMessage;
+ this.actionObject.pathParam = path;
+ this.actionObject.messageParam = message;
+ this.actionObject.hwInteraction = hwInteraction;
+
+
+ // poll for result from cardListener onConnected
+ while (this.actionObject.actionStatus == NfcActionStatus.busy) {
+ TimeUnit.MILLISECONDS.sleep(500);
+ Log.i(TAG, "SATODEBUG SatochipHWWallet signMessage() SLEEP");
+ }
+
+ // get result and reset action
+ this.actionObject.actionStatus = NfcActionStatus.none;
+ this.actionObject.actionType = NfcActionType.none;
+ final String signatureResult = this.actionObject.signatureResult;
+ Log.i(TAG, "SATODEBUG SatochipHWWallet signMessage() signatureResult: " + signatureResult);
+ return new SignMessageResult(signatureResult, null);
+ } catch (Exception e) {
+ Log.e(TAG, "signMessage exception: " + e);
+ } finally {
+ completable.complete(true);
+ }
+
+ //TODO
+ String signature = "TODO-SATOCHIP-SIGN-MSG";
+ return new SignMessageResult(signature, null);
+ }
+
+ @NonNull
+ @Override
+ public SignTransactionResult signTransaction(@NonNull Network network, @NonNull String transaction, @NonNull List inputs, @NonNull List outputs, @Nullable Map transactions, boolean useAeProtocol, @Nullable HardwareWalletInteraction hwInteraction) {
+ Log.i("SatochipHWWallet", "signTransaction start");
+
+ CompletableDeferred completable = CompletableDeferredKt.CompletableDeferred(null);
+
+ final byte[] txBytes = Wally.hex_to_bytes(transaction);
+
+ if(network.isLiquid()){
+
+ try {
+ if (hwInteraction != null) {
+ hwInteraction.requestNfcToast(DeviceBrand.Satochip, "Signing transaction...", completable);
+ }
+
+ final Object wallyTx = Wally.tx_from_bytes(txBytes, Wally.WALLY_TX_FLAG_USE_ELEMENTS);
+
+ // get tx hash signature for each inputs
+ final int inputSize = inputs.size();
+ final List> pathsParam = new ArrayList<>(inputSize);
+ final List hashesParam = new ArrayList<>(inputSize);
+ for (int i = 0; i < inputSize; ++i) {
+ Log.i("SatochipHWWallet", "signTransaction() input index: " + i);
+ final InputOutput in = inputs.get(i);
+ Log.i("SatochipHWWallet", "signTransaction() inputs[i]: " + in);
+ final byte[] script = Wally.hex_to_bytes(in.getPrevoutScript());
+ Log.i("SatochipHWWallet", "signTransaction() input script[i]: " + Wally.hex_from_bytes(script));
+ final byte[] satoshi_bytes = Wally.hex_to_bytes(in.getCommitment()); // ok!
+ Log.i("SatochipHWWallet", "signTransaction() input satoshi_bytes[i]: " + Wally.hex_from_bytes(satoshi_bytes));
+ final long sighash = SIGHASH_ALL;
+ final long flags = Wally.WALLY_TX_FLAG_USE_WITNESS;
+
+ byte[] hash_out = new byte[32];
+ Wally.tx_get_elements_signature_hash(
+ wallyTx,
+ i,
+ script,
+ satoshi_bytes,
+ sighash,
+ flags,
+ hash_out
+ );
+ Log.i("SatochipHWWallet", "signTransaction() input hash_out[i]: " + Wally.hex_from_bytes(hash_out));
+ hashesParam.add(hash_out);
+
+ // get derivation path for each input
+ List path = in.getUserPathAsInts();
+ Log.i("SatochipHWWallet", "signTransaction() input path[i]: " + path);
+ pathsParam.add(in.getUserPathAsInts());
+ }
+
+ // create action
+ this.actionObject.actionStatus = NfcActionStatus.busy;
+ this.actionObject.actionType = NfcActionType.signTransaction;
+ this.actionObject.pathsParam = pathsParam;
+ this.actionObject.hashesParam = hashesParam;
+ this.actionObject.hwInteraction = hwInteraction;
+
+ // poll for result from cardListener onConnected
+ while (this.actionObject.actionStatus == NfcActionStatus.busy) {
+ TimeUnit.MILLISECONDS.sleep(500);
+ Log.i(TAG, "SATODEBUG SatochipHWWallet signTransaction() SLEEP");
+ }
+
+ // get result and reset action
+ this.actionObject.actionStatus = NfcActionStatus.none;
+ this.actionObject.actionType = NfcActionType.none;
+ List sigs = this.actionObject.signaturesResult;
+ Log.i(TAG, "SATODEBUG SatochipHWWallet signTransaction() signatureResult: " + sigs);
+ return new SignTransactionResult(sigs, null);
+
+ } catch (final Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException("Signing Error: " + e.getMessage());
+ } finally {
+ completable.complete(true);
+ }
+
+ }
+
+ try {
+
+ if (useAeProtocol) {
+ throw new RuntimeException("Hardware Wallet does not support the Anti-Exfil protocol");
+ }
+
+ if(hwInteraction != null) {
+ hwInteraction.requestNfcToast(DeviceBrand.Satochip, "Signing transaction...", completable);
+ }
+
+ final Object wallyTx = Wally.tx_from_bytes(txBytes, Wally.WALLY_TX_FLAG_USE_WITNESS);
+
+ // get tx hash signature for each inputs
+ final int inputSize = inputs.size();
+ final List> pathsParam = new ArrayList<>(inputSize);
+ final List hashesParam = new ArrayList<>(inputSize);
+ Log.i("SatochipHWWallet", "signTransaction() inputs.size(): " + inputSize);
+ for (int i = 0; i < inputSize; ++i) {
+ Log.i("SatochipHWWallet", "signTransaction() input index: " + i);
+ final InputOutput in = inputs.get(i);
+ Log.i("SatochipHWWallet", "signTransaction() inputs[i]: " + in);
+ final byte[] script = Wally.hex_to_bytes(in.getPrevoutScript());
+ Log.i("SatochipHWWallet", "signTransaction() input script[i]: " + Wally.hex_from_bytes(script));
+ final long satoshi = in.getSatoshi();
+ Log.i("SatochipHWWallet", "signTransaction() input satoshi[i]: " + satoshi);
+ final long sighash = SIGHASH_ALL;
+ final long flags = Wally.WALLY_TX_FLAG_USE_WITNESS;
+
+ byte[] hash_out = new byte[32];
+ byte[] hash = Wally.tx_get_btc_signature_hash(
+ wallyTx,
+ i,
+ script,
+ satoshi,
+ sighash,
+ flags,
+ hash_out
+ );
+ Log.i("SatochipHWWallet", "signTransaction() input hash_out[i]: " + Wally.hex_from_bytes(hash_out));
+ hashesParam.add(hash_out);
+
+ // get derivation path for each input
+ List path = in.getUserPathAsInts();
+ Log.i("SatochipHWWallet", "signTransaction() input path[i]: " + path);
+ pathsParam.add(in.getUserPathAsInts());
+ }
+
+ // create action
+ this.actionObject.actionStatus = NfcActionStatus.busy;
+ this.actionObject.actionType = NfcActionType.signTransaction;
+ this.actionObject.pathsParam = pathsParam;
+ this.actionObject.hashesParam = hashesParam;
+ this.actionObject.hwInteraction = hwInteraction;
+
+ // poll for result from cardListener onConnected
+ while (this.actionObject.actionStatus == NfcActionStatus.busy) {
+ TimeUnit.MILLISECONDS.sleep(500);
+ Log.i(TAG, "SATODEBUG SatochipHWWallet signTransaction() SLEEP");
+ }
+
+ // get result and reset action
+ this.actionObject.actionStatus = NfcActionStatus.none;
+ this.actionObject.actionType = NfcActionType.none;
+ List sigs = this.actionObject.signaturesResult;
+ Log.i(TAG, "SATODEBUG SatochipHWWallet signTransaction() signatureResult: " + sigs);
+ return new SignTransactionResult(sigs, null);
+
+ } catch (final Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException("Signing Error: " + e.getMessage());
+ } finally {
+ completable.complete(true);
+ }
+
+ }
+
+
+ @NonNull
+ @Override
+ public synchronized String getMasterBlindingKey(@Nullable HardwareWalletInteraction hwInteraction) {
+
+ CompletableDeferred completable = CompletableDeferredKt.CompletableDeferred(null);
+
+ try {
+ if(hwInteraction != null) {
+ hwInteraction.requestNfcToast(DeviceBrand.Satochip, "Exporting Liquid Master Blinding Key...", completable);
+ }
+
+ this.actionObject.actionStatus = NfcActionStatus.busy;
+ this.actionObject.actionType = NfcActionType.getMasterBlindingKey;
+ this.actionObject.hwInteraction = hwInteraction;
+
+ // poll for result from cardListener onConnected
+ while (this.actionObject.actionStatus == NfcActionStatus.busy) {
+ TimeUnit.MILLISECONDS.sleep(500);
+ Log.i(TAG, "SATODEBUG SatochipHWWallet getMasterBlindingKey() SLEEP");
+ }
+
+ // get result and reset action
+ this.actionObject.actionStatus = NfcActionStatus.none;
+ this.actionObject.actionType = NfcActionType.none;
+ final byte[] blindingKey= this.actionObject.blindingKeyResult;
+ return Wally.hex_from_bytes(blindingKey);
+
+ } catch (Exception e) {
+ Log.e("SatochipHWWallet", "getMasterBlindingKey exception: " + e);
+ } finally {
+ completable.complete(true);
+ }
+
+ return null;
+ }
+
+ @Override
+ public synchronized String getBlindingKey(String scriptHex, @Nullable HardwareWalletInteraction hwInteraction) {
+ throw new RuntimeException("Master Blinding Key is not supported");
+ }
+
+ @Override
+ public synchronized String getBlindingNonce(String pubkey, String scriptHex, @Nullable HardwareWalletInteraction hwInteraction) {
+ throw new RuntimeException("Master Blinding Key is not supported");
+ }
+
+ @Override
+ public synchronized BlindingFactorsResult getBlindingFactors(final List inputs, final List outputs, @Nullable HardwareWalletInteraction hwInteraction) {
+ throw new RuntimeException("Master Blinding Key is not supported");
+ }
+
+ @Override
+ public synchronized String getGreenAddress(final Network network, final Account account, final List path, final long csvBlocks, HardwareWalletInteraction hwInteraction) {
+ if (network.isMultisig()) {
+ throw new RuntimeException("Hardware Wallet does not support displaying Green Multisig Shield addresses");
+ }
+
+ if (network.isLiquid()) {
+ throw new RuntimeException("Hardware Wallet does not support displaying Liquid addresses");
+ }
+
+ // todo
+ return "TODO";
+ }
+
+ @Nullable
+ @Override
+ public MutableStateFlow getDisconnectEvent() {
+ Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getDisconnectEvent start");
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String getFirmwareVersion() {
+ Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getFirmwareVersion start");
+ return firmwareVersion;
+ }
+
+ @NonNull
+ @Override
+ public DeviceModel getModel() {
+ Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getModel start");
+ return model;
+ }
+
+ @NonNull
+ @Override
+ public Device getDevice() {
+ Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getDevice start");
+ return device;
+ }
+
+
+ /* */
+ private Bip32Path pathToBip32Path(final List path) throws Exception {
+ final int depth = path.size();
+ if (depth == 0) {
+ Bip32Path bip32Path = new Bip32Path(0, new byte[0]);
+ }
+ if (depth > 10) {
+ throw new Exception("Path too long");
+ }
+ ByteArrayOutputStream result = new ByteArrayOutputStream();
+ for (final Integer element : path) {
+ BufferUtils.writeUint32BE(result, element);
+ }
+ Bip32Path bip32Path = new Bip32Path(depth, result.toByteArray());
+ return bip32Path;
+ }
+
+
+}
diff --git a/hardware/src/main/java/com/satochip/ApduCommand.java b/hardware/src/main/java/com/satochip/ApduCommand.java
new file mode 100644
index 000000000..820324dbf
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/ApduCommand.java
@@ -0,0 +1,147 @@
+package com.satochip;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * ISO7816-4 APDU.
+ */
+public class ApduCommand {
+ protected int cla;
+ protected int ins;
+ protected int p1;
+ protected int p2;
+ protected int lc;
+ protected byte[] data;
+ protected boolean needsLE;
+ public static final String HEXES = "0123456789ABCDEF";
+
+ /**
+ * Constructs an APDU with no response data length field. The data field cannot be null, but can be a zero-length array.
+ *
+ * @param cla class byte
+ * @param ins instruction code
+ * @param p1 P1 parameter
+ * @param p2 P2 parameter
+ * @param data the APDU data
+ */
+ public ApduCommand(int cla, int ins, int p1, int p2, byte[] data) {
+ this(cla, ins, p1, p2, data, false);
+ }
+
+ /**
+ * Constructs an APDU with an optional data length field. The data field cannot be null, but can be a zero-length array.
+ * The LE byte, if sent, is set to 0.
+ *
+ * @param cla class byte
+ * @param ins instruction code
+ * @param p1 P1 parameter
+ * @param p2 P2 parameter
+ * @param data the APDU data
+ * @param needsLE whether the LE byte should be sent or not
+ */
+ public ApduCommand(int cla, int ins, int p1, int p2, byte[] data, boolean needsLE) {
+ this.cla = cla & 0xff;
+ this.ins = ins & 0xff;
+ this.p1 = p1 & 0xff;
+ this.p2 = p2 & 0xff;
+ this.data = data;
+ this.needsLE = needsLE;
+ }
+
+ /**
+ * Serializes the APDU in order to send it to the card.
+ *
+ * @return the byte array representation of the APDU
+ */
+ public byte[] serialize() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ out.write(this.cla);
+ out.write(this.ins);
+ out.write(this.p1);
+ out.write(this.p2);
+ out.write(this.data.length);
+ out.write(this.data);
+
+ if (this.needsLE) {
+ out.write(0); // Response length
+ }
+
+ return out.toByteArray();
+ }
+
+ /**
+ * Serializes the APDU to human readable hex string format
+ *
+ * @return the hex string representation of the APDU
+ */
+ public String toHexString() {
+ try{
+ byte[] raw= this.serialize();
+ if ( raw == null ) {
+ return "";
+ }
+ final StringBuilder hex = new StringBuilder( 2 * raw.length );
+ for ( final byte b : raw ) {
+ hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F)));
+ }
+ return hex.toString();
+ } catch (Exception e){
+ return "Exception in ApduCommand.toHexString()";
+ }
+ }
+
+ /**
+ * Returns the CLA of the APDU
+ *
+ * @return the CLA of the APDU
+ */
+ public int getCla() {
+ return cla;
+ }
+
+ /**
+ * Returns the INS of the APDU
+ *
+ * @return the INS of the APDU
+ */
+ public int getIns() {
+ return ins;
+ }
+
+ /**
+ * Returns the P1 of the APDU
+ *
+ * @return the P1 of the APDU
+ */
+ public int getP1() {
+ return p1;
+ }
+
+ /**
+ * Returns the P2 of the APDU
+ *
+ * @return the P2 of the APDU
+ */
+ public int getP2() {
+ return p2;
+ }
+
+ /**
+ * Returns the data field of the APDU
+ *
+ * @return the data field of the APDU
+ */
+ public byte[] getData() {
+ return data;
+ }
+
+ /**
+ * Returns whether LE is sent or not.
+ *
+ * @return whether LE is sent or not
+ */
+ public boolean getNeedsLE() {
+ return this.needsLE;
+ }
+}
diff --git a/hardware/src/main/java/com/satochip/ApduException.java b/hardware/src/main/java/com/satochip/ApduException.java
new file mode 100644
index 000000000..b43b7390a
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/ApduException.java
@@ -0,0 +1,29 @@
+package com.satochip;
+
+/**
+ * Exception thrown when the response APDU from the card contains unexpected SW or data.
+ */
+public class ApduException extends Exception {
+ public final int sw;
+
+ /**
+ * Creates an exception with SW and message.
+ *
+ * @param sw the status word
+ * @param message a descriptive message of the error
+ */
+ public ApduException(int sw, String message) {
+ super(message + ", 0x" + String.format("%04X", sw));
+ this.sw = sw;
+ }
+
+ /**
+ * Creates an exception with a message.
+ *
+ * @param message a descriptive message of the error
+ */
+ public ApduException(String message) {
+ super(message);
+ this.sw = 0;
+ }
+}
diff --git a/hardware/src/main/java/com/satochip/ApduResponse.java b/hardware/src/main/java/com/satochip/ApduResponse.java
new file mode 100644
index 000000000..c406c0491
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/ApduResponse.java
@@ -0,0 +1,218 @@
+package com.satochip;
+
+/**
+ * ISO7816-4 APDU response.
+ */
+public class ApduResponse {
+ public static final int SW_OK = 0x9000;
+ public static final int SW_SECURITY_CONDITION_NOT_SATISFIED = 0x6982;
+ public static final int SW_AUTHENTICATION_METHOD_BLOCKED = 0x6983;
+ public static final int SW_CARD_LOCKED = 0x6283;
+ public static final int SW_REFERENCED_DATA_NOT_FOUND = 0x6A88;
+ public static final int SW_CONDITIONS_OF_USE_NOT_SATISFIED = 0x6985; // applet may be already installed
+ public static final int SW_WRONG_PIN_MASK = 0x63C0;
+ public static final int SW_WRONG_PIN_LEGACY = 0x9C02;
+ public static final int SW_BLOCKED_PIN = 0x9C0C;
+ public static final int SW_FACTORY_RESET = 0xFF00;
+ public static final String HEXES = "0123456789ABCDEF";
+
+ private byte[] apdu;
+ private byte[] data;
+ private int sw;
+ private int sw1;
+ private int sw2;
+
+ /**
+ * Creates an APDU object by parsing the raw response from the card.
+ *
+ * @param apdu the raw response from the card.
+ */
+ public ApduResponse(byte[] apdu) {
+ if (apdu.length < 2) {
+ throw new IllegalArgumentException("APDU response must be at least 2 bytes");
+ }
+ this.apdu = apdu;
+ this.parse();
+ }
+
+ public ApduResponse(byte[] data, byte sw1, byte sw2) {
+ byte[] apdu= new byte[data.length + 2];
+ System.arraycopy(data, 0, apdu, 0, data.length);
+ apdu[data.length]= sw1;
+ apdu[data.length+1]= sw2;
+ this.apdu = apdu;
+ this.parse();
+ }
+
+
+ /**
+ * Parses the APDU response, separating the response data from SW.
+ */
+ private void parse() {
+ int length = this.apdu.length;
+
+ this.sw1 = this.apdu[length - 2] & 0xff;
+ this.sw2 = this.apdu[length - 1] & 0xff;
+ this.sw = (this.sw1 << 8) | this.sw2;
+
+ this.data = new byte[length - 2];
+ System.arraycopy(this.apdu, 0, this.data, 0, length - 2);
+ }
+
+ /**
+ * Returns true if the SW is 0x9000.
+ *
+ * @return true if the SW is 0x9000.
+ */
+ public boolean isOK() {
+ return this.sw == SW_OK;
+ }
+
+ /**
+ * Asserts that the SW is 0x9000. Throws an exception if it isn't
+ *
+ * @return this object, to simplify chaining
+ * @throws ApduException if the SW is not 0x9000
+ */
+ public ApduResponse checkOK() throws ApduException {
+ return this.checkSW(SW_OK);
+ }
+
+ /**
+ * Asserts that the SW is contained in the given list. Throws an exception if it isn't.
+ *
+ * @param codes the list of SWs to match.
+ * @return this object, to simplify chaining
+ * @throws ApduException if the SW is not 0x9000
+ */
+ public ApduResponse checkSW(int... codes) throws ApduException {
+ for (int code : codes) {
+ if (this.sw == code) {
+ return this;
+ }
+ }
+
+ switch (this.sw) {
+ case SW_SECURITY_CONDITION_NOT_SATISFIED:
+ throw new ApduException(this.sw, "security condition not satisfied");
+ case SW_AUTHENTICATION_METHOD_BLOCKED:
+ throw new ApduException(this.sw, "authentication method blocked");
+ default:
+ throw new ApduException(this.sw, "Unexpected error SW");
+ }
+ }
+
+ /**
+ * Asserts that the SW is 0x9000. Throws an exception with the given message if it isn't
+ *
+ * @param message the error message
+ * @return this object, to simplify chaining
+ * @throws ApduException if the SW is not 0x9000
+ */
+ public ApduResponse checkOK(String message) throws ApduException {
+ return checkSW(message, SW_OK);
+ }
+
+ /**
+ * Asserts that the SW is contained in the given list. Throws an exception with the given message if it isn't.
+ *
+ * @param message the error message
+ * @param codes the list of SWs to match.
+ * @return this object, to simplify chaining
+ * @throws ApduException if the SW is not 0x9000
+ */
+ public ApduResponse checkSW(String message, int... codes) throws ApduException {
+ for (int code : codes) {
+ if (this.sw == code) {
+ return this;
+ }
+ }
+
+ throw new ApduException(this.sw, message);
+ }
+
+ /**
+ * Checks response from an authentication command (VERIFY PIN, UNBLOCK PUK)
+ *
+ * @throws WrongPINException wrong PIN
+ * @throws ApduException unexpected response
+ */
+ public ApduResponse checkAuthOK() throws WrongPINException, WrongPINLegacyException, BlockedPINException, ApduException {
+ if ((this.sw & SW_WRONG_PIN_MASK) == SW_WRONG_PIN_MASK) {
+ throw new WrongPINException(sw2 & 0x0F);
+ } else if (this.sw == SW_WRONG_PIN_LEGACY) {
+ throw new WrongPINLegacyException();
+ } else if (this.sw == SW_BLOCKED_PIN) {
+ throw new BlockedPINException();
+ } else if (this.sw == SW_FACTORY_RESET) {
+ throw new ResetToFactoryException();
+ } else {
+ return checkOK();
+ }
+ }
+
+ /**
+ * Returns the data field of this APDU.
+ *
+ * @return the data field of this APDU
+ */
+ public byte[] getData() {
+ return this.data;
+ }
+
+ /**
+ * Returns the Status Word.
+ *
+ * @return the status word
+ */
+ public int getSw() {
+ return this.sw;
+ }
+
+ /**
+ * Returns the SW1 byte
+ * @return SW1
+ */
+ public int getSw1() {
+ return this.sw1;
+ }
+
+ /**
+ * Returns the SW2 byte
+ * @return SW2
+ */
+ public int getSw2() {
+ return this.sw2;
+ }
+
+ /**
+ * Returns the raw unparsed response.
+ *
+ * @return raw APDU data
+ */
+ public byte[] getBytes() {
+ return this.apdu;
+ }
+
+ /**
+ * Serializes the APDU to human readable hex string format
+ *
+ * @return the hex string representation of the APDU
+ */
+ public String toHexString() {
+ byte[] raw= this.apdu;
+ try{
+ if ( raw == null ) {
+ return "";
+ }
+ final StringBuilder hex = new StringBuilder( 2 * raw.length );
+ for ( final byte b : raw ) {
+ hex.append(HEXES.charAt((b & 0xF0) >> 4))
+ .append(HEXES.charAt((b & 0x0F)));
+ }
+ return hex.toString();
+ } catch(Exception e){
+ return "Exception in ApduResponse.toHexString()";
+ }
+ }
+}
diff --git a/hardware/src/main/java/com/satochip/ApplicationStatus.java b/hardware/src/main/java/com/satochip/ApplicationStatus.java
new file mode 100644
index 000000000..503407eed
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/ApplicationStatus.java
@@ -0,0 +1,133 @@
+package com.satochip;
+
+import com.satochip.ApduResponse;
+
+/**
+ * Parses the result of a GET STATUS command retrieving application status.
+ */
+public class ApplicationStatus {
+
+ private boolean setup_done= false;
+ private boolean is_seeded= false;
+ private boolean needs_secure_channel= false;
+ private boolean needs_2FA= false;
+
+ private byte protocol_major_version= (byte)0;
+ private byte protocol_minor_version= (byte)0;
+ private byte applet_major_version= (byte)0;
+ private byte applet_minor_version= (byte)0;
+
+ private byte PIN0_remaining_tries= (byte)0;
+ private byte PUK0_remaining_tries= (byte)0;
+ private byte PIN1_remaining_tries= (byte)0;
+ private byte PUK1_remaining_tries= (byte)0;
+
+ private int protocol_version= 0; //(d["protocol_major_version"]<<8)+d["protocol_minor_version"]
+
+ // todo: remove
+ // private byte pinRetryCount;
+ // private byte pukRetryCount;
+ // private boolean hasMasterKey;
+
+
+ /**
+ * Constructor from TLV data
+ * @param tlvData the TLV data
+ * @throws IllegalArgumentException if the TLV does not follow the expected format
+ */
+ public ApplicationStatus(ApduResponse rapdu) {
+
+ int sw= rapdu.getSw();
+
+ if (sw==0x9000){
+
+ byte[] data= rapdu.getData();
+ protocol_major_version= data[0];
+ protocol_minor_version= data[1];
+ applet_major_version= data[2];
+ applet_minor_version= data[3];
+ protocol_version= (protocol_major_version<<8) + protocol_minor_version;
+
+ if (data.length >=8){
+ PIN0_remaining_tries= data[4];
+ PUK0_remaining_tries= data[5];
+ PIN1_remaining_tries= data[6];
+ PUK1_remaining_tries= data[7];
+ needs_2FA= false; //default value
+ }
+ if (data.length >=9){
+ needs_2FA= (data[8]==0X00)? false : true;
+ }
+ if (data.length >=10){
+ is_seeded= (data[9]==0X00)? false : true;
+ }
+ if (data.length >=11){
+ setup_done= (data[10]==0X00)? false : true;
+ } else {
+ setup_done= true;
+ }
+ if (data.length >=12){
+ needs_secure_channel= (data[11]==0X00)? false : true;
+ } else {
+ needs_secure_channel= false;
+ needs_2FA= false; //default value
+ }
+ } else if (sw==0x9c04){
+ setup_done= false;
+ is_seeded= false;
+ needs_secure_channel= false;
+ } else{
+ //throws IllegalArgumentException("Wrong getStatus data!"); // should not happen
+ }
+ }
+
+ // getters
+ public boolean isSeeded() {
+ return is_seeded;
+ }
+ public boolean isSetupDone() {
+ return setup_done;
+ }
+ public boolean needsSecureChannel() {
+ return needs_secure_channel;
+ }
+
+ // TODO: other gettters
+ public byte getPin0RemainingCounter(){
+ return PIN0_remaining_tries;
+ }
+ public byte getPuk0RemainingCounter(){
+ return PUK0_remaining_tries;
+ }
+
+ public String toString(){
+ String status_info= "setup_done: " + setup_done + "\n"+
+ "is_seeded: " + is_seeded + "\n"+
+ "needs_2FA: " + needs_2FA + "\n"+
+ "needs_secure_channel: " + needs_secure_channel + "\n"+
+ "protocol_major_version: " + protocol_major_version + "\n"+
+ "protocol_minor_version: " + protocol_minor_version + "\n"+
+ "applet_major_version: " + applet_major_version + "\n"+
+ "applet_minor_version: " + applet_minor_version;
+ return status_info;
+ }
+ public int getCardVersionInt() {
+ return ((int) protocol_major_version * (1 << 24)) +
+ ((int) protocol_minor_version * (1 << 16)) +
+ ((int) applet_major_version * (1 << 8)) +
+ ((int) applet_minor_version);
+ }
+
+ public String getCardVersionString() {
+ String version_string =
+ protocol_major_version + "." +
+ protocol_minor_version + "-" +
+ applet_major_version + "." +
+ applet_minor_version;
+ return version_string;
+ }
+
+ public int getProtocolVersion() {
+ return protocol_version;
+ }
+}
diff --git a/hardware/src/main/java/com/satochip/Bip32Path.java b/hardware/src/main/java/com/satochip/Bip32Path.java
new file mode 100644
index 000000000..bfd3a6450
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/Bip32Path.java
@@ -0,0 +1,26 @@
+package com.satochip;
+
+public class Bip32Path {
+
+ private final Integer depth;
+ private final byte[] bytes;
+ //private final String bip32Path;
+
+ public Bip32Path(Integer depth, byte[] bytes) {
+ this.depth = depth;
+ this.bytes = bytes;
+ //this.bip32Path = bip32Path;
+ }
+
+ public Integer getDepth() {
+ return depth;
+ }
+
+ public byte[] getBytes() {
+ return bytes;
+ }
+
+// public String getBip32Path() {
+// return bip32Path;
+// }
+}
\ No newline at end of file
diff --git a/hardware/src/main/java/com/satochip/BlockedPINException.java b/hardware/src/main/java/com/satochip/BlockedPINException.java
new file mode 100644
index 000000000..97e6aa680
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/BlockedPINException.java
@@ -0,0 +1,14 @@
+package com.satochip;
+
+/**
+ * Exception thrown when checking PIN/PUK
+ */
+public class BlockedPINException extends ApduException {
+
+ /**
+ * Construct an exception with the given number of retry attempts.
+ */
+ public BlockedPINException() {
+ super("PIN blocked");
+ }
+}
diff --git a/hardware/src/main/java/com/satochip/CardChannel.java b/hardware/src/main/java/com/satochip/CardChannel.java
new file mode 100644
index 000000000..3b4c4599e
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/CardChannel.java
@@ -0,0 +1,35 @@
+package com.satochip;
+
+import java.io.IOException;
+
+/**
+ * A channel to transcieve ISO7816-4 APDUs.
+ */
+public interface CardChannel {
+ /**
+ * Sends the given C-APDU and returns an R-APDU.
+ *
+ * @param cmd the command to send
+ * @return the card response
+ * @throws IOException communication error
+ */
+ ApduResponse send(ApduCommand cmd) throws IOException;
+
+ /**
+ * True if connected, false otherwise
+ * @return true if connected, false otherwise
+ */
+ boolean isConnected();
+
+ /**
+ * Returns the iteration count for deriving the pairing key from the pairing password. The default is 50000 and is
+ * should only be changed for devices where the PBKDF2 is calculated on-board and the resource do not permit a
+ * high iteration count. If a lower count is used other security mechanism should be used to prevent brute force
+ * attacks.
+ *
+ * @return the iteration count
+ */
+ default int pairingPasswordPBKDF2IterationCount() {
+ return 50000;
+ }
+}
diff --git a/hardware/src/main/java/com/satochip/CardListener.java b/hardware/src/main/java/com/satochip/CardListener.java
new file mode 100644
index 000000000..fd9684d1e
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/CardListener.java
@@ -0,0 +1,18 @@
+package com.satochip;
+
+/**
+ * Listener for card connection events.
+ */
+public interface CardListener {
+ /**
+ * Executes when the card channel is connected.
+ *
+ * @param channel the connected card channel
+ */
+ void onConnected(CardChannel channel);
+
+ /**
+ * Executes when a previously connected card is disconnected.
+ */
+ void onDisconnected();
+}
diff --git a/hardware/src/main/java/com/satochip/Constants.java b/hardware/src/main/java/com/satochip/Constants.java
new file mode 100644
index 000000000..e6b621c8f
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/Constants.java
@@ -0,0 +1,186 @@
+package com.satochip;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.Collections;
+
+public final class Constants {
+
+ // Prevents instanciation of class
+ private Constants() {}
+
+ /****************************************
+ * Instruction codes *
+ ****************************************/
+ public final static byte CLA = (byte)0xB0;
+ // Applet initialization
+ public final static byte INS_SETUP = (byte) 0x2A;
+ // Keys' use and management
+ public final static byte INS_IMPORT_KEY = (byte) 0x32;
+ public final static byte INS_RESET_KEY = (byte) 0x33;
+ public final static byte INS_GET_PUBLIC_FROM_PRIVATE= (byte)0x35;
+ // External authentication
+ public final static byte INS_CREATE_PIN = (byte) 0x40; //TODO: remove?
+ public final static byte INS_VERIFY_PIN = (byte) 0x42;
+ public final static byte INS_CHANGE_PIN = (byte) 0x44;
+ public final static byte INS_UNBLOCK_PIN = (byte) 0x46;
+ public final static byte INS_LOGOUT_ALL = (byte) 0x60;
+ // Status information
+ public final static byte INS_LIST_PINS = (byte) 0x48;
+ public final static byte INS_GET_STATUS = (byte) 0x3C;
+ public final static byte INS_CARD_LABEL = (byte) 0x3D;
+ // HD wallet
+ public final static byte INS_BIP32_IMPORT_SEED= (byte) 0x6C;
+ public final static byte INS_BIP32_RESET_SEED= (byte) 0x77;
+ public final static byte INS_BIP32_GET_AUTHENTIKEY= (byte) 0x73;
+ public final static byte INS_BIP32_SET_AUTHENTIKEY_PUBKEY= (byte)0x75;
+ public final static byte INS_BIP32_GET_EXTENDED_KEY= (byte) 0x6D;
+ public final static byte INS_BIP32_SET_EXTENDED_PUBKEY= (byte) 0x74;
+ public final static byte INS_SIGN_MESSAGE= (byte) 0x6E;
+ public final static byte INS_SIGN_SHORT_MESSAGE= (byte) 0x72;
+ public final static byte INS_SIGN_TRANSACTION= (byte) 0x6F;
+ public final static byte INS_PARSE_TRANSACTION = (byte) 0x71;
+ public final static byte INS_CRYPT_TRANSACTION_2FA = (byte) 0x76;
+ public final static byte INS_SET_2FA_KEY = (byte) 0x79;
+ public final static byte INS_RESET_2FA_KEY = (byte) 0x78;
+ public final static byte INS_SIGN_TRANSACTION_HASH= (byte) 0x7A;
+ // secure channel
+ public final static byte INS_INIT_SECURE_CHANNEL = (byte) 0x81;
+ public final static byte INS_PROCESS_SECURE_CHANNEL = (byte) 0x82;
+ // secure import from SeedKeeper
+ public final static byte INS_IMPORT_ENCRYPTED_SECRET = (byte) 0xAC;
+ public final static byte INS_IMPORT_TRUSTED_PUBKEY = (byte) 0xAA;
+ public final static byte INS_EXPORT_TRUSTED_PUBKEY = (byte) 0xAB;
+ public final static byte INS_EXPORT_AUTHENTIKEY= (byte) 0xAD;
+ // Personalization PKI support
+ public final static byte INS_IMPORT_PKI_CERTIFICATE = (byte) 0x92;
+ public final static byte INS_EXPORT_PKI_CERTIFICATE = (byte) 0x93;
+ public final static byte INS_SIGN_PKI_CSR = (byte) 0x94;
+ public final static byte INS_EXPORT_PKI_PUBKEY = (byte) 0x98;
+ public final static byte INS_LOCK_PKI = (byte) 0x99;
+ public final static byte INS_CHALLENGE_RESPONSE_PKI= (byte) 0x9A;
+ // reset to factory settings
+ public final static byte INS_RESET_TO_FACTORY = (byte) 0xFF;
+
+
+
+
+
+
+
+ /****************************************
+ * Error codes *
+ ****************************************/
+
+ /** Entered PIN is not correct */
+ public final static short SW_PIN_FAILED = (short)0x63C0;// includes number of tries remaining
+ ///** DEPRECATED - Entered PIN is not correct */
+ //public final static short SW_AUTH_FAILED = (short) 0x9C02;
+ /** Required operation is not allowed in actual circumstances */
+ public final static short SW_OPERATION_NOT_ALLOWED = (short) 0x9C03;
+ /** Required setup is not not done */
+ public final static short SW_SETUP_NOT_DONE = (short) 0x9C04;
+ /** Required setup is already done */
+ public final static short SW_SETUP_ALREADY_DONE = (short) 0x9C07;
+ /** Required feature is not (yet) supported */
+ final static short SW_UNSUPPORTED_FEATURE = (short) 0x9C05;
+ /** Required operation was not authorized because of a lack of privileges */
+ public final static short SW_UNAUTHORIZED = (short) 0x9C06;
+ /** Algorithm specified is not correct */
+ public final static short SW_INCORRECT_ALG = (short) 0x9C09;
+
+ /** There have been memory problems on the card */
+ public final static short SW_NO_MEMORY_LEFT = (short) 0x9C01;
+ ///** DEPRECATED - Required object is missing */
+ //public final static short SW_OBJECT_NOT_FOUND= (short) 0x9C07;
+
+ /** Incorrect P1 parameter */
+ public final static short SW_INCORRECT_P1 = (short) 0x9C10;
+ /** Incorrect P2 parameter */
+ public final static short SW_INCORRECT_P2 = (short) 0x9C11;
+ /** Invalid input parameter to command */
+ public final static short SW_INVALID_PARAMETER = (short) 0x9C0F;
+
+ /** Eckeys initialized */
+ public final static short SW_ECKEYS_INITIALIZED_KEY = (short) 0x9C1A;
+
+ /** Verify operation detected an invalid signature */
+ public final static short SW_SIGNATURE_INVALID = (short) 0x9C0B;
+ /** Operation has been blocked for security reason */
+ public final static short SW_IDENTITY_BLOCKED = (short) 0x9C0C;
+ /** For debugging purposes */
+ public final static short SW_INTERNAL_ERROR = (short) 0x9CFF;
+ /** Very low probability error */
+ public final static short SW_BIP32_DERIVATION_ERROR = (short) 0x9C0E;
+ /** Incorrect initialization of method */
+ public final static short SW_INCORRECT_INITIALIZATION = (short) 0x9C13;
+ /** Bip32 seed is not initialized*/
+ public final static short SW_BIP32_UNINITIALIZED_SEED = (short) 0x9C14;
+ /** Bip32 seed is already initialized (must be reset before change)*/
+ public final static short SW_BIP32_INITIALIZED_SEED = (short) 0x9C17;
+ //** DEPRECATED - Bip32 authentikey pubkey is not initialized*/
+ //public final static short SW_BIP32_UNINITIALIZED_AUTHENTIKEY_PUBKEY= (short) 0x9C16;
+ /** Incorrect transaction hash */
+ public final static short SW_INCORRECT_TXHASH = (short) 0x9C15;
+
+ /** 2FA already initialized*/
+ public final static short SW_2FA_INITIALIZED_KEY = (short) 0x9C18;
+ /** 2FA uninitialized*/
+ public final static short SW_2FA_UNINITIALIZED_KEY = (short) 0x9C19;
+
+ /** HMAC errors */
+ static final short SW_HMAC_UNSUPPORTED_KEYSIZE = (short) 0x9c1E;
+ static final short SW_HMAC_UNSUPPORTED_MSGSIZE = (short) 0x9c1F;
+
+ /** Secure channel */
+ public final static short SW_SECURE_CHANNEL_REQUIRED = (short) 0x9C20;
+ public final static short SW_SECURE_CHANNEL_UNINITIALIZED = (short) 0x9C21;
+ public final static short SW_SECURE_CHANNEL_WRONG_IV= (short) 0x9C22;
+ public final static short SW_SECURE_CHANNEL_WRONG_MAC= (short) 0x9C23;
+
+ /** Secret data is too long for import **/
+ public final static short SW_IMPORTED_DATA_TOO_LONG = (short) 0x9C32;
+ /** Wrong HMAC when importing Secret through Secure import **/
+ public final static short SW_SECURE_IMPORT_WRONG_MAC = (short) 0x9C33;
+ /** Wrong Fingerprint when importing Secret through Secure import **/
+ public final static short SW_SECURE_IMPORT_WRONG_FINGERPRINT = (short) 0x9C34;
+ /** No Trusted Pubkey when importing Secret through Secure import **/
+ public final static short SW_SECURE_IMPORT_NO_TRUSTEDPUBKEY = (short) 0x9C35;
+
+ /** PKI perso error */
+ public final static short SW_PKI_ALREADY_LOCKED = (short) 0x9C40;
+ /** CARD HAS BEEN RESET TO FACTORY */
+ public final static short SW_RESET_TO_FACTORY = (short) 0xFF00;
+ /** For instructions that have been deprecated*/
+ public final static short SW_INS_DEPRECATED = (short) 0x9C26;
+ /** For debugging purposes 2 */
+ public final static short SW_DEBUG_FLAG = (short) 0x9FFF;
+
+ /****************************************
+ * Other constants *
+ ****************************************/
+
+ // KeyBlob Encoding in Key Blobs
+ public final static byte BLOB_ENC_PLAIN = (byte) 0x00;
+
+ // Cipher Operations admitted in ComputeCrypt()
+ public final static byte OP_INIT = (byte) 0x01;
+ public final static byte OP_PROCESS = (byte) 0x02;
+ public final static byte OP_FINALIZE = (byte) 0x03;
+
+ // JC API 2.2.2 does not define these constants:
+ public final static byte ALG_ECDSA_SHA_256= (byte) 33;
+ public final static byte ALG_EC_SVDP_DH_PLAIN= (byte) 3; //https://javacard.kenai.com/javadocs/connected/javacard/security/KeyAgreement.html#ALG_EC_SVDP_DH_PLAIN
+ public final static byte ALG_EC_SVDP_DH_PLAIN_XY= (byte) 6; //https://docs.oracle.com/javacard/3.0.5/api/javacard/security/KeyAgreement.html#ALG_EC_SVDP_DH_PLAIN_XY
+ public final static short LENGTH_EC_FP_256= (short) 256;
+
+
+
+
+
+}
diff --git a/hardware/src/main/java/com/satochip/NfcActionObject.java b/hardware/src/main/java/com/satochip/NfcActionObject.java
new file mode 100644
index 000000000..fad8277fb
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/NfcActionObject.java
@@ -0,0 +1,40 @@
+package com.satochip;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.blockstream.common.gdk.data.Network;
+import com.blockstream.common.gdk.device.HardwareWalletInteraction;
+import com.blockstream.common.gdk.device.SignMessageResult;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class NfcActionObject {
+
+ public NfcActionType actionType = NfcActionType.none;
+ public NfcActionStatus actionStatus = NfcActionStatus.none;
+
+ public HardwareWalletInteraction hwInteraction = null;
+
+ // getXpubs
+ // List getXpubs(@NonNull Network network, @NonNull List extends List> paths, @Nullable HardwareWalletInteraction hwInteraction)
+ public Network networkParam = null;
+ public List extends List> pathsParam = new ArrayList<>();
+ public List xpubsResult = new ArrayList<>();
+
+ // signMessage
+ // SignMessageResult signMessage(@NonNull List path, @NonNull String message, boolean useAeProtocol, @Nullable String aeHostCommitment, @Nullable String aeHostEntropy, @Nullable HardwareWalletInteraction hwInteraction)
+ public List pathParam = new ArrayList<>();
+ public String messageParam = "";
+ public String signatureResult = "";
+
+ // signTransaction
+ //public List extends List> pathsParam = new ArrayList<>();
+ public List hashesParam = new ArrayList<>();
+ public List signaturesResult = new ArrayList<>();
+
+ // getMasterBlindingKey
+ public byte[] blindingKeyResult = new byte[32];
+
+}
diff --git a/hardware/src/main/java/com/satochip/NfcActionStatus.java b/hardware/src/main/java/com/satochip/NfcActionStatus.java
new file mode 100644
index 000000000..c45407580
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/NfcActionStatus.java
@@ -0,0 +1,8 @@
+package com.satochip;
+
+public enum NfcActionStatus {
+ none,
+ error,
+ busy,
+ finished,
+}
diff --git a/hardware/src/main/java/com/satochip/NfcActionType.java b/hardware/src/main/java/com/satochip/NfcActionType.java
new file mode 100644
index 000000000..458f3b112
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/NfcActionType.java
@@ -0,0 +1,9 @@
+package com.satochip;
+
+public enum NfcActionType {
+ none,
+ getXpubs,
+ signMessage,
+ signTransaction,
+ getMasterBlindingKey,
+}
diff --git a/hardware/src/main/java/com/satochip/NfcCardChannel.java b/hardware/src/main/java/com/satochip/NfcCardChannel.java
new file mode 100644
index 000000000..52066d706
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/NfcCardChannel.java
@@ -0,0 +1,34 @@
+package com.satochip;
+
+import android.nfc.tech.IsoDep;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Implementation of the CardChannel interface using the Android NFC API.
+ */
+public class NfcCardChannel implements CardChannel {
+ private static final String TAG = "CardChannel";
+
+ private IsoDep isoDep;
+
+ public NfcCardChannel(IsoDep isoDep) {
+ this.isoDep = isoDep;
+ }
+
+ @Override
+ public ApduResponse send(ApduCommand cmd) throws IOException {
+ byte[] apdu = cmd.serialize();
+ Log.d(TAG, String.format("COMMAND CLA: %02X INS: %02X P1: %02X P2: %02X LC: %02X", cmd.getCla(), cmd.getIns(), cmd.getP1(), cmd.getP2(), cmd.getData().length));
+ byte[] resp = this.isoDep.transceive(apdu);
+ ApduResponse response = new ApduResponse(resp);
+ Log.d(TAG, String.format("RESPONSE LEN: %02X, SW: %04X %n-----------------------", response.getData().length, response.getSw()));
+ return response;
+ }
+
+ @Override
+ public boolean isConnected() {
+ return this.isoDep.isConnected();
+ }
+}
diff --git a/hardware/src/main/java/com/satochip/NfcCardManager.java b/hardware/src/main/java/com/satochip/NfcCardManager.java
new file mode 100644
index 000000000..79d5e97fa
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/NfcCardManager.java
@@ -0,0 +1,124 @@
+package com.satochip;
+
+import android.nfc.NfcAdapter;
+import android.nfc.Tag;
+import android.nfc.tech.IsoDep;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Manages connection of NFC-based cards. Extends Thread and must be started using the start() method. The thread has
+ * a runloop which monitors the connection and from which CardListener callbacks are called.
+ */
+public class NfcCardManager extends Thread implements NfcAdapter.ReaderCallback {
+ private static final String TAG = "NFCCardManager";
+ private static final int DEFAULT_LOOP_SLEEP_MS = 50;
+
+ private IsoDep isoDep;
+ private boolean isRunning;
+ private CardListener cardListener;
+ private int loopSleepMS;
+
+// static {
+// Crypto.addBouncyCastleProvider();
+// }
+
+ /**
+ * Constructs an NFC Card Manager with default delay between loop iterations.
+ */
+ public NfcCardManager() {
+ this(DEFAULT_LOOP_SLEEP_MS);
+ }
+
+ /**
+ * Constructs an NFC Card Manager with the given delay between loop iterations.
+ *
+ * @param loopSleepMS time to sleep between loops
+ */
+ public NfcCardManager(int loopSleepMS) {
+ this.loopSleepMS = loopSleepMS;
+ }
+
+ /**
+ * True if connected, false otherwise.
+ * @return if connected, false otherwise
+ */
+ public boolean isConnected() {
+ try {
+ return isoDep != null && isoDep.isConnected();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ @Override
+ public void onTagDiscovered(Tag tag) {
+ isoDep = IsoDep.get(tag);
+ try {
+ isoDep = IsoDep.get(tag);
+ isoDep.connect();
+ isoDep.setTimeout(120000);
+ } catch (IOException e) {
+ Log.e(TAG, "error connecting to tag");
+ }
+ }
+
+ /**
+ * Runloop. Do NOT invoke directly. Use start() instead.
+ */
+ public void run() {
+ boolean connected = isConnected();
+
+ while (true) {
+ boolean newConnected = isConnected();
+ if (newConnected != connected) {
+ connected = newConnected;
+ Log.i(TAG, "tag " + (connected ? "connected" : "disconnected"));
+
+ if (connected && !isRunning) {
+ onCardConnected();
+ } else {
+ onCardDisconnected();
+ }
+ }
+
+ SystemClock.sleep(loopSleepMS);
+ }
+ }
+
+ /**
+ * Reacts on card connected by calling the callback of the registered listener.
+ */
+ private void onCardConnected() {
+ isRunning = true;
+
+ if (cardListener != null) {
+ cardListener.onConnected(new NfcCardChannel(isoDep));
+ }
+
+ isRunning = false;
+ }
+
+ /**
+ * Reacts on card disconnected by calling the callback of the registered listener.
+ */
+ private void onCardDisconnected() {
+ isRunning = false;
+ isoDep = null;
+ if (cardListener != null) {
+ cardListener.onDisconnected();
+ }
+ }
+
+ /**
+ * Sets the card listener.
+ *
+ * @param listener the new listener
+ */
+ public void setCardListener(CardListener listener) {
+ cardListener = listener;
+ }
+}
diff --git a/hardware/src/main/java/com/satochip/ResetToFactoryException.java b/hardware/src/main/java/com/satochip/ResetToFactoryException.java
new file mode 100644
index 000000000..7fce2fd05
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/ResetToFactoryException.java
@@ -0,0 +1,14 @@
+package com.satochip;
+
+/**
+ * Exception thrown when checking PIN/PUK
+ */
+public class ResetToFactoryException extends ApduException {
+
+ /**
+ * Construct an exception with the given number of retry attempts.
+ */
+ public ResetToFactoryException() {
+ super("Card reset to factory");
+ }
+}
diff --git a/hardware/src/main/java/com/satochip/SatochipCommandSet.java b/hardware/src/main/java/com/satochip/SatochipCommandSet.java
new file mode 100644
index 000000000..29d091938
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/SatochipCommandSet.java
@@ -0,0 +1,493 @@
+package com.satochip;
+
+
+import static com.satochip.Constants.*;
+
+
+import androidx.annotation.NonNull;
+
+import com.blockstream.libwally.Wally;
+
+import org.bouncycastle.crypto.digests.RIPEMD160Digest;
+import org.bouncycastle.util.encoders.Hex;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.logging.Logger;
+import java.util.logging.Level;
+import java.io.IOException;
+
+/**
+ * This class is used to send APDU to the applet. Each method corresponds to an APDU as defined in the APPLICATION.md
+ * file. Some APDUs map to multiple methods for the sake of convenience since their payload or response require some
+ * pre/post processing.
+ */
+public class SatochipCommandSet {
+
+ private static final Logger logger = Logger.getLogger("org.satochip.client");
+
+ private final CardChannel apduChannel;
+ private SecureChannelSession secureChannel;
+ private ApplicationStatus status;
+ private SatochipParser parser = null;
+
+ private byte[] pin0 = null;
+ private List possibleAuthentikeys = new ArrayList();
+
+ private byte[] extendedKey = null;
+ private byte[] extendedChaincode = null;
+
+ // should be Satochip
+ private String cardType = null;
+
+ public static final byte[] SATOCHIP_AID = Hex.decode("5361746f43686970"); //SatoChip
+
+ /**
+ * Creates a SatochipCommandSet using the given APDU Channel
+ *
+ * @param apduChannel APDU channel
+ */
+ public SatochipCommandSet(CardChannel apduChannel) {
+ this.apduChannel = apduChannel;
+ this.secureChannel = new SecureChannelSession();
+ this.parser = new SatochipParser();
+ logger.setLevel(Level.WARNING);
+ }
+
+ public void setLoggerLevel(String level) {
+ switch (level) {
+ case "info":
+ logger.setLevel(Level.INFO);
+ break;
+ case "warning":
+ logger.setLevel(Level.WARNING);
+ break;
+ default:
+ logger.setLevel(Level.WARNING);
+ break;
+ }
+ }
+
+ public void setLoggerLevel(Level level) {
+ logger.setLevel(level);
+ }
+
+ /**
+ * Returns the application info as stored from the last sent SELECT command. Returns null if no succesful SELECT
+ * command has been sent using this command set.
+ *
+ * @return the application info object
+ */
+ public ApplicationStatus getApplicationStatus() {
+ return status;
+ }
+
+
+ public SatochipParser getParser() {
+ return parser;
+ }
+
+ public ApduResponse cardTransmit(ApduCommand plainApdu) {
+
+ // we try to transmit the APDU until we receive the answer or we receive an unrecoverable error
+ boolean isApduTransmitted = false;
+ do {
+ try {
+ byte[] apduBytes = plainApdu.serialize();
+ byte ins = apduBytes[1];
+ boolean isEncrypted = false;
+
+ // check if status available
+ if (status == null) {
+ ApduCommand statusCapdu = new ApduCommand(0xB0, INS_GET_STATUS, 0x00, 0x00, new byte[0]);
+ ApduResponse statusRapdu = apduChannel.send(statusCapdu);
+ status = new ApplicationStatus(statusRapdu);
+ logger.info("SATOCHIPLIB: Status cardGetStatus:" + status.toString());
+ }
+
+ ApduCommand capdu = null;
+ if (status.needsSecureChannel() && (ins != 0xA4) && (ins != 0x81) && (ins != 0x82) && (ins != INS_GET_STATUS)) {
+
+ if (!secureChannel.initializedSecureChannel()) {
+ cardInitiateSecureChannel();
+ logger.info("SATOCHIPLIB: secure Channel initiated!");
+ }
+ // encrypt apdu
+ //logger.info("SATOCHIPLIB: Capdu before encryption:"+ plainApdu.toHexString());
+ capdu = secureChannel.encrypt_secure_channel(plainApdu);
+ isEncrypted = true;
+ //logger.info("SATOCHIPLIB: Capdu encrypted:"+ capdu.toHexString());
+ } else {
+ // plain adpu
+ capdu = plainApdu;
+ }
+
+ ApduResponse rapdu = apduChannel.send(capdu);
+ int sw12 = rapdu.getSw();
+
+ // check answer
+ if (sw12 == 0x9000) { // ok!
+ if (isEncrypted) {
+ // decrypt
+ //logger.info("SATOCHIPLIB: Rapdu encrypted:"+ rapdu.toHexString());
+ rapdu = secureChannel.decrypt_secure_channel(rapdu);
+ //logger.info("SATOCHIPLIB: Rapdu decrypted:"+ rapdu.toHexString());
+ }
+ isApduTransmitted = true; // leave loop
+ return rapdu;
+ }
+ // PIN authentication is required
+ else if (sw12 == 0x9C06) {
+ cardVerifyPIN();
+ }
+ // SecureChannel is not initialized
+ else if (sw12 == 0x9C21) {
+ secureChannel.resetSecureChannel();
+ } else {
+ // cannot resolve issue at this point
+ isApduTransmitted = true; // leave loop
+ return rapdu;
+ }
+
+ } catch (Exception e) {
+ logger.warning("SATOCHIPLIB: Exception in cardTransmit: " + e);
+ return new ApduResponse(new byte[0], (byte) 0x00, (byte) 0x00); // return empty ApduResponse
+ }
+
+ } while (!isApduTransmitted);
+
+ return new ApduResponse(new byte[0], (byte) 0x00, (byte) 0x00); // should not happen
+ }
+
+ public void cardDisconnect() {
+ secureChannel.resetSecureChannel();
+ status = null;
+ pin0 = null;
+ }
+
+ /**
+ * Selects a Satochip instance. The applet is assumed to have been installed with its default AID.
+ *
+ * @return the raw card response
+ * @throws IOException communication error
+ */
+ public ApduResponse cardSelect(String cardType) throws IOException {
+
+ ApduCommand selectApplet;
+ if (cardType.equals("satochip")) {
+ selectApplet = new ApduCommand(0x00, 0xA4, 0x04, 0x00, SATOCHIP_AID);
+ } else {
+ selectApplet = new ApduCommand(0x00, 0xA4, 0x04, 0x00, null);
+ }
+
+ logger.info("SATOCHIPLIB: C-APDU cardSelect:" + selectApplet.toHexString());
+ ApduResponse respApdu = apduChannel.send(selectApplet);
+ logger.info("SATOCHIPLIB: R-APDU cardSelect:" + respApdu.toHexString());
+
+ if (respApdu.getSw() == 0x9000) {
+ this.cardType = cardType;
+ logger.info("SATOCHIPLIB: Satochip-java: CardSelect: found a " + this.cardType);
+ }
+ return respApdu;
+ }
+
+ public ApduResponse cardGetStatus() {
+ ApduCommand plainApdu = new ApduCommand(0xB0, INS_GET_STATUS, 0x00, 0x00, new byte[0]);
+
+ logger.info("SATOCHIPLIB: C-APDU cardGetStatus:" + plainApdu.toHexString());
+ ApduResponse respApdu = this.cardTransmit(plainApdu);
+ logger.info("SATOCHIPLIB: R-APDU cardGetStatus:" + respApdu.toHexString());
+
+ status = new ApplicationStatus(respApdu);
+ logger.info("SATOCHIPLIB: Status from cardGetStatus:" + status.toString());
+
+ return respApdu;
+ }
+
+ // do setup secure channel in this method
+ public List cardInitiateSecureChannel() throws IOException {
+
+ byte[] pubkey = secureChannel.getPublicKey();
+
+ ApduCommand plainApdu = new ApduCommand(0xB0, INS_INIT_SECURE_CHANNEL, 0x00, 0x00, pubkey);
+
+ logger.info("SATOCHIPLIB: C-APDU cardInitiateSecureChannel:" + plainApdu.toHexString());
+ ApduResponse respApdu = apduChannel.send(plainApdu);
+ logger.info("SATOCHIPLIB: R-APDU cardInitiateSecureChannel:" + respApdu.toHexString());
+
+ byte[] keyData = parser.parseInitiateSecureChannel(respApdu);
+ possibleAuthentikeys = parser.parseInitiateSecureChannelGetPossibleAuthentikeys(respApdu);
+ // setup secure channel
+ secureChannel.initiateSecureChannel(keyData);
+
+ return possibleAuthentikeys;
+ }
+
+ /****************************************
+ * PIN MGMT *
+ ****************************************/
+ public void setPin0(byte[] pin) {
+ this.pin0 = new byte[pin.length];
+ System.arraycopy(pin, 0, this.pin0, 0, pin.length);
+ }
+
+ public ApduResponse cardVerifyPIN(byte[] pin) throws Exception {
+
+ byte[] mypin = pin;
+ if (mypin == null){
+ if (pin0 == null) {
+ // TODO: specific exception
+ throw new RuntimeException("PIN required!");
+ }
+ mypin = pin0;
+ }
+
+ try {
+ ApduCommand plainApdu = new ApduCommand(0xB0, INS_VERIFY_PIN, 0x00, 0x00, mypin);
+ //logger.info("SATOCHIPLIB: C-APDU cardVerifyPIN:" + plainApdu.toHexString());
+ logger.info("SATOCHIPLIB: C-APDU cardVerifyPIN");
+ ApduResponse rapdu = this.cardTransmit(plainApdu);
+ logger.info("SATOCHIPLIB: R-APDU cardVerifyPIN:" + rapdu.toHexString());
+
+ rapdu.checkAuthOK();
+ this.pin0 = mypin; // cache new pin
+ return rapdu;
+
+ } catch (WrongPINException | WrongPINLegacyException | BlockedPINException e) {
+ this.pin0 = null;
+ throw e;
+ } catch (ApduException e){
+ this.pin0 = null;
+ throw e;
+ } catch (Exception e){
+ this.pin0 = null;
+ throw e;
+ }
+ }
+
+ public ApduResponse cardVerifyPIN() throws Exception {
+ return cardVerifyPIN(this.pin0);
+ }
+
+ /****************************************
+ * BIP32 *
+ ****************************************/
+
+ public byte[][] cardBip32GetExtendedKey(String stringPath) throws Exception {
+ Bip32Path parsedPath = parser.parseBip32PathToBytes(stringPath);
+ return cardBip32GetExtendedKey(parsedPath);
+ }
+
+ public byte[][] cardBip32GetExtendedKey(@NonNull Bip32Path parsedPath) throws Exception {
+ logger.warning("SATOCHIPLIB: cardBip32GetExtendedKey");
+ if (parsedPath.getDepth() > 10) {
+ throw new Exception("Path length exceeds maximum depth of 10: " + parsedPath.getDepth());
+ }
+
+ byte p1 = parsedPath.getDepth().byteValue();
+ byte optionFlags = (byte) 0x40;
+ byte p2 = optionFlags;
+
+ byte[] data = parsedPath.getBytes();
+
+ while (true) {
+ ApduCommand plainApdu = new ApduCommand(
+ 0xB0,
+ INS_BIP32_GET_EXTENDED_KEY,
+ p1,
+ p2,
+ data
+ );
+ logger.warning("SATOCHIPLIB: C-APDU cardBip32GetExtendedKey:" + plainApdu.toHexString());
+ ApduResponse respApdu = this.cardTransmit(plainApdu);
+ logger.warning("SATOCHIPLIB: R-APDU cardBip32GetExtendedKey:" + respApdu.toHexString());
+ if (respApdu.getSw() == 0x9C01) {
+ logger.warning("SATOCHIPLIB: cardBip32GetExtendedKey: Reset memory...");
+ // reset memory flag
+ p2 = (byte) (p2 ^ 0x80);
+ plainApdu = new ApduCommand(
+ 0xB0,
+ INS_BIP32_GET_EXTENDED_KEY,
+ p1,
+ p2,
+ data
+ );
+ respApdu = this.cardTransmit(plainApdu);
+ // reset the flag then restart
+ p2 = optionFlags;
+ continue;
+ }
+ // other (unexpected) error
+ if (respApdu.getSw() != 0x9000) {
+ throw new Exception("SATOCHIPLIB: cardBip32GetExtendedKey:" +
+ "Unexpected error during BIP32 derivation. SW: " +
+ respApdu.getSw() + " " + respApdu.toHexString()
+ );
+ }
+ // success
+ if (respApdu.getSw() == 0x9000) {
+ logger.warning("SATOCHIPLIB: cardBip32GetExtendedKey: return 0x9000");
+ byte[] response = respApdu.getData();
+ if ((response[32] & 0x80) == 0x80) {
+ logger.info("SATOCHIPLIB: cardBip32GetExtendedKey: Child Derivation optimization...");
+ throw new Exception("Unsupported legacy option during BIP32 derivation");
+ }
+ byte[][] extendedKeyData = parser.parseBip32GetExtendedKey(respApdu);
+ extendedKey = extendedKeyData[0];
+ extendedChaincode = extendedKeyData[1];
+ //String extendedKeyHex = SatochipParser.toHexString(extendedKey);
+ return extendedKeyData;
+ }
+ }
+ }
+
+ /*
+ * Get the BIP32 xpub for given path.
+ *
+ * Parameters:
+ * path (str): the path; if given as a string, it will be converted to bytes (4 bytes for each path index)
+ * xtype (str): the type of transaction such as 'standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh'
+ * is_mainnet (bool): is mainnet or testnet
+ *
+ * Return:
+ * xpub (str): the corresponding xpub value
+ */
+ public String cardBip32GetXpub(String path, int xtype) throws Exception {
+ Bip32Path bip32Path = parser.parseBip32PathToBytes(path);
+ return cardBip32GetXpub(bip32Path, xtype);
+ }
+ public String cardBip32GetXpub(Bip32Path bip32Path, int xtype) throws Exception {
+ logger.warning("SATOCHIPLIB: cardBip32GetXpub");
+
+ byte[] childPubkey, childChaincode;
+
+ // Get extended key
+ logger.warning("SATOCHIPLIB: cardBip32GetXpub: getting card cardBip32GetExtendedKey");
+ cardBip32GetExtendedKey(bip32Path);
+ logger.warning("SATOCHIPLIB: cardBip32GetXpub: got it "+ extendedKey.length);
+
+ childPubkey = extendedKey;
+ childChaincode = extendedChaincode;
+
+ // Pubkey should be in compressed form
+ if (extendedKey.length != 33) {
+ childPubkey = parser.compressPubKey(extendedKey);
+ }
+
+ //Bip32Path parsedPath = parser.parseBip32PathToBytes(path);
+ int depth = bip32Path.getDepth();
+ byte[] bytePath = bip32Path.getBytes();
+ byte[] fingerprintBytes = new byte[4];
+ byte[] childNumberBytes = new byte[4];
+
+ if (depth > 0) {
+ // Get parent info
+ byte[] parentBytePath = Arrays.copyOfRange(bytePath, 0, bytePath.length-4);
+ Bip32Path parentBip32Path = new Bip32Path(depth-1, parentBytePath);
+
+ cardBip32GetExtendedKey(parentBip32Path);
+ byte[] parentPubkeyBytes = extendedKey;
+
+ // Pubkey should be in compressed form
+ if (parentPubkeyBytes.length != 33) {
+ parentPubkeyBytes = parser.compressPubKey(parentPubkeyBytes);
+ }
+
+ fingerprintBytes = Arrays.copyOfRange(digestRipeMd160(Wally.sha256(parentPubkeyBytes)), 0, 4);
+
+ childNumberBytes = Arrays.copyOfRange(bytePath, bytePath.length - 4, bytePath.length);
+ }
+
+ byte[] xtypeBytes = ByteBuffer.allocate(4).putInt((int) xtype).array();
+ byte[] xpubBytes = new byte[78];
+ System.arraycopy(xtypeBytes, 0, xpubBytes, 0, 4);
+ xpubBytes[4] = (byte) depth;
+ System.arraycopy(fingerprintBytes, 0, xpubBytes, 5, 4);
+ System.arraycopy(childNumberBytes, 0, xpubBytes, 9, 4);
+ System.arraycopy(childChaincode, 0, xpubBytes, 13, 32);
+ System.arraycopy(childPubkey, 0, xpubBytes, 45, 33);
+
+ String xpub = Wally.base58check_from_bytes(xpubBytes);
+ logger.info("SATOCHIPLIB: cardBip32GetXpub() xpub: " + xpub);
+ return xpub;
+ }
+
+ public byte[] cardBip32GetLiquidMasterBlindingKey() throws Exception {
+ logger.warning("SATOCHIPLIB: cardBip32GetLiquidMasterBlindingKey");
+
+ byte p1 = 0x00;
+ byte p2 = 0x00;
+ byte[] data = new byte[0];
+ ApduCommand plainApdu = new ApduCommand(
+ 0xB0,
+ 0x7D,
+ p1,
+ p2,
+ data
+ );
+ ApduResponse respApdu = this.cardTransmit(plainApdu);
+
+ if (respApdu.getSw() != 0x9000) {
+ throw new Exception("SATOCHIPLIB: cardBip32GetLiquidMasterBlindingKey error: " + respApdu.toHexString());
+ }
+
+ byte[] response = respApdu.getData();
+ int offset=0;
+ int keySize= 256*(response[offset++] & 0xff) + response[offset++];
+ byte[] blindingKey= new byte[keySize];
+ System.arraycopy(response, offset, blindingKey, 0, keySize);
+ offset+=keySize;
+
+ int sigSize= 256*response[offset++] + response[offset++];
+ byte[] sig= new byte[sigSize];
+ System.arraycopy(response, offset, sig, 0, sigSize);
+ offset+=sigSize;
+
+ return blindingKey;
+ }
+
+ public static byte[] digestRipeMd160(byte[] input) {
+ RIPEMD160Digest digest = new RIPEMD160Digest();
+ digest.update(input, 0, input.length);
+ byte[] ripmemdHash = new byte[20];
+ digest.doFinal(ripmemdHash, 0);
+ return ripmemdHash;
+ }
+
+ /****************************************
+ * SIGNATURES *
+ ****************************************/
+
+ public ApduResponse cardSignHash(byte keynbr, byte[] txhash, byte[] chalresponse) {
+
+ byte[] data;
+ if (txhash.length != 32) {
+ throw new RuntimeException("Wrong txhash length (should be 32)");
+ }
+ if (chalresponse == null) {
+ data = new byte[32];
+ System.arraycopy(txhash, 0, data, 0, txhash.length);
+ } else if (chalresponse.length == 20) {
+ data = new byte[32 + 2 + 20];
+ int offset = 0;
+ System.arraycopy(txhash, 0, data, offset, txhash.length);
+ offset += 32;
+ data[offset++] = (byte) 0x80; // 2 middle bytes for 2FA flag
+ data[offset++] = (byte) 0x00;
+ System.arraycopy(chalresponse, 0, data, offset, chalresponse.length);
+ } else {
+ throw new RuntimeException("Wrong challenge-response length (should be 20)");
+ }
+ ApduCommand plainApdu = new ApduCommand(0xB0, INS_SIGN_TRANSACTION_HASH, keynbr, 0x00, data);
+
+ logger.info("SATOCHIPLIB: C-APDU cardSignTransactionHash:" + plainApdu.toHexString());
+ ApduResponse respApdu = this.cardTransmit(plainApdu);
+ logger.info("SATOCHIPLIB: R-APDU cardSignTransactionHash:" + respApdu.toHexString());
+ // TODO: check SW code for particular status
+
+ return respApdu;
+ }
+
+}
diff --git a/hardware/src/main/java/com/satochip/SatochipException.java b/hardware/src/main/java/com/satochip/SatochipException.java
new file mode 100644
index 000000000..6583b7b82
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/SatochipException.java
@@ -0,0 +1,43 @@
+package com.satochip;
+
+import com.btchip.comm.LedgerException;
+
+public class SatochipException extends RuntimeException {
+
+
+ public enum ExceptionReason {
+ PIN_UNDEFINED, /** Returned if no pin is defined */
+ INVALID_PARAMETER, /** Returned if a parameter passed to a function is invalid */
+ IO_ERROR, /** Returned if the communication with the device fails */
+ APPLICATION_ERROR, /** Returned if an unexpected message is received from the device */
+ INTERNAL_ERROR, /** Returned if an unexpected protocol error occurs when communicating with the device */
+ OTHER
+ };
+
+ private SatochipException.ExceptionReason reason;
+ private int sw;
+
+ public SatochipException(SatochipException.ExceptionReason reason) {
+ this.reason = reason;
+ }
+
+ public SatochipException(SatochipException.ExceptionReason reason, String details) {
+ super(details);
+ this.reason = reason;
+ }
+
+ public SatochipException(String message) {
+ super(message);
+ this.reason = SatochipException.ExceptionReason.OTHER;
+ }
+
+ public SatochipException.ExceptionReason getReason() {
+ return reason;
+ }
+
+ public String toString() {
+ return reason.toString() + " " + super.toString();
+ }
+
+
+}
diff --git a/hardware/src/main/java/com/satochip/SatochipParser.java b/hardware/src/main/java/com/satochip/SatochipParser.java
new file mode 100644
index 000000000..a834826af
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/SatochipParser.java
@@ -0,0 +1,607 @@
+package com.satochip;
+
+import org.bouncycastle.asn1.*;
+import org.bouncycastle.asn1.x9.X9ECParameters;
+import org.bouncycastle.asn1.x9.X9IntegerConverter;
+import org.bouncycastle.crypto.digests.SHA256Digest;
+import org.bouncycastle.crypto.ec.CustomNamedCurves;
+import org.bouncycastle.crypto.params.*;
+import org.bouncycastle.crypto.signers.ECDSASigner;
+import org.bouncycastle.math.ec.ECAlgorithms;
+import org.bouncycastle.math.ec.ECPoint;
+import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve;
+import org.bouncycastle.util.Properties;
+
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.math.BigInteger;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+
+public class SatochipParser{
+
+ private static final Logger logger = Logger.getLogger("org.satochip.client");
+
+ public static final String HEXES = "0123456789ABCDEF";
+ private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
+ public static final ECDomainParameters CURVE;
+ public static final BigInteger HALF_CURVE_ORDER, CURVE_ORDER;
+ static {
+ // Tell Bouncy Castle to precompute data that's needed during secp256k1 calculations.
+ //FixedPointUtil.precompute(CURVE_PARAMS.getG());
+ CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), CURVE_PARAMS.getH());
+ CURVE_ORDER= CURVE_PARAMS.getN();
+ HALF_CURVE_ORDER = CURVE_PARAMS.getN().shiftRight(1);
+ }
+
+ private byte[] authentikey= null;
+
+ public SatochipParser(){
+
+ }
+
+ public byte[] compressPubKey(byte[] pubkey) throws Exception {
+ if (pubkey.length == 33) {
+ // Already compressed
+ return pubkey;
+ } else if (pubkey.length == 65) {
+ // In uncompressed form
+ byte[] pubkeyComp = Arrays.copyOfRange(pubkey, 0, 33);
+ // Compute compression byte
+ int parity = pubkey[64] % 2;
+ if (parity == 0) {
+ pubkeyComp[0] = (byte) 0x02;
+ } else {
+ pubkeyComp[0] = (byte) 0x03;
+ }
+ return pubkeyComp;
+ } else {
+ throw new Exception("Wrong public key length: " + pubkey.length + ", expected: 65");
+ }
+ }
+
+ /****************************************
+ * PARSER *
+ ****************************************/
+
+ public Bip32Path parseBip32PathToBytes(String bip32path) throws Exception {
+ logger.info("SATOCHIPLIB: parseBip32PathToBytes: Start ");
+
+ String[] splitPath = bip32path.split("/");
+ if (splitPath[0].equals("m")) {
+ splitPath = Arrays.copyOfRange(splitPath, 1, splitPath.length);
+ }
+
+ int depth = splitPath.length;
+ byte[] bytePath = new byte[depth * 4];
+
+ int byteIndex = 0;
+ for (int index = 0; index < depth; index++) {
+ String subpathString = splitPath[index];
+ long subpathInt;
+ if (subpathString.endsWith("'") || subpathString.endsWith("h")) {
+ subpathString = subpathString.replace("'", "").replace("h", "");
+ try {
+ long tmp = Long.parseLong(subpathString);
+ subpathInt = tmp + 0x80000000L;
+ } catch (NumberFormatException e) {
+ throw new Exception("Failed to parse Bip32 path: " + bip32path);
+ }
+ } else {
+ try {
+ subpathInt = Long.parseLong(subpathString);
+ } catch (NumberFormatException e) {
+ throw new Exception("Failed to parse Bip32 path: " + bip32path);
+ }
+ }
+ byte[] subPathBytes = ByteBuffer.allocate(4).putInt((int) subpathInt).array();
+ System.arraycopy(subPathBytes, 0, bytePath, byteIndex, subPathBytes.length);
+ byteIndex += 4;
+ }
+
+ return new Bip32Path(depth, bytePath);
+ }
+
+ public byte[] parseInitiateSecureChannel(ApduResponse rapdu){
+
+ try{
+ byte[] data= rapdu.getData();
+ logger.info("SATOCHIPLIB: parseInitiateSecureChannel data: " + toHexString(data));
+
+ // data= [coordxSize | coordx | sig1Size | sig1 | sig2Size | sig2]
+ int offset=0;
+ int coordxSize= 256*data[offset++] + data[offset++];
+
+ byte[] coordx= new byte[coordxSize];
+ System.arraycopy(data, offset, coordx, 0, coordxSize);
+ offset+=coordxSize;
+
+ // msg1 is [coordx_size | coordx]
+ byte[] msg1= new byte[2+coordxSize];
+ System.arraycopy(data, 0, msg1, 0, msg1.length);
+
+ int sig1Size= 256*data[offset++] + data[offset++];
+ byte[] sig1= new byte[sig1Size];
+ System.arraycopy(data, offset, sig1, 0, sig1Size);
+ offset+=sig1Size;
+
+ // msg2 is [coordxSize | coordx | sig1Size | sig1]
+ byte[] msg2= new byte[2+coordxSize + 2 + sig1Size];
+ System.arraycopy(data, 0, msg2, 0, msg2.length);
+
+ int sig2Size= 256*data[offset++] + data[offset++];
+ byte[] sig2= new byte[sig2Size];
+ System.arraycopy(data, offset, sig2, 0, sig2Size);
+ offset+=sig2Size;
+
+ byte[] pubkey= recoverPubkey(msg1, sig1, coordx);
+
+ return pubkey;
+ } catch(Exception e) {
+ throw new RuntimeException("Exception in parseInitiateSecureChannel: ", e);
+ }
+ }
+
+
+ public List parseInitiateSecureChannelGetPossibleAuthentikeys(ApduResponse rapdu){
+
+ try{
+ byte[] data= rapdu.getData();
+ int dataLength = data.length;
+ logger.info("SATOCHIPLIB: parseInitiateSecureChannel data: " + toHexString(data));
+
+ // data= [coordxSize | coordx | sig1Size | sig1 | sig2Size | sig2 | coordxSize(optional) | coordxAuthentikey(optional)]
+ int offset=0;
+ int coordxSize= 256*data[offset++] + data[offset++];
+
+ byte[] coordx= new byte[coordxSize];
+ System.arraycopy(data, offset, coordx, 0, coordxSize);
+ offset+=coordxSize;
+
+ // msg1 is [coordx_size | coordx]
+ byte[] msg1= new byte[2+coordxSize];
+ System.arraycopy(data, 0, msg1, 0, msg1.length);
+
+ int sig1Size= 256*data[offset++] + data[offset++];
+ byte[] sig1= new byte[sig1Size];
+ System.arraycopy(data, offset, sig1, 0, sig1Size);
+ offset+=sig1Size;
+
+ // msg2 is [coordxSize | coordx | sig1Size | sig1]
+ byte[] msg2= new byte[2+coordxSize + 2 + sig1Size];
+ System.arraycopy(data, 0, msg2, 0, msg2.length);
+
+ int sig2Size= 256*data[offset++] + data[offset++];
+ byte[] sig2= new byte[sig2Size];
+ System.arraycopy(data, offset, sig2, 0, sig2Size);
+ offset+=sig2Size;
+
+ // if authentikey coordx are available
+ // (currently only for Seedkeeper v0.2 and higher)
+ if (dataLength>offset+1){
+ int coordxAuthentikeySize = 256*data[offset++] + data[offset++];
+ if (dataLength>offset+coordxAuthentikeySize){}
+ byte[] coordxAuthentikey= new byte[coordxAuthentikeySize];
+ System.arraycopy(data, offset, coordxAuthentikey, 0, coordxAuthentikeySize);
+
+ byte[] authentikey= recoverPubkey(msg2, sig2, coordxAuthentikey);
+ List possibleAuthentikeys = new ArrayList();
+ possibleAuthentikeys.add(authentikey);
+ return possibleAuthentikeys;
+
+ } else {
+ // if authentikey coordx is not provided, two possible pubkeys can be recovered as par ECDSA properties
+ // recover all possible authentikeys from msg2, sig2
+ List possibleAuthentikeys = recoverPossiblePubkeys(msg2, sig2);
+
+ return possibleAuthentikeys;
+ }
+
+ } catch(Exception e) {
+ throw new RuntimeException("Exception in parseInitiateSecureChannelGetPossibleAuthentikeys:", e);
+ }
+ }
+
+
+ public byte[] parseBip32GetAuthentikey(ApduResponse rapdu){
+ try{
+ byte[] data= rapdu.getData();
+ logger.info("SATOCHIPLIB: parseBip32GetAuthentikey data: " + toHexString(data));
+ // data: [coordx_size(2b) | coordx | sig_size(2b) | sig ]
+
+ int offset=0;
+ int coordxSize= 256*(data[offset++] & 0xff) + data[offset++];
+ byte[] coordx= new byte[coordxSize];
+ System.arraycopy(data, offset, coordx, 0, coordxSize);
+ offset+=coordxSize;
+
+ // msg1 is [coordx_size | coordx]
+ byte[] msg1= new byte[2+coordxSize];
+ System.arraycopy(data, 0, msg1, 0, msg1.length);
+
+ int sig1Size= 256*data[offset++] + data[offset++];
+ byte[] sig1= new byte[sig1Size];
+ System.arraycopy(data, offset, sig1, 0, sig1Size);
+ offset+=sig1Size;
+
+ byte[] pubkey= recoverPubkey(msg1, sig1, coordx);
+ authentikey= new byte[pubkey.length];
+ System.arraycopy(pubkey, 0, authentikey, 0, pubkey.length);
+ return pubkey;
+ } catch(Exception e) {
+ throw new RuntimeException("Exception during Authentikey recovery", e);
+ }
+ }
+
+ public byte[][] parseBip32GetExtendedKey(ApduResponse rapdu){//todo: return a wrapped
+
+ try{
+ byte[] data= rapdu.getData();
+ logger.info("SATOCHIPLIB: parseBip32GetExtendedKey data: " + toHexString(data));
+ //data: [chaincode(32b) | coordx_size(2b) | coordx | sig_size(2b) | sig | sig_size(2b) | sig2]
+
+ int offset=0;
+ byte[] chaincode= new byte[32];
+ System.arraycopy(data, offset, chaincode, 0, chaincode.length);
+ offset+=32;
+
+ int coordxSize= 256*(data[offset++] & 0x7f) + data[offset++]; // (data[32] & 0x80) is ignored (optimization flag)
+ byte[] coordx= new byte[coordxSize];
+ System.arraycopy(data, offset, coordx, 0, coordxSize);
+ offset+=coordxSize;
+
+ // msg1 is [chaincode | coordx_size | coordx]
+ byte[] msg1= new byte[32+2+coordxSize];
+ System.arraycopy(data, 0, msg1, 0, msg1.length);
+
+ int sig1Size= 256*data[offset++] + data[offset++];
+ byte[] sig1= new byte[sig1Size];
+ System.arraycopy(data, offset, sig1, 0, sig1Size);
+ offset+=sig1Size;
+
+ // msg2 is [chaincode | coordxSize | coordx | sig1Size | sig1]
+ byte[] msg2= new byte[32 + 2+coordxSize + 2 + sig1Size];
+ System.arraycopy(data, 0, msg2, 0, msg2.length);
+
+ int sig2Size= 256*data[offset++] + data[offset++];
+ byte[] sig2= new byte[sig2Size];
+ System.arraycopy(data, offset, sig2, 0, sig2Size);
+ offset+=sig2Size;
+
+ byte[] pubkey= recoverPubkey(msg1, sig1, coordx);
+
+ // todo: recover from si2
+ return new byte[][] {pubkey, chaincode};
+ } catch(Exception e) {
+ System.out.println("SATOCHIPLIB parseBip32GetExtendedKey() exception: "+e);
+ e.printStackTrace();
+ throw new RuntimeException("Is BouncyCastle in the classpath?", e);
+ }
+ }
+
+ /****************************************
+ * recovery methods *
+ ****************************************/
+
+ // based on https://github.com/bitcoinj/bitcoinj/blob/4dc4cf743df9de996282b1aa3fd1d092859774cb/core/src/main/java/org/bitcoinj/core/ECKey.java#L977
+
+ public int recoverRecId(byte[] hash, BigInteger[] sigBig, byte[] coordx){
+
+ ECPoint point=null;
+ for (int recid=0; recid<4; recid++){
+ point= Recover(hash, sigBig, recid, false);
+
+ // convert to byte[]
+ byte[] pubkey= point.getEncoded(false); // uncompressed
+ byte[] coordx2= new byte[32];
+ System.arraycopy(pubkey, 1, coordx2, 0, 32);
+
+ // compare with known coordx
+ if (Arrays.equals(coordx, coordx2)){
+ logger.info("SATOCHIPLIB: Found coordx: " + toHexString(coordx2));
+ logger.info("SATOCHIPLIB: Found pubkey: " + toHexString(pubkey));
+ return recid;
+ }
+ }
+ return -1; // could not recover pubkey
+
+ }
+
+ public byte[] recoverPubkey(byte[] msg, byte[] sig, byte[] coordx) {
+
+ // convert msg to hash
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance("SHA256", "BC");
+ } catch(Exception e) {
+ System.out.println("SATOCHIPLIB recoverPubkey() exception: "+e);
+ e.printStackTrace();
+ throw new RuntimeException("Is BouncyCastle in the classpath?", e);
+ }
+ byte[] hash= md.digest(msg);
+
+ // convert sig array to big integer
+ byte[] sigCompact= parseToCompactSignature(sig);
+ byte[] r= new byte[32];
+ System.arraycopy(sigCompact, 0, r, 0, 32);
+ byte[] s= new byte[32];
+ System.arraycopy(sigCompact, 32, s, 0, 32);
+
+ BigInteger[] sigBig= new BigInteger[2] ;
+ sigBig[0]= new BigInteger(1, r);
+ sigBig[1]= new BigInteger(1, s);
+
+ ECPoint point=null;
+ for (int recid=0; recid<4; recid++){
+ point= Recover(hash, sigBig, recid, false);
+
+ // convert to byte[]
+ byte[] pubkey= point.getEncoded(false); // uncompressed
+ byte[] coordx2= new byte[32];
+ System.arraycopy(pubkey, 1, coordx2, 0, 32);
+
+ //BigInteger xx= point.getAffineX();
+ //byte[] coordx2 = X9IntegerConverter.IntegerToBytes(xx, X9IntegerConverter.GetByteLength(CURVE));
+
+ // compare with known coordx
+ if (Arrays.equals(coordx, coordx2)){
+ logger.info("SATOCHIPLIB: Found coordx: " + toHexString(coordx2));
+ //BigInteger yy= point.getAffineY();
+ //byte[] coordy = X9IntegerConverter.IntegerToBytes(yy, X9IntegerConverter.GetByteLength(CURVE));
+ //byte[] pubkey = new byte[1 + coordx2.Length + coordy.length];
+ //pubkey[0]= 0x04;
+ //System.arraycopy(coordx2, 0, pubkey, 1, coordx2.lenght);
+ //System.arraycopy(coordy, 0, pubkey, 33, coordy.lenght);
+ logger.info("SATOCHIPLIB: Found pubkey: " + toHexString(pubkey));
+ return pubkey;
+ }
+ }
+ return null; // could not recover pubkey
+ }
+
+ public List recoverPossiblePubkeys(byte[] msg, byte[] sig) {
+ List pubkeys = new ArrayList();
+
+ // convert msg to hash
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance("SHA256", "BC");
+ } catch(Exception e) {
+ System.out.println("SATOCHIPLIB recoverPossiblePubkeys() exception: "+e);
+ e.printStackTrace();
+ throw new RuntimeException("Is BouncyCastle in the classpath?", e);
+ }
+ byte[] hash= md.digest(msg);
+
+ // convert sig array to big integer
+ byte[] sigCompact= parseToCompactSignature(sig);
+ byte[] r= new byte[32];
+ System.arraycopy(sigCompact, 0, r, 0, 32);
+ byte[] s= new byte[32];
+ System.arraycopy(sigCompact, 32, s, 0, 32);
+
+ BigInteger[] sigBig= new BigInteger[2] ;
+ sigBig[0]= new BigInteger(1, r);
+ sigBig[1]= new BigInteger(1, s);
+
+ ECPoint point=null;
+ for (int recid=0; recid<4; recid++){
+ point= Recover(hash, sigBig, recid, false);
+ if (point==null){
+ logger.warning("SATOCHIPLIB: null point for recid: " + recid);
+ continue;
+ }
+
+ // convert to byte[]
+ byte[] pubkey= point.getEncoded(false); // uncompressed
+
+ // add to list
+ pubkeys.add(pubkey);
+ logger.warning("SATOCHIPLIB: Found potential pubkey: " + toHexString(pubkey));
+ }
+ return pubkeys;
+ }
+
+ public byte[] parseToCompactSignature(byte[] sigIn){
+
+ // sig is DER format, starting with 30 45
+ int sigInSize= sigIn.length;
+
+ int offset=0;
+ if (sigIn[offset++] != 0x30){
+ throw new RuntimeException("Wrong signature byte (should be 0x30) !");
+ }
+ int lt= sigIn[offset++];
+ int check= sigIn[offset++];
+ if (check != 0x02){
+ throw new RuntimeException("Wrong signature check byte (should be 0x02) !");
+ }
+
+ int lr= sigIn[offset++]; // should be 0x20 or 0x21 if first r msb is 1
+ byte[] r= new byte[32];
+ if (lr== 0x20){
+ System.arraycopy(sigIn, offset, r, 0, 32);
+ offset+=32;
+ }else if (lr== 0x21){
+ offset++; // skip zero byte
+ System.arraycopy(sigIn, offset, r, 0, 32);
+ offset+=32;
+ }
+ else{
+ throw new RuntimeException("Wrong signature r length: " + lr + " (should be 0x20 or 0x21) !");
+ }
+
+ check= sigIn[offset++];
+ if (check != 0x02){
+ throw new RuntimeException("Wrong signature check byte (should be 0x02) !");
+ }
+
+ int ls= sigIn[offset++]; // should be 0x20 or 0x21 if first s msb is 1
+ byte[] s= new byte[32];
+ if (ls== 0x20){
+ System.arraycopy(sigIn, offset, s, 0, 32);
+ offset+=32;
+ } else if (ls== 0x21){
+ offset++; // skip zero byte
+ System.arraycopy(sigIn, offset, s, 0, 32);
+ offset+=32;
+ } else{
+ throw new RuntimeException("Wrong signature s length: " + ls + " (should be 0x20 or 0x21) !");
+ }
+
+ int sigOutSize= 64;
+ byte[] sigOut= new byte[sigOutSize];
+ System.arraycopy(r, 0, sigOut, 0, r.length);
+ System.arraycopy(s, 0, sigOut, 32, s.length);
+
+ return sigOut;
+ }
+
+ public ECPoint Recover(byte[] hash, BigInteger[] sig, int recId, boolean check){
+
+ BigInteger r= sig[0];
+ BigInteger s= sig[1];
+
+ BigInteger n = CURVE.getN(); // Curve order.
+ BigInteger i = BigInteger.valueOf((long) recId / 2);
+ BigInteger x = r.add(i.multiply(n));
+ BigInteger prime = SecP256K1Curve.q;
+
+ if (x.compareTo(prime) >= 0) {
+ // Cannot have point co-ordinates larger than this as everything takes place modulo Q.
+ return null;
+ }
+ ECPoint R = decompressKey(x, (recId & 1) == 1);
+ if (!R.multiply(n).isInfinity())
+ return null;
+
+ BigInteger e = new BigInteger(1, hash); //message.toBigInteger();
+
+ BigInteger eInv = BigInteger.ZERO.subtract(e).mod(n);
+ BigInteger rInv = r.modInverse(n);
+ BigInteger srInv = rInv.multiply(s).mod(n);
+ BigInteger eInvrInv = rInv.multiply(eInv).mod(n);
+ ECPoint q = ECAlgorithms.sumOfTwoMultiplies(CURVE.getG(), eInvrInv, R, srInv);
+
+ return q;
+ //return ECKey.fromPublicOnly(q, compressed);
+ }
+
+ /** Decompress a compressed public key (x co-ord and low-bit of y-coord). */
+ private ECPoint decompressKey(BigInteger xBN, boolean yBit) {
+ X9IntegerConverter x9 = new X9IntegerConverter();
+ byte[] compEnc = x9.integerToBytes(xBN, 1 + x9.getByteLength(CURVE.getCurve()));
+ compEnc[0] = (byte)(yBit ? 0x03 : 0x02);
+ return CURVE.getCurve().decodePoint(compEnc);
+ }
+
+
+ //based on https://github.com/bitcoinj/bitcoinj/blob/master/core/src/main/java/org/bitcoinj/core/ECKey.java
+ public BigInteger[] decodeFromDER(byte[] bytes) {
+ ASN1InputStream decoder = null;
+ try {
+ // BouncyCastle by default is strict about parsing ASN.1 integers. We relax this check, because some
+ // Bitcoin signatures would not parse.
+ Properties.setThreadOverride("org.bouncycastle.asn1.allow_unsafe_integer", true);
+ decoder = new ASN1InputStream(bytes);
+ final ASN1Primitive seqObj = decoder.readObject();
+ if (seqObj == null)
+ throw new RuntimeException("Reached past end of ASN.1 stream.");
+ if (!(seqObj instanceof DLSequence))
+ throw new RuntimeException("Read unexpected class: " + seqObj.getClass().getName());
+ final DLSequence seq = (DLSequence) seqObj;
+ ASN1Integer r, s;
+ try {
+ r = (ASN1Integer) seq.getObjectAt(0);
+ s = (ASN1Integer) seq.getObjectAt(1);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ // enforce low-S signature (BIP 62)
+ BigInteger s2= s.getPositiveValue();
+ if (s2.compareTo(HALF_CURVE_ORDER) > 0){
+ s2= CURVE_ORDER.subtract(s2);
+ }
+
+ BigInteger[] sigBig= new BigInteger[2];
+ sigBig[0]= r.getPositiveValue();
+ sigBig[1]= s2; //s.getPositiveValue();
+ return sigBig;
+
+ // OpenSSL deviates from the DER spec by interpreting these values as unsigned, though they should not be
+ // Thus, we always use the positive versions. See: http://r6.ca/blog/20111119T211504Z.html
+ //return new ECDSASignature(r.getPositiveValue(), s.getPositiveValue());
+ } catch (Exception e) {
+ //throw new SignatureDecodeException(e);
+ throw new RuntimeException("Exception in decodeFromDER() ", e);
+ } finally {
+ if (decoder != null)
+ try { decoder.close(); } catch (IOException x) {}
+ Properties.removeThreadOverride("org.bouncycastle.asn1.allow_unsafe_integer");
+ }
+ }
+
+ public boolean verifySig(byte[] msg, byte[] dersig, byte[] pub) {
+ logger.info("SATOCHIPLIB: In verifySig() ");
+ logger.info("SATOCHIPLIB: verifySig: authentikey: " + toHexString(pub));
+
+ // compute hash of message
+ SHA256Digest digest = new SHA256Digest();
+ byte[] hash= new byte[digest.getDigestSize()];
+ digest.update(msg, 0, msg.length);
+ digest.doFinal(hash, 0);
+ logger.info("SATOCHIPLIB: verifySig: hash: " + toHexString(hash));
+
+ // convert der-sig to bigInteger[]
+ BigInteger[] rs= decodeFromDER(dersig);
+
+ ECDSASigner signer = new ECDSASigner();
+ ECPublicKeyParameters params = new ECPublicKeyParameters(CURVE.getCurve().decodePoint(pub), CURVE);
+ signer.init(false, params);
+ try {
+ logger.info("SATOCHIPLIB: verifySig: hash: verifySignature: Start" );
+ return signer.verifySignature(hash, rs[0], rs[1]);
+ } catch (NullPointerException e) {
+ logger.warning("SATOCHIPLIB: Caught NPE inside bouncy castle"+ e);
+ return false;
+ }
+ }
+
+
+ /**
+ * Serializes the APDU to human readable hex string format
+ *
+ * @return the hex string representation of the APDU
+ */
+ public static String toHexString(byte[] raw) {
+ try{
+ if ( raw == null ) {
+ return "";
+ }
+ final StringBuilder hex = new StringBuilder( 2 * raw.length );
+ for ( final byte b : raw ) {
+ hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F)));
+ }
+ return hex.toString();
+ } catch(Exception e){
+ return "Exception in Util.toHexString()";
+ }
+ }
+
+ public static byte[] fromHexString(String hex){
+
+ if ((hex.length() % 2) != 0)
+ throw new IllegalArgumentException("Input string must contain an even number of characters");
+
+ int len = hex.length();
+ byte[] data = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i+1), 16));
+ }
+ return data;
+ }
+
+}
\ No newline at end of file
diff --git a/hardware/src/main/java/com/satochip/SecureChannelSession.java b/hardware/src/main/java/com/satochip/SecureChannelSession.java
new file mode 100644
index 000000000..a45cadc41
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/SecureChannelSession.java
@@ -0,0 +1,260 @@
+package com.satochip;
+
+import org.bouncycastle.crypto.params.KeyParameter;
+import org.bouncycastle.jce.ECNamedCurveTable;
+import org.bouncycastle.jce.interfaces.ECPublicKey;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.jce.spec.ECParameterSpec;
+import org.bouncycastle.jce.spec.ECPublicKeySpec;
+import org.bouncycastle.crypto.digests.SHA1Digest;
+import org.bouncycastle.crypto.macs.HMac;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyAgreement;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.*;
+import java.util.logging.Logger;
+import java.nio.ByteBuffer;
+
+/**
+ * Handles a SecureChannel session with the card.
+ */
+public class SecureChannelSession {
+
+ private static final Logger logger = Logger.getLogger("org.satochip.client");
+
+ public static final int SC_SECRET_LENGTH = 16;
+ public static final int SC_BLOCK_SIZE = 16;
+ public static final int IV_SIZE = 16;
+ public static final int MAC_SIZE= 20;
+
+ // secure channel constants
+ private final static byte INS_INIT_SECURE_CHANNEL = (byte) 0x81;
+ private final static byte INS_PROCESS_SECURE_CHANNEL = (byte) 0x82;
+ private final static short SW_SECURE_CHANNEL_REQUIRED = (short) 0x9C20;
+ private final static short SW_SECURE_CHANNEL_UNINITIALIZED = (short) 0x9C21;
+ private final static short SW_SECURE_CHANNEL_WRONG_IV= (short) 0x9C22;
+ private final static short SW_SECURE_CHANNEL_WRONG_MAC= (short) 0x9C23;
+
+ private boolean initialized_secure_channel= false;
+
+ // secure channel keys
+ private byte[] secret;
+ private byte[] iv;
+ private int ivCounter;
+ byte[] derived_key;
+ byte[] mac_key;
+
+ // for ECDH
+ ECParameterSpec ecSpec;
+ private KeyPair keyPair;
+ private byte[] publicKey;
+
+ // for session encryption
+ private Cipher sessionCipher;
+ private SecretKeySpec sessionEncKey;
+ private SecureRandom random;
+ private boolean open;
+
+ /**
+ * Constructs a SecureChannel session on the client.
+ */
+ public SecureChannelSession() {
+ random = new SecureRandom();
+ open = false;
+
+ try {
+ // generate keypair
+ Security.removeProvider("BC");
+ Security.insertProviderAt(new BouncyCastleProvider(), 1);
+ ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
+ KeyPairGenerator g = KeyPairGenerator.getInstance("EC");
+ g.initialize(ecSpec, random);
+ keyPair = g.generateKeyPair();
+ publicKey = ((ECPublicKey) keyPair.getPublic()).getQ().getEncoded(false);
+ } catch (Exception e) {
+ logger.warning("SATOCHIPLIB: Exception in SecureChannelSession() constructor: "+ e);
+ System.out.println("SATOCHIPLIB SecureChannelSession() exception: "+e);
+ e.printStackTrace();
+ throw new RuntimeException("Is BouncyCastle in the classpath?", e);
+ }
+ }
+
+
+ /**
+ * Generates a pairing secret. This should be called before each session. The public key of the card is used as input
+ * for the EC-DH algorithm. The output is stored as the secret.
+ *
+ * @param keyData the public key returned by the applet as response to the SELECT command
+ */
+ public void initiateSecureChannel(byte[] keyData) { //TODO: check keyData format
+ try {
+
+ // Diffie-Hellman
+ // ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
+ // KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH", "BC");
+ // g.initialize(ecSpec, random);
+ // KeyPair keyPair = g.generateKeyPair();
+ // publicKey = ((ECPublicKey) keyPair.getPublic()).getQ().getEncoded(false);
+
+ KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH", "BC");
+ keyAgreement.init(keyPair.getPrivate());
+
+ ECPublicKeySpec cardKeySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(keyData), ecSpec);
+ ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA", "BC").generatePublic(cardKeySpec);
+
+ keyAgreement.doPhase(cardKey, true);
+ secret = keyAgreement.generateSecret();
+
+ // derive session keys
+ HMac hMac = new HMac(new SHA1Digest());
+ hMac.init(new KeyParameter(secret));
+ byte[] msg_key= "sc_key".getBytes();
+ hMac.update(msg_key, 0, msg_key.length);
+ byte[] out = new byte[20];
+ hMac.doFinal(out, 0);
+ derived_key= new byte[16];
+ System.arraycopy(out, 0, derived_key, 0, 16);
+
+ hMac.reset();
+ byte[] msg_mac= "sc_mac".getBytes();
+ hMac.update(msg_mac, 0, msg_mac.length);
+ mac_key = new byte[20];
+ hMac.doFinal(mac_key, 0);
+
+ ivCounter= 1;
+ initialized_secure_channel= true;
+ } catch (Exception e) {
+ logger.warning("SATOCHIPLIB: Exception in initiateSecureChannel: "+ e);
+ System.out.println("SATOCHIPLIB initiateSecureChannel() exception: "+e);
+ e.printStackTrace();
+ throw new RuntimeException("Is BouncyCastle in the classpath?", e);
+ }
+ }
+
+ public ApduCommand encrypt_secure_channel(ApduCommand plainApdu){
+
+ try {
+
+ byte[] plainBytes= plainApdu.serialize();
+
+ // set iv
+ iv = new byte[SC_BLOCK_SIZE];
+ random.nextBytes(iv);
+ ByteBuffer bb = ByteBuffer.allocate(4);
+ bb.putInt(ivCounter); // big endian
+ byte[] ivCounterBytes= bb.array();
+ System.arraycopy(ivCounterBytes, 0, iv, 12, 4);
+ ivCounter+=2;
+ logger.info("SATOCHIPLIB: ivCounter: "+ ivCounter);
+ logger.info("SATOCHIPLIB: ivCounterBytes: "+ SatochipParser.toHexString(ivCounterBytes));
+ logger.info("SATOCHIPLIB: iv: "+ SatochipParser.toHexString(iv));
+
+ // encrypt data
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
+ sessionEncKey = new SecretKeySpec(derived_key, "AES");
+ sessionCipher = Cipher.getInstance("AES/CBC/PKCS7PADDING", "BC");
+ sessionCipher.init(Cipher.ENCRYPT_MODE, sessionEncKey, ivParameterSpec);
+ byte[] encrypted = sessionCipher.doFinal(plainBytes);
+ // logger.info("SATOCHIPLIB: encrypted: "+ SatochipParser.toHexString(derived_key));
+ // logger.info("SATOCHIPLIB: encrypted: "+ SatochipParser.toHexString(encrypted));
+
+ // mac
+ int offset= 0;
+ byte[] data_to_mac= new byte[IV_SIZE + 2 + encrypted.length];
+ System.arraycopy(iv, offset, data_to_mac, offset, IV_SIZE);
+ offset+=IV_SIZE;
+ data_to_mac[offset++]= (byte)(encrypted.length>>8);
+ data_to_mac[offset++]= (byte)(encrypted.length%256);
+ System.arraycopy(encrypted, 0, data_to_mac, offset, encrypted.length);
+ // logger.info("SATOCHIPLIB: data_to_mac: "+ SatochipParser.toHexString(data_to_mac));
+
+ HMac hMac = new HMac(new SHA1Digest());
+ hMac.init(new KeyParameter(mac_key));
+ hMac.update(data_to_mac, 0, data_to_mac.length);
+ byte[] mac = new byte[20];
+ hMac.doFinal(mac, 0);
+ // logger.info("SATOCHIPLIB: mac: "+ SatochipParser.toHexString(mac));
+
+ //data= list(iv) + [len(ciphertext)>>8, len(ciphertext)&0xff] + list(ciphertext) + [len(mac)>>8, len(mac)&0xff] + list(mac)
+ byte[] data= new byte[IV_SIZE + 2 + encrypted.length + 2 + MAC_SIZE];
+ offset= 0;
+ System.arraycopy(iv, offset, data, offset, IV_SIZE);
+ offset+=IV_SIZE;
+ data[offset++]= (byte)(encrypted.length>>8);
+ data[offset++]= (byte)(encrypted.length%256);
+ System.arraycopy(encrypted, 0, data, offset, encrypted.length);
+ offset+=encrypted.length;
+ data[offset++]= (byte)(mac.length>>8);
+ data[offset++]= (byte)(mac.length%256);
+ System.arraycopy(mac, 0, data, offset, mac.length);
+ // logger.info("SATOCHIPLIB: data: "+ SatochipParser.toHexString(data));
+
+ // convert to C-APDU
+ ApduCommand encryptedApdu= new ApduCommand(0xB0, INS_PROCESS_SECURE_CHANNEL, 0x00, 0x00, data);
+ return encryptedApdu;
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ logger.warning("SATOCHIPLIB: Exception in encrypt_secure_channel: "+ e);
+ System.out.println("SATOCHIPLIB encrypt_secure_channel() exception: "+e);
+ e.printStackTrace();
+ throw new RuntimeException("Is BouncyCastle in the classpath?", e);
+ }
+
+ }
+
+ public ApduResponse decrypt_secure_channel(ApduResponse encryptedApdu){
+
+ try {
+
+ byte[] encryptedBytes= encryptedApdu.getData();
+ if (encryptedBytes.length==0){
+ return encryptedApdu; // no decryption needed
+ } else if (encryptedBytes.length<18){
+ throw new RuntimeException("Encrypted response has wrong length!");
+ }
+
+ byte[] iv= new byte[IV_SIZE];
+ int offset= 0;
+ System.arraycopy(encryptedBytes, offset, iv, 0, IV_SIZE);
+ offset+=IV_SIZE;
+ int ciphertext_size= ((encryptedBytes[offset++] & 0xff)<<8) + (encryptedBytes[offset++] & 0xff);
+ if ((encryptedBytes.length - offset)!= ciphertext_size){
+ throw new RuntimeException("Encrypted response has wrong length!");
+ }
+ byte[] ciphertext= new byte[ciphertext_size];
+ System.arraycopy(encryptedBytes, offset, ciphertext, 0, ciphertext.length);
+
+ // decrypt data
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
+ sessionEncKey = new SecretKeySpec(derived_key, "AES");
+ sessionCipher = Cipher.getInstance("AES/CBC/PKCS7PADDING", "BC");
+ sessionCipher.init(Cipher.DECRYPT_MODE, sessionEncKey, ivParameterSpec);
+ byte[] decrypted = sessionCipher.doFinal(ciphertext);
+
+ ApduResponse plainResponse= new ApduResponse(decrypted, (byte)0x90, (byte)0x00);
+ return plainResponse;
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ logger.warning("SATOCHIPLIB: Exception in decrypt_secure_channel: "+ e);
+ throw new RuntimeException("Exception during secure channel decryption: ", e);
+ }
+
+ }
+
+ public boolean initializedSecureChannel(){
+ return initialized_secure_channel;
+ }
+
+ public byte[] getPublicKey(){
+ return publicKey;
+ }
+
+ public void resetSecureChannel(){
+ initialized_secure_channel= false;
+ }
+
+}
diff --git a/hardware/src/main/java/com/satochip/WrongPINException.java b/hardware/src/main/java/com/satochip/WrongPINException.java
new file mode 100644
index 000000000..bc6bb5752
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/WrongPINException.java
@@ -0,0 +1,27 @@
+package com.satochip;
+
+/**
+ * Exception thrown when checking PIN/PUK
+ */
+public class WrongPINException extends ApduException {
+ private int retryAttempts;
+
+ /**
+ * Construct an exception with the given number of retry attempts.
+ *
+ * @param retryAttempts the number of retry attempts
+ */
+ public WrongPINException(int retryAttempts) {
+ super("Wrong PIN");
+ this.retryAttempts = retryAttempts;
+ }
+
+ /**
+ * Returns the number of available retry attempts.
+ *
+ * @return the number of retry attempts
+ */
+ public int getRetryAttempts() {
+ return retryAttempts;
+ }
+}
diff --git a/hardware/src/main/java/com/satochip/WrongPINLegacyException.java b/hardware/src/main/java/com/satochip/WrongPINLegacyException.java
new file mode 100644
index 000000000..9b3379a66
--- /dev/null
+++ b/hardware/src/main/java/com/satochip/WrongPINLegacyException.java
@@ -0,0 +1,14 @@
+package com.satochip;
+
+/**
+ * Exception thrown when checking PIN/PUK
+ */
+public class WrongPINLegacyException extends ApduException {
+
+ /**
+ * Construct an exception with the given number of retry attempts.
+ */
+ public WrongPINLegacyException() {
+ super("Wrong PIN Legacy");
+ }
+}