Skip to content

Add Satochip NFC Hardware wallet support #251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
90822fc
Satochip integration: add NFC related code (WIP)
Toporin Feb 6, 2025
e79c885
Inject activity into DeviceManagerAndroid
Toporin Feb 6, 2025
12f3c88
Satochip integration (wip)
Toporin Feb 7, 2025
672cf60
Satochip integration (wip)
Toporin Feb 10, 2025
812f383
Satochip integration: implement getXpubs() (wip)
Toporin Feb 13, 2025
dd39808
Satochip integration: implement signMessage() (wip)
Toporin Feb 13, 2025
062218e
Satochip integration: implement signTransaction() (wip)
Toporin Feb 18, 2025
e2ed77a
remove old unused code
Toporin Feb 18, 2025
5f1d3c7
Add PIN screen for satochip
Toporin Feb 20, 2025
e6d49e2
Add more debug logs (wip)
Toporin Feb 21, 2025
a776c8b
Better PIN handling and error mgmgt
Toporin Feb 21, 2025
9335c10
Add bottom sheet for NFC scan
Toporin Feb 24, 2025
8a391a1
Clean & remove unused code
Toporin Feb 24, 2025
87192ee
Simplify & remove unused code
Toporin Feb 24, 2025
88713fb
Simplify & remove unused code
Toporin Feb 24, 2025
1024437
remove unused code
Toporin Feb 24, 2025
aa6131c
Make NFC support more generic
Toporin Feb 25, 2025
0a185c3
Remove debug traces
Toporin Feb 26, 2025
234bf43
Remove more debug traces
Toporin Feb 26, 2025
66eac1d
(minor) code cleaning
Toporin Feb 26, 2025
229e79e
Add translation (wip)
Toporin Feb 27, 2025
68bf199
Merge branch 'master' into satochip-support
Toporin Feb 27, 2025
7573385
Add satochip in DeviceModel.zendeskValue
Toporin Feb 27, 2025
ef8185a
Update NfcActionStatus to none when no action requested
Toporin Feb 28, 2025
0c83288
Add support for Liquid-Bitcoin with Satochip (firmware v0.14-0.5+)
Toporin Mar 10, 2025
35d67a3
Show error message if Satochip card is not seeded
Toporin Mar 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .runDebug/GreenAndroid.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="GreenAndroid" type="AndroidRunConfigurationType" factoryName="Android App">
<option name="ANDROID_RUN_CONFIGURATION_SCHEMA_VERSION" value="1" />
<option name="DEPLOY" value="true" />
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
<option name="DEPLOY_AS_INSTANT" value="false" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="ALL_USERS" value="false" />
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
<option name="ALLOW_ASSUME_VERIFIED" value="false" />
<option name="CLEAR_APP_STORAGE" value="false" />
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="specific_activity" />
<option name="RESTORE_ENABLED" value="false" />
<option name="RESTORE_FILE" value="" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Java" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Hybrid>
<Java>
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Java>
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Java/Kotlin Method Sample (legacy)" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY" value="" />
<option name="ACTIVITY_CLASS" value="com.blockstream.green.GreenActivity" />
<option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />
<option name="SKIP_ACTIVITY_VALIDATION" value="false" />
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.blockstream.common.devices

import android.app.Activity

interface ActivityProvider {
fun getCurrentActivity(): Activity?
}
Original file line number Diff line number Diff line change
@@ -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<Activity>? = null

fun setActivity(activity: Activity) {
weakActivity = WeakReference(activity)
}

override fun getCurrentActivity(): Activity? {
return weakActivity?.get()
}

fun clearActivity() {
weakActivity = null
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Loading