From 90822fc9925af3ab026e993f001a088443547cc2 Mon Sep 17 00:00:00 2001 From: Toporin Date: Thu, 6 Feb 2025 21:03:22 +0100 Subject: [PATCH 01/25] Satochip integration: add NFC related code (WIP) --- .runDebug/GreenAndroid.run.xml | 72 ++++ .../common/devices/APDUCommand.java | 148 ++++++++ .../common/devices/APDUException.java | 29 ++ .../common/devices/APDUResponse.java | 220 ++++++++++++ .../blockstream/common/devices/ApduCommand.kt | 92 +++++ .../common/devices/ApduException.kt | 27 ++ .../common/devices/ApduResponse.kt | 158 +++++++++ .../common/devices/ApplicationStatus.java | 131 +++++++ .../common/devices/ApplicationStatus.kt | 104 ++++++ .../common/devices/CardChannel.java | 25 ++ .../blockstream/common/devices/CardChannel.kt | 21 ++ .../common/devices/CardListener.java | 18 + .../common/devices/CardListener.kt | 18 + .../common/devices/NfcCardChannel.java | 37 ++ .../common/devices/NfcCardChannel.kt | 32 ++ .../common/devices/NfcCardManager.java | 124 +++++++ .../common/devices/NfcCardManager.kt | 107 ++++++ .../common/devices/SatochipCardListener.kt | 47 +++ .../common/devices/SatochipCommandSet.java | 328 ++++++++++++++++++ .../common/devices/SatochipCommandSet.kt | 193 +++++++++++ .../blockstream/common/data/ErrorReport.kt | 1 + .../common/devices/ConnectionType.kt | 2 +- .../blockstream/common/devices/DeviceBrand.kt | 6 +- .../blockstream/common/devices/DeviceModel.kt | 2 + .../blockstream/common/devices/GreenDevice.kt | 10 + .../com/blockstream/common/gdk/GdkSession.kt | 1 + .../com/blockstream/common/gdk/data/Device.kt | 4 + .../common/managers/DeviceManager.kt | 3 + .../DeviceConnectionManagerAndroid.kt | 1 + .../compose/extensions/Resources.kt | 1 + gradle.properties | 5 +- green/build.gradle.kts | 4 +- 32 files changed, 1966 insertions(+), 5 deletions(-) create mode 100644 .runDebug/GreenAndroid.run.xml create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/APDUCommand.java create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/APDUException.java create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/APDUResponse.java create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/ApduCommand.kt create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/ApduException.kt create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/ApduResponse.kt create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/ApplicationStatus.java create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/ApplicationStatus.kt create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/CardChannel.java create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/CardChannel.kt create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/CardListener.java create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/CardListener.kt create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardChannel.java create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardChannel.kt create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardManager.java create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardManager.kt create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCardListener.kt create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.java create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.kt 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/APDUCommand.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/APDUCommand.java new file mode 100644 index 000000000..0c35a4c4d --- /dev/null +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/APDUCommand.java @@ -0,0 +1,148 @@ +//package com.blockstream.common.devices; +// +//import java.io.ByteArrayOutputStream; +//import java.io.IOException; +//import java.io.*; +// +///** +// * 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/common/src/androidMain/kotlin/com/blockstream/common/devices/APDUException.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/APDUException.java new file mode 100644 index 000000000..deb0ab8a7 --- /dev/null +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/APDUException.java @@ -0,0 +1,29 @@ +//package com.blockstream.common.devices; +// +///** +// * 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/common/src/androidMain/kotlin/com/blockstream/common/devices/APDUResponse.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/APDUResponse.java new file mode 100644 index 000000000..9981417fb --- /dev/null +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/APDUResponse.java @@ -0,0 +1,220 @@ +//package com.blockstream.common.devices; +// +//import java.io.*; +// +///** +// * 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/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/ApplicationStatus.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/ApplicationStatus.java new file mode 100644 index 000000000..3b9c787d3 --- /dev/null +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/ApplicationStatus.java @@ -0,0 +1,131 @@ +//package com.blockstream.common.devices; +// +///** +// * 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 +// * +// * @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/common/src/androidMain/kotlin/com/blockstream/common/devices/ApplicationStatus.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/ApplicationStatus.kt new file mode 100644 index 000000000..3c907918b --- /dev/null +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/ApplicationStatus.kt @@ -0,0 +1,104 @@ +package com.blockstream.common.devices + +/** + * Parses the result of a GET STATUS command retrieving application status. + */ +class ApplicationStatus(rapdu: ApduResponse) { + private var setupDone: Boolean = false + private var isSeeded: Boolean = false + private var needsSecureChannel: Boolean = false + private var needs2FA: Boolean = false + + private var protocolMajorVersion: Byte = 0 + private var protocolMinorVersion: Byte = 0 + private var appletMajorVersion: Byte = 0 + private var appletMinorVersion: Byte = 0 + + private var pin0RemainingTries: Byte = 0 + private var puk0RemainingTries: Byte = 0 + private var pin1RemainingTries: Byte = 0 + private var puk1RemainingTries: Byte = 0 + + private var protocolVersion: Int = 0 + + init { + val sw = rapdu.getSw() + + when (sw) { + 0x9000 -> { + val data = rapdu.getData() + protocolMajorVersion = data[0] + protocolMinorVersion = data[1] + appletMajorVersion = data[2] + appletMinorVersion = data[3] + protocolVersion = (protocolMajorVersion.toInt() shl 8) + protocolMinorVersion + + if (data.size >= 8) { + pin0RemainingTries = data[4] + puk0RemainingTries = data[5] + pin1RemainingTries = data[6] + puk1RemainingTries = data[7] + needs2FA = false // default value + } + if (data.size >= 9) { + needs2FA = data[8] != 0x00.toByte() + } + if (data.size >= 10) { + isSeeded = data[9] != 0x00.toByte() + } + if (data.size >= 11) { + setupDone = data[10] != 0x00.toByte() + } else { + setupDone = true + } + if (data.size >= 12) { + needsSecureChannel = data[11] != 0x00.toByte() + } else { + needsSecureChannel = false + needs2FA = false // default value + } + } + 0x9c04 -> { + setupDone = false + isSeeded = false + needsSecureChannel = false + } + else -> { + // throws IllegalArgumentException("Wrong getStatus data!") // should not happen + } + } + } + + // Getters + fun isSeeded(): Boolean = isSeeded + fun isSetupDone(): Boolean = setupDone + fun needsSecureChannel(): Boolean = needsSecureChannel + fun getPin0RemainingCounter(): Byte = pin0RemainingTries + fun getPuk0RemainingCounter(): Byte = puk0RemainingTries + + override fun toString(): String { + return """ + setup_done: $setupDone + is_seeded: $isSeeded + needs_2FA: $needs2FA + needs_secure_channel: $needsSecureChannel + protocol_major_version: $protocolMajorVersion + protocol_minor_version: $protocolMinorVersion + applet_major_version: $appletMajorVersion + applet_minor_version: $appletMinorVersion + """.trimIndent() + } + + fun getCardVersionInt(): Int { + return (protocolMajorVersion.toInt() * (1 shl 24)) + + (protocolMinorVersion.toInt() * (1 shl 16)) + + (appletMajorVersion.toInt() * (1 shl 8)) + + appletMinorVersion.toInt() + } + + fun getCardVersionString(): String { + return "$protocolMajorVersion.$protocolMinorVersion-$appletMajorVersion.$appletMinorVersion" + } + + fun getProtocolVersion(): Int = protocolVersion +} \ No newline at end of file diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/CardChannel.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/CardChannel.java new file mode 100644 index 000000000..7e9de7743 --- /dev/null +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/CardChannel.java @@ -0,0 +1,25 @@ +//package com.blockstream.common.devices; +// +//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(); +// +//} 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.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/CardListener.java new file mode 100644 index 000000000..d5bfbd7f2 --- /dev/null +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/CardListener.java @@ -0,0 +1,18 @@ +//package com.blockstream.common.devices; +// +///** +// * 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(); +//} \ 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/NfcCardChannel.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardChannel.java new file mode 100644 index 000000000..aee5cb0c1 --- /dev/null +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardChannel.java @@ -0,0 +1,37 @@ +//package com.blockstream.common.devices; +// +//import android.nfc.tech.IsoDep; +//import android.util.Log; +////import org.satochip.io.APDUCommand; +////import org.satochip.io.APDUResponse; +////import org.satochip.io.CardChannel; +// +//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/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.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardManager.java new file mode 100644 index 000000000..e84312656 --- /dev/null +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardManager.java @@ -0,0 +1,124 @@ +//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 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/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..5cb45e4f1 --- /dev/null +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardManager.kt @@ -0,0 +1,107 @@ +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 + +// companion object { +// init { +// Crypto.addBouncyCastleProvider() +// } +// } + + /** + * 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/SatochipCardListener.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCardListener.kt new file mode 100644 index 000000000..65dde5d03 --- /dev/null +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCardListener.kt @@ -0,0 +1,47 @@ +package com.blockstream.common.devices + +//import android.util.Log + +//import org.satochip.client.SatochipCommandSet +//import org.satochip.io.CardChannel +//import org.satochip.io.CardListener +//import org.satochip.satodimeapp.data.NfcResultCode + +private const val TAG = "SatochipCardListener" + +object SatochipCardListenerForAction : CardListener { + + override fun onConnected(channel: CardChannel) { + + println("SATODEBUG onConnected: Card is connected") + try { + val cmdSet = SatochipCommandSet(channel) + // start to interact with card + //NfcCardService.initialize(cmdSet) + + val rapduSelect = cmdSet.cardSelect("satochip").checkOK() + // cardStatus + val rapduStatus = cmdSet.cardGetStatus()//To update status if it's not the first reading + val cardStatus = cmdSet.getApplicationStatus() //applicationStatus ?: return + println("SATODEBUG readCard cardStatus: $cardStatus") + println("SATODEBUG readCard cardStatus: ${cardStatus.toString()}") + + // TODO: disconnect? + println("onConnected: trigger disconnection!") + onDisconnected() + + // disable scanning once finished + //Thread.sleep(100) // delay to let resultCodeLive update (avoid race condition?) + + } catch (e: Exception) { + println("onConnected: an exception has been thrown during card init.") + //Log.e(TAG, Log.getStackTraceString(e)) + onDisconnected() + } + } + + override fun onDisconnected() { + //NfcCardService.isConnected.postValue(false) + println("onDisconnected: Card disconnected!") + } +} \ No newline at end of file diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.java new file mode 100644 index 000000000..d7c4b9fdc --- /dev/null +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.java @@ -0,0 +1,328 @@ +//package com.blockstream.common.devices; +// +////import org.bitcoinj.core.Base58; +////import org.bitcoinj.core.Sha256Hash; +////import org.bouncycastle.crypto.digests.RIPEMD160Digest; +////import org.satochip.client.seedkeeper.*; +////import org.satochip.io.*; +////import org.bouncycastle.util.encoders.Hex; +// +////import static com.satochip.Constants.INS_GET_STATUS; +// +//import java.nio.ByteBuffer; +//import java.security.SecureRandom; +//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; +//import java.io.InputStream; +//import java.io.ByteArrayInputStream; +//import java.nio.charset.StandardCharsets; +//import java.security.cert.CertPathValidator; +//import java.security.cert.CertPath; +//import java.security.cert.CertificateFactory; +//import java.security.cert.Certificate; +//import java.security.cert.PKIXParameters; +//import java.security.KeyStore; +//import java.security.PublicKey; +// +// +// +// +///** +// * 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"); +// +// public final static byte INS_GET_STATUS = (byte) 0x3C; +// +// 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[] authentikey = null; +// private String authentikeyHex = null; +// private String defaultBip32path = null; +// private byte[] extendedKey = null; +// private byte[] extendedChaincode = null; +// private String extendedKeyHex = null; +// private byte[] extendedPrivKey = null; +// private String extendedPrivKeyHex = null; +// +// // Satodime, SeedKeeper or Satochip? +// private String cardType = null; +// private String certPem = null; // PEM certificate of device, if any +// +// // satodime +// // SatodimeStatus satodimeStatus = null; +// +// public static final byte[] SATOCHIP_AID = hexToBytes("5361746f43686970"); //SatoChip +// public static final byte[] SEEDKEEPER_AID = hexToBytes("536565644b6565706572"); //SeedKeeper +// public static final byte[] SATODIME_AID = hexToBytes("5361746f44696d65"); //SatoDime +// +// +// /** +// * 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(); +//// this.satodimeStatus = new SatodimeStatus(); +// 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 SatodimeStatus getSatodimeStatus() { +//// this.satodimeGetStatus(); +//// return this.satodimeStatus; +//// } +// +//// public byte[] getSatodimeUnlockSecret() { +//// return this.satodimeStatus.getUnlockSecret(); +//// } +//// +//// public void setSatodimeUnlockSecret(byte[] unlockSecret) { +//// this.satodimeStatus.setUnlockSecret(unlockSecret); +//// } +// +// /* s must be an even-length string. */ +// public static byte[] hexToBytes(String s) { +// int len = s.length(); +// byte[] data = new byte[len / 2]; +// for (int i = 0; i < len; i += 2) { +// data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) +// + Character.digit(s.charAt(i+1), 16)); +// } +// return data; +// } +// +// /**************************************** +// * AUTHENTIKEY * +// ****************************************/ +//// public byte[] getAuthentikey() { +//// if (authentikey == null) { +//// cardGetAuthentikey(); +//// } +//// return authentikey; +//// } +// +//// public String getAuthentikeyHex() { +//// if (authentikeyHex == null) { +//// cardGetAuthentikey(); +//// } +//// return authentikeyHex; +//// } +// +//// public byte[] getBip32Authentikey() { +//// if (authentikey == null) { +//// cardBip32GetAuthentikey(); +//// } +//// return authentikey; +//// } +// +//// public String getBip32AuthentikeyHex() { +//// if (authentikeyHex == null) { +//// cardBip32GetAuthentikey(); +//// } +//// return authentikeyHex; +//// } +// +// public List getPossibleAuthentikeys(){ +// return this.possibleAuthentikeys; +// } +// +//// public SatochipParser getParser() { +//// return parser; +//// } +// +// public void setDefaultBip32path(String bip32path) { +// defaultBip32path = bip32path; +// } +// +// /** +// * Set the SecureChannel object +// * +// * param secureChannel secure channel +// */ +//// protected void setSecureChannel(SecureChannelSession secureChannel) { +//// this.secureChannel = secureChannel; +//// } +// +// +// +// 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/Satodime/SeedKeeper 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() throws IOException { +// +// ApduResponse rapdu = cardSelect("satochip"); +// if (rapdu.getSw() != 0x9000) { +// rapdu = cardSelect("seedkeeper"); +// if (rapdu.getSw() != 0x9000) { +// rapdu = cardSelect("satodime"); +// if (rapdu.getSw() != 0x9000) { +// this.cardType = "unknown"; +// logger.warning("SATOCHIPLIB: CardSelect: could not select a known applet"); +// } +// } +// } +// +// return rapdu; +// } +// +// public ApduResponse cardSelect(String cardType) throws IOException { +// +// ApduCommand selectApplet; +// if (cardType.equals("satochip")) { +// selectApplet = new ApduCommand(0x00, 0xA4, 0x04, 0x00, SATOCHIP_AID); +// } else if (cardType.equals("seedkeeper")) { +// selectApplet = new ApduCommand(0x00, 0xA4, 0x04, 0x00, SEEDKEEPER_AID); +// } else { +// selectApplet = new ApduCommand(0x00, 0xA4, 0x04, 0x00, SATODIME_AID); +// } +// +// 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; +// } +// +//} 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..30377bf97 --- /dev/null +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.kt @@ -0,0 +1,193 @@ +package com.blockstream.common.devices + +import java.nio.ByteBuffer +import java.security.SecureRandom +import java.util.ArrayList +import java.util.Arrays +import java.util.logging.Logger +import java.util.logging.Level +import java.io.IOException +import java.io.InputStream +import java.io.ByteArrayInputStream +import java.nio.charset.StandardCharsets +import java.security.cert.CertPathValidator +import java.security.cert.CertPath +import java.security.cert.CertificateFactory +import java.security.cert.Certificate +import java.security.cert.PKIXParameters +import java.security.KeyStore +import java.security.PublicKey + +/** + * 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") + const val INS_GET_STATUS: Int = 0x3C + + val SATOCHIP_AID: ByteArray = hexToBytes("5361746f43686970") // SatoChip + val SEEDKEEPER_AID: ByteArray = hexToBytes("536565644b6565706572") // SeedKeeper + val SATODIME_AID: ByteArray = hexToBytes("5361746f44696d65") // SatoDime + + /* 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 + } + } + + private var status: ApplicationStatus? = null + private var pin0: ByteArray? = null + private val possibleAuthentikeys = ArrayList() + private var authentikey: ByteArray? = null + private var authentikeyHex: String? = null + private var defaultBip32path: String? = null + private var extendedKey: ByteArray? = null + private var extendedChaincode: ByteArray? = null + private var extendedKeyHex: String? = null + private var extendedPrivKey: ByteArray? = null + private var extendedPrivKeyHex: String? = null + + // Satodime, SeedKeeper or Satochip? + private var cardType: String? = null + private var certPem: String? = null // PEM certificate of device, if any + + init { + logger.level = Level.WARNING + } + + fun setLoggerLevel(level: String) { + logger.level = when (level) { + "info" -> Level.INFO + "warning" -> Level.WARNING + else -> Level.WARNING + } + } + + fun setLoggerLevel(level: Level) { + logger.level = level + } + + fun getApplicationStatus(): ApplicationStatus? = status + + fun getPossibleAuthentikeys(): List = possibleAuthentikeys + + fun setDefaultBip32path(bip32path: String) { + defaultBip32path = bip32path + } + + fun cardTransmit(plainApdu: ApduCommand): ApduResponse { + var isApduTransmitted = false + do { + try { + val apduBytes = plainApdu.serialize() + val ins = apduBytes[1].toUByte().toInt() + var isEncrypted = false + + // check if status available + if (status == null) { + val statusCapdu = ApduCommand(0xB0, INS_GET_STATUS, 0x00, 0x00, ByteArray(0)) + val statusRapdu = apduChannel.send(statusCapdu) + status = ApplicationStatus(statusRapdu) + logger.info("SATOCHIPLIB: Status cardGetStatus:${status.toString()}") + } + + val capdu = if (status?.needsSecureChannel() == true && + ins != 0xA4 && + ins != 0x81 && + ins != 0x82 && + ins != INS_GET_STATUS) { + isEncrypted = true + plainApdu + } else { + plainApdu + } + + val rapdu = apduChannel.send(capdu) + val sw12 = rapdu.getSw() + + when (sw12) { + 0x9000 -> { + isApduTransmitted = true + return rapdu + } + 0x9C21 -> { + // SecureChannel is not initialized + } + else -> { + isApduTransmitted = true + return rapdu + } + } + + } catch (e: Exception) { + logger.warning("SATOCHIPLIB: Exception in cardTransmit: $e") + return ApduResponse(ByteArray(0), 0x00, 0x00) + } + } while (!isApduTransmitted) + + return ApduResponse(ByteArray(0), 0x00, 0x00) + } + + fun cardDisconnect() { + status = null + pin0 = null + } + + @Throws(IOException::class) + fun cardSelect(): ApduResponse { + var rapdu = cardSelect("satochip") + if (rapdu.getSw() != 0x9000) { + rapdu = cardSelect("seedkeeper") + if (rapdu.getSw() != 0x9000) { + rapdu = cardSelect("satodime") + if (rapdu.getSw() != 0x9000) { + cardType = "unknown" + logger.warning("SATOCHIPLIB: CardSelect: could not select a known applet") + } + } + } + return rapdu + } + + @Throws(IOException::class) + fun cardSelect(cardType: String): ApduResponse { + val selectApplet = when (cardType) { + "satochip" -> ApduCommand(0x00, 0xA4, 0x04, 0x00, SATOCHIP_AID) + "seedkeeper" -> ApduCommand(0x00, 0xA4, 0x04, 0x00, SEEDKEEPER_AID) + else -> ApduCommand(0x00, 0xA4, 0x04, 0x00, SATODIME_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 cardGetStatus(): ApduResponse { + val plainApdu = ApduCommand(0xB0, INS_GET_STATUS, 0x00, 0x00, ByteArray(0)) + + logger.info("SATOCHIPLIB: C-APDU cardGetStatus:${plainApdu.toHexString()}") + val respApdu = cardTransmit(plainApdu) + logger.info("SATOCHIPLIB: R-APDU cardGetStatus:${respApdu.toHexString()}") + + status = ApplicationStatus(respApdu) + logger.info("SATOCHIPLIB: Status from cardGetStatus:${status.toString()}") + + return respApdu + } +} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/blockstream/common/data/ErrorReport.kt b/common/src/commonMain/kotlin/com/blockstream/common/data/ErrorReport.kt index d34b996dc..f33267633 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/data/ErrorReport.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/data/ErrorReport.kt @@ -38,6 +38,7 @@ data class ErrorReport( DeviceModel.TrezorGeneric -> "trezor" DeviceModel.LedgerGeneric -> "ledger" DeviceModel.Generic -> "generic" + DeviceModel.SatochipGeneric -> "satochip" } } } 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..696595fb1 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 // SATODEBUG } \ 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..d69ffd264 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,10 @@ enum class DeviceBrand(val brand: String) { val isJade get() = this == Blockstream + // SATODEBUG + 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 5ec8decbb..0c154f674 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"), // SATODEBUG 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 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..2225b3971 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 // SATODEBUG 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 // SATODEBUG val isOffline: Boolean val isConnected: Boolean val heartbeat: Long @@ -105,6 +107,10 @@ abstract class GreenDeviceImpl constructor( override val isBle get() = type == ConnectionType.BLUETOOTH + // SATODEBUG + override val isNfc + get() = type == ConnectionType.NFC + override val isOffline: Boolean get() = deviceState.value == DeviceState.DISCONNECTED @@ -120,6 +126,10 @@ abstract class GreenDeviceImpl constructor( override val isLedger: Boolean get() = deviceBrand.isLedger + // SATODEBUG + 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 b67b643e3..4cd2eed22 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt @@ -1341,6 +1341,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..04b1a71a8 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,10 @@ data class Device constructor( val isLedger get() = name.lowercase() == "ledger" + // SATODEBUG + val isSatochip + get() = name.lowercase() == "satochip" + override fun kSerializer(): KSerializer { return serializer() } 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..a6581c109 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,8 @@ open class DeviceManager constructor( protected val usbDevices = MutableStateFlow>(listOf()) private val bleDevices = MutableStateFlow>(listOf()) + // TODO SATODEBUG + protected val nfcDevices = MutableStateFlow>(listOf()) private val _status = MutableStateFlow(ScanStatus.Stopped) val status = _status.asStateFlow() @@ -71,6 +73,7 @@ open class DeviceManager constructor( } } + // TODO SATODEBUG val devices = combine(usbDevices, bleDevices, disconnectEvent) { usb, ble, _ -> ble.filter { it.deviceState.value == DeviceState.CONNECTED } + usb }.stateIn(scope, SharingStarted.Eagerly, emptyList()) 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..8c1ea1bdf 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt @@ -59,6 +59,7 @@ class DeviceConnectionManagerAndroid constructor( } ?: (device as? LedgerDevice)?.let { connectLedgerDevice(it, interaction) } ?: super.connectDevice(device, httpRequestHandler, interaction)) + // TODO SATODEBUG } override suspend fun disconnectDevice(device: GreenDevice) { 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..2532a445e 100644 --- a/compose/src/commonMain/kotlin/com/blockstream/compose/extensions/Resources.kt +++ b/compose/src/commonMain/kotlin/com/blockstream/compose/extensions/Resources.kt @@ -123,6 +123,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.generic_device // todo SATODEBUG } fun DeviceModel.actionIcon(): DrawableResource = when (this) { diff --git a/gradle.properties b/gradle.properties index 894bfcec1..b1b34635f 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/green/build.gradle.kts b/green/build.gradle.kts index 308e89c63..a9ff60ba5 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) From e79c8857f5789dc4712a0186168317bc9357d017 Mon Sep 17 00:00:00 2001 From: Toporin Date: Thu, 6 Feb 2025 21:04:42 +0100 Subject: [PATCH 02/25] Inject activity into DeviceManagerAndroid App activity is required for NfcAdapter to send apdu commands to the card. Use Koin framework for this... --- .../common/devices/ActivityProvider.kt | 7 +++ .../common/devices/AndroidActivityProvider.kt | 21 +++++++ .../common/devices/DeviceManagerAndroid.kt | 60 ++++++++++++++++++- .../com/blockstream/green/GreenActivity.kt | 9 +++ .../com/blockstream/green/di/GreenModules.kt | 10 ++++ 5 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/ActivityProvider.kt create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/AndroidActivityProvider.kt 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/DeviceManagerAndroid.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt index a49500319..293ef01cf 100644 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt @@ -1,6 +1,7 @@ package com.blockstream.common.devices import android.annotation.SuppressLint +import android.app.Activity import android.app.PendingIntent import android.app.PendingIntent.FLAG_IMMUTABLE import android.bluetooth.BluetoothDevice @@ -8,6 +9,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.nfc.NfcAdapter // SATODEBUG import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager import androidx.core.content.ContextCompat @@ -25,9 +27,9 @@ import com.juul.kable.PlatformAdvertisement import com.juul.kable.peripheral import java.lang.ref.WeakReference - class DeviceManagerAndroid constructor( scope: ApplicationScope, + val activityProvider: ActivityProvider, val context: Context, sessionManager: SessionManager, bluetoothManager: BluetoothManager, @@ -75,6 +77,7 @@ class DeviceManagerAndroid constructor( } init { + logger.i { "SATODEBUG DeviceManagerAndroid init() start" } val intentFilter = IntentFilter().also { it.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED) it.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED) @@ -89,11 +92,16 @@ class DeviceManagerAndroid constructor( ContextCompat.RECEIVER_EXPORTED ) + + scanNfcDevices() // SATODEBUG scanUsbDevices() + logger.i { "SATODEBUG DeviceManagerAndroid init() end" } } + // SATODEBUG: for bluetooth? override fun advertisedDevice(advertisement: PlatformAdvertisement) { + logger.i { "SATODEBUG advertisedDevice() start" } val isJade = advertisement.isJade // Jade is added in Common code @@ -108,11 +116,13 @@ class DeviceManagerAndroid constructor( addBluetoothDevice(it) } } + logger.i { "SATODEBUG advertisedDevice() end" } } fun hasPermissions(device: UsbDevice) = usbManager.hasPermission(device) fun askForUsbPermissions(device: UsbDevice, onSuccess: (() -> Unit), onError: ((throwable: Throwable?) -> Unit)? = null) { + logger.i { "SATODEBUG askForUsbPermissions() start" } onPermissionSuccess = WeakReference(onSuccess) onPermissionError = onError?.let { WeakReference(it) } val permissionIntent = PendingIntent.getBroadcast(context, 748, Intent(ACTION_USB_PERMISSION).also { @@ -120,21 +130,28 @@ class DeviceManagerAndroid constructor( it.putExtra(UsbManager.EXTRA_DEVICE, device) }, FLAG_IMMUTABLE) usbManager.requestPermission(device, permissionIntent) + logger.i { "SATODEBUG askForUsbPermissions() end" } } override fun refreshDevices(){ super.refreshDevices() - + logger.i { "SATODEBUG refreshDevices() start" } + scanNfcDevices() // SATODEBUG scanUsbDevices() + logger.i { "SATODEBUG refreshDevices() end" } } fun scanUsbDevices() { + logger.i { "SATODEBUG scanUsbDevices() start" } logger.i { "Scan for USB devices" } val newUsbDevices = usbManager.deviceList.values + logger.i { "SATODEBUG scanUsbDevices() newUsbDevices: $newUsbDevices" } + // Disconnect devices val oldDevices = usbDevices.value.filter { + logger.i { "SATODEBUG scanUsbDevices() usbDevice: $it" } if(newUsbDevices.contains(it.toAndroidDevice()?.usbDevice)){ true }else{ @@ -142,9 +159,11 @@ class DeviceManagerAndroid constructor( false } } + logger.i { "SATODEBUG scanUsbDevices() oldDevices: $oldDevices" } val newDevices = mutableListOf() for (usbDevice in newUsbDevices){ + logger.i { "SATODEBUG scanUsbDevices() usbDevice: $usbDevice" } if(oldDevices.find { it.toAndroidDevice()?.usbDevice == usbDevice } == null) { // Jade or UsbDeviceMapper @@ -154,10 +173,47 @@ class DeviceManagerAndroid constructor( } } } + logger.i { "SATODEBUG scanUsbDevices() newDevices: $newDevices" } usbDevices.value = oldDevices + newDevices + logger.i { "SATODEBUG scanUsbDevices() end" } + } + + // SATODEBUG + fun scanNfcDevices() { + logger.i { "SATODEBUG scanNfcDevices() start" } + logger.i { "Scan for NFC devices" } + + val cardManager = NfcCardManager() + cardManager.setCardListener(SatochipCardListenerForAction) + cardManager.start() + logger.i { "SATODEBUG scanNfcDevices() after cardManager start" } + + // ugly hack + //val countly = sessionManager.countly as Countly + //val activity = countly.activityCopy + val activity = activityProvider.getCurrentActivity() + + //val activity = activity //context as Activity? + //val activity = context as Activity? + //val activity = LocalActivity.current //activity + //val activity = LocalContext + val nfcAdapter = NfcAdapter.getDefaultAdapter(context) //context) + nfcAdapter?.enableReaderMode( + activity, + cardManager, + NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_NFC_B or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, + null + ) + logger.d { "SATODEBUG scanNfcDevices() after nfcAdapter" } + + + logger.i { "SATODEBUG scanNfcDevices() end" } } + + + companion object : Loggable() { private const val ACTION_USB_PERMISSION = "com.blockstream.green.USB_PERMISSION" } diff --git a/green/src/main/java/com/blockstream/green/GreenActivity.kt b/green/src/main/java/com/blockstream/green/GreenActivity.kt index 4a1e904ad..636d9bc91 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,8 @@ 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 activityProvider: AndroidActivityProvider by inject() private val mainViewModel: MainViewModel by viewModel() @@ -54,6 +57,7 @@ class GreenActivity : FragmentActivity() { installSplashScreen() super.onCreate(savedInstanceState) + activityProvider.setActivity(this) enableEdgeToEdge() setContent { @@ -183,6 +187,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..62cb2022d 100644 --- a/green/src/main/java/com/blockstream/green/di/GreenModules.kt +++ b/green/src/main/java/com/blockstream/green/di/GreenModules.kt @@ -5,7 +5,11 @@ package com.blockstream.green.di import android.content.Context import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager +import androidx.fragment.app.FragmentActivity +//import androidx.fragment.app.FragmentActivity 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.fcm.FcmCommon import com.blockstream.common.interfaces.DeviceConnectionInterface @@ -15,6 +19,7 @@ import com.blockstream.compose.devices.LedgerDevice import com.blockstream.compose.devices.TrezorDevice import com.blockstream.compose.managers.DeviceConnectionManager import com.blockstream.compose.managers.DeviceConnectionManagerAndroid +import com.blockstream.green.GreenActivity import com.blockstream.green.managers.FcmAndroid import com.blockstream.green.managers.NotificationManagerAndroid import com.blockstream.jade.connection.JadeBleConnection @@ -36,8 +41,12 @@ val greenModules = module { get() ) } binds (arrayOf(NotificationManagerAndroid::class, NotificationManager::class)) + + single { AndroidActivityProvider() } binds arrayOf(ActivityProvider::class) + single { DeviceManagerAndroid( + get(), get(), androidContext(), get(), @@ -54,6 +63,7 @@ val greenModules = module { } ?: peripheral?.let { LedgerDevice.fromScan(deviceManager = deviceManagerAndroid, bleService = bleService, peripheral = peripheral, isBonded = isBonded == true) } + // TODO SATODEBUG } } binds (arrayOf(DeviceManager::class, DeviceManagerAndroid::class)) single { From 12f3c88244e08f0b02425deda7cd8377c819e09d Mon Sep 17 00:00:00 2001 From: Toporin Date: Fri, 7 Feb 2025 16:18:30 +0100 Subject: [PATCH 03/25] Satochip integration (wip) Scan card + list card + create SatochipDevice --- .../common/devices/DeviceManagerAndroid.kt | 74 +++++++++++++-- .../common/devices/SatochipCardListener.kt | 1 + .../blockstream/common/devices/GreenDevice.kt | 2 +- .../common/managers/DeviceManager.kt | 7 +- .../common/models/GreenViewModel.kt | 1 + .../models/devices/AbstractDeviceViewModel.kt | 1 + .../models/devices/DeviceInfoViewModel.kt | 13 ++- .../models/devices/DeviceListViewModel.kt | 4 + .../compose/devices/SatochipDevice.kt | 92 ++++++++++++++++++ .../DeviceConnectionManagerAndroid.kt | 93 ++++++++++++++++++- .../screens/devices/DeviceInfoScreen.kt | 2 + .../screens/devices/DeviceListScreen.kt | 5 +- .../com/blockstream/green/di/GreenModules.kt | 6 +- 13 files changed, 282 insertions(+), 19 deletions(-) create mode 100644 compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt 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 293ef01cf..9c2c32625 100644 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt @@ -29,18 +29,23 @@ import java.lang.ref.WeakReference class DeviceManagerAndroid constructor( scope: ApplicationScope, - val activityProvider: ActivityProvider, + 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?, + 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 @@ -111,7 +116,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) ?.also { addBluetoothDevice(it) } @@ -168,7 +173,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))?.let { newDevices += it } } @@ -185,7 +190,8 @@ class DeviceManagerAndroid constructor( logger.i { "Scan for NFC devices" } val cardManager = NfcCardManager() - cardManager.setCardListener(SatochipCardListenerForAction) + //cardManager.setCardListener(SatochipCardListenerForAction) + cardManager.setCardListener(this) cardManager.start() logger.i { "SATODEBUG scanNfcDevices() after cardManager start" } @@ -198,7 +204,7 @@ class DeviceManagerAndroid constructor( //val activity = context as Activity? //val activity = LocalActivity.current //activity //val activity = LocalContext - val nfcAdapter = NfcAdapter.getDefaultAdapter(context) //context) + //val nfcAdapter = NfcAdapter.getDefaultAdapter(context) //context) nfcAdapter?.enableReaderMode( activity, cardManager, @@ -211,7 +217,59 @@ class DeviceManagerAndroid constructor( logger.i { "SATODEBUG scanNfcDevices() end" } } + // SATODEBUG + override fun onConnected(channel: CardChannel) { + + println("SATODEBUG DeviceManagerAndroid onConnected: Card is connected") + try { + val cmdSet = SatochipCommandSet(channel) + // start to interact with card + //NfcCardService.initialize(cmdSet) + + val rapduSelect = cmdSet.cardSelect("satochip").checkOK() + // cardStatus + val rapduStatus = cmdSet.cardGetStatus()//To update status if it's not the first reading + val cardStatus = cmdSet.getApplicationStatus() //applicationStatus ?: return + println("SATODEBUG DeviceManagerAndroid readCard cardStatus: $cardStatus") + println("SATODEBUG DeviceManagerAndroid readCard cardStatus: ${cardStatus.toString()}") + + // add device + val newDevices = mutableListOf() + deviceMapper.invoke(this, null, null, null, null, activityProvider)?.let { + newDevices += it + println("SATODEBUG DeviceManagerAndroid readCard newDevice: ${it}") + println("SATODEBUG DeviceManagerAndroid readCard newDevice manufacturer: ${it.manufacturer}") + println("SATODEBUG DeviceManagerAndroid readCard newDevice name: ${it.name}") + println("SATODEBUG DeviceManagerAndroid readCard newDevice deviceBrand: ${it.deviceBrand}") + println("SATODEBUG DeviceManagerAndroid readCard newDevice deviceModel: ${it.deviceModel}") + println("SATODEBUG DeviceManagerAndroid readCard newDevice deviceState: ${it.deviceState}") + println("SATODEBUG DeviceManagerAndroid readCard newDevice type: ${it.type}") + println("SATODEBUG DeviceManagerAndroid readCard newDevice connectionIdentifier: ${it.connectionIdentifier}") + println("SATODEBUG DeviceManagerAndroid readCard newDevice uniqueIdentifier: ${it.uniqueIdentifier}") + } + println("SATODEBUG DeviceManagerAndroid readCard newDevices: ${newDevices}") + nfcDevices.value = newDevices + + // TODO: disconnect? + println("SATODEBUG DeviceManagerAndroid onConnected: trigger disconnection!") + onDisconnected() + + // stop polling? + val activity = activityProvider.getCurrentActivity() + nfcAdapter?.disableReaderMode(activity) + } catch (e: Exception) { + println("SATODEBUG DeviceManagerAndroid onConnected: an exception has been thrown during card init.") + //Log.e(TAG, Log.getStackTraceString(e)) + onDisconnected() + } + } + + // SATODEBUG + override fun onDisconnected() { + //NfcCardService.isConnected.postValue(false) + println("SATODEBUG DeviceManagerAndroid onDisconnected: Card disconnected!") + } companion object : Loggable() { diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCardListener.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCardListener.kt index 65dde5d03..c4f46b164 100644 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCardListener.kt +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCardListener.kt @@ -9,6 +9,7 @@ package com.blockstream.common.devices private const val TAG = "SatochipCardListener" +// todo remove deprecated object SatochipCardListenerForAction : CardListener { override fun onConnected(channel: CardChannel) { 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 2225b3971..73ac86823 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/devices/GreenDevice.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/devices/GreenDevice.kt @@ -92,7 +92,7 @@ abstract class GreenDeviceImpl constructor( get() = name override val deviceModel: DeviceModel? - get() = gdkHardwareWallet?.model + get() = gdkHardwareWallet?.model // todo final override var heartbeat: Long = Clock.System.now().toEpochMilliseconds() private set 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 a6581c109..99d0a8293 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/managers/DeviceManager.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/managers/DeviceManager.kt @@ -73,9 +73,9 @@ open class DeviceManager constructor( } } - // TODO SATODEBUG - val devices = combine(usbDevices, bleDevices, disconnectEvent) { usb, ble, _ -> - ble.filter { it.deviceState.value == DeviceState.CONNECTED } + usb + // SATODEBUG + 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 @@ -109,6 +109,7 @@ open class DeviceManager constructor( } fun getDevice(deviceId: String?): GreenDevice? { + logger.d { "SATODEBUG DeviceManager getDevice Start deviceId: $deviceId" } return devices.value.find { it.connectionIdentifier == deviceId } // Check if device is already in a session ?: sessionManager.getConnectedHardwareWalletSessions() 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 ea18af58a..9cbc10ab7 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/GreenViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/GreenViewModel.kt @@ -394,6 +394,7 @@ open class GreenViewModel constructor( } open suspend fun handleEvent(event: Event) { + println("SATODEBUG GreenViewModel handleEvent() event: $event") when(event){ is Events.ProvideCipher -> { event.platformCipher?.also { diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/AbstractDeviceViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/AbstractDeviceViewModel.kt index e0c4e0cac..a0fbcaa80 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/AbstractDeviceViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/AbstractDeviceViewModel.kt @@ -94,6 +94,7 @@ abstract class AbstractDeviceViewModel constructor( override suspend fun handleEvent(event: Event) { super.handleEvent(event) + println("SATODEBUG AbstractDeviceViewModel handleEvent() event: $event") if(event is LocalEvents.Refresh) { deviceManager.refreshDevices() diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceInfoViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceInfoViewModel.kt index fc4fe232e..dfc42a095 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceInfoViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceInfoViewModel.kt @@ -60,17 +60,21 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs } init { + println("SATODEBUG DeviceInfoViewModel init()") deviceOrNull = deviceManager.getDevice(deviceId) if(deviceOrNull == null){ + println("SATODEBUG DeviceInfoViewModel init() device is null!") postSideEffect(SideEffects.NavigateBack()) }else { if (device.gdkHardwareWallet == null) { + println("SATODEBUG DeviceInfoViewModel init() gdkHardwareWallet is null") connectDevice() } device.deviceState.onEach { + println("SATODEBUG DeviceInfoViewModel init() deviceState loop: $it") // Device went offline if (it == DeviceState.DISCONNECTED) { postSideEffect(SideEffects.Snackbar(StringHolder(stringResource = Res.string.id_your_device_was_disconnected))) @@ -80,6 +84,7 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs } viewModelScope.launch { + println("SATODEBUG DeviceInfoViewModel init() viewModelScope.launch") _navData.value = NavData( title = deviceOrNull?.deviceBrand?.name, onBackPressed = { @@ -109,11 +114,12 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs override suspend fun handleEvent(event: Event) { super.handleEvent(event) + println("SATODEBUG DeviceInfoViewModel handleEvent() event: $event") if (event is LocalEvents.AuthenticateAndContinue) { authenticateAndContinue(event.updateFirmwareFromChannel) } else if (event is LocalEvents.SelectEnviroment) { - + println("SATODEBUG DeviceInfoViewModel handleEvent() LocalEvents.SelectEnviroment") if (event.isTestnet == null) { requestNetworkEmitter?.completeExceptionally(Exception("id_action_canceled")) } else { @@ -125,11 +131,14 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs } private fun connectDevice() { + println("SATODEBUG DeviceInfoViewModel connectDevice()") doAsync({ + println("SATODEBUG DeviceInfoViewModel connectDevice() doAsync start") deviceConnectionManager.connectDevice(device, sessionManager.httpRequestHandler, this).also { countly.hardwareConnect(device) } }, onSuccess = { + println("SATODEBUG DeviceInfoViewModel connectDevice() doAsync onSuccess") deviceIsConnected.value = true countly.hardwareConnected(device) _jadeIsUninitialized.value = it.isJadeUninitialized == true @@ -146,6 +155,7 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs } }, onError = { + println("SATODEBUG DeviceInfoViewModel connectDevice() doAsync onError") it.printStackTrace() if (it is ConnectionLostException) { @@ -157,6 +167,7 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs } private fun authenticateAndContinue(updateFirmwareFromChannel: String? = null) { + println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() start") val gdkHardwareWallet = device.gdkHardwareWallet ?: return doAsync({ diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceListViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceListViewModel.kt index 294970d65..8699270f0 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceListViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceListViewModel.kt @@ -42,6 +42,7 @@ class DeviceListViewModel(isJade: Boolean = true) : }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), listOf()) init { + println("SATODEBUG DeviceListViewModel init()") viewModelScope.launch { _navData.value = NavData(title = if (isJade) "Blockstream Jade" else "", actions = listOfNotNull( @@ -56,6 +57,7 @@ class DeviceListViewModel(isJade: Boolean = true) : override suspend fun handleEvent(event: Event) { super.handleEvent(event) + println("SATODEBUG DeviceListViewModel handleEvent() event: $event") if(event is LocalEvents.ConnectViaQR){ if(isJade) { @@ -70,6 +72,8 @@ class DeviceListViewModel(isJade: Boolean = true) : postSideEffect(SideEffects.NavigateTo(NavigateDestinations.ImportPubKey(deviceModel = DeviceModel.Generic))) } }else if (event is LocalEvents.SelectDevice) { + println("SATODEBUG DeviceListViewModel handleEvent() event is SelectDevice") + val navigateTo = SideEffects.NavigateTo(NavigateDestinations.DeviceInfo(deviceId = event.device.connectionIdentifier)) if (event.device.hasPermissions()) { 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..4a5e97f7c --- /dev/null +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt @@ -0,0 +1,92 @@ +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.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, + activityProvider: ActivityProvider? = null +) : AndroidDevice( + context = context, + deviceManager = deviceManager, + usbDevice = usbDevice, + deviceBrand = DeviceBrand.Satochip, + type = type, + peripheral = peripheral, + isBonded = isBonded +) { + + 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() { + //const val VENDOR_TREZOR = 0x534c + //const val VENDOR_TREZOR_V2 = 0x1209 + + private fun hasSuportedVendorId(usbDevice: UsbDevice): Boolean { + return false +// val vId = usbDevice.vendorId +// return (vId == VENDOR_TREZOR || +// vId == VENDOR_TREZOR_V2) + } + + // TODO remove + fun fromUsbDevice( + deviceManager: DeviceManagerAndroid, + usbDevice: UsbDevice + ): SatochipDevice? { +// if (hasSuportedVendorId(usbDevice)) { +// return TrezorDevice( +// context = deviceManager.context, +// deviceManager = deviceManager, +// type = ConnectionType.USB, +// usbDevice = usbDevice, +// ) +// } + return null + } + + fun fromNfcDevice( + deviceManager: DeviceManagerAndroid, + activityProvider: ActivityProvider?, + // TODO add params? + // add AID? + ): SatochipDevice? { + // todo check AID + return SatochipDevice( + context = deviceManager.context, + deviceManager = deviceManager, + type = ConnectionType.NFC, + usbDevice = null, + activityProvider = activityProvider, + ) + //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 8c1ea1bdf..f0cd728b4 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt @@ -1,10 +1,18 @@ package com.blockstream.compose.managers +import android.app.Activity import android.bluetooth.BluetoothAdapter +import android.nfc.NfcAdapter import com.blockstream.common.data.AppInfo import com.blockstream.common.devices.AndroidDevice +import com.blockstream.common.devices.CardChannel +import com.blockstream.common.devices.CardListener import com.blockstream.common.devices.DeviceBrand +import com.blockstream.common.devices.DeviceManagerAndroid +import com.blockstream.common.devices.DeviceManagerAndroid.Companion import com.blockstream.common.devices.GreenDevice +import com.blockstream.common.devices.NfcCardManager +import com.blockstream.common.devices.SatochipCommandSet import com.blockstream.common.devices.toAndroidDevice import com.blockstream.common.gdk.Gdk import com.blockstream.common.gdk.Wally @@ -14,6 +22,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 @@ -43,12 +52,15 @@ class DeviceConnectionManagerAndroid constructor( private val appInfo: AppInfo, scope: CoroutineScope, private val bluetoothAdapter: BluetoothAdapter -) : DeviceConnectionManager( +) : CardListener, DeviceConnectionManager( gdk = gdk, wally = wally, scope = scope ) { + private var nfcAdapter: NfcAdapter? = null + private var activity: Activity? = null + override suspend fun connectDevice( device: GreenDevice, httpRequestHandler: HttpRequestHandler, @@ -58,8 +70,10 @@ class DeviceConnectionManagerAndroid constructor( connectTrezorDevice(it, interaction) } ?: (device as? LedgerDevice)?.let { connectLedgerDevice(it, interaction) + } ?: (device as? SatochipDevice)?.let { + connectSatochipDevice(it, interaction) // SATODEBUG } ?: super.connectDevice(device, httpRequestHandler, interaction)) - // TODO SATODEBUG + } override suspend fun disconnectDevice(device: GreenDevice) { @@ -147,6 +161,81 @@ class DeviceConnectionManagerAndroid constructor( return ConnectionResult() } + + // SATODEBUG + private suspend fun connectSatochipDevice(device: SatochipDevice, interaction: HardwareConnectInteraction): ConnectionResult { + logger.i {"SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() start device: $device"} + + val cardManager = NfcCardManager() + cardManager.setCardListener(this) + cardManager.start() + logger.i { "SATODEBUG DeviceConnectionManagerAndroid scanNfcDevices() after cardManager start" } + + activity = device.activityProvider?.getCurrentActivity() + nfcAdapter = NfcAdapter.getDefaultAdapter(device.context) + nfcAdapter?.enableReaderMode( + activity, + cardManager, + NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_NFC_B or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, + null + ) + + logger.i { "SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() end" } + + + //return onSatochipConnected(device) + return ConnectionResult() + } + + private fun onSatochipConnected(device: AndroidDevice): ConnectionResult { + logger.d { "SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() start" } + logger.d { "Creating Satochip HW wallet" } + + return ConnectionResult() + } + + // SATODEBUG + override fun onConnected(channel: CardChannel) { + + println("SATODEBUG DeviceConnectionManagerAndroid onConnected: Card is connected") + try { + val cmdSet = SatochipCommandSet(channel) + // start to interact with card + //NfcCardService.initialize(cmdSet) + + val rapduSelect = cmdSet.cardSelect("satochip").checkOK() + // cardStatus + val rapduStatus = cmdSet.cardGetStatus()//To update status if it's not the first reading + val cardStatus = cmdSet.getApplicationStatus() //applicationStatus ?: return + println("SATODEBUG DeviceConnectionManagerAndroid readCard cardStatus: $cardStatus") + println("SATODEBUG DeviceConnectionManagerAndroid readCard cardStatus: ${cardStatus.toString()}") + + // TODO: disconnect? + println("SATODEBUG DeviceConnectionManagerAndroid onConnected: trigger disconnection!") + onDisconnected() + + // stop polling? + nfcAdapter?.disableReaderMode(activity) + + // + //return onSatochipConnected(device) + + } catch (e: Exception) { + println("SATODEBUG DeviceConnectionManagerAndroid onConnected: an exception has been thrown during card init.") + //Log.e(TAG, Log.getStackTraceString(e)) + onDisconnected() + } + } + + // SATODEBUG + override fun onDisconnected() { + //NfcCardService.isConnected.postValue(false) + println("SATODEBUG DeviceConnectionManagerAndroid onDisconnected: Card disconnected!") + } + + //============================== + + private suspend fun connectLedgerDevice( device: LedgerDevice, interaction: HardwareConnectInteraction diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceInfoScreen.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceInfoScreen.kt index bd3b0d0c2..94d198e86 100644 --- a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceInfoScreen.kt +++ b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceInfoScreen.kt @@ -68,6 +68,8 @@ data class DeviceInfoScreen(val deviceId: String) : Screen, Parcelable { @Composable override fun Content() { + println("SATODEBUG DeviceInfoScreen Content() Start deviceId: $deviceId") + val viewModel = koinScreenModel { parametersOf(deviceId) } diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceListScreen.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceListScreen.kt index 07c775736..f564ca83a 100644 --- a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceListScreen.kt +++ b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceListScreen.kt @@ -92,6 +92,8 @@ data class DeviceListScreen(val isJade: Boolean) : Screen, Parcelable { @Composable override fun Content() { + println("SATODEBUG DeviceListScreen Content() Start isJade: $isJade") + val viewModel = koinScreenModel { parametersOf(isJade) } @@ -114,6 +116,7 @@ data class DeviceListScreen(val isJade: Boolean) : Screen, Parcelable { @Composable fun DeviceListItem(device: GreenDevice, modifier: Modifier, onClick: () -> Unit) { + println("SATODEBUG DeviceListScreen DeviceListItem() Start device: $device") GreenCard(onClick = onClick, padding = 0, modifier = modifier) { Image( painter = painterResource(device.icon()), @@ -128,7 +131,7 @@ fun DeviceListItem(device: GreenDevice, modifier: Modifier, onClick: () -> Unit) modifier = Modifier.align(Alignment.CenterStart) ) { Image( - painter = painterResource(if (device.isUsb) Res.drawable.usb else Res.drawable.ble), + painter = painterResource(if (device.isUsb) Res.drawable.usb else Res.drawable.ble), // TODO add NFC icon modifier = Modifier.size(24.dp), contentDescription = null ) 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 62cb2022d..8cf5873c0 100644 --- a/green/src/main/java/com/blockstream/green/di/GreenModules.kt +++ b/green/src/main/java/com/blockstream/green/di/GreenModules.kt @@ -16,6 +16,7 @@ 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 @@ -53,7 +54,7 @@ val greenModules = module { 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?, activityProvider: ActivityProvider? -> usbDevice?.let { TrezorDevice.fromUsbDevice(deviceManager = deviceManagerAndroid, usbDevice = usbDevice) ?: LedgerDevice.fromUsbDevice( @@ -62,8 +63,7 @@ val greenModules = module { ) } ?: peripheral?.let { LedgerDevice.fromScan(deviceManager = deviceManagerAndroid, bleService = bleService, peripheral = peripheral, isBonded = isBonded == true) - } - // TODO SATODEBUG + } ?: SatochipDevice.fromNfcDevice(deviceManager = deviceManagerAndroid, activityProvider = activityProvider) // SATODEBUG } } binds (arrayOf(DeviceManager::class, DeviceManagerAndroid::class)) single { From 672cf60315b3595f02d3a0e65e0b8fa6b8c84688 Mon Sep 17 00:00:00 2001 From: Toporin Date: Mon, 10 Feb 2025 08:43:18 +0100 Subject: [PATCH 04/25] Satochip integration (wip) add SatochipHWWallet.java --- .../DeviceConnectionManagerAndroid.kt | 34 ++- .../greenbits/wallets/SatochipHWWallet.java | 251 ++++++++++++++++++ 2 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java 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 f0cd728b4..d16385196 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt @@ -35,6 +35,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 @@ -60,6 +61,7 @@ class DeviceConnectionManagerAndroid constructor( private var nfcAdapter: NfcAdapter? = null private var activity: Activity? = null + private var satochipDevice: SatochipDevice? = null override suspend fun connectDevice( device: GreenDevice, @@ -166,6 +168,8 @@ class DeviceConnectionManagerAndroid constructor( private suspend fun connectSatochipDevice(device: SatochipDevice, interaction: HardwareConnectInteraction): ConnectionResult { logger.i {"SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() start device: $device"} + satochipDevice = device + val cardManager = NfcCardManager() cardManager.setCardListener(this) cardManager.start() @@ -183,6 +187,18 @@ class DeviceConnectionManagerAndroid constructor( logger.i { "SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() end" } +// val satochipDevice = com.blockstream.common.gdk.data.Device( +// name = "Satochip", +// supportsArbitraryScripts = false, +// supportsLowR = false, +// supportsHostUnblinding = false, +// supportsExternalBlinding = false, +// supportsLiquid = DeviceSupportsLiquid.None, +// supportsAntiExfilProtocol = DeviceSupportsAntiExfilProtocol.None +// ) +// +// device.gdkHardwareWallet = SatochipHWWallet(satochipDevice) + //return onSatochipConnected(device) return ConnectionResult() } @@ -210,16 +226,32 @@ class DeviceConnectionManagerAndroid constructor( println("SATODEBUG DeviceConnectionManagerAndroid readCard cardStatus: $cardStatus") println("SATODEBUG DeviceConnectionManagerAndroid readCard cardStatus: ${cardStatus.toString()}") + + val satoDevice = com.blockstream.common.gdk.data.Device( + name = "Satochip", + supportsArbitraryScripts = false, + supportsLowR = false, + supportsHostUnblinding = false, + supportsExternalBlinding = false, + supportsLiquid = DeviceSupportsLiquid.None, + supportsAntiExfilProtocol = DeviceSupportsAntiExfilProtocol.None + ) + + // provide channel for later request? + // todo: provide activity and context instead?? + satochipDevice?.gdkHardwareWallet = SatochipHWWallet(satoDevice, channel) + // TODO: disconnect? println("SATODEBUG DeviceConnectionManagerAndroid onConnected: trigger disconnection!") onDisconnected() // stop polling? - nfcAdapter?.disableReaderMode(activity) + //nfcAdapter?.disableReaderMode(activity) // //return onSatochipConnected(device) + } catch (e: Exception) { println("SATODEBUG DeviceConnectionManagerAndroid onConnected: an exception has been thrown during card init.") //Log.e(TAG, Log.getStackTraceString(e)) 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..c08e89f1c --- /dev/null +++ b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java @@ -0,0 +1,251 @@ +package com.greenaddress.greenbits.wallets; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.blockstream.common.devices.ApduException; +import com.blockstream.common.devices.ApduResponse; +import com.blockstream.common.devices.ApplicationStatus; +import com.blockstream.common.devices.CardChannel; +import com.blockstream.common.devices.DeviceModel; +import com.blockstream.common.devices.SatochipCommandSet; +import com.blockstream.common.extensions.GdkExtensionsKt; +import com.blockstream.common.gdk.data.Account; +import com.blockstream.common.gdk.data.AccountType; +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.devices.DeviceBrand; +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.google.common.base.Joiner; +import com.google.protobuf.ByteString; +import com.google.protobuf.Message; +import com.satoshilabs.trezor.Trezor; +import com.satoshilabs.trezor.protobuf.TrezorMessage; +import com.satoshilabs.trezor.protobuf.TrezorType; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +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 { + + private static final String TAG = SatochipHWWallet.class.getSimpleName(); + + private final Map mUserXPubs = new HashMap<>(); + private final Map mServiceXPubs = new HashMap<>(); + private final Map mRecoveryXPubs = new HashMap<>(); + private final Map mPrevTxs = new HashMap<>(); + + private final Device device; + private final DeviceModel model; + private final String firmwareVersion = "x.y-z.w"; //todo + + private final CardChannel channel = null; + private final SatochipCommandSet cmdSet; + + public SatochipHWWallet(Device device, CardChannel channel) { + Log.i("SatochipHWWallet", "constructor start"); + //mTrezor = t; + this.device = device; + String model = "Satochip"; + this.model = DeviceModel.SatochipGeneric; + + this.cmdSet = new SatochipCommandSet(channel); + + } + + @Override + public synchronized void disconnect() { + Log.i("SatochipHWWallet", "disconnect start"); + // No-op + } + + @NonNull + @Override + public synchronized List getXpubs(@NonNull Network network, @NonNull List> paths, @Nullable HardwareWalletInteraction hwInteraction) { + Log.i("SatochipHWWallet", "getXpubs start"); + Log.i("SatochipHWWallet", "getXpubs paths: " + paths); + Log.i("SatochipHWWallet", "getXpubs HardwareWalletInteraction: " + hwInteraction); + + // debug + try { + ApduResponse rapduSelect = cmdSet.cardSelect("satochip").checkOK(); + ApduResponse rapduStatus = cmdSet.cardGetStatus();//To update status if it's not the first reading + ApplicationStatus cardStatus = cmdSet.getApplicationStatus(); //applicationStatus ?: return + Log.i("SatochipHWWallet", "getXpubs readCard cardStatus: $cardStatus"); + Log.i("SatochipHWWallet", "getXpubs readCard cardStatus: ${cardStatus.toString()}"); + } catch (Exception e) { + Log.i("SatochipHWWallet", "getXpubs exception: " + e); + } + + final List xpubs = new ArrayList<>(paths.size()); + + // TODO + xpubs.add("xpub6EPNojiyVmkzkEMmkwfjVc4YdyBXoQH8QwdifcPaKqVszSyg6oczSEgfP5AYUUs5hNG9kNfosAbTSLZqjEfMGdA85F7dx3kk6qDFP6va7mz"); + if (paths.size()>=2) { + xpubs.add("xpub69CqtDngRoBFv667kKodkCxvGegsJZwVUTmE8K46e3HKL5JVYL4ZpZZSBvosZPdgVmFxa3fFvkYxYfXBQyYTkME99wHhugHAARLECJe64Ee"); + } + if (paths.size()>=3) { + xpubs.add("xpub69CqtDngRoBFv667kKodkCxvGegsJZwVUTmE8K46e3HKL5JVYL4ZpZZSBvosZPdgVmFxa3fFvkYxYfXBQyYTkME99wHhugHAARLECJe64Ee"); + } + return xpubs; + } + + @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"); + if (useAeProtocol) { + throw new RuntimeException("Hardware Wallet does not support the Anti-Exfil protocol"); + } + + //TODO + String signature = "aabbccddee"; + 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"); + if(network.isLiquid()){ + throw new RuntimeException(network.getCanonicalName() + " is not supported"); + } + try { + + if (useAeProtocol) { + throw new RuntimeException("Hardware Wallet does not support the Anti-Exfil protocol"); + } + + return signTransactionImpl(network, hwInteraction, transaction, inputs, outputs, transactions); + } finally { + // Free all wally txs to ensure we don't leak any memory + for (Map.Entry entry : mPrevTxs.entrySet()) { + Wally.tx_free(entry.getValue()); + } + mPrevTxs.clear(); + } + } + + private synchronized SignTransactionResult signTransactionImpl(final Network network, + @Nullable HardwareWalletInteraction hwInteraction, + final String transaction, + final List inputs, + final List outputs, + final Map transactions) + { + Log.i("SatochipHWWallet", "SignTransactionImpl start"); + final String[] signatures = new String[inputs.size()]; + + final byte[] txBytes = Wally.hex_to_bytes(transaction); + final Object wallytx = Wally.tx_from_bytes(txBytes, Wally.WALLY_TX_FLAG_USE_WITNESS); + + final int txVersion = Wally.tx_get_version(wallytx); + final int txLocktime = Wally.tx_get_locktime(wallytx); + + if (transactions != null) { + for (Map.Entry t : transactions.entrySet()) + mPrevTxs.put(t.getKey(), Wally.tx_from_hex(t.getValue(), Wally.WALLY_TX_FLAG_USE_WITNESS)); + } + + // todo + return new SignTransactionResult(Arrays.asList(signatures), null); + + } + + @NonNull + @Override + public synchronized String getMasterBlindingKey(@Nullable HardwareWalletInteraction hwInteraction) { + throw new RuntimeException("Master Blinding Key is not supported"); + } + + @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"); + } + + + + private static List getIntegerPath(final List unsigned) { + //return unsigned.stream().map(Long::intValue).collect(Collectors.toList()); + final List signed = new ArrayList<>(unsigned.size()); + for (final Long n : unsigned) { + signed.add(n.intValue()); + } + return signed; + } + + private static Integer unharden(final Integer i) { + return Integer.MIN_VALUE + i; + } + + + @Override + public synchronized String getGreenAddress(final Network network, final Account account, final List path, final long csvBlocks, HardwareWalletInteraction hwInteraction) { + Log.i("SatochipHWWallet", "getGreenAddress start"); + 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", "getDisconnectEvent start"); + return null; + } + + @Nullable + @Override + public String getFirmwareVersion() { + Log.i("SatochipHWWallet", "getFirmwareVersion start"); + return firmwareVersion; + } + + @NonNull + @Override + public DeviceModel getModel() { + Log.i("SatochipHWWallet", "getModel start"); + return model; + } + + @NonNull + @Override + public Device getDevice() { + Log.i("SatochipHWWallet", "getDevice start"); + return device; + } +} From 812f38375a89a1a7e6a6a3680cecbd76229d1cec Mon Sep 17 00:00:00 2001 From: Toporin Date: Thu, 13 Feb 2025 10:04:27 +0100 Subject: [PATCH 05/25] Satochip integration: implement getXpubs() (wip) --- .../DeviceConnectionManagerAndroid.kt | 20 +- gradle.properties | 2 +- gradle/libs.versions.toml | 6 + hardware/build.gradle.kts | 5 + .../greenbits/wallets/SatochipHWWallet.java | 291 ++++- .../main/java/com/satochip/ApduCommand.java | 147 +++ .../main/java/com/satochip/ApduException.java | 29 + .../main/java/com/satochip/ApduResponse.java | 218 ++++ .../java/com/satochip/ApplicationStatus.java | 133 ++ .../src/main/java/com/satochip/Bip32Path.java | 26 + .../com/satochip/BlockedPINException.java | 14 + .../main/java/com/satochip/CardChannel.java | 35 + .../main/java/com/satochip/CardListener.java | 18 + .../src/main/java/com/satochip/Constants.java | 186 +++ .../java/com/satochip/NfcActionObject.java | 20 + .../java/com/satochip/NfcActionResult.java | 22 + .../java/com/satochip/NfcActionStatus.java | 8 + .../main/java/com/satochip/NfcActionType.java | 7 + .../java/com/satochip/NfcCardChannel.java | 34 + .../java/com/satochip/NfcCardManager.java | 124 ++ .../com/satochip/ResetToFactoryException.java | 14 + .../java/com/satochip/SatochipCommandSet.java | 1154 +++++++++++++++++ .../java/com/satochip/SatochipParser.java | 739 +++++++++++ .../com/satochip/SecureChannelSession.java | 284 ++++ .../java/com/satochip/WrongPINException.java | 27 + .../com/satochip/WrongPINLegacyException.java | 14 + 26 files changed, 3523 insertions(+), 54 deletions(-) create mode 100644 hardware/src/main/java/com/satochip/ApduCommand.java create mode 100644 hardware/src/main/java/com/satochip/ApduException.java create mode 100644 hardware/src/main/java/com/satochip/ApduResponse.java create mode 100644 hardware/src/main/java/com/satochip/ApplicationStatus.java create mode 100644 hardware/src/main/java/com/satochip/Bip32Path.java create mode 100644 hardware/src/main/java/com/satochip/BlockedPINException.java create mode 100644 hardware/src/main/java/com/satochip/CardChannel.java create mode 100644 hardware/src/main/java/com/satochip/CardListener.java create mode 100644 hardware/src/main/java/com/satochip/Constants.java create mode 100644 hardware/src/main/java/com/satochip/NfcActionObject.java create mode 100644 hardware/src/main/java/com/satochip/NfcActionResult.java create mode 100644 hardware/src/main/java/com/satochip/NfcActionStatus.java create mode 100644 hardware/src/main/java/com/satochip/NfcActionType.java create mode 100644 hardware/src/main/java/com/satochip/NfcCardChannel.java create mode 100644 hardware/src/main/java/com/satochip/NfcCardManager.java create mode 100644 hardware/src/main/java/com/satochip/ResetToFactoryException.java create mode 100644 hardware/src/main/java/com/satochip/SatochipCommandSet.java create mode 100644 hardware/src/main/java/com/satochip/SatochipParser.java create mode 100644 hardware/src/main/java/com/satochip/SecureChannelSession.java create mode 100644 hardware/src/main/java/com/satochip/WrongPINException.java create mode 100644 hardware/src/main/java/com/satochip/WrongPINLegacyException.java 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 d16385196..b7d8b8e21 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt @@ -226,6 +226,13 @@ class DeviceConnectionManagerAndroid constructor( println("SATODEBUG DeviceConnectionManagerAndroid readCard cardStatus: $cardStatus") println("SATODEBUG DeviceConnectionManagerAndroid readCard cardStatus: ${cardStatus.toString()}") + // TODO: disconnect? + println("SATODEBUG DeviceConnectionManagerAndroid onConnected: trigger disconnection!") + onDisconnected() + + // stop polling? + nfcAdapter?.disableReaderMode(activity) + println("SATODEBUG DeviceConnectionManagerAndroid NFC DISABLED") val satoDevice = com.blockstream.common.gdk.data.Device( name = "Satochip", @@ -239,18 +246,7 @@ class DeviceConnectionManagerAndroid constructor( // provide channel for later request? // todo: provide activity and context instead?? - satochipDevice?.gdkHardwareWallet = SatochipHWWallet(satoDevice, channel) - - // TODO: disconnect? - println("SATODEBUG DeviceConnectionManagerAndroid onConnected: trigger disconnection!") - onDisconnected() - - // stop polling? - //nfcAdapter?.disableReaderMode(activity) - - // - //return onSatochipConnected(device) - + satochipDevice?.gdkHardwareWallet = SatochipHWWallet(satoDevice, activity, satochipDevice?.context) } catch (e: Exception) { println("SATODEBUG DeviceConnectionManagerAndroid onConnected: an exception has been thrown during card init.") diff --git a/gradle.properties b/gradle.properties index b1b34635f..62d68bdfe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -40,4 +40,4 @@ org.gradle.caching=true #kotlin.incremental.native=true # satochip debug -#org.gradle.dependency.verification=off \ No newline at end of file +org.gradle.dependency.verification=off \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6927709e1..e6f639aa5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,6 +72,8 @@ room = "2.5.2" protobuf-java = "3.4.0" media3 = "1.4.0" +bouncycastle = "1.69" +spongycastle = "1.58.0.0" [libraries] accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } @@ -170,6 +172,10 @@ 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" } +com-spongycastle = { module = "com.madgag.spongycastle:core", version.ref = "spongycastle" } + + [plugins] kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/hardware/build.gradle.kts b/hardware/build.gradle.kts index 714446cc7..77c07f814 100644 --- a/hardware/build.gradle.kts +++ b/hardware/build.gradle.kts @@ -42,6 +42,11 @@ dependencies { implementation(libs.jackson.datatype.json.org) /** ----------------------------------------------------------------------------------------- */ + /** For satochip */ + implementation(libs.org.bouncycastle) + //implementation(libs.com.spongycastle) + //implementation(libs.org.bitcoinj) + 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 index c08e89f1c..e44aa6cce 100644 --- a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java +++ b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java @@ -1,116 +1,304 @@ 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 com.blockstream.common.devices.ApduException; -import com.blockstream.common.devices.ApduResponse; -import com.blockstream.common.devices.ApplicationStatus; -import com.blockstream.common.devices.CardChannel; +import java.io.ByteArrayOutputStream; +import java.util.concurrent.TimeUnit; + +//import com.blockstream.common.devices.ApduException; +//import com.blockstream.common.devices.ApduResponse; +//import com.blockstream.common.devices.ApplicationStatus; +//import com.blockstream.common.devices.CardChannel; +//import com.blockstream.common.devices.CardListener; +//import com.blockstream.common.devices.DeviceModel; +//import com.blockstream.common.devices.NfcCardManager; +//import com.blockstream.common.devices.SatochipCommandSet; //todo switch to import com.blockstream.common.devices.DeviceModel; -import com.blockstream.common.devices.SatochipCommandSet; -import com.blockstream.common.extensions.GdkExtensionsKt; import com.blockstream.common.gdk.data.Account; -import com.blockstream.common.gdk.data.AccountType; 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.devices.DeviceBrand; 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.BTChipException; +import com.btchip.utils.BufferUtils; import com.google.common.base.Joiner; -import com.google.protobuf.ByteString; -import com.google.protobuf.Message; -import com.satoshilabs.trezor.Trezor; -import com.satoshilabs.trezor.protobuf.TrezorMessage; +import com.satochip.ApduException; +import com.satochip.ApduResponse; +import com.satochip.ApplicationStatus; +import com.satochip.Bip32Path; +import com.satochip.CardChannel; +import com.satochip.CardListener; +import com.satochip.NfcActionObject; +import com.satochip.NfcActionResult; +import com.satochip.NfcActionStatus; +import com.satochip.NfcActionType; +import com.satochip.NfcCardManager; +import com.satochip.SatochipCommandSet; import com.satoshilabs.trezor.protobuf.TrezorType; import java.io.IOException; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; 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 { +public class SatochipHWWallet extends GdkHardwareWallet implements CardListener { private static final String TAG = SatochipHWWallet.class.getSimpleName(); - private final Map mUserXPubs = new HashMap<>(); - private final Map mServiceXPubs = new HashMap<>(); - private final Map mRecoveryXPubs = new HashMap<>(); + private final Map mUserXPubs = new HashMap<>(); +// private final Map mServiceXPubs = new HashMap<>(); +// private final Map mRecoveryXPubs = new HashMap<>(); private final Map mPrevTxs = new HashMap<>(); private final Device device; private final DeviceModel model; private final String firmwareVersion = "x.y-z.w"; //todo + private final Activity activity; + private final Context context; private final CardChannel channel = null; - private final SatochipCommandSet cmdSet; + //private final SatochipCommandSet cmdSet; + private NfcAdapter nfcAdapter = null; + + //private NfcActionType actionType = NfcActionType.none; + //private NfcActionResult actionResult = null; + private NfcActionObject actionObject = new NfcActionObject(); - public SatochipHWWallet(Device device, CardChannel channel) { - Log.i("SatochipHWWallet", "constructor start"); + + public SatochipHWWallet(Device device, Activity activity, Context context){ + Log.i(TAG, "constructor start"); //mTrezor = t; this.device = device; - String model = "Satochip"; + //String model = "Satochip"; this.model = DeviceModel.SatochipGeneric; - this.cmdSet = new SatochipCommandSet(channel); + this.activity = activity; + this.context = context; + + //this.cmdSet = new SatochipCommandSet(channel); + + NfcCardManager cardManager = new NfcCardManager(); + cardManager.setCardListener(this); + cardManager.start(); + Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() after 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 + ); + Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() after NfcAdapter.enableReaderMode"); + + } @Override public synchronized void disconnect() { - Log.i("SatochipHWWallet", "disconnect start"); + Log.i(TAG, "disconnect start"); // No-op } - @NonNull - @Override - public synchronized List getXpubs(@NonNull Network network, @NonNull List> paths, @Nullable HardwareWalletInteraction hwInteraction) { - Log.i("SatochipHWWallet", "getXpubs start"); - Log.i("SatochipHWWallet", "getXpubs paths: " + paths); - Log.i("SatochipHWWallet", "getXpubs HardwareWalletInteraction: " + hwInteraction); - // debug + public void onConnected(CardChannel channel) { + + Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() Card is connected"); try { + SatochipCommandSet cmdSet = new SatochipCommandSet(channel); + Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() cmdSet created"); + // start to interact with card + //NfcCardService.initialize(cmdSet) + 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("SatochipHWWallet", "getXpubs readCard cardStatus: $cardStatus"); - Log.i("SatochipHWWallet", "getXpubs readCard cardStatus: ${cardStatus.toString()}"); + Log.i(TAG, "SATODEBUG SatochipHWWallet readCard cardStatus: $cardStatus"); + Log.i(TAG, "SATODEBUG SatochipHWWallet readCard cardStatus: ${cardStatus.toString()}"); + + + // verify PIN + String pinStr = "123456"; //todo + byte[] pinBytes = pinStr.getBytes(Charset.forName("UTF-8")); + ApduResponse rapduPin = cmdSet.cardVerifyPIN(pinBytes); + Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() pin verified: " + rapduPin.getSw()); + Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() pin verified: " + rapduPin.toHexString()); + + // execute commands depending on actionType + if (this.actionObject.actionType == NfcActionType.getXpub){ + onConnectedGetXpubs(cmdSet); + } + + + // TODO: disconnect? + Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() trigger disconnection!"); + onDisconnected(); + + // stop polling? check if actionStatus is not busy, or finished? + //nfcAdapter.disableReaderMode(activity); + + + } catch (ApduException e) { + //throw new RuntimeException(e); + Log.e(TAG, "SATODEBUG onConnected: A an exception has been thrown during card init: "+ e); + e.printStackTrace(); + onDisconnected(); + } catch (IOException e) { + //throw new RuntimeException(e); + Log.e(TAG, "SATODEBUG onConnected: B an exception has been thrown during card init: " + e); + e.printStackTrace(); + onDisconnected(); } catch (Exception e) { - Log.i("SatochipHWWallet", "getXpubs exception: " + e); + Log.e(TAG, "SATODEBUG onConnected: C an exception has been thrown during card init.: " + e); + e.printStackTrace(); + onDisconnected(); } + } + + public void onConnectedGetXpubs(SatochipCommandSet cmdSet) throws Exception { + // get paths + List> paths = this.actionObject.pathsParam; final List xpubs = new ArrayList<>(paths.size()); - // TODO - xpubs.add("xpub6EPNojiyVmkzkEMmkwfjVc4YdyBXoQH8QwdifcPaKqVszSyg6oczSEgfP5AYUUs5hNG9kNfosAbTSLZqjEfMGdA85F7dx3kk6qDFP6va7mz"); - if (paths.size()>=2) { - xpubs.add("xpub69CqtDngRoBFv667kKodkCxvGegsJZwVUTmE8K46e3HKL5JVYL4ZpZZSBvosZPdgVmFxa3fFvkYxYfXBQyYTkME99wHhugHAARLECJe64Ee"); + for (List path : paths) { + Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedGetXpubs() path: " + path); + final String key = Joiner.on("/").join(path); +// Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedGetXpubs() key: " + key + "."); + + 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 + //pathStr = "m/44'/0'/0'/0"; + int xtype = this.actionObject.networkParam.getVerPublic(); //0x0488B21E; + Integer sid = null; + String xpub = cmdSet.cardBip32GetXpub(bip32path, xtype, sid); + 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)); } - if (paths.size()>=3) { - xpubs.add("xpub69CqtDngRoBFv667kKodkCxvGegsJZwVUTmE8K46e3HKL5JVYL4ZpZZSBvosZPdgVmFxa3fFvkYxYfXBQyYTkME99wHhugHAARLECJe64Ee"); + + this.actionObject.xpubsResult = xpubs; + + // action finished + this.actionObject.actionStatus = NfcActionStatus.finished; + } + + // SATODEBUG + public void onDisconnected() { + //NfcCardService.isConnected.postValue(false) + Log.i(TAG, "SATODEBUG SatochipHWWallet onDisconnected: Card disconnected!"); + } + + + @NonNull + @Override + public synchronized List getXpubs(@NonNull Network network, @NonNull List> paths, @Nullable HardwareWalletInteraction hwInteraction) { + Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getXpubs start"); + Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getXpubs paths: " + paths); + Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getXpubs HardwareWalletInteraction: " + hwInteraction); + + // debug + try { + this.actionObject.actionStatus = NfcActionStatus.busy; + this.actionObject.actionType = NfcActionType.getXpub; + this.actionObject.networkParam = network; + this.actionObject.pathsParam = paths; + +// NfcCardManager cardManager = new NfcCardManager(); +// cardManager.setCardListener(this); +// cardManager.start(); +// Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() after 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 +// ); +// +// Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() after NfcAdapter.enableReaderMode"); + + + // poll for result from cardListener onConnected +// Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() before SLEEP"); +// TimeUnit.SECONDS.sleep(10); +// Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() after SLEEP"); + while (this.actionObject.actionStatus == NfcActionStatus.busy) { + TimeUnit.MILLISECONDS.sleep(500); + Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() SLEEP"); + } + + final List xpubs = this.actionObject.xpubsResult; + return xpubs; + + + } catch (Exception e) { + Log.i("SatochipHWWallet", "getXpubs exception: " + e); } - return xpubs; + + return null; + +// final List xpubs = new ArrayList<>(paths.size()); +// +// // TODO +// xpubs.add("xpub6EPNojiyVmkzkEMmkwfjVc4YdyBXoQH8QwdifcPaKqVszSyg6oczSEgfP5AYUUs5hNG9kNfosAbTSLZqjEfMGdA85F7dx3kk6qDFP6va7mz"); +// if (paths.size()>=2) { +// xpubs.add("xpub69CqtDngRoBFv667kKodkCxvGegsJZwVUTmE8K46e3HKL5JVYL4ZpZZSBvosZPdgVmFxa3fFvkYxYfXBQyYTkME99wHhugHAARLECJe64Ee"); +// } +// if (paths.size()>=3) { +// xpubs.add("xpub69CqtDngRoBFv667kKodkCxvGegsJZwVUTmE8K46e3HKL5JVYL4ZpZZSBvosZPdgVmFxa3fFvkYxYfXBQyYTkME99wHhugHAARLECJe64Ee"); +// } +// +// Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() before RETURN"); +// +// return xpubs; } @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 useAeProtocol: " + useAeProtocol); + Log.i("SatochipHWWallet", "signMessage start aeHostCommitment: " + aeHostCommitment); + Log.i("SatochipHWWallet", "signMessage start aeHostEntropy: " + aeHostEntropy); + Log.i("SatochipHWWallet", "signMessage start hwInteraction: " + hwInteraction); + if (useAeProtocol) { throw new RuntimeException("Hardware Wallet does not support the Anti-Exfil protocol"); } @@ -248,4 +436,25 @@ public Device getDevice() { Log.i("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(); + //result.write((byte)depth); + 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..4c34becf4 --- /dev/null +++ b/hardware/src/main/java/com/satochip/NfcActionObject.java @@ -0,0 +1,20 @@ +package com.satochip; + +import com.blockstream.common.gdk.data.Network; + +import java.util.ArrayList; +import java.util.List; + +public class NfcActionObject { + + public NfcActionType actionType = NfcActionType.none; + public NfcActionStatus actionStatus = NfcActionStatus.none; + + // getXpubs + // List getXpubs(@NonNull Network network, @NonNull List> paths, @Nullable HardwareWalletInteraction hwInteraction) { + public Network networkParam = null; + public List> pathsParam = new ArrayList<>(); + public List xpubsResult = new ArrayList<>(); + + +} diff --git a/hardware/src/main/java/com/satochip/NfcActionResult.java b/hardware/src/main/java/com/satochip/NfcActionResult.java new file mode 100644 index 000000000..50f2d0885 --- /dev/null +++ b/hardware/src/main/java/com/satochip/NfcActionResult.java @@ -0,0 +1,22 @@ +package com.satochip; + + +import java.util.List; + +public class NfcActionResult { + + public NfcActionType actionType = NfcActionType.none; + + public NfcActionStatus actionStatus = NfcActionStatus.none; + + public List xpubs; + + public NfcActionResult() { + + } + + + + + +} 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..67564baaf --- /dev/null +++ b/hardware/src/main/java/com/satochip/NfcActionType.java @@ -0,0 +1,7 @@ +package com.satochip; + +public enum NfcActionType { + none, + getXpub, + derivePubkey, +} 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..fb9c90e40 --- /dev/null +++ b/hardware/src/main/java/com/satochip/SatochipCommandSet.java @@ -0,0 +1,1154 @@ +package com.satochip; + + +import static com.satochip.Constants.*; + + +import com.blockstream.libwally.Wally; + +//import org.bitcoinj.core.Base58; +//import org.bitcoinj.core.Sha256Hash; +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[] authentikey = null; + private String authentikeyHex = null; + private String defaultBip32path = null; + private byte[] extendedKey = null; + private byte[] extendedChaincode = null; + private String extendedKeyHex = null; + private byte[] extendedPrivKey = null; + private String extendedPrivKeyHex = null; + + // Satodime, SeedKeeper or Satochip? + private String cardType = null; + private String certPem = null; // PEM certificate of device, if any + + + + public static final byte[] SATOCHIP_AID = Hex.decode("5361746f43686970"); //SatoChip + public static final byte[] SEEDKEEPER_AID = Hex.decode("536565644b6565706572"); //SeedKeeper + public static final byte[] SATODIME_AID = Hex.decode("5361746f44696d65"); //SatoDime + + + /** + * 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; + } + + /**************************************** + * AUTHENTIKEY * + ****************************************/ + public byte[] getAuthentikey() { + if (authentikey == null) { + cardGetAuthentikey(); + } + return authentikey; + } + + public String getAuthentikeyHex() { + if (authentikeyHex == null) { + cardGetAuthentikey(); + } + return authentikeyHex; + } + + public byte[] getBip32Authentikey() { + if (authentikey == null) { + cardBip32GetAuthentikey(); + } + return authentikey; + } + + public String getBip32AuthentikeyHex() { + if (authentikeyHex == null) { + cardBip32GetAuthentikey(); + } + return authentikeyHex; + } + + public List getPossibleAuthentikeys(){ + return this.possibleAuthentikeys; + } + + public SatochipParser getParser() { + return parser; + } + + public void setDefaultBip32path(String bip32path) { + defaultBip32path = bip32path; + } + + /** + * Set the SecureChannel object + * + * @param secureChannel secure channel + */ + protected void setSecureChannel(SecureChannelSession secureChannel) { + this.secureChannel = secureChannel; + } + + + + 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/Satodime/SeedKeeper 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() throws IOException { + + ApduResponse rapdu = cardSelect("satochip"); + if (rapdu.getSw() != 0x9000) { + rapdu = cardSelect("seedkeeper"); + if (rapdu.getSw() != 0x9000) { + rapdu = cardSelect("satodime"); + if (rapdu.getSw() != 0x9000) { + this.cardType = "unknown"; + logger.warning("SATOCHIPLIB: CardSelect: could not select a known applet"); + } + } + } + + return rapdu; + } + + public ApduResponse cardSelect(String cardType) throws IOException { + + ApduCommand selectApplet; + if (cardType.equals("satochip")) { + selectApplet = new ApduCommand(0x00, 0xA4, 0x04, 0x00, SATOCHIP_AID); + } else if (cardType.equals("seedkeeper")) { + selectApplet = new ApduCommand(0x00, 0xA4, 0x04, 0x00, SEEDKEEPER_AID); + } else { + selectApplet = new ApduCommand(0x00, 0xA4, 0x04, 0x00, SATODIME_AID); + } + + 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; + } + // only valid for v0.12 and higher + public byte[] cardGetAuthentikey() { + + ApduCommand plainApdu = new ApduCommand(0xB0, INS_EXPORT_AUTHENTIKEY, 0x00, 0x00, new byte[0]); + logger.info("SATOCHIPLIB: C-APDU cardExportAuthentikey:" + plainApdu.toHexString()); + ApduResponse respApdu = this.cardTransmit(plainApdu); + logger.info("SATOCHIPLIB: R-APDU cardExportAuthentikey:" + respApdu.toHexString()); + + // parse and recover pubkey + authentikey = parser.parseBip32GetAuthentikey(respApdu); + authentikeyHex = parser.toHexString(authentikey); + logger.info("SATOCHIPLIB: Authentikey from cardExportAuthentikey:" + authentikeyHex); + + return authentikey; + } + + public ApduResponse cardBip32GetAuthentikey() { + + ApduCommand plainApdu = new ApduCommand(0xB0, INS_BIP32_GET_AUTHENTIKEY, 0x00, 0x00, new byte[0]); + logger.info("SATOCHIPLIB: C-APDU cardBip32GetAuthentikey:" + plainApdu.toHexString()); + ApduResponse respApdu = this.cardTransmit(plainApdu); + logger.info("SATOCHIPLIB: R-APDU cardBip32GetAuthentikey:" + respApdu.toHexString()); + + // parse and recover pubkey + authentikey = parser.parseBip32GetAuthentikey(respApdu); + authentikeyHex = parser.toHexString(authentikey); + logger.info("SATOCHIPLIB: Authentikey from cardBip32GetAuthentikey:" + authentikeyHex); + + return respApdu; + } + +// public ApduResponse cardExportPkiPubkey() { +// +// ApduCommand plainApdu = new ApduCommand(0xB0, INS_EXPORT_PKI_PUBKEY, 0x00, 0x00, new byte[0]); +// logger.info("SATOCHIPLIB: C-APDU cardExportPkiPubkey:" + plainApdu.toHexString()); +// ApduResponse respApdu = this.cardTransmit(plainApdu); +// logger.info("SATOCHIPLIB: R-APDU cardExportPkiPubkey:" + respApdu.toHexString()); +// +// // parse and recover pubkey +// authentikey = parser.parseExportPkiPubkey(respApdu); +// authentikeyHex = parser.toHexString(authentikey); +// logger.info("SATOCHIPLIB: Authentikey from cardExportPkiPubkey:" + authentikeyHex); +// +// return respApdu; +// } + + /**************************************** + * CARD MGMT * + ****************************************/ + +// public ApduResponse cardSetup(byte pin_tries0, byte[] pin0) { +// +// // use random values for pin1, ublk0, ublk1 +// SecureRandom random = new SecureRandom(); +// byte[] ublk0 = new byte[8]; +// byte[] ublk1 = new byte[8]; +// byte[] pin1 = new byte[8]; +// random.nextBytes(ublk0); +// random.nextBytes(ublk1); +// random.nextBytes(pin1); +// +// byte ublk_tries0 = (byte) 0x01; +// byte ublk_tries1 = (byte) 0x01; +// byte pin_tries1 = (byte) 0x01; +// +// return cardSetup(pin_tries0, ublk_tries0, pin0, ublk0, pin_tries1, ublk_tries1, pin1, ublk1); +// } + +// public ApduResponse cardSetup( +// byte pin_tries0, byte ublk_tries0, byte[] pin0, byte[] ublk0, +// byte pin_tries1, byte ublk_tries1, byte[] pin1, byte[] ublk1) { +// +// byte[] pin = {0x4D, 0x75, 0x73, 0x63, 0x6C, 0x65, 0x30, 0x30}; //default pin +// byte cla = (byte) 0xB0; +// byte ins = INS_SETUP; +// byte p1 = 0; +// byte p2 = 0; +// +// // data=[pin_length(1) | pin | +// // pin_tries0(1) | ublk_tries0(1) | pin0_length(1) | pin0 | ublk0_length(1) | ublk0 | +// // pin_tries1(1) | ublk_tries1(1) | pin1_length(1) | pin1 | ublk1_length(1) | ublk1 | +// // memsize(2) | memsize2(2) | ACL(3) | +// // option_flags(2) | hmacsha160_key(20) | amount_limit(8)] +// int optionsize = 0; +// int option_flags = 0; // do not use option (mostly deprecated) +// int offset = 0; +// int datasize = 16 + pin.length + pin0.length + pin1.length + ublk0.length + ublk1.length + optionsize; +// byte[] data = new byte[datasize]; +// +// data[offset++] = (byte) pin.length; +// System.arraycopy(pin, 0, data, offset, pin.length); +// offset += pin.length; +// // pin0 & ublk0 +// data[offset++] = pin_tries0; +// data[offset++] = ublk_tries0; +// data[offset++] = (byte) pin0.length; +// System.arraycopy(pin0, 0, data, offset, pin0.length); +// offset += pin0.length; +// data[offset++] = (byte) ublk0.length; +// System.arraycopy(ublk0, 0, data, offset, ublk0.length); +// offset += ublk0.length; +// // pin1 & ublk1 +// data[offset++] = pin_tries1; +// data[offset++] = ublk_tries1; +// data[offset++] = (byte) pin1.length; +// System.arraycopy(pin1, 0, data, offset, pin1.length); +// offset += pin1.length; +// data[offset++] = (byte) ublk1.length; +// System.arraycopy(ublk1, 0, data, offset, ublk1.length); +// offset += ublk1.length; +// +// // memsize default (deprecated) +// data[offset++] = (byte) 00; +// data[offset++] = (byte) 32; +// data[offset++] = (byte) 00; +// data[offset++] = (byte) 32; +// +// // ACL (deprecated) +// data[offset++] = (byte) 0x01; +// data[offset++] = (byte) 0x01; +// data[offset++] = (byte) 0x01; +// +// ApduCommand plainApdu = new ApduCommand(cla, ins, p1, p2, data); +// //logger.info("SATOCHIPLIB: C-APDU cardSetup:" + plainApdu.toHexString()); +// logger.info("SATOCHIPLIB: C-APDU cardSetup"); +// ApduResponse respApdu = this.cardTransmit(plainApdu); +// logger.info("SATOCHIPLIB: R-APDU cardSetup:" + respApdu.toHexString()); +// +// if (respApdu.isOK()) { +// setPin0(pin0); +// +// if (this.cardType.equals("satodime")) { // cache values +// this.satodimeStatus.updateStatusFromSetup(respApdu); +// } +// } +// +// return respApdu; +// } + +// public ApduResponse cardSendResetCommand() throws Exception { +// byte[] data = new byte[]{}; +// +// ApduCommand plainApdu = new ApduCommand( +// 0xB0, +// INS_RESET_TO_FACTORY, +// 0x00, +// 0x00, +// data +// ); +// +// // reset command must be sent in clear, without other commands interferring between reset commands +// logger.warning("SATOCHIPLIB: C-APDU cardSendResetCommand:" + plainApdu.toHexString()); +// ApduResponse respApdu = apduChannel.send(plainApdu); +// logger.warning("SATOCHIPLIB: R-APDU cardSendResetCommand:" + respApdu.toHexString()); +// +// return respApdu; +// } + + /**************************************** + * 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 e) { + this.pin0 = null; + throw e; + } catch (WrongPINLegacyException e) { + this.pin0 = null; + throw e; + } catch (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); + } + +// public ApduResponse cardChangePin(byte[] oldPin, byte[] newPin) throws Exception { +// logger.info("SATOCHIPLIB: changeCardPin START"); +// +// byte[] data = new byte[1 + oldPin.length + 1 + newPin.length]; +// data[0] = (byte) oldPin.length; +// System.arraycopy(oldPin, 0, data, 1, oldPin.length); +// data[1 + oldPin.length] = (byte) newPin.length; +// System.arraycopy(newPin, 0, data, 2 + oldPin.length, newPin.length); +// setPin0(newPin); +// try{ +// ApduCommand plainApdu = new ApduCommand(0xB0, INS_CHANGE_PIN, 0x00, 0x00, data); +// //logger.info("SATOCHIPLIB: C-APDU changeCardPin:"+ plainApdu.toHexString()); +// logger.info("SATOCHIPLIB: C-APDU changeCardPin"); +// ApduResponse rapdu = this.cardTransmit(plainApdu); +// logger.info("SATOCHIPLIB: R-APDU changeCardPin:"+ rapdu.toHexString()); +// +// rapdu.checkAuthOK(); +// return rapdu; +// +// } catch (WrongPINException e) { +// this.pin0 = null; +// throw e; +// } catch (WrongPINLegacyException e) { +// this.pin0 = null; +// throw e; +// } catch (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 cardUnblockPin(byte[] puk) throws Exception { +// ApduCommand plainApdu = new ApduCommand( +// 0xB0, +// INS_UNBLOCK_PIN, +// 0x00, +// 0x00, +// puk +// ); +// +// try{ +// //logger.info("SATOCHIPLIB: C-APDU cardUnblockPin:" + plainApdu.toHexString()); +// logger.info("SATOCHIPLIB: C-APDU cardUnblockPin"); +// ApduResponse rapdu = this.cardTransmit(plainApdu); +// logger.info("SATOCHIPLIB: R-APDU cardUnblockPin:" + rapdu.toHexString()); +// +// rapdu.checkAuthOK(); +// return rapdu; +// +// } catch (WrongPINException e) { +// this.pin0 = null; +// throw e; +// } catch (WrongPINLegacyException e) { +// this.pin0 = null; +// throw e; +// } catch (BlockedPINException e) { +// this.pin0 = null; +// throw e; +// } catch (ResetToFactoryException e) { +// this.pin0 = null; +// throw e; +// } catch (ApduException e){ +// this.pin0 = null; +// throw e; +// } catch (Exception e){ +// this.pin0 = null; +// throw e; +// } +// +// } + + /**************************************** + * BIP32 * + ****************************************/ + +// public ApduResponse cardBip32ImportSeed(byte[] masterseed) { +// //TODO: check seed (length...) +// ApduCommand plainApdu = new ApduCommand(0xB0, INS_BIP32_IMPORT_SEED, masterseed.length, 0x00, masterseed); +// +// //logger.info("SATOCHIPLIB: C-APDU cardBip32ImportSeed:" + plainApdu.toHexString()); +// logger.info("SATOCHIPLIB: C-APDU cardBip32ImportSeed"); +// ApduResponse respApdu = this.cardTransmit(plainApdu); +// logger.info("SATOCHIPLIB: R-APDU cardBip32ImportSeed:" + respApdu.toHexString()); +// +// return respApdu; +// } + +// public ApduResponse cardResetSeed(byte[] pin, byte[] chalresponse) { +// +// byte p1 = (byte) pin.length; +// byte[] data; +// if (chalresponse == null) { +// data = new byte[pin.length]; +// System.arraycopy(pin, 0, data, 0, pin.length); +// } else if (chalresponse.length == 20) { +// data = new byte[pin.length + 20]; +// int offset = 0; +// System.arraycopy(pin, 0, data, offset, pin.length); +// offset += pin.length; +// 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_BIP32_RESET_SEED, p1, 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; +// } + +// public byte[][] cardBip32GetExtendedKey() throws Exception { +// if (defaultBip32path == null) { +// defaultBip32path = "m/44'/60'/0'/0/0"; +// } +// return cardBip32GetExtendedKey(defaultBip32path, null, null); +// } + + public byte[][] cardBip32GetExtendedKey(String stringPath, Byte flags, Integer sid) throws Exception { + Bip32Path parsedPath = parser.parseBip32PathToBytes(stringPath); + return cardBip32GetExtendedKey(parsedPath, flags, sid); + } + + public byte[][] cardBip32GetExtendedKey(Bip32Path parsedPath, Byte flags, Integer sid) 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; + if (flags != null) { + optionFlags = flags; + } + byte p2 = optionFlags; + + byte[] data = parsedPath.getBytes(); + + if (sid != null) { + data = Arrays.copyOf(data, data.length + 2); + data[data.length - 2] = (byte) ((sid >> 8) & 0xFF); + data[data.length - 1] = (byte) (sid & 0xFF); + } + + 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 ((optionFlags & 0x04) == 0x04) { // BIP85 + //todo: enable? + logger.warning("SATOCHIPLIB: cardBip32GetExtendedKey: in BIP85"); + extendedKey = parser.parseBip85GetExtendedKey(respApdu)[0]; + extendedKeyHex = parser.toHexString(extendedKey); + } else if ((optionFlags & 0x02) == 0x00) { // BIP32 pubkey + logger.warning("SATOCHIPLIB: cardBip32GetExtendedKey: in BIP39"); + 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];// todo: return array + extendedChaincode = extendedKeyData[1]; + extendedKeyHex = parser.toHexString(extendedKey); + return extendedKeyData; + } else { // BIP32 privkey + byte[][] extendedPrivKeyData = parser.parseBip32GetExtendedKey(respApdu); + extendedPrivKey = extendedPrivKeyData[0]; + extendedPrivKeyHex = parser.toHexString(extendedPrivKey); + return extendedPrivKeyData; + } + } + } + } + + // todo: only for testing testCardBip32GetExtendedkeyBip85 + public byte[] getExtendedKey() { + return extendedKey; + } + + /* + * 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, Integer sid) throws Exception { + Bip32Path bip32Path = parser.parseBip32PathToBytes(path); + return cardBip32GetXpub(bip32Path, xtype, sid); + } + public String cardBip32GetXpub(Bip32Path bip32Path, int xtype, Integer sid) throws Exception { + logger.warning("SATOCHIPLIB: cardBip32GetXpub"); + + byte[] childPubkey, childChaincode; + byte optionFlags = (byte) 0x40; + + // Get extended key + logger.warning("SATOCHIPLIB: cardBip32GetXpub: getting card cardBip32GetExtendedKey"); + cardBip32GetExtendedKey(bip32Path, optionFlags, sid); + 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 + //String parentPath = parser.getBip32PathParentPath(path); + //logger.warning("SATOCHIPLIB: cardBip32GetXpub: parentPathString: "+ parentPath); + byte[] parentBytePath = Arrays.copyOfRange(bytePath, 0, bytePath.length-4); + //parentBytePath = Arrays.copyOfRange(bytePath, 0, bytePath.length-1-4); + Bip32Path parentBip32Path = new Bip32Path(depth-1, parentBytePath); + + cardBip32GetExtendedKey(parentBip32Path, optionFlags, sid); + byte[] parentPubkeyBytes = extendedKey; + + // Pubkey should be in compressed form + if (parentPubkeyBytes.length != 33) { + parentPubkeyBytes = parser.compressPubKey(parentPubkeyBytes); + } + + //fingerprintBytes = Arrays.copyOfRange(digestRipeMd160(Sha256Hash.hash(parentPubkeyBytes)), 0, 4); + fingerprintBytes = Arrays.copyOfRange(digestRipeMd160(Wally.sha256(parentPubkeyBytes)), 0, 4); // debug + + 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); + + if (xpubBytes.length != 78) { + throw new Exception("wrongXpubLength " + xpubBytes.length + " " + 78); + } + + //String xpub = encodeChecked(xpubBytes); + String xpub = Wally.base58check_from_bytes(xpubBytes); + logger.warning("SATOCHIPLIB: cardBip32GetXpub() xpub: " + xpub); + return xpub; + } + +// private String encodeChecked(byte[] bytes) { +// +// byte[] checksum = calculateChecksum(bytes); +// byte[] checksummedBytes = new byte[bytes.length + 4]; +// System.arraycopy(bytes, 0, checksummedBytes, 0, bytes.length); +// System.arraycopy(checksum, 0, checksummedBytes, bytes.length, 4); +// return Base58.encode(checksummedBytes); +// } + +// private byte[] calculateChecksum(byte[] bytes) { +// byte[] hash = Wally.sha256d(bytes); //Sha256Hash.hashTwice(bytes); +// byte[] checksum = new byte[4]; +// System.arraycopy(hash, 0, checksum, 0, 4); +// return checksum; +// } + + 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; + } + + // public ApduResponse cardSignMessage(int keyNbr, byte[] pubkey, String message, byte[] hmac, String altcoin){ + // } + + /**************************************** + * 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; + } + + /**************************************** + * 2FA commands * + ****************************************/ + + + +// public String getCardLabel() { +// logger.info("SATOCHIPLIB: getCardLabel START"); +// +// ApduCommand plainApdu = new ApduCommand(0xB0, INS_CARD_LABEL, 0x00, 0x01, new byte[0]); +// logger.info("SATOCHIPLIB: C-APDU getCardLabel:"+ plainApdu.toHexString()); +// ApduResponse respApdu = this.cardTransmit(plainApdu); +// logger.info("SATOCHIPLIB: R-APDU getCardLabel:"+ respApdu.toHexString()); +// int sw = respApdu.getSw(); +// String label; +// if (sw == 0x9000){ +// byte labelSize = respApdu.getData()[0]; +// try { +// label = new String(respApdu.getData(), 1, labelSize, StandardCharsets.UTF_8); +// } catch (Exception e) { +// logger.warning("SATOCHIPLIB: getCardLabel UnicodeDecodeError while decoding card label!"); +// label = new String(respApdu.getData(), 1, respApdu.getData().length - 1, StandardCharsets.UTF_8); +// } +// } else if (sw == 0x6D00) { +// logger.info("SATOCHIPLIB: getCardLabel label not set:" + sw); +// label = "(none)"; +// } else { +// logger.warning("SATOCHIPLIB: getCardLabel Error while recovering card label:" + sw); +// label = "(unknown)"; +// } +// return label; +// } +// +// public Boolean setCardLabel(String label) { +// logger.info("SATOCHIPLIB: setCardLabel START"); +// +// byte[] labelData = label.getBytes(StandardCharsets.UTF_8); +// byte[] data = new byte[1 + labelData.length]; +// data[0] = (byte) labelData.length; +// System.arraycopy(labelData, 0, data, 1, labelData.length); +// +// ApduCommand plainApdu = new ApduCommand(0xB0, INS_CARD_LABEL, 0x00, 0x00, data); +// logger.info("SATOCHIPLIB: C-APDU setCardLabel:"+ plainApdu.toHexString()); +// ApduResponse respApdu = this.cardTransmit(plainApdu); +// logger.info("SATOCHIPLIB: R-APDU setCardLabel:"+ respApdu.toHexString()); +// int sw = respApdu.getSw(); +// return sw == 0x9000; +// } + + /**************************************** + * PKI commands * + ****************************************/ + +// public ApduResponse cardExportPersoPubkey(){ +// +// ApduCommand plainApdu = new ApduCommand(0xB0, INS_EXPORT_PKI_PUBKEY, 0x00, 0x00, new byte[0]); +// logger.info("SATOCHIPLIB: C-APDU cardExportPersoPubkey:"+ plainApdu.toHexString()); +// ApduResponse respApdu = this.cardTransmit(plainApdu); +// logger.info("SATOCHIPLIB: R-APDU cardExportPersoPubkey:"+ respApdu.toHexString()); +// +// return respApdu; +// } +// +// public String cardExportPersoCertificate() throws ApduException { +// +// // init +// byte p1 = 0x00; +// byte p2 = 0x01; // init +// ApduCommand plainApdu = new ApduCommand(0xB0, INS_EXPORT_PKI_CERTIFICATE, p1, p2, new byte[0]); +// logger.info("SATOCHIPLIB: C-APDU cardExportPersoCertificate - init:"+ plainApdu.toHexString()); +// ApduResponse respApdu = this.cardTransmit(plainApdu); +// logger.info("SATOCHIPLIB: R-APDU cardExportPersoCertificate - init:"+ respApdu.toHexString()); +// respApdu.checkOK(); +// int sw = respApdu.getSw(); +// byte[] response = null; +// int certificate_size = 0; +// if (sw == 0x9000){ +// response= respApdu.getData(); +// certificate_size= (response[0] & 0xFF) * 256 + (response[1] & 0xFF); +// logger.warning("SATOCHIPLIB: personalization certificate export: code:" + sw + "certificate size: " + certificate_size); +// } else if (sw == 0x6D00){ +// logger.warning("SATOCHIPLIB: Error during personalization certificate export: command unsupported(0x6D00)"); +// return "Error during personalization certificate export: command unsupported(0x6D00)"; +// } else if (sw == 0x0000){ +// logger.warning("SATOCHIPLIB: Error during personalization certificate export: no card present(0x0000)"); +// return "Error during personalization certificate export: no card present(0x0000)"; +// } +// +// if (certificate_size==0){ +// return ""; //new byte[0]; //"(empty)"; +// } +// +// // UPDATE apdu: certificate data in chunks +// p2= 0x02; //update +// byte[] certificate = new byte[certificate_size];//certificate_size*[0] +// short chunk_size = 128; +// byte[] chunk = new byte[chunk_size]; +// int remaining_size = certificate_size; +// int cert_offset = 0; +// byte[] data = new byte[4]; +// while(remaining_size > 128){ +// // data=[ chunk_offset(2b) | chunk_size(2b) ] +// data[0]= (byte) ((cert_offset>>8)&0xFF); +// data[1]= (byte) (cert_offset&0xFF); +// data[2]= (byte) ((chunk_size>>8)&0xFF);; +// data[3]= (byte) (chunk_size & 0xFF); +// plainApdu = new ApduCommand(0xB0, INS_EXPORT_PKI_CERTIFICATE, p1, p2, data); +// logger.warning("SATOCHIPLIB: C-APDU cardExportPersoCertificate - update:"+ plainApdu.toHexString()); +// respApdu = this.cardTransmit(plainApdu); +// logger.warning("SATOCHIPLIB: R-APDU cardExportPersoCertificate - update:"+ respApdu.toHexString()); +// respApdu.checkOK(); +// // update certificate +// response= respApdu.getData(); +// System.arraycopy(response, 0, certificate, cert_offset, chunk_size); +// remaining_size-=chunk_size; +// cert_offset+=chunk_size; +// } +// +// // last chunk +// data[0]= (byte) ((cert_offset>>8)&0xFF); +// data[1]= (byte) (cert_offset&0xFF); +// data[2]= (byte) ((remaining_size>>8)&0xFF);; +// data[3]= (byte) (remaining_size & 0xFF); +// plainApdu = new ApduCommand(0xB0, INS_EXPORT_PKI_CERTIFICATE, p1, p2, data); +// logger.warning("SATOCHIPLIB: C-APDU cardExportPersoCertificate - final:"+ plainApdu.toHexString()); +// respApdu = this.cardTransmit(plainApdu); +// logger.warning("SATOCHIPLIB: R-APDU cardExportPersoCertificate - final:"+ respApdu.toHexString()); +// respApdu.checkOK(); +// // update certificate +// response= respApdu.getData(); +// System.arraycopy(response, 0, certificate, cert_offset, remaining_size); +// cert_offset+=remaining_size; +// +// // parse and return raw certificate +// String cert_pem= parser.convertBytesToStringPem(certificate); +// logger.warning("SATOCHIPLIB: cardExportPersoCertificate checking certificate:" + Arrays.toString(certificate)); +// +// return cert_pem; +// } +// +// public ApduResponse cardChallengeResponsePerso(byte[] challenge_from_host){ +// +// ApduCommand plainApdu = new ApduCommand(0xB0, INS_CHALLENGE_RESPONSE_PKI, 0x00, 0x00, challenge_from_host); +// logger.info("SATOCHIPLIB: C-APDU cardChallengeResponsePerso:"+ plainApdu.toHexString()); +// ApduResponse respApdu = this.cardTransmit(plainApdu); +// logger.info("SATOCHIPLIB: R-APDU cardChallengeResponsePerso:"+ respApdu.toHexString()); +// +// return respApdu; +// } +// +// public String[] cardVerifyAuthenticity(){ +// +// String txt_error=""; +// String txt_ca="(empty)"; +// String txt_subca="(empty)"; +// String txt_device="(empty)"; +// final String FAIL= "FAIL"; +// final String OK= "OK"; +// +// // get certificate from device +// String cert_pem=""; +// try{ +// cert_pem = cardExportPersoCertificate(); +// logger.warning("SATOCHIPLIB: Cert PEM: "+ cert_pem); +// } catch (Exception e){ +// logger.warning("SATOCHIPLIB: Exception in cardVerifyAuthenticity:"+ e); +// txt_error= "Unable to retrieve device certificate!"; +// //String[] out = new String [5]; +// //out[0]={"a","b","c","d"}; +// String[] out = new String [] {FAIL, txt_ca, txt_subca, txt_device, txt_error}; +// return out; +// } +// +// // verify certificate chain +// boolean isValidated= false; +// PublicKey pubkeyDevice= null; +// try{ +// // load certs +// InputStream isCa = this.getClass().getClassLoader().getResourceAsStream("cert/ca.cert"); +// InputStream isSubca; +// if (cardType.equals("satochip")) { +// isSubca = this.getClass().getClassLoader().getResourceAsStream("cert/subca-satochip.cert"); +// } else if (cardType.equals("seedkeeper")) { +// isSubca = this.getClass().getClassLoader().getResourceAsStream("cert/subca-seedkeeper.cert"); +// } else { +// isSubca = this.getClass().getClassLoader().getResourceAsStream("cert/subca-satodime.cert"); +// } +// InputStream isDevice = new ByteArrayInputStream(cert_pem.getBytes(StandardCharsets.UTF_8)); +// // gen certs +// CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509", "BC"); // without BC provider, validation fails... +// Certificate certCa = certificateFactory.generateCertificate(isCa); +// txt_ca= certCa.toString(); +// logger.warning("SATOCHIPLIB: certCa: " + txt_ca); +// Certificate certSubca = certificateFactory.generateCertificate(isSubca); +// txt_subca= certSubca.toString(); +// logger.warning("SATOCHIPLIB: certSubca: " + txt_subca); +// Certificate certDevice = certificateFactory.generateCertificate(isDevice); +// logger.warning("SATOCHIPLIB: certDevice: " + certDevice); +// txt_device= certDevice.toString(); +// logger.warning("SATOCHIPLIB: txtCertDevice: " + txt_device); +// +// pubkeyDevice= certDevice.getPublicKey(); +// logger.warning("SATOCHIPLIB: certDevice pubkey: " + pubkeyDevice.toString()); +// +// // cert chain +// Certificate[] chain= new Certificate[2]; +// chain[0]= certDevice; +// chain[1]= certSubca; +// CertPath certPath = certificateFactory.generateCertPath(Arrays.asList(chain)); +// +// // keystore +// KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); +// ks.load(null, null); +// KeyStore.TrustedCertificateEntry tcEntry= new KeyStore.TrustedCertificateEntry(certCa); +// //KeyStore.TrustedCertificateEntry tcEntry= new KeyStore.TrustedCertificateEntry(certSubca); +// ks.setEntry("SatodimeCA", tcEntry, null); +// +// // validator +// PKIXParameters params = new PKIXParameters(ks); +// params.setRevocationEnabled(false); +// CertPathValidator certValidator = CertPathValidator.getInstance(CertPathValidator.getDefaultType()); // PKIX +// certValidator.validate(certPath, params); +// isValidated=true; +// logger.info("SATOCHIPLIB: Certificate chain validated!"); +// +// }catch (Exception e){ +// logger.warning("SATOCHIPLIB: Exception in cardVerifyAuthenticity:"+ e); +// e.printStackTrace(); +// isValidated=false; +// txt_error= "Failed to validate certificate chain! \r\n\r\n" + e.toString(); +// String[] out = new String [] {FAIL, txt_ca, txt_subca, txt_device, txt_error}; +// return out; +// } +// +// // perform challenge-response with the card to ensure that the key is correctly loaded in the device +// try{ +// SecureRandom random = new SecureRandom(); +// byte[] challenge_from_host= new byte[32]; +// random.nextBytes(challenge_from_host); +// ApduResponse rapduChalresp= cardChallengeResponsePerso(challenge_from_host); +// byte[][] parsedData= parser.parseVerifyChallengeResponsePerso(rapduChalresp); +// byte[] challenge_from_device= parsedData[0]; +// byte[] sig= parsedData[1]; +// +// // build challenge byte[] +// int offset=0; +// String chalHeaderString= "Challenge:"; +// byte[] chalHeaderBytes= chalHeaderString.getBytes(StandardCharsets.UTF_8); +// byte[] chalFullBytes= new byte[chalHeaderBytes.length + 32 + 32]; +// System.arraycopy(chalHeaderBytes, 0, chalFullBytes, offset, chalHeaderBytes.length); +// offset+= chalHeaderBytes.length; +// System.arraycopy(challenge_from_device, 0, chalFullBytes, offset, 32); +// offset+= 32; +// System.arraycopy(challenge_from_host, 0, chalFullBytes, offset, 32); +// +// // verify sig with pubkeyDevice +// byte[] pubkey= new byte[65]; +// byte[] pubkeyEncoded= pubkeyDevice.getEncoded(); +// System.arraycopy(pubkeyEncoded, (pubkeyEncoded.length-65), pubkey, 0, 65); // extract pubkey from ASN1 encoding +// boolean isChalrespOk= parser.verifySig(chalFullBytes, sig, pubkey); +// if (!isChalrespOk){ +// throw new RuntimeException("Failed to verify challenge-response signature!"); +// } +// // TODO: pubkeyDevice should be equal to authentikey +// }catch (Exception e){ +// logger.warning("SATOCHIPLIB: Exception in cardVerifyAuthenticity:"+ e); +// e.printStackTrace(); +// txt_error= "Failed to verify challenge-response! \r\n\r\n" + e.toString(); +// String[] out = new String [] {FAIL, txt_ca, txt_subca, txt_device, txt_error}; +// return out; +// } +// +// String[] out = new String [] {OK, txt_ca, txt_subca, txt_device, txt_error}; +// return out; +// } +} 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..3a24a851c --- /dev/null +++ b/hardware/src/main/java/com/satochip/SatochipParser.java @@ -0,0 +1,739 @@ +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.HashMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Base64; +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 String getBip32PathParentPath(String bip32path) throws Exception { +// System.out.println("In getBip32PathParentPath"); +// String[] splitPath = bip32path.split("/"); +// if (splitPath.length <= 1) { +// throw new Exception("Invalid BIP32 path: " + bip32path); +// } +// String[] parentPathArray = Arrays.copyOf(splitPath, splitPath.length - 1); +// String parentPath = String.join("/", parentPathArray); +// return parentPath; +// } + + public byte[][] parseBip85GetExtendedKey(ApduResponse rapdu){ + logger.warning("SATOCHIPLIB: parseBip85GetExtendedKey: Start "); + + try { + byte[] data = rapdu.getData(); + logger.warning("SATOCHIPLIB: parseBip85GetExtendedKey data: " + toHexString(data)); + + int entropySize = 256 * (data[0] & 0xFF) + (data[1] & 0xFF); + byte[] entropyBytes = Arrays.copyOfRange(data, 2, 2 + entropySize); + + return new byte[][] {entropyBytes, new byte[0]}; + } catch(Exception e) { + System.out.println("SATOCHIPLIB parseBip85GetExtendedKey() exception: "+e); + e.printStackTrace(); + throw new RuntimeException("Is BouncyCastle in the classpath?", e); + } + } + + 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[] parseExportPkiPubkey(ApduResponse rapdu){ + try{ + byte[] data= rapdu.getData(); + logger.info("SATOCHIPLIB: parseExportPkiPubkey data: " + toHexString(data)); + // data: [autehntikey(65b) | sig_size(2b - option) | sig(option) ] + byte[] pubkey= new byte[65]; + System.arraycopy(data, 0, pubkey, 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); + } + } + + + + public BigInteger[] parse_rsi_from_dersig(byte[] hash, byte[] dersig, byte[] coordx){ + + BigInteger[] sigBig = decodeFromDER(dersig); + + int recid= recoverRecId(hash, sigBig, coordx); + if (recid==-1){ + throw new RuntimeException("Exception in parse_rsv_from_dersig: could not recover recid"); + } + + BigInteger[] rsi= new BigInteger[3]; + rsi[0]= sigBig[0]; + rsi[1]= sigBig[1]; + rsi[2]= BigInteger.valueOf(recid); + + return rsi; + } + + /**************************************** + * 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 (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 (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); + } + + // public static String toHexString(byte[] raw) { + + // 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))); + //hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F))).append(" "); + // } + // return hex.toString(); + // } + + + //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; + } + } + + /** + * PKI PARSER + **/ + public String convertBytesToStringPem(byte[] certBytes){ + logger.info("SATOCHIPLIB: In convertBytesToStringPem"); + String certBase64Raw= Base64.getEncoder().encodeToString(certBytes); + logger.info("SATOCHIPLIB: certBase64Raw"+ certBase64Raw); + + // divide in fixed size chunk + int chunkSize=64; + String certBase64= "-----BEGIN CERTIFICATE-----\r\n"; + for (int offset=0; offset> 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..194fa97f2 --- /dev/null +++ b/hardware/src/main/java/com/satochip/SecureChannelSession.java @@ -0,0 +1,284 @@ +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.ECNamedCurveParameterSpec; +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 org.spongycastle. +//import org.spongycastle.jce.provider.BouncyCastleProvider; +//import org.spongycastle.crypto.params.KeyParameter; +//import org.spongycastle.jce.ECNamedCurveTable; +//import org.spongycastle.jce.interfaces.ECPublicKey; +//import org.spongycastle.jce.provider.BouncyCastleProvider; +//import org.spongycastle.jce.spec.ECNamedCurveParameterSpec; +//import org.spongycastle.jce.spec.ECParameterSpec; +//import org.spongycastle.jce.spec.ECPublicKeySpec; +//import org.spongycastle.crypto.digests.SHA1Digest; +//import org.spongycastle.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.security.spec.ECGenParameterSpec; +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(BouncyCastleProvider(), 1); + Security.insertProviderAt(new BouncyCastleProvider(), 1); + ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1"); +// ECNamedCurveParameterSpec namedSpec = ECNamedCurveTable.getParameterSpec("secp256k1"); +// ECParameterSpec ecSpec = new ECParameterSpec ( +// namedSpec.getCurve(), +// namedSpec.getG(), +// namedSpec.getN(), +// namedSpec.getH() +// ); + KeyPairGenerator g = KeyPairGenerator.getInstance("EC"); + g.initialize(ecSpec, random); + //g.initialize(new ECGenParameterSpec("secp256k1"), 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; + return; + } + +} 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"); + } +} From dd398086bd122c7984174261731d81c845d18599 Mon Sep 17 00:00:00 2001 From: Toporin Date: Thu, 13 Feb 2025 23:04:53 +0100 Subject: [PATCH 06/25] Satochip integration: implement signMessage() (wip) --- .../com/blockstream/common/gdk/GdkSession.kt | 28 ++++ .../greenbits/wallets/SatochipHWWallet.java | 130 ++++++++++++++---- .../java/com/satochip/NfcActionObject.java | 12 +- .../java/com/satochip/NfcActionResult.java | 22 --- .../main/java/com/satochip/NfcActionType.java | 3 +- .../java/com/satochip/SatochipParser.java | 3 +- 6 files changed, 147 insertions(+), 51 deletions(-) delete mode 100644 hardware/src/main/java/com/satochip/NfcActionResult.java 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 4cd2eed22..2ceb707cd 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt @@ -155,6 +155,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import kotlinx.datetime.Clock import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.add import kotlinx.serialization.json.buildJsonObject @@ -1202,6 +1203,13 @@ class GdkSession constructor( isCreate: Boolean, isRestore: Boolean, ): LoginData { + println("SATODEBUG GdkSession loginWithMnemonic() wallet: " + wallet) + println("SATODEBUG GdkSession loginWithMnemonic() loginCredentialsParams: " + loginCredentialsParams) + println("SATODEBUG GdkSession loginWithMnemonic() START") + println("SATODEBUG GdkSession loginWithMnemonic() START") + println("SATODEBUG GdkSession loginWithMnemonic() START") + + return loginWithLoginCredentials( prominentNetwork = prominentNetwork(isTestnet), wallet = wallet, @@ -1281,6 +1289,13 @@ class GdkSession constructor( hardwareWalletResolver: HardwareWalletResolver, hwInteraction: HardwareWalletInteraction? = null, ): LoginData { + println("SATODEBUG GdkSession loginWithDevice() START") + println("SATODEBUG GdkSession loginWithDevice() START wallet: " + wallet) + println("SATODEBUG GdkSession loginWithDevice() START device: " + device) + println("SATODEBUG GdkSession loginWithDevice() START derivedLightningMnemonic: " + derivedLightningMnemonic) + println("SATODEBUG GdkSession loginWithDevice() START hardwareWalletResolver: " + hardwareWalletResolver) + + // If last used network is Lightning, change to bitcoin as the ln network can't be used for login val lastUsedNetwork = (wallet.activeNetwork .takeIf { !Network.isLightning(it) } ?: Network.ElectrumMainnet) @@ -1328,6 +1343,15 @@ class GdkSession constructor( hardwareWalletResolver: HardwareWalletResolver? = null, hwInteraction: HardwareWalletInteraction? = null, ): LoginData { + println("SATODEBUG GdkSession loginWithLoginCredentials() START") + println("SATODEBUG GdkSession loginWithLoginCredentials() START prominentNetwork: " + prominentNetwork) + println("SATODEBUG GdkSession loginWithLoginCredentials() START wallet: " + wallet) + println("SATODEBUG GdkSession loginWithLoginCredentials() START walletLoginCredentialsParams: " + walletLoginCredentialsParams) + println("SATODEBUG GdkSession loginWithLoginCredentials() START appGreenlightCredentials: " + appGreenlightCredentials) + println("SATODEBUG GdkSession loginWithLoginCredentials() START device: " + device) + println("SATODEBUG GdkSession loginWithLoginCredentials() START appGreenlightCredentials: " + appGreenlightCredentials) + println("SATODEBUG GdkSession loginWithLoginCredentials() START derivedLightningMnemonic: " + derivedLightningMnemonic) + isWatchOnly = walletLoginCredentialsParams.isWatchOnly isNoBlobWatchOnly = isWatchOnly && richWatchOnly == null isRichWatchOnly = isWatchOnly && richWatchOnly != null @@ -1636,6 +1660,8 @@ class GdkSession constructor( initAccount: Long?, initializeSession: Boolean ) { + println("SATODEBUG GdkSession onLoginSuccess() START") + _isConnectedState.value = true xPubHashId = if(isNoBlobWatchOnly) loginData.networkHashId else loginData.xpubHashId @@ -1649,6 +1675,8 @@ class GdkSession constructor( } private suspend fun initializeSessionData(initNetwork: String?, initAccount: Long?) { + println("SATODEBUG GdkSession initializeSessionData() START") + // Check if active account index was archived from 1) a different client (multisig) or 2) from cached Singlesig hww session // Expect refresh = true to be already called updateAccounts() diff --git a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java index e44aa6cce..e0e59aa23 100644 --- a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java +++ b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java @@ -11,6 +11,7 @@ import androidx.annotation.Nullable; import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; //import com.blockstream.common.devices.ApduException; @@ -32,8 +33,8 @@ import com.blockstream.common.gdk.device.SignMessageResult; import com.blockstream.common.gdk.device.SignTransactionResult; import com.blockstream.libwally.Wally; -import com.btchip.BTChipException; import com.btchip.utils.BufferUtils; +import com.btchip.utils.VarintUtils; import com.google.common.base.Joiner; import com.satochip.ApduException; import com.satochip.ApduResponse; @@ -42,12 +43,11 @@ import com.satochip.CardChannel; import com.satochip.CardListener; import com.satochip.NfcActionObject; -import com.satochip.NfcActionResult; import com.satochip.NfcActionStatus; import com.satochip.NfcActionType; import com.satochip.NfcCardManager; import com.satochip.SatochipCommandSet; -import com.satoshilabs.trezor.protobuf.TrezorType; +import com.satochip.SatochipParser; import java.io.IOException; import java.nio.charset.Charset; @@ -64,6 +64,10 @@ 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 final Map mUserXPubs = new HashMap<>(); // private final Map mServiceXPubs = new HashMap<>(); // private final Map mRecoveryXPubs = new HashMap<>(); @@ -147,8 +151,10 @@ public void onConnected(CardChannel channel) { Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() pin verified: " + rapduPin.toHexString()); // execute commands depending on actionType - if (this.actionObject.actionType == NfcActionType.getXpub){ + if (this.actionObject.actionType == NfcActionType.getXpubs){ onConnectedGetXpubs(cmdSet); + } else if (this.actionObject.actionType == NfcActionType.signMessage) { + onConnectedSignMessage(cmdSet); } @@ -217,6 +223,79 @@ public void onConnectedGetXpubs(SatochipCommandSet cmdSet) throws Exception { 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 DEBUG +// String pathStr = "m/84'/0'/0'/0/0"; +// Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() pathStr: " + pathStr); //debug +// byte optionFlags = (byte) 0x40; +// byte[][] extendedKey = cmdSet.cardBip32GetExtendedKey(pathStr, optionFlags, null); + + // 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 optionFlags = (byte) 0x40; + byte[][] extendedKey = cmdSet.cardBip32GetExtendedKey(bip32path, optionFlags, null); + 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); +// VarInt size = new VarInt(messageBytes.length); +// bos.write(size.encode()); + 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 + //byte[] hashBytes = Wally.sha256(formatedMessageBytes); // single hash debug? + 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(formatedMessageBytes, sigBytes, extendedKey[0]); + 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 + this.actionObject.signatureResult = sigHex; + + // action finished + this.actionObject.actionStatus = NfcActionStatus.finished; + } + // SATODEBUG public void onDisconnected() { //NfcCardService.isConnected.postValue(false) @@ -231,33 +310,15 @@ public synchronized List getXpubs(@NonNull Network network, @NonNull Lis Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getXpubs paths: " + paths); Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getXpubs HardwareWalletInteraction: " + hwInteraction); - // debug try { this.actionObject.actionStatus = NfcActionStatus.busy; - this.actionObject.actionType = NfcActionType.getXpub; + this.actionObject.actionType = NfcActionType.getXpubs; this.actionObject.networkParam = network; this.actionObject.pathsParam = paths; -// NfcCardManager cardManager = new NfcCardManager(); -// cardManager.setCardListener(this); -// cardManager.start(); -// Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() after 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 -// ); -// -// Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() after NfcAdapter.enableReaderMode"); // poll for result from cardListener onConnected -// Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() before SLEEP"); -// TimeUnit.SECONDS.sleep(10); -// Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() after SLEEP"); while (this.actionObject.actionStatus == NfcActionStatus.busy) { TimeUnit.MILLISECONDS.sleep(500); Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() SLEEP"); @@ -266,7 +327,6 @@ public synchronized List getXpubs(@NonNull Network network, @NonNull Lis final List xpubs = this.actionObject.xpubsResult; return xpubs; - } catch (Exception e) { Log.i("SatochipHWWallet", "getXpubs exception: " + e); } @@ -294,6 +354,7 @@ public synchronized List getXpubs(@NonNull Network network, @NonNull Lis 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); Log.i("SatochipHWWallet", "signMessage start useAeProtocol: " + useAeProtocol); Log.i("SatochipHWWallet", "signMessage start aeHostCommitment: " + aeHostCommitment); Log.i("SatochipHWWallet", "signMessage start aeHostEntropy: " + aeHostEntropy); @@ -303,8 +364,27 @@ public SignMessageResult signMessage(@NonNull List path, @NonNull Strin throw new RuntimeException("Hardware Wallet does not support the Anti-Exfil protocol"); } + try { + this.actionObject.actionStatus = NfcActionStatus.busy; + this.actionObject.actionType = NfcActionType.signMessage; + this.actionObject.pathParam = path; + this.actionObject.messageParam = message; + + // poll for result from cardListener onConnected + while (this.actionObject.actionStatus == NfcActionStatus.busy) { + TimeUnit.MILLISECONDS.sleep(500); + Log.i(TAG, "SATODEBUG SatochipHWWallet signMessage() SLEEP"); + } + + final String signatureResult = this.actionObject.signatureResult; + Log.i(TAG, "SATODEBUG SatochipHWWallet signMessage() signatureResult: " + signatureResult); + return new SignMessageResult(signatureResult, null); + } catch (Exception e) { + Log.e("SatochipHWWallet", "signMessage exception: " + e); + } + //TODO - String signature = "aabbccddee"; + String signature = "TODO-SATOCHIP-SIGN-MSG"; return new SignMessageResult(signature, null); } diff --git a/hardware/src/main/java/com/satochip/NfcActionObject.java b/hardware/src/main/java/com/satochip/NfcActionObject.java index 4c34becf4..b497b081f 100644 --- a/hardware/src/main/java/com/satochip/NfcActionObject.java +++ b/hardware/src/main/java/com/satochip/NfcActionObject.java @@ -1,6 +1,11 @@ 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; @@ -11,10 +16,15 @@ public class NfcActionObject { public NfcActionStatus actionStatus = NfcActionStatus.none; // getXpubs - // List getXpubs(@NonNull Network network, @NonNull List> paths, @Nullable HardwareWalletInteraction hwInteraction) { + // List getXpubs(@NonNull Network network, @NonNull List> paths, @Nullable HardwareWalletInteraction hwInteraction) public Network networkParam = null; public 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 = ""; } diff --git a/hardware/src/main/java/com/satochip/NfcActionResult.java b/hardware/src/main/java/com/satochip/NfcActionResult.java deleted file mode 100644 index 50f2d0885..000000000 --- a/hardware/src/main/java/com/satochip/NfcActionResult.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.satochip; - - -import java.util.List; - -public class NfcActionResult { - - public NfcActionType actionType = NfcActionType.none; - - public NfcActionStatus actionStatus = NfcActionStatus.none; - - public List xpubs; - - public NfcActionResult() { - - } - - - - - -} diff --git a/hardware/src/main/java/com/satochip/NfcActionType.java b/hardware/src/main/java/com/satochip/NfcActionType.java index 67564baaf..6159cc3d8 100644 --- a/hardware/src/main/java/com/satochip/NfcActionType.java +++ b/hardware/src/main/java/com/satochip/NfcActionType.java @@ -2,6 +2,7 @@ public enum NfcActionType { none, - getXpub, + getXpubs, + signMessage, derivePubkey, } diff --git a/hardware/src/main/java/com/satochip/SatochipParser.java b/hardware/src/main/java/com/satochip/SatochipParser.java index 3a24a851c..5039f40e6 100644 --- a/hardware/src/main/java/com/satochip/SatochipParser.java +++ b/hardware/src/main/java/com/satochip/SatochipParser.java @@ -629,8 +629,7 @@ public boolean verifySig(byte[] msg, byte[] dersig, byte[] pub) { 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); From 062218e4f42049432aa63b4a58becda9ca9bbc95 Mon Sep 17 00:00:00 2001 From: Toporin Date: Tue, 18 Feb 2025 15:45:57 +0100 Subject: [PATCH 07/25] Satochip integration: implement signTransaction() (wip) --- .../greenbits/wallets/SatochipHWWallet.java | 260 ++++++++++++------ .../java/com/satochip/NfcActionObject.java | 6 + .../main/java/com/satochip/NfcActionType.java | 2 +- 3 files changed, 188 insertions(+), 80 deletions(-) diff --git a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java index e0e59aa23..28aa96561 100644 --- a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java +++ b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java @@ -12,16 +12,9 @@ import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.concurrent.TimeUnit; -//import com.blockstream.common.devices.ApduException; -//import com.blockstream.common.devices.ApduResponse; -//import com.blockstream.common.devices.ApplicationStatus; -//import com.blockstream.common.devices.CardChannel; -//import com.blockstream.common.devices.CardListener; -//import com.blockstream.common.devices.DeviceModel; -//import com.blockstream.common.devices.NfcCardManager; -//import com.blockstream.common.devices.SatochipCommandSet; //todo switch to import com.blockstream.common.devices.DeviceModel; import com.blockstream.common.gdk.data.Account; import com.blockstream.common.gdk.data.Device; @@ -52,7 +45,6 @@ import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -68,10 +60,9 @@ public class SatochipHWWallet extends GdkHardwareWallet implements CardListener 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 Map mServiceXPubs = new HashMap<>(); -// private final Map mRecoveryXPubs = new HashMap<>(); - private final Map mPrevTxs = new HashMap<>(); private final Device device; private final DeviceModel model; @@ -155,17 +146,14 @@ public void onConnected(CardChannel channel) { onConnectedGetXpubs(cmdSet); } else if (this.actionObject.actionType == NfcActionType.signMessage) { onConnectedSignMessage(cmdSet); + } else if (this.actionObject.actionType == NfcActionType.signTransaction){ + onConnectedSignTransaction(cmdSet); } - // TODO: disconnect? Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() trigger disconnection!"); onDisconnected(); - // stop polling? check if actionStatus is not busy, or finished? - //nfcAdapter.disableReaderMode(activity); - - } catch (ApduException e) { //throw new RuntimeException(e); Log.e(TAG, "SATODEBUG onConnected: A an exception has been thrown during card init: "+ e); @@ -296,13 +284,86 @@ public void onConnectedSignMessage(SatochipCommandSet cmdSet) throws Exception { this.actionObject.actionStatus = NfcActionStatus.finished; } + public void onConnectedSignTransaction(SatochipCommandSet cmdSet) throws Exception { + + // get paths, hashes & set sig result + final 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 optionFlags = (byte) 0x40; + byte[][] extendedKey = cmdSet.cardBip32GetExtendedKey(bip32path, optionFlags, null); + 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 card + 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; + } + // SATODEBUG public void onDisconnected() { //NfcCardService.isConnected.postValue(false) Log.i(TAG, "SATODEBUG SatochipHWWallet onDisconnected: Card disconnected!"); } - @NonNull @Override public synchronized List getXpubs(@NonNull Network network, @NonNull List> paths, @Nullable HardwareWalletInteraction hwInteraction) { @@ -310,14 +371,31 @@ public synchronized List getXpubs(@NonNull Network network, @NonNull Lis Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getXpubs paths: " + paths); Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getXpubs HardwareWalletInteraction: " + hwInteraction); + + // first step: check if xpubs are 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; + } + + // request to card if not cached already try { this.actionObject.actionStatus = NfcActionStatus.busy; this.actionObject.actionType = NfcActionType.getXpubs; this.actionObject.networkParam = network; this.actionObject.pathsParam = paths; - - // poll for result from cardListener onConnected while (this.actionObject.actionStatus == NfcActionStatus.busy) { TimeUnit.MILLISECONDS.sleep(500); @@ -332,21 +410,6 @@ public synchronized List getXpubs(@NonNull Network network, @NonNull Lis } return null; - -// final List xpubs = new ArrayList<>(paths.size()); -// -// // TODO -// xpubs.add("xpub6EPNojiyVmkzkEMmkwfjVc4YdyBXoQH8QwdifcPaKqVszSyg6oczSEgfP5AYUUs5hNG9kNfosAbTSLZqjEfMGdA85F7dx3kk6qDFP6va7mz"); -// if (paths.size()>=2) { -// xpubs.add("xpub69CqtDngRoBFv667kKodkCxvGegsJZwVUTmE8K46e3HKL5JVYL4ZpZZSBvosZPdgVmFxa3fFvkYxYfXBQyYTkME99wHhugHAARLECJe64Ee"); -// } -// if (paths.size()>=3) { -// xpubs.add("xpub69CqtDngRoBFv667kKodkCxvGegsJZwVUTmE8K46e3HKL5JVYL4ZpZZSBvosZPdgVmFxa3fFvkYxYfXBQyYTkME99wHhugHAARLECJe64Ee"); -// } -// -// Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() before RETURN"); -// -// return xpubs; } @NonNull @@ -392,51 +455,107 @@ public SignMessageResult signMessage(@NonNull List path, @NonNull Strin @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"); + Log.i("SatochipHWWallet", "signTransaction start network: " + network); + Log.i("SatochipHWWallet", "signTransaction start transaction: " + transaction); + Log.i("SatochipHWWallet", "signTransaction start inputs: " + inputs); + Log.i("SatochipHWWallet", "signTransaction start outputs: " + outputs); + Log.i("SatochipHWWallet", "signTransaction start transactions: " + transactions); + + final byte[] txBytes = Wally.hex_to_bytes(transaction); + Log.i("SatochipHWWallet", "signTransaction txBytes: " + Wally.hex_from_bytes(txBytes)); + if(network.isLiquid()){ throw new RuntimeException(network.getCanonicalName() + " is not supported"); } + try { if (useAeProtocol) { throw new RuntimeException("Hardware Wallet does not support the Anti-Exfil protocol"); } - return signTransactionImpl(network, hwInteraction, transaction, inputs, outputs, transactions); - } finally { - // Free all wally txs to ensure we don't leak any memory - for (Map.Entry entry : mPrevTxs.entrySet()) { - Wally.tx_free(entry.getValue()); + final Object wallyTx = Wally.tx_from_bytes(txBytes, Wally.WALLY_TX_FLAG_USE_WITNESS); + Log.i("SatochipHWWallet", "signTransaction wallyTx: " + wallyTx); + + boolean sw = false; + boolean p2sh = false; + for (final InputOutput in : inputs) { + Log.i("SatochipHWWallet", "signTransaction inputs[i]: " + in); + if (in.isSegwit()) { + sw = true; + } else { + p2sh = true; + } } - mPrevTxs.clear(); - } - } + Log.i("SatochipHWWallet", "signTransaction sw: " + sw); + Log.i("SatochipHWWallet", "signTransaction p2sh: " + p2sh); - private synchronized SignTransactionResult signTransactionImpl(final Network network, - @Nullable HardwareWalletInteraction hwInteraction, - final String transaction, - final List inputs, - final List outputs, - final Map transactions) - { - Log.i("SatochipHWWallet", "SignTransactionImpl start"); - final String[] signatures = new String[inputs.size()]; + // debug + for (final InputOutput out : outputs) { + Log.i("SatochipHWWallet", "signTransaction outputs[i]: " + out); + } - final byte[] txBytes = Wally.hex_to_bytes(transaction); - 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]; + //tx_get_btc_signature_hash(Object jarg1, long jarg2, byte[] jarg3, long jarg5, long jarg6, long jarg7, byte[] jarg8); + 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)); + //Log.i("SatochipHWWallet", "signTransaction() input hash[i]: " + hash.getClass().getName()); + hashesParam.add(hash_out); + + // derive key for path + List path = in.getUserPathAsInts(); + Log.i("SatochipHWWallet", "signTransaction() input path[i]: " + path); + pathsParam.add(in.getUserPathAsInts()); - final int txVersion = Wally.tx_get_version(wallytx); - final int txLocktime = Wally.tx_get_locktime(wallytx); + } - if (transactions != null) { - for (Map.Entry t : transactions.entrySet()) - mPrevTxs.put(t.getKey(), Wally.tx_from_hex(t.getValue(), Wally.WALLY_TX_FLAG_USE_WITNESS)); - } + // create action + this.actionObject.actionStatus = NfcActionStatus.busy; + this.actionObject.actionType = NfcActionType.signTransaction; + this.actionObject.pathsParam = pathsParam; + this.actionObject.hashesParam = hashesParam; - // todo - return new SignTransactionResult(Arrays.asList(signatures), null); + // poll for result from cardListener onConnected + while (this.actionObject.actionStatus == NfcActionStatus.busy) { + TimeUnit.MILLISECONDS.sleep(500); + Log.i(TAG, "SATODEBUG SatochipHWWallet signTransaction() SLEEP"); + } + + 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()); + } } + @NonNull @Override public synchronized String getMasterBlindingKey(@Nullable HardwareWalletInteraction hwInteraction) { @@ -458,22 +577,6 @@ public synchronized BlindingFactorsResult getBlindingFactors(final List getIntegerPath(final List unsigned) { - //return unsigned.stream().map(Long::intValue).collect(Collectors.toList()); - final List signed = new ArrayList<>(unsigned.size()); - for (final Long n : unsigned) { - signed.add(n.intValue()); - } - return signed; - } - - private static Integer unharden(final Integer i) { - return Integer.MIN_VALUE + i; - } - - @Override public synchronized String getGreenAddress(final Network network, final Account account, final List path, final long csvBlocks, HardwareWalletInteraction hwInteraction) { Log.i("SatochipHWWallet", "getGreenAddress start"); @@ -528,7 +631,6 @@ private Bip32Path pathToBip32Path(final List path) throws Exception { throw new Exception("Path too long"); } ByteArrayOutputStream result = new ByteArrayOutputStream(); - //result.write((byte)depth); for (final Integer element : path) { BufferUtils.writeUint32BE(result, element); } diff --git a/hardware/src/main/java/com/satochip/NfcActionObject.java b/hardware/src/main/java/com/satochip/NfcActionObject.java index b497b081f..000db2ce8 100644 --- a/hardware/src/main/java/com/satochip/NfcActionObject.java +++ b/hardware/src/main/java/com/satochip/NfcActionObject.java @@ -27,4 +27,10 @@ public class NfcActionObject { public String messageParam = ""; public String signatureResult = ""; + // signTransaction + //public List> pathsParam = new ArrayList<>(); + public List hashesParam = new ArrayList<>(); + public List signaturesResult = new ArrayList<>(); + + } diff --git a/hardware/src/main/java/com/satochip/NfcActionType.java b/hardware/src/main/java/com/satochip/NfcActionType.java index 6159cc3d8..3d14e950a 100644 --- a/hardware/src/main/java/com/satochip/NfcActionType.java +++ b/hardware/src/main/java/com/satochip/NfcActionType.java @@ -4,5 +4,5 @@ public enum NfcActionType { none, getXpubs, signMessage, - derivePubkey, + signTransaction, } From e2ed77a22c318a57d8dd1fb379df965e5c826b24 Mon Sep 17 00:00:00 2001 From: Toporin Date: Tue, 18 Feb 2025 16:01:33 +0100 Subject: [PATCH 08/25] remove old unused code --- .../common/devices/APDUCommand.java | 148 -------- .../common/devices/APDUException.java | 29 -- .../common/devices/APDUResponse.java | 220 ------------ .../common/devices/ApplicationStatus.java | 131 ------- .../common/devices/CardChannel.java | 25 -- .../common/devices/CardListener.java | 18 - .../common/devices/NfcCardChannel.java | 37 -- .../common/devices/NfcCardManager.java | 124 ------- .../common/devices/SatochipCommandSet.java | 328 ------------------ 9 files changed, 1060 deletions(-) delete mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/APDUCommand.java delete mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/APDUException.java delete mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/APDUResponse.java delete mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/ApplicationStatus.java delete mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/CardChannel.java delete mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/CardListener.java delete mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardChannel.java delete mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardManager.java delete mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.java diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/APDUCommand.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/APDUCommand.java deleted file mode 100644 index 0c35a4c4d..000000000 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/APDUCommand.java +++ /dev/null @@ -1,148 +0,0 @@ -//package com.blockstream.common.devices; -// -//import java.io.ByteArrayOutputStream; -//import java.io.IOException; -//import java.io.*; -// -///** -// * 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/common/src/androidMain/kotlin/com/blockstream/common/devices/APDUException.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/APDUException.java deleted file mode 100644 index deb0ab8a7..000000000 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/APDUException.java +++ /dev/null @@ -1,29 +0,0 @@ -//package com.blockstream.common.devices; -// -///** -// * 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/common/src/androidMain/kotlin/com/blockstream/common/devices/APDUResponse.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/APDUResponse.java deleted file mode 100644 index 9981417fb..000000000 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/APDUResponse.java +++ /dev/null @@ -1,220 +0,0 @@ -//package com.blockstream.common.devices; -// -//import java.io.*; -// -///** -// * 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/common/src/androidMain/kotlin/com/blockstream/common/devices/ApplicationStatus.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/ApplicationStatus.java deleted file mode 100644 index 3b9c787d3..000000000 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/ApplicationStatus.java +++ /dev/null @@ -1,131 +0,0 @@ -//package com.blockstream.common.devices; -// -///** -// * 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 -// * -// * @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/common/src/androidMain/kotlin/com/blockstream/common/devices/CardChannel.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/CardChannel.java deleted file mode 100644 index 7e9de7743..000000000 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/CardChannel.java +++ /dev/null @@ -1,25 +0,0 @@ -//package com.blockstream.common.devices; -// -//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(); -// -//} diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/CardListener.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/CardListener.java deleted file mode 100644 index d5bfbd7f2..000000000 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/CardListener.java +++ /dev/null @@ -1,18 +0,0 @@ -//package com.blockstream.common.devices; -// -///** -// * 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(); -//} \ No newline at end of file diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardChannel.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardChannel.java deleted file mode 100644 index aee5cb0c1..000000000 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardChannel.java +++ /dev/null @@ -1,37 +0,0 @@ -//package com.blockstream.common.devices; -// -//import android.nfc.tech.IsoDep; -//import android.util.Log; -////import org.satochip.io.APDUCommand; -////import org.satochip.io.APDUResponse; -////import org.satochip.io.CardChannel; -// -//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/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardManager.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardManager.java deleted file mode 100644 index e84312656..000000000 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardManager.java +++ /dev/null @@ -1,124 +0,0 @@ -//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 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/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.java b/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.java deleted file mode 100644 index d7c4b9fdc..000000000 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.java +++ /dev/null @@ -1,328 +0,0 @@ -//package com.blockstream.common.devices; -// -////import org.bitcoinj.core.Base58; -////import org.bitcoinj.core.Sha256Hash; -////import org.bouncycastle.crypto.digests.RIPEMD160Digest; -////import org.satochip.client.seedkeeper.*; -////import org.satochip.io.*; -////import org.bouncycastle.util.encoders.Hex; -// -////import static com.satochip.Constants.INS_GET_STATUS; -// -//import java.nio.ByteBuffer; -//import java.security.SecureRandom; -//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; -//import java.io.InputStream; -//import java.io.ByteArrayInputStream; -//import java.nio.charset.StandardCharsets; -//import java.security.cert.CertPathValidator; -//import java.security.cert.CertPath; -//import java.security.cert.CertificateFactory; -//import java.security.cert.Certificate; -//import java.security.cert.PKIXParameters; -//import java.security.KeyStore; -//import java.security.PublicKey; -// -// -// -// -///** -// * 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"); -// -// public final static byte INS_GET_STATUS = (byte) 0x3C; -// -// 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[] authentikey = null; -// private String authentikeyHex = null; -// private String defaultBip32path = null; -// private byte[] extendedKey = null; -// private byte[] extendedChaincode = null; -// private String extendedKeyHex = null; -// private byte[] extendedPrivKey = null; -// private String extendedPrivKeyHex = null; -// -// // Satodime, SeedKeeper or Satochip? -// private String cardType = null; -// private String certPem = null; // PEM certificate of device, if any -// -// // satodime -// // SatodimeStatus satodimeStatus = null; -// -// public static final byte[] SATOCHIP_AID = hexToBytes("5361746f43686970"); //SatoChip -// public static final byte[] SEEDKEEPER_AID = hexToBytes("536565644b6565706572"); //SeedKeeper -// public static final byte[] SATODIME_AID = hexToBytes("5361746f44696d65"); //SatoDime -// -// -// /** -// * 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(); -//// this.satodimeStatus = new SatodimeStatus(); -// 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 SatodimeStatus getSatodimeStatus() { -//// this.satodimeGetStatus(); -//// return this.satodimeStatus; -//// } -// -//// public byte[] getSatodimeUnlockSecret() { -//// return this.satodimeStatus.getUnlockSecret(); -//// } -//// -//// public void setSatodimeUnlockSecret(byte[] unlockSecret) { -//// this.satodimeStatus.setUnlockSecret(unlockSecret); -//// } -// -// /* s must be an even-length string. */ -// public static byte[] hexToBytes(String s) { -// int len = s.length(); -// byte[] data = new byte[len / 2]; -// for (int i = 0; i < len; i += 2) { -// data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) -// + Character.digit(s.charAt(i+1), 16)); -// } -// return data; -// } -// -// /**************************************** -// * AUTHENTIKEY * -// ****************************************/ -//// public byte[] getAuthentikey() { -//// if (authentikey == null) { -//// cardGetAuthentikey(); -//// } -//// return authentikey; -//// } -// -//// public String getAuthentikeyHex() { -//// if (authentikeyHex == null) { -//// cardGetAuthentikey(); -//// } -//// return authentikeyHex; -//// } -// -//// public byte[] getBip32Authentikey() { -//// if (authentikey == null) { -//// cardBip32GetAuthentikey(); -//// } -//// return authentikey; -//// } -// -//// public String getBip32AuthentikeyHex() { -//// if (authentikeyHex == null) { -//// cardBip32GetAuthentikey(); -//// } -//// return authentikeyHex; -//// } -// -// public List getPossibleAuthentikeys(){ -// return this.possibleAuthentikeys; -// } -// -//// public SatochipParser getParser() { -//// return parser; -//// } -// -// public void setDefaultBip32path(String bip32path) { -// defaultBip32path = bip32path; -// } -// -// /** -// * Set the SecureChannel object -// * -// * param secureChannel secure channel -// */ -//// protected void setSecureChannel(SecureChannelSession secureChannel) { -//// this.secureChannel = secureChannel; -//// } -// -// -// -// 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/Satodime/SeedKeeper 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() throws IOException { -// -// ApduResponse rapdu = cardSelect("satochip"); -// if (rapdu.getSw() != 0x9000) { -// rapdu = cardSelect("seedkeeper"); -// if (rapdu.getSw() != 0x9000) { -// rapdu = cardSelect("satodime"); -// if (rapdu.getSw() != 0x9000) { -// this.cardType = "unknown"; -// logger.warning("SATOCHIPLIB: CardSelect: could not select a known applet"); -// } -// } -// } -// -// return rapdu; -// } -// -// public ApduResponse cardSelect(String cardType) throws IOException { -// -// ApduCommand selectApplet; -// if (cardType.equals("satochip")) { -// selectApplet = new ApduCommand(0x00, 0xA4, 0x04, 0x00, SATOCHIP_AID); -// } else if (cardType.equals("seedkeeper")) { -// selectApplet = new ApduCommand(0x00, 0xA4, 0x04, 0x00, SEEDKEEPER_AID); -// } else { -// selectApplet = new ApduCommand(0x00, 0xA4, 0x04, 0x00, SATODIME_AID); -// } -// -// 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; -// } -// -//} From 5f1d3c75b47552cc9e91a4a18d4a25bb3c3f682b Mon Sep 17 00:00:00 2001 From: Toporin Date: Thu, 20 Feb 2025 11:10:15 +0100 Subject: [PATCH 09/25] Add PIN screen for satochip --- .../common/models/GreenViewModel.kt | 20 ++++++- .../models/devices/AbstractDeviceViewModel.kt | 6 ++ .../models/devices/DeviceInfoViewModel.kt | 15 ++++- .../DeviceConnectionManagerAndroid.kt | 14 +++-- .../managers/DeviceConnectionManager.kt | 2 + .../blockstream/compose/utils/SideEffects.kt | 21 ++++++- .../greenbits/wallets/SatochipHWWallet.java | 56 +++++++++++++++---- .../java/com/satochip/SatochipCommandSet.java | 8 +-- .../java/com/satochip/SatochipException.java | 43 ++++++++++++++ 9 files changed, 158 insertions(+), 27 deletions(-) create mode 100644 hardware/src/main/java/com/satochip/SatochipException.java 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 9cbc10ab7..6bb2f747a 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,9 @@ 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 @@ -338,6 +340,7 @@ open class GreenViewModel constructor( } viewModelScope.coroutineScope.launch { + logger.d { "postSideEffect: launch/send $sideEffect" } _sideEffect.send(sideEffect) } } @@ -505,13 +508,17 @@ open class GreenViewModel constructor( countly.renameWallet() }) } + is Events.DeviceRequestResponse -> { + println("SATODEBUG GreenViewModel handleEvent() DeviceRequestResponse event: $event, deviceRequest: $_deviceRequest") if(event.data == null){ _deviceRequest?.completeExceptionally(Exception("id_action_canceled")) }else{ _deviceRequest?.complete(event.data) } + println("SATODEBUG After completing deviceRequest: $_deviceRequest, isCompleted: ${_deviceRequest?.isCompleted}") } + is Events.SelectDenomination -> { viewModelScope.coroutineScope.launch { denominatedValue()?.also { @@ -872,10 +879,17 @@ open class GreenViewModel constructor( } final override fun requestPassphrase(deviceBrand: DeviceBrand?): String { + println("SATODEBUG GreenViewModel requestPassphrase start") return CompletableDeferred().let { + println("SATODEBUG GreenViewModel requestPassphrase continue") _deviceRequest = it + println("SATODEBUG _deviceRequest before posting side effect: $_deviceRequest") postSideEffect(SideEffects.DeviceRequestPassphrase) - runBlocking { it.await() } + println("SATODEBUG _deviceRequest after posting side effect: $_deviceRequest") + runBlocking { + println("SATODEBUG GreenViewModel requestPassphrase runblocking") + it.await() + } } } @@ -1035,5 +1049,7 @@ open class GreenViewModel constructor( companion object: Loggable(){ fun preview() = object : GreenViewModel() { } + private var _deviceRequest: CompletableDeferred? = null //satodebug } + } diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/AbstractDeviceViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/AbstractDeviceViewModel.kt index a0fbcaa80..874845467 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/AbstractDeviceViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/AbstractDeviceViewModel.kt @@ -218,12 +218,17 @@ abstract class AbstractDeviceViewModel constructor( } override fun requestPinBlocking(deviceBrand: DeviceBrand): String { + logger.i { "AbstractDeviceViewModel requestPinBlocking deviceBrand: ${deviceBrand}" } postSideEffect(LocalSideEffects.RequestPin(deviceBrand)) return CompletableDeferred().also { + logger.i { "AbstractDeviceViewModel requestPinBlocking requestPinEmitter before" } requestPinEmitter = it + logger.i { "AbstractDeviceViewModel requestPinBlocking requestPinEmitter after" } }.let { + logger.i { "AbstractDeviceViewModel requestPinBlocking runBlocking before" } runBlocking { it.await() } + } } @@ -234,6 +239,7 @@ abstract class AbstractDeviceViewModel constructor( override fun requestNetwork(): Network? = greenWalletOrNull?.let { if (it.isMainnet) gdk.networks().bitcoinElectrum else gdk.networks().testnetBitcoinElectrum } ?: if (settingsManager.appSettings.testnet) { + logger.i { "AbstractDeviceViewModel requestNetwork " } requestNetworkEmitter = CompletableDeferred() postSideEffect(SideEffects.SelectEnvironment) runBlocking { requestNetworkEmitter!!.await() } diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceInfoViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceInfoViewModel.kt index dfc42a095..44e55e9f2 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceInfoViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceInfoViewModel.kt @@ -167,10 +167,12 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs } private fun authenticateAndContinue(updateFirmwareFromChannel: String? = null) { - println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() start") + println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() start updateFirmwareFromChannel: "+ updateFirmwareFromChannel) + println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() device.gdkHardwareWallet: " + device.gdkHardwareWallet) val gdkHardwareWallet = device.gdkHardwareWallet ?: return doAsync({ + println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() doAsync") // Authenticate device if needed deviceConnectionManager.authenticateDeviceIfNeeded( gdkHardwareWallet = gdkHardwareWallet, @@ -187,7 +189,10 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs ) val network = device.getOperatingNetwork(device, gdk, interaction = this)!! + println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() network: " + network) + val isEphemeral = !settingsManager.appSettings.rememberHardwareDevices + println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() isEphemeral: " + isEphemeral) val previousSession = (if (device.isLedger) { sessionManager.getDeviceSessionForNetworkAllPolicies(device, network, isEphemeral) @@ -198,6 +203,7 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs isEphemeral ) }) + println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() previousSession: " + previousSession) if (previousSession != null) { // Session already setup @@ -210,10 +216,14 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs // Disconnect any previous hww connection it.disconnect() } + println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() session: " + session) val walletHashId = getWalletHashId(session, network, device) + println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() walletHashId: " + walletHashId) + // Disable Jade wallet fingerprint, keep the device name // getWalletName(session, network, device) val walletName = device.name + println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() walletName: " + walletName) val wallet: GreenWallet if (isEphemeral) { @@ -265,6 +275,7 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs ).toSet().toList() // Make it unique wallet.deviceIdentifiers = combinedLoginCredentials + println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() wallet.deviceIdentifiers: " + wallet.deviceIdentifiers) if (isNewWallet) { database.insertWallet(wallet) @@ -273,6 +284,7 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs } session = sessionManager.getWalletSessionOrCreate(wallet) + println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() session: " + session) countly.importWallet(session) } @@ -281,6 +293,7 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs }, postAction = { onProgress.value = it == null }, onSuccess = { + println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() onSuccess: ") disconnectDeviceOnCleared = false deviceManager.savedDevice = device 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 b7d8b8e21..98056a583 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt @@ -8,8 +8,6 @@ import com.blockstream.common.devices.AndroidDevice import com.blockstream.common.devices.CardChannel import com.blockstream.common.devices.CardListener import com.blockstream.common.devices.DeviceBrand -import com.blockstream.common.devices.DeviceManagerAndroid -import com.blockstream.common.devices.DeviceManagerAndroid.Companion import com.blockstream.common.devices.GreenDevice import com.blockstream.common.devices.NfcCardManager import com.blockstream.common.devices.SatochipCommandSet @@ -59,9 +57,11 @@ class DeviceConnectionManagerAndroid constructor( scope = scope ) { + // Satochip private var nfcAdapter: NfcAdapter? = null private var activity: Activity? = null private var satochipDevice: SatochipDevice? = null + private var satochipInteraction: HardwareConnectInteraction? = null override suspend fun connectDevice( device: GreenDevice, @@ -169,6 +169,7 @@ class DeviceConnectionManagerAndroid constructor( logger.i {"SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() start device: $device"} satochipDevice = device + satochipInteraction = interaction val cardManager = NfcCardManager() cardManager.setCardListener(this) @@ -212,8 +213,13 @@ class DeviceConnectionManagerAndroid constructor( // SATODEBUG override fun onConnected(channel: CardChannel) { - println("SATODEBUG DeviceConnectionManagerAndroid onConnected: Card is connected") + + //val pin: String? = satochipInteraction?.requestPinBlocking(DeviceBrand.Satochip) + //val pin: String? = satochipInteraction?.requestPinMatrix(DeviceBrand.Satochip) + val pin: String? = satochipInteraction?.requestPassphrase(DeviceBrand.Satochip) + println("SATODEBUG DeviceConnectionManagerAndroid onConnected: PIN: " + pin) + try { val cmdSet = SatochipCommandSet(channel) // start to interact with card @@ -246,7 +252,7 @@ class DeviceConnectionManagerAndroid constructor( // provide channel for later request? // todo: provide activity and context instead?? - satochipDevice?.gdkHardwareWallet = SatochipHWWallet(satoDevice, activity, satochipDevice?.context) + satochipDevice?.gdkHardwareWallet = SatochipHWWallet(satoDevice, pin, activity, satochipDevice?.context) } catch (e: Exception) { println("SATODEBUG DeviceConnectionManagerAndroid onConnected: an exception has been thrown during card init.") diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManager.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManager.kt index 573031490..5e46e6e48 100644 --- a/compose/src/commonMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManager.kt +++ b/compose/src/commonMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManager.kt @@ -105,6 +105,7 @@ open class DeviceConnectionManager( gdkHardwareWallet: GdkHardwareWallet, jadeFirmwareManager: JadeFirmwareManager? ) { + println("SATODEBUG DeviceConnectionManager authenticateDeviceIfNeeded() start") if (gdkHardwareWallet is JadeHWWallet && gdkHardwareWallet.getVersionInfo().jadeState != JadeState.READY) { try { gdkHardwareWallet.authenticate(interaction, jadeFirmwareManager ?: JadeFirmwareManager( @@ -139,5 +140,6 @@ open class DeviceConnectionManager( // force update if needed jadeFirmwareManager.checkFirmware(jade = gdkHardwareWallet.jade) } + println("SATODEBUG DeviceConnectionManager authenticateDeviceIfNeeded() end") } } \ No newline at end of file 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 00ec9aaa1..f7bf3769d 100644 --- a/compose/src/commonMain/kotlin/com/blockstream/compose/utils/SideEffects.kt +++ b/compose/src/commonMain/kotlin/com/blockstream/compose/utils/SideEffects.kt @@ -197,8 +197,10 @@ fun HandleSideEffect( val drawer = LocalDrawer.current val scope = rememberCoroutineScope() val biometricsState = LocalBiometricState.current - var twoFactorResolverData by remember { mutableStateOf(null) } + + println("SATODEBUG SideEffects HandleSideEffect() START") + twoFactorResolverData?.also { resolverData -> resolverData.methods?.also { methods -> SingleChoiceDialog( @@ -259,6 +261,7 @@ fun HandleSideEffect( // Device Passphrase PassphraseBottomSheet.getResult { + println("SATODEBUG SideEffects HandleSideEffect() PassphraseBottomSheet.getResult: " + it) viewModel.postEvent(Events.DeviceRequestResponse(it)) } @@ -272,9 +275,14 @@ fun HandleSideEffect( // Handle sideEffect only on resumed state LaunchedEffect(state) { + + println("SATODEBUG SideEffects HandleSideEffect() LaunchedEffect(state) state: " + state) + if(state != Lifecycle.State.RESUMED) return@LaunchedEffect viewModel.sideEffect.onEach { + + println("SATODEBUG SideEffects HandleSideEffect() LaunchedEffect(state) onEach sideEffect: " + it) handler.invoke(this, it) when (it) { @@ -546,7 +554,16 @@ fun HandleSideEffect( } is SideEffects.DeviceRequestPassphrase -> { - bottomSheetNavigator?.show(PassphraseBottomSheet) + + println("SATODEBUG HandleSideEffect received DeviceRequestPassphrase") + bottomSheetNavigator?.also { + println("SATODEBUG bottomSheetNavigator is not null, showing sheet") + it.show(PassphraseBottomSheet) + } ?: run { + println("SATODEBUG bottomSheetNavigator is null!") + } + + //bottomSheetNavigator?.show(PassphraseBottomSheet) } is SideEffects.DeviceRequestPin -> { diff --git a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java index 28aa96561..728b54928 100644 --- a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java +++ b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java @@ -15,6 +15,7 @@ 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; @@ -33,6 +34,7 @@ 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; @@ -40,7 +42,10 @@ 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; @@ -68,6 +73,7 @@ public class SatochipHWWallet extends GdkHardwareWallet implements CardListener 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; @@ -79,18 +85,16 @@ public class SatochipHWWallet extends GdkHardwareWallet implements CardListener private NfcActionObject actionObject = new NfcActionObject(); - public SatochipHWWallet(Device device, Activity activity, Context context){ + public SatochipHWWallet(Device device, String pin, Activity activity, Context context){ Log.i(TAG, "constructor start"); //mTrezor = t; this.device = device; //String model = "Satochip"; this.model = DeviceModel.SatochipGeneric; - + this.pin = pin; this.activity = activity; this.context = context; - //this.cmdSet = new SatochipCommandSet(channel); - NfcCardManager cardManager = new NfcCardManager(); cardManager.setCardListener(this); cardManager.start(); @@ -105,8 +109,6 @@ public SatochipHWWallet(Device device, Activity activity, Context context){ ); Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() after NfcAdapter.enableReaderMode"); - - } @Override @@ -120,6 +122,13 @@ public void onConnected(CardChannel channel) { Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() Card is connected"); try { + + if (this.actionObject.actionType == NfcActionType.none) { + Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() nothing to do => disconnection!"); + onDisconnected(); + return; + } + SatochipCommandSet cmdSet = new SatochipCommandSet(channel); Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() cmdSet created"); // start to interact with card @@ -135,11 +144,16 @@ public void onConnected(CardChannel channel) { // verify PIN - String pinStr = "123456"; //todo - byte[] pinBytes = pinStr.getBytes(Charset.forName("UTF-8")); - ApduResponse rapduPin = cmdSet.cardVerifyPIN(pinBytes); - Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() pin verified: " + rapduPin.getSw()); - Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() pin verified: " + rapduPin.toHexString()); + if (this.pin != null) { + //String pinStr = "123456"; //todo + byte[] pinBytes = this.pin.getBytes(Charset.forName("UTF-8")); + ApduResponse rapduPin = cmdSet.cardVerifyPIN(pinBytes); + Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() pin verified: " + rapduPin.getSw()); + Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() pin verified: " + rapduPin.toHexString()); + // todo: wrong pin/blockecd pin... + } else { + throw new SatochipException(SatochipException.ExceptionReason.PIN_UNDEFINED); + } // execute commands depending on actionType if (this.actionObject.actionType == NfcActionType.getXpubs){ @@ -154,6 +168,26 @@ public void onConnected(CardChannel channel) { Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() trigger disconnection!"); onDisconnected(); + } catch (SatochipException e) { + this.pin = null; + Log.e(TAG, "SATODEBUG onConnected: ERROR: "+ e); + onDisconnected(); + throw new RuntimeException(e); // todo: msg user instead!! + } catch (WrongPINException e) { + this.pin = null; + Log.e(TAG, "SATODEBUG onConnected: WRONG PIN! "+ e); + onDisconnected(); + throw new RuntimeException(e); // todo: msg user instead!! + } catch (WrongPINLegacyException e) { + this.pin = null; + Log.e(TAG, "SATODEBUG onConnected: WRONG PIN LEGACY! "+ e); + onDisconnected(); + throw new RuntimeException(e); // todo: msg user instead!! + } catch (BlockedPINException e) { + this.pin = null; + Log.e(TAG, "SATODEBUG onConnected: A an exception has been thrown during card init: "+ e); + onDisconnected(); + throw new RuntimeException(e); // todo: msg user instead!! } catch (ApduException e) { //throw new RuntimeException(e); Log.e(TAG, "SATODEBUG onConnected: A an exception has been thrown during card init: "+ e); diff --git a/hardware/src/main/java/com/satochip/SatochipCommandSet.java b/hardware/src/main/java/com/satochip/SatochipCommandSet.java index fb9c90e40..5fa296d2a 100644 --- a/hardware/src/main/java/com/satochip/SatochipCommandSet.java +++ b/hardware/src/main/java/com/satochip/SatochipCommandSet.java @@ -491,13 +491,7 @@ public ApduResponse cardVerifyPIN(byte[] pin) throws Exception { this.pin0 = mypin; // cache new pin return rapdu; - } catch (WrongPINException e) { - this.pin0 = null; - throw e; - } catch (WrongPINLegacyException e) { - this.pin0 = null; - throw e; - } catch (BlockedPINException e) { + } catch (WrongPINException | WrongPINLegacyException | BlockedPINException e) { this.pin0 = null; throw e; } catch (ApduException e){ 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(); + } + + +} From e6d49e27ace87e1e7ba9e367030aa035008dbc0c Mon Sep 17 00:00:00 2001 From: Toporin Date: Fri, 21 Feb 2025 10:47:05 +0100 Subject: [PATCH 10/25] Add more debug logs (wip) --- .../com/blockstream/common/gdk/GdkSession.kt | 5 ++ .../models/devices/DeviceScanViewModel.kt | 25 +++++++++- .../compose/devices/SatochipDevice.kt | 41 +++++++++-------- .../DeviceConnectionManagerAndroid.kt | 46 +++++++------------ .../managers/DeviceConnectionManager.kt | 1 + .../greenbits/wallets/SatochipHWWallet.java | 16 +++---- 6 files changed, 74 insertions(+), 60 deletions(-) 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 2ceb707cd..f12826b4b 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt @@ -1760,6 +1760,7 @@ class GdkSession constructor( connectionParams = createConnectionParams(network), loginCredentialsParams = loginCredentialsParams?.takeIf { !it.mnemonic.isNullOrBlank() } ?: (gdkHwWallet ?: this.gdkHwWallet)?.let { + println("SATODEBUG GdkSession getWalletIdentifier ") LoginCredentialsParams( masterXpub = it.getXpubs(network, listOf(listOf()), hwInteraction).first() ) @@ -2760,6 +2761,10 @@ class GdkSession constructor( suspend fun signTransaction(network: Network, createTransaction: CreateTransaction): CreateTransaction = if(network.isLightning){ createTransaction // no need to sign on gdk side }else{ + logger.d { "SATODEBUG GdkSession signTransaction() network: ${network}" } + logger.d { "SATODEBUG GdkSession signTransaction() createTransaction: ${createTransaction}" } + logger.d { "SATODEBUG GdkSession signTransaction() createTransaction.jsonElement: ${createTransaction.jsonElement}" } + authHandler( network, gdk.signTransaction(gdkSession(network), createTransaction = createTransaction.jsonElement!!) diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceScanViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceScanViewModel.kt index bd0d57b6a..228237585 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceScanViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceScanViewModel.kt @@ -43,9 +43,14 @@ class DeviceScanViewModel(greenWallet: GreenWallet) : } init { + println("SATODEBUG DeviceScanViewModel init") + println("SATODEBUG DeviceScanViewModel init session: $session") + println("SATODEBUG DeviceScanViewModel init session.device: ${session.device}") + println("SATODEBUG DeviceScanViewModel init session.isConnected: ${session.isConnected}") session.device.takeIf { session.isConnected }?.also { device -> deviceManager.savedDevice = device + println("SATODEBUG DeviceScanViewModel init deviceManager.savedDevice: ${deviceManager.savedDevice}") postSideEffect( SideEffects.NavigateTo( @@ -55,24 +60,29 @@ class DeviceScanViewModel(greenWallet: GreenWallet) : ) ) ) + println("SATODEBUG DeviceScanViewModel init after postSideEffect") } ?: run { combine(deviceFlow, deviceManager.devices) { _, devices -> - + println("SATODEBUG DeviceScanViewModel init run combine devices: $devices") if(deviceFlow.value == null) { var foundDevice = devices.firstOrNull { device -> + println("SATODEBUG DeviceScanViewModel init run combine step1 device: $device") greenWallet.deviceIdentifiers?.any { it.uniqueIdentifier == device.uniqueIdentifier } == true } + println("SATODEBUG DeviceScanViewModel init run combine step2 foundDevice: $foundDevice") // Find a BLE device or request a usb authentication foundDevice = foundDevice ?: devices.firstOrNull { it.needsUsbPermissionsToIdentify() } + println("SATODEBUG DeviceScanViewModel init run combine step3 foundDevice: $foundDevice") // TODO if device is disconnected, do not rescan it!!!! foundDevice?.also { + println("SATODEBUG DeviceScanViewModel init run combine step4 foundDevice: $foundDevice") selectDevice(it) } } @@ -93,13 +103,15 @@ class DeviceScanViewModel(greenWallet: GreenWallet) : } private fun selectDevice(device: GreenDevice) { + println("SATODEBUG DeviceScanViewModel selectDevice() device: $device") _deviceFlow.value = device if (device.hasPermissions()) { doAsync({ - + println("SATODEBUG DeviceScanViewModel selectDevice() doAsync START") if (device.gdkHardwareWallet == null) { + println("SATODEBUG DeviceScanViewModel selectDevice() doAsync device.gdkHardwareWallet is NULL") session.disconnect() deviceConnectionManager.connectDevice( device, @@ -109,12 +121,16 @@ class DeviceScanViewModel(greenWallet: GreenWallet) : countly.hardwareConnect(device) } + println("SATODEBUG DeviceScanViewModel selectDevice() doAsync device.gdkHardwareWallet: ${device.gdkHardwareWallet}") val gdkHardwareWallet = device.gdkHardwareWallet ?: throw Exception("Not HWWallet initiated") + println("SATODEBUG DeviceScanViewModel selectDevice() doAsync BEFORE deviceConnectionManager.authenticateDeviceIfNeeded()") deviceConnectionManager.authenticateDeviceIfNeeded(httpRequestHandler = sessionManager.httpRequestHandler, interaction = this, gdkHardwareWallet = gdkHardwareWallet) + println("SATODEBUG DeviceScanViewModel selectDevice() doAsync AFTER deviceConnectionManager.authenticateDeviceIfNeeded()") val network = device.getOperatingNetworkForEnviroment(device, gdk, greenWallet.isTestnet) ?: throw Exception("No network is available") + println("SATODEBUG DeviceScanViewModel selectDevice() doAsync network: $network") if(greenWallet.isTestnet != network.isTestnet){ throw Exception("The device is operating on the wrong Environment") @@ -126,6 +142,7 @@ class DeviceScanViewModel(greenWallet: GreenWallet) : } val walletHashId = getWalletHashId(session, network, device) + println("SATODEBUG DeviceScanViewModel selectDevice() doAsync walletHashId: $walletHashId") if (greenWallet.xPubHashId.isNotBlank() && greenWallet.xPubHashId != walletHashId) { // Disconnect only if there are no other connected sessions @@ -174,15 +191,18 @@ class DeviceScanViewModel(greenWallet: GreenWallet) : greenWallet to device }, onError = { + println("SATODEBUG DeviceScanViewModel selectDevice() onError : ${it}") if (it !is ConnectionLostException) { postSideEffect(SideEffects.ErrorSnackbar(it)) } _deviceFlow.value = null }, onSuccess = { + println("SATODEBUG DeviceScanViewModel selectDevice() onSuccess : ${it}") disconnectDeviceOnCleared = false deviceManager.savedDevice = it.second + println("SATODEBUG DeviceScanViewModel selectDevice() onSuccess BEFORE postSideEffect NavigateTo(NavigateDestinations.Login)") postSideEffect( SideEffects.NavigateTo( NavigateDestinations.Login( @@ -192,6 +212,7 @@ class DeviceScanViewModel(greenWallet: GreenWallet) : ) ) countly.hardwareConnected(device) + println("SATODEBUG DeviceScanViewModel selectDevice() onSuccess END") }) } else { askForPermission(device) diff --git a/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt b/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt index 4a5e97f7c..4755ef56d 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt @@ -47,28 +47,28 @@ class SatochipDevice constructor( //const val VENDOR_TREZOR = 0x534c //const val VENDOR_TREZOR_V2 = 0x1209 - private fun hasSuportedVendorId(usbDevice: UsbDevice): Boolean { - return false -// val vId = usbDevice.vendorId -// return (vId == VENDOR_TREZOR || -// vId == VENDOR_TREZOR_V2) - } +// private fun hasSuportedVendorId(usbDevice: UsbDevice): Boolean { +// return false +//// val vId = usbDevice.vendorId +//// return (vId == VENDOR_TREZOR || +//// vId == VENDOR_TREZOR_V2) +// } // TODO remove - fun fromUsbDevice( - deviceManager: DeviceManagerAndroid, - usbDevice: UsbDevice - ): SatochipDevice? { -// if (hasSuportedVendorId(usbDevice)) { -// return TrezorDevice( -// context = deviceManager.context, -// deviceManager = deviceManager, -// type = ConnectionType.USB, -// usbDevice = usbDevice, -// ) -// } - return null - } +// fun fromUsbDevice( +// deviceManager: DeviceManagerAndroid, +// usbDevice: UsbDevice +// ): SatochipDevice? { +//// if (hasSuportedVendorId(usbDevice)) { +//// return TrezorDevice( +//// context = deviceManager.context, +//// deviceManager = deviceManager, +//// type = ConnectionType.USB, +//// usbDevice = usbDevice, +//// ) +//// } +// return null +// } fun fromNfcDevice( deviceManager: DeviceManagerAndroid, @@ -76,6 +76,7 @@ class SatochipDevice constructor( // TODO add params? // add AID? ): SatochipDevice? { + println("SATODEBUG SatochipDevice fromNfcDevice()") // todo check AID return SatochipDevice( context = deviceManager.context, 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 98056a583..0df867177 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt @@ -174,7 +174,7 @@ class DeviceConnectionManagerAndroid constructor( val cardManager = NfcCardManager() cardManager.setCardListener(this) cardManager.start() - logger.i { "SATODEBUG DeviceConnectionManagerAndroid scanNfcDevices() after cardManager start" } + logger.i { "SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() after cardManager start" } activity = device.activityProvider?.getCurrentActivity() nfcAdapter = NfcAdapter.getDefaultAdapter(device.context) @@ -187,38 +187,24 @@ class DeviceConnectionManagerAndroid constructor( logger.i { "SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() end" } - -// val satochipDevice = com.blockstream.common.gdk.data.Device( -// name = "Satochip", -// supportsArbitraryScripts = false, -// supportsLowR = false, -// supportsHostUnblinding = false, -// supportsExternalBlinding = false, -// supportsLiquid = DeviceSupportsLiquid.None, -// supportsAntiExfilProtocol = DeviceSupportsAntiExfilProtocol.None -// ) -// -// device.gdkHardwareWallet = SatochipHWWallet(satochipDevice) - - //return onSatochipConnected(device) return ConnectionResult() } - private fun onSatochipConnected(device: AndroidDevice): ConnectionResult { - logger.d { "SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() start" } - logger.d { "Creating Satochip HW wallet" } - - return ConnectionResult() - } +// private fun onSatochipConnected(device: AndroidDevice): ConnectionResult { +// logger.d { "SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() start" } +// logger.d { "Creating Satochip HW wallet" } +// +// return ConnectionResult() +// } // SATODEBUG override fun onConnected(channel: CardChannel) { - println("SATODEBUG DeviceConnectionManagerAndroid onConnected: Card is connected") + println("SATODEBUG DeviceConnectionManagerAndroid onConnected(): Card is connected") //val pin: String? = satochipInteraction?.requestPinBlocking(DeviceBrand.Satochip) //val pin: String? = satochipInteraction?.requestPinMatrix(DeviceBrand.Satochip) val pin: String? = satochipInteraction?.requestPassphrase(DeviceBrand.Satochip) - println("SATODEBUG DeviceConnectionManagerAndroid onConnected: PIN: " + pin) + println("SATODEBUG DeviceConnectionManagerAndroid onConnected(): PIN: " + pin) try { val cmdSet = SatochipCommandSet(channel) @@ -229,16 +215,15 @@ class DeviceConnectionManagerAndroid constructor( // cardStatus val rapduStatus = cmdSet.cardGetStatus()//To update status if it's not the first reading val cardStatus = cmdSet.getApplicationStatus() //applicationStatus ?: return - println("SATODEBUG DeviceConnectionManagerAndroid readCard cardStatus: $cardStatus") - println("SATODEBUG DeviceConnectionManagerAndroid readCard cardStatus: ${cardStatus.toString()}") + println("SATODEBUG DeviceConnectionManagerAndroid onConnected() cardStatus: $cardStatus") // TODO: disconnect? - println("SATODEBUG DeviceConnectionManagerAndroid onConnected: trigger disconnection!") + println("SATODEBUG DeviceConnectionManagerAndroid onConnected() trigger disconnection!") onDisconnected() // stop polling? nfcAdapter?.disableReaderMode(activity) - println("SATODEBUG DeviceConnectionManagerAndroid NFC DISABLED") + println("SATODEBUG DeviceConnectionManagerAndroid onConnected() NFC DISABLED") val satoDevice = com.blockstream.common.gdk.data.Device( name = "Satochip", @@ -250,12 +235,13 @@ class DeviceConnectionManagerAndroid constructor( supportsAntiExfilProtocol = DeviceSupportsAntiExfilProtocol.None ) - // provide channel for later request? - // todo: provide activity and context instead?? + println("SATODEBUG DeviceConnectionManagerAndroid onConnected() creating gdkHardwareWallet") + // provide activity and context needed for NFC satochipDevice?.gdkHardwareWallet = SatochipHWWallet(satoDevice, pin, activity, satochipDevice?.context) + println("SATODEBUG DeviceConnectionManagerAndroid onConnected() created gdkHardwareWallet!") } catch (e: Exception) { - println("SATODEBUG DeviceConnectionManagerAndroid onConnected: an exception has been thrown during card init.") + println("SATODEBUG DeviceConnectionManagerAndroid onConnected() exception: $e") //Log.e(TAG, Log.getStackTraceString(e)) onDisconnected() } diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManager.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManager.kt index 5e46e6e48..041f7c934 100644 --- a/compose/src/commonMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManager.kt +++ b/compose/src/commonMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManager.kt @@ -28,6 +28,7 @@ open class DeviceConnectionManager( @Throws(Exception::class) override suspend fun connectDevice(device: GreenDevice, httpRequestHandler: HttpRequestHandler, interaction: HardwareConnectInteraction): ConnectionResult { + println("SATODEBUG DeviceConnectionManager connectDevice() device: $device") device.frozeHeartbeat() try { diff --git a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java index 728b54928..c41361031 100644 --- a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java +++ b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java @@ -86,7 +86,7 @@ public class SatochipHWWallet extends GdkHardwareWallet implements CardListener public SatochipHWWallet(Device device, String pin, Activity activity, Context context){ - Log.i(TAG, "constructor start"); + Log.i(TAG, "SATODEBUG SatochipHWWallet constructor start"); //mTrezor = t; this.device = device; //String model = "Satochip"; @@ -98,7 +98,7 @@ public SatochipHWWallet(Device device, String pin, Activity activity, Context c NfcCardManager cardManager = new NfcCardManager(); cardManager.setCardListener(this); cardManager.start(); - Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() after cardManager start"); + Log.i(TAG, "SATODEBUG SatochipHWWallet constructor after cardManager start"); nfcAdapter = NfcAdapter.getDefaultAdapter(this.context); nfcAdapter.enableReaderMode( @@ -107,13 +107,13 @@ public SatochipHWWallet(Device device, String pin, Activity activity, Context c NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_NFC_B | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, null ); - Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() after NfcAdapter.enableReaderMode"); + Log.i(TAG, "SATODEBUG SatochipHWWallet constructor after NfcAdapter.enableReaderMode"); } @Override public synchronized void disconnect() { - Log.i(TAG, "disconnect start"); + Log.i(TAG, "SATODEBUG SatochipHWWallet disconnect start"); // No-op } @@ -629,28 +629,28 @@ public synchronized String getGreenAddress(final Network network, final Account @Nullable @Override public MutableStateFlow getDisconnectEvent() { - Log.i("SatochipHWWallet", "getDisconnectEvent start"); + Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getDisconnectEvent start"); return null; } @Nullable @Override public String getFirmwareVersion() { - Log.i("SatochipHWWallet", "getFirmwareVersion start"); + Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getFirmwareVersion start"); return firmwareVersion; } @NonNull @Override public DeviceModel getModel() { - Log.i("SatochipHWWallet", "getModel start"); + Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getModel start"); return model; } @NonNull @Override public Device getDevice() { - Log.i("SatochipHWWallet", "getDevice start"); + Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getDevice start"); return device; } From a776c8becc20b106ebdd031829d12ab348d6a637 Mon Sep 17 00:00:00 2001 From: Toporin Date: Fri, 21 Feb 2025 15:25:53 +0100 Subject: [PATCH 11/25] Better PIN handling and error mgmgt --- .../DeviceConnectionManagerAndroid.kt | 5 +- .../greenbits/wallets/SatochipHWWallet.java | 46 ++++++++++++++++--- .../java/com/satochip/NfcActionObject.java | 2 + 3 files changed, 44 insertions(+), 9 deletions(-) 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 0df867177..f0b5cea84 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt @@ -201,9 +201,8 @@ class DeviceConnectionManagerAndroid constructor( override fun onConnected(channel: CardChannel) { println("SATODEBUG DeviceConnectionManagerAndroid onConnected(): Card is connected") - //val pin: String? = satochipInteraction?.requestPinBlocking(DeviceBrand.Satochip) - //val pin: String? = satochipInteraction?.requestPinMatrix(DeviceBrand.Satochip) - val pin: String? = satochipInteraction?.requestPassphrase(DeviceBrand.Satochip) + //val pin: String? = satochipInteraction?.requestPassphrase(DeviceBrand.Satochip) + val pin: String? = null; println("SATODEBUG DeviceConnectionManagerAndroid onConnected(): PIN: " + pin) try { diff --git a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java index c41361031..4c8a974fb 100644 --- a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java +++ b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java @@ -148,11 +148,17 @@ public void onConnected(CardChannel channel) { //String pinStr = "123456"; //todo byte[] pinBytes = this.pin.getBytes(Charset.forName("UTF-8")); ApduResponse rapduPin = cmdSet.cardVerifyPIN(pinBytes); - Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() pin verified: " + rapduPin.getSw()); Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() pin verified: " + rapduPin.toHexString()); // todo: wrong pin/blockecd pin... } else { - throw new SatochipException(SatochipException.ExceptionReason.PIN_UNDEFINED); + 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); + Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() pin verified: " + rapduPin.toHexString()); + } else { + throw new SatochipException(SatochipException.ExceptionReason.PIN_UNDEFINED); + } } // execute commands depending on actionType @@ -172,22 +178,37 @@ public void onConnected(CardChannel channel) { this.pin = null; Log.e(TAG, "SATODEBUG onConnected: ERROR: "+ e); onDisconnected(); - throw new RuntimeException(e); // todo: msg user instead!! + if (actionObject.hwInteraction != null){ + actionObject.hwInteraction.interactionRequest(this, "No PIN available", false, null); + } + //throw new RuntimeException(e); // todo: msg user instead!! } catch (WrongPINException e) { this.pin = null; Log.e(TAG, "SATODEBUG onConnected: WRONG PIN! "+ e); onDisconnected(); - throw new RuntimeException(e); // todo: msg user instead!! + if (actionObject.hwInteraction != null){ + String message = "WRONG PIN!\n"+e.getRetryAttempts() + " tries remaining"; + actionObject.hwInteraction.interactionRequest(this, message, false, null); + } + //throw new RuntimeException(e); // todo: msg user instead!! } catch (WrongPINLegacyException e) { this.pin = null; Log.e(TAG, "SATODEBUG onConnected: WRONG PIN LEGACY! "+ e); onDisconnected(); - throw new RuntimeException(e); // todo: msg user instead!! + if (actionObject.hwInteraction != null){ + String message = "WRONG PIN!\n"; + actionObject.hwInteraction.interactionRequest(this, message, false, null); + } + //throw new RuntimeException(e); // todo: msg user instead!! } catch (BlockedPINException e) { this.pin = null; Log.e(TAG, "SATODEBUG onConnected: A an exception has been thrown during card init: "+ e); onDisconnected(); - throw new RuntimeException(e); // todo: msg user instead!! + if (actionObject.hwInteraction != null){ + String message = "CARD BLOCKED!\nYou need to reset your card."; + actionObject.hwInteraction.interactionRequest(this, message, false, null); + } + //throw new RuntimeException(e); // todo: msg user instead!! } catch (ApduException e) { //throw new RuntimeException(e); Log.e(TAG, "SATODEBUG onConnected: A an exception has been thrown during card init: "+ e); @@ -429,6 +450,7 @@ public synchronized List getXpubs(@NonNull Network network, @NonNull Lis 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) { @@ -436,6 +458,9 @@ public synchronized List getXpubs(@NonNull Network network, @NonNull Lis 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; @@ -466,6 +491,8 @@ public SignMessageResult signMessage(@NonNull List path, @NonNull Strin 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) { @@ -473,6 +500,9 @@ public SignMessageResult signMessage(@NonNull List path, @NonNull Strin 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); @@ -572,6 +602,7 @@ public SignTransactionResult signTransaction(@NonNull Network network, @NonNull 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) { @@ -579,6 +610,9 @@ public SignTransactionResult signTransaction(@NonNull Network network, @NonNull 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); diff --git a/hardware/src/main/java/com/satochip/NfcActionObject.java b/hardware/src/main/java/com/satochip/NfcActionObject.java index 000db2ce8..30782ad9f 100644 --- a/hardware/src/main/java/com/satochip/NfcActionObject.java +++ b/hardware/src/main/java/com/satochip/NfcActionObject.java @@ -15,6 +15,8 @@ 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> paths, @Nullable HardwareWalletInteraction hwInteraction) public Network networkParam = null; From 9335c10a6e57626f94814d7d7d1e392a12ba17bc Mon Sep 17 00:00:00 2001 From: Toporin Date: Mon, 24 Feb 2025 16:24:43 +0100 Subject: [PATCH 12/25] Add bottom sheet for NFC scan Also simplify nfc scan in DeviceConnectionManagerAndroid --- .../composeResources/drawable/nfc_scan.xml | 13 ++ .../common/gdk/device/GdkHardwareWallet.kt | 1 + .../common/models/GreenViewModel.kt | 4 + .../common/sideeffects/SideEffects.kt | 1 + .../DeviceConnectionManagerAndroid.kt | 47 ++++++- .../compose/extensions/Resources.kt | 4 +- .../sheets/DeviceInteractionBottomSheet.kt | 2 +- .../compose/sheets/NfcToastBottomSheet.kt | 128 ++++++++++++++++++ .../blockstream/compose/utils/SideEffects.kt | 13 ++ .../greenbits/wallets/SatochipHWWallet.java | 29 +++- .../java/com/satochip/SatochipCommandSet.java | 2 +- .../java/com/satochip/SatochipParser.java | 4 +- 12 files changed, 235 insertions(+), 13 deletions(-) create mode 100644 common/src/commonMain/composeResources/drawable/nfc_scan.xml create mode 100644 compose/src/commonMain/kotlin/com/blockstream/compose/sheets/NfcToastBottomSheet.kt 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/kotlin/com/blockstream/common/gdk/device/GdkHardwareWallet.kt b/common/src/commonMain/kotlin/com/blockstream/common/gdk/device/GdkHardwareWallet.kt index ffa7d1016..ad2c1064a 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?)// satodebug } abstract class GdkHardwareWallet { 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 6bb2f747a..fec938334 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/GreenViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/GreenViewModel.kt @@ -901,6 +901,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): ErrorReport? { return 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 3321e52eb..9b5bf1198 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/managers/DeviceConnectionManagerAndroid.kt b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt index f0b5cea84..370a5cc4e 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt @@ -164,13 +164,52 @@ class DeviceConnectionManagerAndroid constructor( } - // SATODEBUG private suspend fun connectSatochipDevice(device: SatochipDevice, interaction: HardwareConnectInteraction): ConnectionResult { logger.i {"SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() start device: $device"} + val satoDevice = com.blockstream.common.gdk.data.Device( + name = "Satochip", + supportsArbitraryScripts = false, + supportsLowR = false, + supportsHostUnblinding = false, + supportsExternalBlinding = false, + supportsLiquid = DeviceSupportsLiquid.None, + supportsAntiExfilProtocol = DeviceSupportsAntiExfilProtocol.None + ) + + val pin: String? = null; + //val pin: String? = satochipInteraction?.requestPassphrase(DeviceBrand.Satochip) + println("SATODEBUG DeviceConnectionManagerAndroid onConnected(): PIN: " + pin) + + // provide activity and context needed for NFC + activity = device.activityProvider?.getCurrentActivity() + + println("SATODEBUG DeviceConnectionManagerAndroid onConnected() creating gdkHardwareWallet") + device.gdkHardwareWallet = SatochipHWWallet(satoDevice, pin, activity, device.context) + println("SATODEBUG DeviceConnectionManagerAndroid onConnected() created gdkHardwareWallet!") + + logger.i { "SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() end" } + + return ConnectionResult() + } + + // SATODEBUG + private suspend fun connectSatochipDeviceOld(device: SatochipDevice, interaction: HardwareConnectInteraction): ConnectionResult { + logger.i {"SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() start device: $device"} + satochipDevice = device satochipInteraction = interaction + val satoDevice = com.blockstream.common.gdk.data.Device( + name = "Satochip", + supportsArbitraryScripts = false, + supportsLowR = false, + supportsHostUnblinding = false, + supportsExternalBlinding = false, + supportsLiquid = DeviceSupportsLiquid.None, + supportsAntiExfilProtocol = DeviceSupportsAntiExfilProtocol.None + ) + val cardManager = NfcCardManager() cardManager.setCardListener(this) cardManager.start() @@ -190,12 +229,6 @@ class DeviceConnectionManagerAndroid constructor( return ConnectionResult() } -// private fun onSatochipConnected(device: AndroidDevice): ConnectionResult { -// logger.d { "SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() start" } -// logger.d { "Creating Satochip HW wallet" } -// -// return ConnectionResult() -// } // SATODEBUG override fun onConnected(channel: CardChannel) { 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 2532a445e..3a14f2a78 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,7 +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.generic_device // todo SATODEBUG + DeviceModel.SatochipGeneric -> Res.drawable.nfc_scan // todo SATODEBUG } 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..261bff6ea 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) "Tap your card to sign" 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..7671d9ccb --- /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 = "Please 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 f7bf3769d..11456f28d 100644 --- a/compose/src/commonMain/kotlin/com/blockstream/compose/utils/SideEffects.kt +++ b/compose/src/commonMain/kotlin/com/blockstream/compose/utils/SideEffects.kt @@ -141,6 +141,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 @@ -570,6 +571,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/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java index 4c8a974fb..f249ecbbb 100644 --- a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java +++ b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java @@ -54,6 +54,8 @@ import java.util.List; import java.util.Map; +import kotlinx.coroutines.CompletableDeferred; +import kotlinx.coroutines.CompletableDeferredKt; import kotlinx.coroutines.flow.MutableStateFlow; @@ -427,7 +429,7 @@ public synchronized List getXpubs(@NonNull Network network, @NonNull Lis Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getXpubs HardwareWalletInteraction: " + hwInteraction); - // first step: check if xpubs are available in cache + // first step: check if xpubs are all available in cache boolean isCached = true; final List cachedXpubs = new ArrayList<>(paths.size()); for (List path : paths) { @@ -444,8 +446,14 @@ public synchronized List getXpubs(@NonNull Network network, @NonNull Lis return cachedXpubs; } + CompletableDeferred completable = CompletableDeferredKt.CompletableDeferred(null); + // request to card if not cached already try { + if(hwInteraction != null) { + hwInteraction.requestNfcToast(DeviceBrand.Satochip, "Scan card to get xpub...", completable); + } + this.actionObject.actionStatus = NfcActionStatus.busy; this.actionObject.actionType = NfcActionType.getXpubs; this.actionObject.networkParam = network; @@ -466,6 +474,8 @@ public synchronized List getXpubs(@NonNull Network network, @NonNull Lis } catch (Exception e) { Log.i("SatochipHWWallet", "getXpubs exception: " + e); + } finally { + completable.complete(true); } return null; @@ -486,7 +496,13 @@ public SignMessageResult signMessage(@NonNull List path, @NonNull Strin 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, "Scan card to sign message...", completable); + } + this.actionObject.actionStatus = NfcActionStatus.busy; this.actionObject.actionType = NfcActionType.signMessage; this.actionObject.pathParam = path; @@ -508,6 +524,8 @@ public SignMessageResult signMessage(@NonNull List path, @NonNull Strin return new SignMessageResult(signatureResult, null); } catch (Exception e) { Log.e("SatochipHWWallet", "signMessage exception: " + e); + } finally { + completable.complete(true); } //TODO @@ -525,6 +543,8 @@ public SignTransactionResult signTransaction(@NonNull Network network, @NonNull Log.i("SatochipHWWallet", "signTransaction start outputs: " + outputs); Log.i("SatochipHWWallet", "signTransaction start transactions: " + transactions); + CompletableDeferred completable = CompletableDeferredKt.CompletableDeferred(null); + final byte[] txBytes = Wally.hex_to_bytes(transaction); Log.i("SatochipHWWallet", "signTransaction txBytes: " + Wally.hex_from_bytes(txBytes)); @@ -538,6 +558,10 @@ public SignTransactionResult signTransaction(@NonNull Network network, @NonNull throw new RuntimeException("Hardware Wallet does not support the Anti-Exfil protocol"); } + if(hwInteraction != null) { + hwInteraction.requestNfcToast(DeviceBrand.Satochip, "Scan card to sign transaction...", completable); + } + final Object wallyTx = Wally.tx_from_bytes(txBytes, Wally.WALLY_TX_FLAG_USE_WITNESS); Log.i("SatochipHWWallet", "signTransaction wallyTx: " + wallyTx); @@ -620,7 +644,10 @@ public SignTransactionResult signTransaction(@NonNull Network network, @NonNull } catch (final Exception e) { e.printStackTrace(); throw new RuntimeException("Signing Error: " + e.getMessage()); + } finally { + completable.complete(true); } + } diff --git a/hardware/src/main/java/com/satochip/SatochipCommandSet.java b/hardware/src/main/java/com/satochip/SatochipCommandSet.java index 5fa296d2a..7c91607f2 100644 --- a/hardware/src/main/java/com/satochip/SatochipCommandSet.java +++ b/hardware/src/main/java/com/satochip/SatochipCommandSet.java @@ -703,7 +703,7 @@ public byte[][] cardBip32GetExtendedKey(Bip32Path parsedPath, Byte flags, Intege extendedKey = parser.parseBip85GetExtendedKey(respApdu)[0]; extendedKeyHex = parser.toHexString(extendedKey); } else if ((optionFlags & 0x02) == 0x00) { // BIP32 pubkey - logger.warning("SATOCHIPLIB: cardBip32GetExtendedKey: in BIP39"); + logger.warning("SATOCHIPLIB: cardBip32GetExtendedKey: in BIP32"); if ((response[32] & 0x80) == 0x80) { logger.info("SATOCHIPLIB: cardBip32GetExtendedKey: Child Derivation optimization..."); throw new Exception("Unsupported legacy option during BIP32 derivation"); diff --git a/hardware/src/main/java/com/satochip/SatochipParser.java b/hardware/src/main/java/com/satochip/SatochipParser.java index 5039f40e6..4491b09fd 100644 --- a/hardware/src/main/java/com/satochip/SatochipParser.java +++ b/hardware/src/main/java/com/satochip/SatochipParser.java @@ -493,7 +493,7 @@ public byte[] parseToCompactSignature(byte[] sigIn){ offset+=32; } else{ - throw new RuntimeException("Wrong signature r length (should be 0x20 or 0x21) !"); + throw new RuntimeException("Wrong signature r length: " + lr + " (should be 0x20 or 0x21) !"); } check= sigIn[offset++]; @@ -511,7 +511,7 @@ public byte[] parseToCompactSignature(byte[] sigIn){ System.arraycopy(sigIn, offset, s, 0, 32); offset+=32; } else{ - throw new RuntimeException("Wrong signature s length (should be 0x20 or 0x21) !"); + throw new RuntimeException("Wrong signature s length: " + ls + " (should be 0x20 or 0x21) !"); } int sigOutSize= 64; From 8a391a167ddddf81441f2fcfe7fe536be1c467c0 Mon Sep 17 00:00:00 2001 From: Toporin Date: Mon, 24 Feb 2025 16:43:22 +0100 Subject: [PATCH 13/25] Clean & remove unused code --- .../DeviceConnectionManagerAndroid.kt | 113 +----------------- 1 file changed, 3 insertions(+), 110 deletions(-) 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 370a5cc4e..5c638f937 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt @@ -2,15 +2,10 @@ package com.blockstream.compose.managers import android.app.Activity import android.bluetooth.BluetoothAdapter -import android.nfc.NfcAdapter import com.blockstream.common.data.AppInfo import com.blockstream.common.devices.AndroidDevice -import com.blockstream.common.devices.CardChannel -import com.blockstream.common.devices.CardListener import com.blockstream.common.devices.DeviceBrand import com.blockstream.common.devices.GreenDevice -import com.blockstream.common.devices.NfcCardManager -import com.blockstream.common.devices.SatochipCommandSet import com.blockstream.common.devices.toAndroidDevice import com.blockstream.common.gdk.Gdk import com.blockstream.common.gdk.Wally @@ -51,18 +46,12 @@ class DeviceConnectionManagerAndroid constructor( private val appInfo: AppInfo, scope: CoroutineScope, private val bluetoothAdapter: BluetoothAdapter -) : CardListener, DeviceConnectionManager( +) : DeviceConnectionManager( gdk = gdk, wally = wally, scope = scope ) { - // Satochip - private var nfcAdapter: NfcAdapter? = null - private var activity: Activity? = null - private var satochipDevice: SatochipDevice? = null - private var satochipInteraction: HardwareConnectInteraction? = null - override suspend fun connectDevice( device: GreenDevice, httpRequestHandler: HttpRequestHandler, @@ -162,8 +151,7 @@ class DeviceConnectionManagerAndroid constructor( return ConnectionResult() } - - + private suspend fun connectSatochipDevice(device: SatochipDevice, interaction: HardwareConnectInteraction): ConnectionResult { logger.i {"SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() start device: $device"} @@ -182,7 +170,7 @@ class DeviceConnectionManagerAndroid constructor( println("SATODEBUG DeviceConnectionManagerAndroid onConnected(): PIN: " + pin) // provide activity and context needed for NFC - activity = device.activityProvider?.getCurrentActivity() + val activity: Activity? = device.activityProvider?.getCurrentActivity() println("SATODEBUG DeviceConnectionManagerAndroid onConnected() creating gdkHardwareWallet") device.gdkHardwareWallet = SatochipHWWallet(satoDevice, pin, activity, device.context) @@ -193,101 +181,6 @@ class DeviceConnectionManagerAndroid constructor( return ConnectionResult() } - // SATODEBUG - private suspend fun connectSatochipDeviceOld(device: SatochipDevice, interaction: HardwareConnectInteraction): ConnectionResult { - logger.i {"SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() start device: $device"} - - satochipDevice = device - satochipInteraction = interaction - - val satoDevice = com.blockstream.common.gdk.data.Device( - name = "Satochip", - supportsArbitraryScripts = false, - supportsLowR = false, - supportsHostUnblinding = false, - supportsExternalBlinding = false, - supportsLiquid = DeviceSupportsLiquid.None, - supportsAntiExfilProtocol = DeviceSupportsAntiExfilProtocol.None - ) - - val cardManager = NfcCardManager() - cardManager.setCardListener(this) - cardManager.start() - logger.i { "SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() after cardManager start" } - - activity = device.activityProvider?.getCurrentActivity() - nfcAdapter = NfcAdapter.getDefaultAdapter(device.context) - nfcAdapter?.enableReaderMode( - activity, - cardManager, - NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_NFC_B or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, - null - ) - - logger.i { "SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() end" } - - return ConnectionResult() - } - - - // SATODEBUG - override fun onConnected(channel: CardChannel) { - println("SATODEBUG DeviceConnectionManagerAndroid onConnected(): Card is connected") - - //val pin: String? = satochipInteraction?.requestPassphrase(DeviceBrand.Satochip) - val pin: String? = null; - println("SATODEBUG DeviceConnectionManagerAndroid onConnected(): PIN: " + pin) - - try { - val cmdSet = SatochipCommandSet(channel) - // start to interact with card - //NfcCardService.initialize(cmdSet) - - val rapduSelect = cmdSet.cardSelect("satochip").checkOK() - // cardStatus - val rapduStatus = cmdSet.cardGetStatus()//To update status if it's not the first reading - val cardStatus = cmdSet.getApplicationStatus() //applicationStatus ?: return - println("SATODEBUG DeviceConnectionManagerAndroid onConnected() cardStatus: $cardStatus") - - // TODO: disconnect? - println("SATODEBUG DeviceConnectionManagerAndroid onConnected() trigger disconnection!") - onDisconnected() - - // stop polling? - nfcAdapter?.disableReaderMode(activity) - println("SATODEBUG DeviceConnectionManagerAndroid onConnected() NFC DISABLED") - - val satoDevice = com.blockstream.common.gdk.data.Device( - name = "Satochip", - supportsArbitraryScripts = false, - supportsLowR = false, - supportsHostUnblinding = false, - supportsExternalBlinding = false, - supportsLiquid = DeviceSupportsLiquid.None, - supportsAntiExfilProtocol = DeviceSupportsAntiExfilProtocol.None - ) - - println("SATODEBUG DeviceConnectionManagerAndroid onConnected() creating gdkHardwareWallet") - // provide activity and context needed for NFC - satochipDevice?.gdkHardwareWallet = SatochipHWWallet(satoDevice, pin, activity, satochipDevice?.context) - println("SATODEBUG DeviceConnectionManagerAndroid onConnected() created gdkHardwareWallet!") - - } catch (e: Exception) { - println("SATODEBUG DeviceConnectionManagerAndroid onConnected() exception: $e") - //Log.e(TAG, Log.getStackTraceString(e)) - onDisconnected() - } - } - - // SATODEBUG - override fun onDisconnected() { - //NfcCardService.isConnected.postValue(false) - println("SATODEBUG DeviceConnectionManagerAndroid onDisconnected: Card disconnected!") - } - - //============================== - - private suspend fun connectLedgerDevice( device: LedgerDevice, interaction: HardwareConnectInteraction From 87192ee2070052da863a1b4c6339c4f9d0042ce0 Mon Sep 17 00:00:00 2001 From: Toporin Date: Mon, 24 Feb 2025 21:26:39 +0100 Subject: [PATCH 14/25] Simplify & remove unused code --- .../common/devices/ApplicationStatus.kt | 104 ------------- .../common/devices/DeviceManagerAndroid.kt | 37 +---- .../common/devices/NfcCardManager.kt | 6 - .../common/devices/SatochipCardListener.kt | 48 ------ .../common/devices/SatochipCommandSet.kt | 143 +----------------- .../DeviceConnectionManagerAndroid.kt | 2 +- 6 files changed, 7 insertions(+), 333 deletions(-) delete mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/ApplicationStatus.kt delete mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCardListener.kt diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/ApplicationStatus.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/ApplicationStatus.kt deleted file mode 100644 index 3c907918b..000000000 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/ApplicationStatus.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.blockstream.common.devices - -/** - * Parses the result of a GET STATUS command retrieving application status. - */ -class ApplicationStatus(rapdu: ApduResponse) { - private var setupDone: Boolean = false - private var isSeeded: Boolean = false - private var needsSecureChannel: Boolean = false - private var needs2FA: Boolean = false - - private var protocolMajorVersion: Byte = 0 - private var protocolMinorVersion: Byte = 0 - private var appletMajorVersion: Byte = 0 - private var appletMinorVersion: Byte = 0 - - private var pin0RemainingTries: Byte = 0 - private var puk0RemainingTries: Byte = 0 - private var pin1RemainingTries: Byte = 0 - private var puk1RemainingTries: Byte = 0 - - private var protocolVersion: Int = 0 - - init { - val sw = rapdu.getSw() - - when (sw) { - 0x9000 -> { - val data = rapdu.getData() - protocolMajorVersion = data[0] - protocolMinorVersion = data[1] - appletMajorVersion = data[2] - appletMinorVersion = data[3] - protocolVersion = (protocolMajorVersion.toInt() shl 8) + protocolMinorVersion - - if (data.size >= 8) { - pin0RemainingTries = data[4] - puk0RemainingTries = data[5] - pin1RemainingTries = data[6] - puk1RemainingTries = data[7] - needs2FA = false // default value - } - if (data.size >= 9) { - needs2FA = data[8] != 0x00.toByte() - } - if (data.size >= 10) { - isSeeded = data[9] != 0x00.toByte() - } - if (data.size >= 11) { - setupDone = data[10] != 0x00.toByte() - } else { - setupDone = true - } - if (data.size >= 12) { - needsSecureChannel = data[11] != 0x00.toByte() - } else { - needsSecureChannel = false - needs2FA = false // default value - } - } - 0x9c04 -> { - setupDone = false - isSeeded = false - needsSecureChannel = false - } - else -> { - // throws IllegalArgumentException("Wrong getStatus data!") // should not happen - } - } - } - - // Getters - fun isSeeded(): Boolean = isSeeded - fun isSetupDone(): Boolean = setupDone - fun needsSecureChannel(): Boolean = needsSecureChannel - fun getPin0RemainingCounter(): Byte = pin0RemainingTries - fun getPuk0RemainingCounter(): Byte = puk0RemainingTries - - override fun toString(): String { - return """ - setup_done: $setupDone - is_seeded: $isSeeded - needs_2FA: $needs2FA - needs_secure_channel: $needsSecureChannel - protocol_major_version: $protocolMajorVersion - protocol_minor_version: $protocolMinorVersion - applet_major_version: $appletMajorVersion - applet_minor_version: $appletMinorVersion - """.trimIndent() - } - - fun getCardVersionInt(): Int { - return (protocolMajorVersion.toInt() * (1 shl 24)) + - (protocolMinorVersion.toInt() * (1 shl 16)) + - (appletMajorVersion.toInt() * (1 shl 8)) + - appletMinorVersion.toInt() - } - - fun getCardVersionString(): String { - return "$protocolMajorVersion.$protocolMinorVersion-$appletMajorVersion.$appletMinorVersion" - } - - fun getProtocolVersion(): Int = protocolVersion -} \ 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 9c2c32625..b3b9f1c21 100644 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt @@ -1,7 +1,6 @@ package com.blockstream.common.devices import android.annotation.SuppressLint -import android.app.Activity import android.app.PendingIntent import android.app.PendingIntent.FLAG_IMMUTABLE import android.bluetooth.BluetoothDevice @@ -9,7 +8,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.nfc.NfcAdapter // SATODEBUG +import android.nfc.NfcAdapter import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager import androidx.core.content.ContextCompat @@ -184,27 +183,17 @@ class DeviceManagerAndroid constructor( logger.i { "SATODEBUG scanUsbDevices() end" } } - // SATODEBUG fun scanNfcDevices() { logger.i { "SATODEBUG scanNfcDevices() start" } logger.i { "Scan for NFC devices" } val cardManager = NfcCardManager() - //cardManager.setCardListener(SatochipCardListenerForAction) cardManager.setCardListener(this) cardManager.start() logger.i { "SATODEBUG scanNfcDevices() after cardManager start" } - // ugly hack - //val countly = sessionManager.countly as Countly - //val activity = countly.activityCopy val activity = activityProvider.getCurrentActivity() - //val activity = activity //context as Activity? - //val activity = context as Activity? - //val activity = LocalActivity.current //activity - //val activity = LocalContext - //val nfcAdapter = NfcAdapter.getDefaultAdapter(context) //context) nfcAdapter?.enableReaderMode( activity, cardManager, @@ -217,57 +206,37 @@ class DeviceManagerAndroid constructor( logger.i { "SATODEBUG scanNfcDevices() end" } } - // SATODEBUG override fun onConnected(channel: CardChannel) { println("SATODEBUG DeviceManagerAndroid onConnected: Card is connected") try { val cmdSet = SatochipCommandSet(channel) - // start to interact with card - //NfcCardService.initialize(cmdSet) val rapduSelect = cmdSet.cardSelect("satochip").checkOK() - // cardStatus - val rapduStatus = cmdSet.cardGetStatus()//To update status if it's not the first reading - val cardStatus = cmdSet.getApplicationStatus() //applicationStatus ?: return - println("SATODEBUG DeviceManagerAndroid readCard cardStatus: $cardStatus") - println("SATODEBUG DeviceManagerAndroid readCard cardStatus: ${cardStatus.toString()}") // add device val newDevices = mutableListOf() deviceMapper.invoke(this, null, null, null, null, activityProvider)?.let { newDevices += it - println("SATODEBUG DeviceManagerAndroid readCard newDevice: ${it}") - println("SATODEBUG DeviceManagerAndroid readCard newDevice manufacturer: ${it.manufacturer}") - println("SATODEBUG DeviceManagerAndroid readCard newDevice name: ${it.name}") - println("SATODEBUG DeviceManagerAndroid readCard newDevice deviceBrand: ${it.deviceBrand}") - println("SATODEBUG DeviceManagerAndroid readCard newDevice deviceModel: ${it.deviceModel}") - println("SATODEBUG DeviceManagerAndroid readCard newDevice deviceState: ${it.deviceState}") - println("SATODEBUG DeviceManagerAndroid readCard newDevice type: ${it.type}") - println("SATODEBUG DeviceManagerAndroid readCard newDevice connectionIdentifier: ${it.connectionIdentifier}") - println("SATODEBUG DeviceManagerAndroid readCard newDevice uniqueIdentifier: ${it.uniqueIdentifier}") } println("SATODEBUG DeviceManagerAndroid readCard newDevices: ${newDevices}") nfcDevices.value = newDevices - // TODO: disconnect? + // disconnect card println("SATODEBUG DeviceManagerAndroid onConnected: trigger disconnection!") onDisconnected() - // stop polling? + // stop polling val activity = activityProvider.getCurrentActivity() nfcAdapter?.disableReaderMode(activity) } catch (e: Exception) { println("SATODEBUG DeviceManagerAndroid onConnected: an exception has been thrown during card init.") - //Log.e(TAG, Log.getStackTraceString(e)) onDisconnected() } } - // SATODEBUG override fun onDisconnected() { - //NfcCardService.isConnected.postValue(false) println("SATODEBUG DeviceManagerAndroid onDisconnected: Card disconnected!") } diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardManager.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardManager.kt index 5cb45e4f1..dc161c27a 100644 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardManager.kt +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcCardManager.kt @@ -19,12 +19,6 @@ class NfcCardManager @JvmOverloads constructor( private var isRunning = false private var cardListener: CardListener? = null -// companion object { -// init { -// Crypto.addBouncyCastleProvider() -// } -// } - /** * True if connected, false otherwise. * @return if connected, false otherwise diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCardListener.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCardListener.kt deleted file mode 100644 index c4f46b164..000000000 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCardListener.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.blockstream.common.devices - -//import android.util.Log - -//import org.satochip.client.SatochipCommandSet -//import org.satochip.io.CardChannel -//import org.satochip.io.CardListener -//import org.satochip.satodimeapp.data.NfcResultCode - -private const val TAG = "SatochipCardListener" - -// todo remove deprecated -object SatochipCardListenerForAction : CardListener { - - override fun onConnected(channel: CardChannel) { - - println("SATODEBUG onConnected: Card is connected") - try { - val cmdSet = SatochipCommandSet(channel) - // start to interact with card - //NfcCardService.initialize(cmdSet) - - val rapduSelect = cmdSet.cardSelect("satochip").checkOK() - // cardStatus - val rapduStatus = cmdSet.cardGetStatus()//To update status if it's not the first reading - val cardStatus = cmdSet.getApplicationStatus() //applicationStatus ?: return - println("SATODEBUG readCard cardStatus: $cardStatus") - println("SATODEBUG readCard cardStatus: ${cardStatus.toString()}") - - // TODO: disconnect? - println("onConnected: trigger disconnection!") - onDisconnected() - - // disable scanning once finished - //Thread.sleep(100) // delay to let resultCodeLive update (avoid race condition?) - - } catch (e: Exception) { - println("onConnected: an exception has been thrown during card init.") - //Log.e(TAG, Log.getStackTraceString(e)) - onDisconnected() - } - } - - override fun onDisconnected() { - //NfcCardService.isConnected.postValue(false) - println("onDisconnected: Card disconnected!") - } -} \ 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 index 30377bf97..ad4cdd8dc 100644 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.kt +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.kt @@ -1,22 +1,8 @@ package com.blockstream.common.devices -import java.nio.ByteBuffer -import java.security.SecureRandom -import java.util.ArrayList -import java.util.Arrays import java.util.logging.Logger import java.util.logging.Level import java.io.IOException -import java.io.InputStream -import java.io.ByteArrayInputStream -import java.nio.charset.StandardCharsets -import java.security.cert.CertPathValidator -import java.security.cert.CertPath -import java.security.cert.CertificateFactory -import java.security.cert.Certificate -import java.security.cert.PKIXParameters -import java.security.KeyStore -import java.security.PublicKey /** * This class is used to send APDU to the applet. Each method corresponds to an APDU as defined in the APPLICATION.md @@ -27,11 +13,8 @@ class SatochipCommandSet(private val apduChannel: CardChannel) { companion object { private val logger: Logger = Logger.getLogger("org.satochip.client") - const val INS_GET_STATUS: Int = 0x3C val SATOCHIP_AID: ByteArray = hexToBytes("5361746f43686970") // SatoChip - val SEEDKEEPER_AID: ByteArray = hexToBytes("536565644b6565706572") // SeedKeeper - val SATODIME_AID: ByteArray = hexToBytes("5361746f44696d65") // SatoDime /* s must be an even-length string. */ fun hexToBytes(s: String): ByteArray { @@ -45,126 +28,18 @@ class SatochipCommandSet(private val apduChannel: CardChannel) { } } - private var status: ApplicationStatus? = null - private var pin0: ByteArray? = null - private val possibleAuthentikeys = ArrayList() - private var authentikey: ByteArray? = null - private var authentikeyHex: String? = null - private var defaultBip32path: String? = null - private var extendedKey: ByteArray? = null - private var extendedChaincode: ByteArray? = null - private var extendedKeyHex: String? = null - private var extendedPrivKey: ByteArray? = null - private var extendedPrivKeyHex: String? = null - - // Satodime, SeedKeeper or Satochip? + // Satochip or... private var cardType: String? = null - private var certPem: String? = null // PEM certificate of device, if any init { logger.level = Level.WARNING } - fun setLoggerLevel(level: String) { - logger.level = when (level) { - "info" -> Level.INFO - "warning" -> Level.WARNING - else -> Level.WARNING - } - } - - fun setLoggerLevel(level: Level) { - logger.level = level - } - - fun getApplicationStatus(): ApplicationStatus? = status - - fun getPossibleAuthentikeys(): List = possibleAuthentikeys - - fun setDefaultBip32path(bip32path: String) { - defaultBip32path = bip32path - } - - fun cardTransmit(plainApdu: ApduCommand): ApduResponse { - var isApduTransmitted = false - do { - try { - val apduBytes = plainApdu.serialize() - val ins = apduBytes[1].toUByte().toInt() - var isEncrypted = false - - // check if status available - if (status == null) { - val statusCapdu = ApduCommand(0xB0, INS_GET_STATUS, 0x00, 0x00, ByteArray(0)) - val statusRapdu = apduChannel.send(statusCapdu) - status = ApplicationStatus(statusRapdu) - logger.info("SATOCHIPLIB: Status cardGetStatus:${status.toString()}") - } - - val capdu = if (status?.needsSecureChannel() == true && - ins != 0xA4 && - ins != 0x81 && - ins != 0x82 && - ins != INS_GET_STATUS) { - isEncrypted = true - plainApdu - } else { - plainApdu - } - - val rapdu = apduChannel.send(capdu) - val sw12 = rapdu.getSw() - - when (sw12) { - 0x9000 -> { - isApduTransmitted = true - return rapdu - } - 0x9C21 -> { - // SecureChannel is not initialized - } - else -> { - isApduTransmitted = true - return rapdu - } - } - - } catch (e: Exception) { - logger.warning("SATOCHIPLIB: Exception in cardTransmit: $e") - return ApduResponse(ByteArray(0), 0x00, 0x00) - } - } while (!isApduTransmitted) - - return ApduResponse(ByteArray(0), 0x00, 0x00) - } - - fun cardDisconnect() { - status = null - pin0 = null - } - - @Throws(IOException::class) - fun cardSelect(): ApduResponse { - var rapdu = cardSelect("satochip") - if (rapdu.getSw() != 0x9000) { - rapdu = cardSelect("seedkeeper") - if (rapdu.getSw() != 0x9000) { - rapdu = cardSelect("satodime") - if (rapdu.getSw() != 0x9000) { - cardType = "unknown" - logger.warning("SATOCHIPLIB: CardSelect: could not select a known applet") - } - } - } - return rapdu - } - @Throws(IOException::class) fun cardSelect(cardType: String): ApduResponse { val selectApplet = when (cardType) { "satochip" -> ApduCommand(0x00, 0xA4, 0x04, 0x00, SATOCHIP_AID) - "seedkeeper" -> ApduCommand(0x00, 0xA4, 0x04, 0x00, SEEDKEEPER_AID) - else -> ApduCommand(0x00, 0xA4, 0x04, 0x00, SATODIME_AID) + else -> ApduCommand(0x00, 0xA4, 0x04, 0x00, SATOCHIP_AID) } logger.info("SATOCHIPLIB: C-APDU cardSelect:${selectApplet.toHexString()}") @@ -177,17 +52,5 @@ class SatochipCommandSet(private val apduChannel: CardChannel) { } return respApdu } - - fun cardGetStatus(): ApduResponse { - val plainApdu = ApduCommand(0xB0, INS_GET_STATUS, 0x00, 0x00, ByteArray(0)) - - logger.info("SATOCHIPLIB: C-APDU cardGetStatus:${plainApdu.toHexString()}") - val respApdu = cardTransmit(plainApdu) - logger.info("SATOCHIPLIB: R-APDU cardGetStatus:${respApdu.toHexString()}") - - status = ApplicationStatus(respApdu) - logger.info("SATOCHIPLIB: Status from cardGetStatus:${status.toString()}") - - return respApdu - } + } \ 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 5c638f937..bb7d92762 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt @@ -151,7 +151,7 @@ class DeviceConnectionManagerAndroid constructor( return ConnectionResult() } - + private suspend fun connectSatochipDevice(device: SatochipDevice, interaction: HardwareConnectInteraction): ConnectionResult { logger.i {"SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() start device: $device"} From 88713fbef61430014efa8cbc04a77aef27147035 Mon Sep 17 00:00:00 2001 From: Toporin Date: Mon, 24 Feb 2025 22:14:06 +0100 Subject: [PATCH 15/25] Simplify & remove unused code --- .../common/devices/SatochipCommandSet.kt | 2 +- .../greenbits/wallets/SatochipHWWallet.java | 8 +- .../java/com/satochip/SatochipCommandSet.java | 739 +----------------- .../java/com/satochip/SatochipParser.java | 135 +--- .../com/satochip/SecureChannelSession.java | 24 - 5 files changed, 31 insertions(+), 877 deletions(-) diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.kt index ad4cdd8dc..e3047bb7d 100644 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.kt +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.kt @@ -52,5 +52,5 @@ class SatochipCommandSet(private val apduChannel: CardChannel) { } return respApdu } - + } \ No newline at end of file diff --git a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java index f249ecbbb..c42a84cd6 100644 --- a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java +++ b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java @@ -248,8 +248,7 @@ public void onConnectedGetXpubs(SatochipCommandSet cmdSet) throws Exception { // get xpub from satochip //pathStr = "m/44'/0'/0'/0"; int xtype = this.actionObject.networkParam.getVerPublic(); //0x0488B21E; - Integer sid = null; - String xpub = cmdSet.cardBip32GetXpub(bip32path, xtype, sid); + String xpub = cmdSet.cardBip32GetXpub(bip32path, xtype); Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedGetXpubs() xpub: " + xpub); // cache xpub @@ -284,8 +283,7 @@ public void onConnectedSignMessage(SatochipCommandSet cmdSet) throws Exception { 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 optionFlags = (byte) 0x40; - byte[][] extendedKey = cmdSet.cardBip32GetExtendedKey(bip32path, optionFlags, null); + 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 @@ -359,7 +357,7 @@ public void onConnectedSignTransaction(SatochipCommandSet cmdSet) throws Excepti Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignTransaction() depth: " + bip32path.getDepth()); //debug Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignTransaction() bip32path: " + hex(bip32path.getBytes())); //debug byte optionFlags = (byte) 0x40; - byte[][] extendedKey = cmdSet.cardBip32GetExtendedKey(bip32path, optionFlags, null); + 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 diff --git a/hardware/src/main/java/com/satochip/SatochipCommandSet.java b/hardware/src/main/java/com/satochip/SatochipCommandSet.java index 7c91607f2..ceef0e2cc 100644 --- a/hardware/src/main/java/com/satochip/SatochipCommandSet.java +++ b/hardware/src/main/java/com/satochip/SatochipCommandSet.java @@ -4,10 +4,10 @@ import static com.satochip.Constants.*; +import androidx.annotation.NonNull; + import com.blockstream.libwally.Wally; -//import org.bitcoinj.core.Base58; -//import org.bitcoinj.core.Sha256Hash; import org.bouncycastle.crypto.digests.RIPEMD160Digest; import org.bouncycastle.util.encoders.Hex; @@ -35,25 +35,14 @@ public class SatochipCommandSet { private byte[] pin0 = null; private List possibleAuthentikeys = new ArrayList(); - private byte[] authentikey = null; - private String authentikeyHex = null; - private String defaultBip32path = null; + private byte[] extendedKey = null; private byte[] extendedChaincode = null; - private String extendedKeyHex = null; - private byte[] extendedPrivKey = null; - private String extendedPrivKeyHex = null; - // Satodime, SeedKeeper or Satochip? + // should be Satochip private String cardType = null; - private String certPem = null; // PEM certificate of device, if any - - public static final byte[] SATOCHIP_AID = Hex.decode("5361746f43686970"); //SatoChip - public static final byte[] SEEDKEEPER_AID = Hex.decode("536565644b6565706572"); //SeedKeeper - public static final byte[] SATODIME_AID = Hex.decode("5361746f44696d65"); //SatoDime - /** * Creates a SatochipCommandSet using the given APDU Channel @@ -95,60 +84,11 @@ public ApplicationStatus getApplicationStatus() { return status; } - /**************************************** - * AUTHENTIKEY * - ****************************************/ - public byte[] getAuthentikey() { - if (authentikey == null) { - cardGetAuthentikey(); - } - return authentikey; - } - - public String getAuthentikeyHex() { - if (authentikeyHex == null) { - cardGetAuthentikey(); - } - return authentikeyHex; - } - - public byte[] getBip32Authentikey() { - if (authentikey == null) { - cardBip32GetAuthentikey(); - } - return authentikey; - } - - public String getBip32AuthentikeyHex() { - if (authentikeyHex == null) { - cardBip32GetAuthentikey(); - } - return authentikeyHex; - } - - public List getPossibleAuthentikeys(){ - return this.possibleAuthentikeys; - } public SatochipParser getParser() { return parser; } - public void setDefaultBip32path(String bip32path) { - defaultBip32path = bip32path; - } - - /** - * Set the SecureChannel object - * - * @param secureChannel secure channel - */ - protected void setSecureChannel(SecureChannelSession secureChannel) { - this.secureChannel = secureChannel; - } - - - public ApduResponse cardTransmit(ApduCommand plainApdu) { // we try to transmit the APDU until we receive the answer or we receive an unrecoverable error @@ -228,37 +168,18 @@ public void cardDisconnect() { } /** - * Selects a Satochip/Satodime/SeedKeeper instance. The applet is assumed to have been installed with its default AID. + * 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() throws IOException { - - ApduResponse rapdu = cardSelect("satochip"); - if (rapdu.getSw() != 0x9000) { - rapdu = cardSelect("seedkeeper"); - if (rapdu.getSw() != 0x9000) { - rapdu = cardSelect("satodime"); - if (rapdu.getSw() != 0x9000) { - this.cardType = "unknown"; - logger.warning("SATOCHIPLIB: CardSelect: could not select a known applet"); - } - } - } - - return rapdu; - } - public ApduResponse cardSelect(String cardType) throws IOException { ApduCommand selectApplet; if (cardType.equals("satochip")) { selectApplet = new ApduCommand(0x00, 0xA4, 0x04, 0x00, SATOCHIP_AID); - } else if (cardType.equals("seedkeeper")) { - selectApplet = new ApduCommand(0x00, 0xA4, 0x04, 0x00, SEEDKEEPER_AID); } else { - selectApplet = new ApduCommand(0x00, 0xA4, 0x04, 0x00, SATODIME_AID); + selectApplet = new ApduCommand(0x00, 0xA4, 0x04, 0x00, null); } logger.info("SATOCHIPLIB: C-APDU cardSelect:" + selectApplet.toHexString()); @@ -303,163 +224,6 @@ public List cardInitiateSecureChannel() throws IOException { return possibleAuthentikeys; } - // only valid for v0.12 and higher - public byte[] cardGetAuthentikey() { - - ApduCommand plainApdu = new ApduCommand(0xB0, INS_EXPORT_AUTHENTIKEY, 0x00, 0x00, new byte[0]); - logger.info("SATOCHIPLIB: C-APDU cardExportAuthentikey:" + plainApdu.toHexString()); - ApduResponse respApdu = this.cardTransmit(plainApdu); - logger.info("SATOCHIPLIB: R-APDU cardExportAuthentikey:" + respApdu.toHexString()); - - // parse and recover pubkey - authentikey = parser.parseBip32GetAuthentikey(respApdu); - authentikeyHex = parser.toHexString(authentikey); - logger.info("SATOCHIPLIB: Authentikey from cardExportAuthentikey:" + authentikeyHex); - - return authentikey; - } - - public ApduResponse cardBip32GetAuthentikey() { - - ApduCommand plainApdu = new ApduCommand(0xB0, INS_BIP32_GET_AUTHENTIKEY, 0x00, 0x00, new byte[0]); - logger.info("SATOCHIPLIB: C-APDU cardBip32GetAuthentikey:" + plainApdu.toHexString()); - ApduResponse respApdu = this.cardTransmit(plainApdu); - logger.info("SATOCHIPLIB: R-APDU cardBip32GetAuthentikey:" + respApdu.toHexString()); - - // parse and recover pubkey - authentikey = parser.parseBip32GetAuthentikey(respApdu); - authentikeyHex = parser.toHexString(authentikey); - logger.info("SATOCHIPLIB: Authentikey from cardBip32GetAuthentikey:" + authentikeyHex); - - return respApdu; - } - -// public ApduResponse cardExportPkiPubkey() { -// -// ApduCommand plainApdu = new ApduCommand(0xB0, INS_EXPORT_PKI_PUBKEY, 0x00, 0x00, new byte[0]); -// logger.info("SATOCHIPLIB: C-APDU cardExportPkiPubkey:" + plainApdu.toHexString()); -// ApduResponse respApdu = this.cardTransmit(plainApdu); -// logger.info("SATOCHIPLIB: R-APDU cardExportPkiPubkey:" + respApdu.toHexString()); -// -// // parse and recover pubkey -// authentikey = parser.parseExportPkiPubkey(respApdu); -// authentikeyHex = parser.toHexString(authentikey); -// logger.info("SATOCHIPLIB: Authentikey from cardExportPkiPubkey:" + authentikeyHex); -// -// return respApdu; -// } - - /**************************************** - * CARD MGMT * - ****************************************/ - -// public ApduResponse cardSetup(byte pin_tries0, byte[] pin0) { -// -// // use random values for pin1, ublk0, ublk1 -// SecureRandom random = new SecureRandom(); -// byte[] ublk0 = new byte[8]; -// byte[] ublk1 = new byte[8]; -// byte[] pin1 = new byte[8]; -// random.nextBytes(ublk0); -// random.nextBytes(ublk1); -// random.nextBytes(pin1); -// -// byte ublk_tries0 = (byte) 0x01; -// byte ublk_tries1 = (byte) 0x01; -// byte pin_tries1 = (byte) 0x01; -// -// return cardSetup(pin_tries0, ublk_tries0, pin0, ublk0, pin_tries1, ublk_tries1, pin1, ublk1); -// } - -// public ApduResponse cardSetup( -// byte pin_tries0, byte ublk_tries0, byte[] pin0, byte[] ublk0, -// byte pin_tries1, byte ublk_tries1, byte[] pin1, byte[] ublk1) { -// -// byte[] pin = {0x4D, 0x75, 0x73, 0x63, 0x6C, 0x65, 0x30, 0x30}; //default pin -// byte cla = (byte) 0xB0; -// byte ins = INS_SETUP; -// byte p1 = 0; -// byte p2 = 0; -// -// // data=[pin_length(1) | pin | -// // pin_tries0(1) | ublk_tries0(1) | pin0_length(1) | pin0 | ublk0_length(1) | ublk0 | -// // pin_tries1(1) | ublk_tries1(1) | pin1_length(1) | pin1 | ublk1_length(1) | ublk1 | -// // memsize(2) | memsize2(2) | ACL(3) | -// // option_flags(2) | hmacsha160_key(20) | amount_limit(8)] -// int optionsize = 0; -// int option_flags = 0; // do not use option (mostly deprecated) -// int offset = 0; -// int datasize = 16 + pin.length + pin0.length + pin1.length + ublk0.length + ublk1.length + optionsize; -// byte[] data = new byte[datasize]; -// -// data[offset++] = (byte) pin.length; -// System.arraycopy(pin, 0, data, offset, pin.length); -// offset += pin.length; -// // pin0 & ublk0 -// data[offset++] = pin_tries0; -// data[offset++] = ublk_tries0; -// data[offset++] = (byte) pin0.length; -// System.arraycopy(pin0, 0, data, offset, pin0.length); -// offset += pin0.length; -// data[offset++] = (byte) ublk0.length; -// System.arraycopy(ublk0, 0, data, offset, ublk0.length); -// offset += ublk0.length; -// // pin1 & ublk1 -// data[offset++] = pin_tries1; -// data[offset++] = ublk_tries1; -// data[offset++] = (byte) pin1.length; -// System.arraycopy(pin1, 0, data, offset, pin1.length); -// offset += pin1.length; -// data[offset++] = (byte) ublk1.length; -// System.arraycopy(ublk1, 0, data, offset, ublk1.length); -// offset += ublk1.length; -// -// // memsize default (deprecated) -// data[offset++] = (byte) 00; -// data[offset++] = (byte) 32; -// data[offset++] = (byte) 00; -// data[offset++] = (byte) 32; -// -// // ACL (deprecated) -// data[offset++] = (byte) 0x01; -// data[offset++] = (byte) 0x01; -// data[offset++] = (byte) 0x01; -// -// ApduCommand plainApdu = new ApduCommand(cla, ins, p1, p2, data); -// //logger.info("SATOCHIPLIB: C-APDU cardSetup:" + plainApdu.toHexString()); -// logger.info("SATOCHIPLIB: C-APDU cardSetup"); -// ApduResponse respApdu = this.cardTransmit(plainApdu); -// logger.info("SATOCHIPLIB: R-APDU cardSetup:" + respApdu.toHexString()); -// -// if (respApdu.isOK()) { -// setPin0(pin0); -// -// if (this.cardType.equals("satodime")) { // cache values -// this.satodimeStatus.updateStatusFromSetup(respApdu); -// } -// } -// -// return respApdu; -// } - -// public ApduResponse cardSendResetCommand() throws Exception { -// byte[] data = new byte[]{}; -// -// ApduCommand plainApdu = new ApduCommand( -// 0xB0, -// INS_RESET_TO_FACTORY, -// 0x00, -// 0x00, -// data -// ); -// -// // reset command must be sent in clear, without other commands interferring between reset commands -// logger.warning("SATOCHIPLIB: C-APDU cardSendResetCommand:" + plainApdu.toHexString()); -// ApduResponse respApdu = apduChannel.send(plainApdu); -// logger.warning("SATOCHIPLIB: R-APDU cardSendResetCommand:" + respApdu.toHexString()); -// -// return respApdu; -// } /**************************************** * PIN MGMT * @@ -507,138 +271,16 @@ public ApduResponse cardVerifyPIN() throws Exception { return cardVerifyPIN(this.pin0); } -// public ApduResponse cardChangePin(byte[] oldPin, byte[] newPin) throws Exception { -// logger.info("SATOCHIPLIB: changeCardPin START"); -// -// byte[] data = new byte[1 + oldPin.length + 1 + newPin.length]; -// data[0] = (byte) oldPin.length; -// System.arraycopy(oldPin, 0, data, 1, oldPin.length); -// data[1 + oldPin.length] = (byte) newPin.length; -// System.arraycopy(newPin, 0, data, 2 + oldPin.length, newPin.length); -// setPin0(newPin); -// try{ -// ApduCommand plainApdu = new ApduCommand(0xB0, INS_CHANGE_PIN, 0x00, 0x00, data); -// //logger.info("SATOCHIPLIB: C-APDU changeCardPin:"+ plainApdu.toHexString()); -// logger.info("SATOCHIPLIB: C-APDU changeCardPin"); -// ApduResponse rapdu = this.cardTransmit(plainApdu); -// logger.info("SATOCHIPLIB: R-APDU changeCardPin:"+ rapdu.toHexString()); -// -// rapdu.checkAuthOK(); -// return rapdu; -// -// } catch (WrongPINException e) { -// this.pin0 = null; -// throw e; -// } catch (WrongPINLegacyException e) { -// this.pin0 = null; -// throw e; -// } catch (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 cardUnblockPin(byte[] puk) throws Exception { -// ApduCommand plainApdu = new ApduCommand( -// 0xB0, -// INS_UNBLOCK_PIN, -// 0x00, -// 0x00, -// puk -// ); -// -// try{ -// //logger.info("SATOCHIPLIB: C-APDU cardUnblockPin:" + plainApdu.toHexString()); -// logger.info("SATOCHIPLIB: C-APDU cardUnblockPin"); -// ApduResponse rapdu = this.cardTransmit(plainApdu); -// logger.info("SATOCHIPLIB: R-APDU cardUnblockPin:" + rapdu.toHexString()); -// -// rapdu.checkAuthOK(); -// return rapdu; -// -// } catch (WrongPINException e) { -// this.pin0 = null; -// throw e; -// } catch (WrongPINLegacyException e) { -// this.pin0 = null; -// throw e; -// } catch (BlockedPINException e) { -// this.pin0 = null; -// throw e; -// } catch (ResetToFactoryException e) { -// this.pin0 = null; -// throw e; -// } catch (ApduException e){ -// this.pin0 = null; -// throw e; -// } catch (Exception e){ -// this.pin0 = null; -// throw e; -// } -// -// } - /**************************************** * BIP32 * ****************************************/ -// public ApduResponse cardBip32ImportSeed(byte[] masterseed) { -// //TODO: check seed (length...) -// ApduCommand plainApdu = new ApduCommand(0xB0, INS_BIP32_IMPORT_SEED, masterseed.length, 0x00, masterseed); -// -// //logger.info("SATOCHIPLIB: C-APDU cardBip32ImportSeed:" + plainApdu.toHexString()); -// logger.info("SATOCHIPLIB: C-APDU cardBip32ImportSeed"); -// ApduResponse respApdu = this.cardTransmit(plainApdu); -// logger.info("SATOCHIPLIB: R-APDU cardBip32ImportSeed:" + respApdu.toHexString()); -// -// return respApdu; -// } - -// public ApduResponse cardResetSeed(byte[] pin, byte[] chalresponse) { -// -// byte p1 = (byte) pin.length; -// byte[] data; -// if (chalresponse == null) { -// data = new byte[pin.length]; -// System.arraycopy(pin, 0, data, 0, pin.length); -// } else if (chalresponse.length == 20) { -// data = new byte[pin.length + 20]; -// int offset = 0; -// System.arraycopy(pin, 0, data, offset, pin.length); -// offset += pin.length; -// 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_BIP32_RESET_SEED, p1, 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; -// } - -// public byte[][] cardBip32GetExtendedKey() throws Exception { -// if (defaultBip32path == null) { -// defaultBip32path = "m/44'/60'/0'/0/0"; -// } -// return cardBip32GetExtendedKey(defaultBip32path, null, null); -// } - - public byte[][] cardBip32GetExtendedKey(String stringPath, Byte flags, Integer sid) throws Exception { + public byte[][] cardBip32GetExtendedKey(String stringPath) throws Exception { Bip32Path parsedPath = parser.parseBip32PathToBytes(stringPath); - return cardBip32GetExtendedKey(parsedPath, flags, sid); + return cardBip32GetExtendedKey(parsedPath); } - public byte[][] cardBip32GetExtendedKey(Bip32Path parsedPath, Byte flags, Integer sid) throws Exception { + 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()); @@ -646,19 +288,10 @@ public byte[][] cardBip32GetExtendedKey(Bip32Path parsedPath, Byte flags, Intege byte p1 = parsedPath.getDepth().byteValue(); byte optionFlags = (byte) 0x40; - if (flags != null) { - optionFlags = flags; - } byte p2 = optionFlags; byte[] data = parsedPath.getBytes(); - if (sid != null) { - data = Arrays.copyOf(data, data.length + 2); - data[data.length - 2] = (byte) ((sid >> 8) & 0xFF); - data[data.length - 1] = (byte) (sid & 0xFF); - } - while (true) { ApduCommand plainApdu = new ApduCommand( 0xB0, @@ -695,39 +328,21 @@ public byte[][] cardBip32GetExtendedKey(Bip32Path parsedPath, Byte flags, Intege } // success if (respApdu.getSw() == 0x9000) { - logger.warning("SATOCHIPLIB: cardBip32GetExtendedKey: return 0x9000..."); + logger.warning("SATOCHIPLIB: cardBip32GetExtendedKey: return 0x9000"); byte[] response = respApdu.getData(); - if ((optionFlags & 0x04) == 0x04) { // BIP85 - //todo: enable? - logger.warning("SATOCHIPLIB: cardBip32GetExtendedKey: in BIP85"); - extendedKey = parser.parseBip85GetExtendedKey(respApdu)[0]; - extendedKeyHex = parser.toHexString(extendedKey); - } else if ((optionFlags & 0x02) == 0x00) { // BIP32 pubkey - logger.warning("SATOCHIPLIB: cardBip32GetExtendedKey: in BIP32"); - 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];// todo: return array - extendedChaincode = extendedKeyData[1]; - extendedKeyHex = parser.toHexString(extendedKey); - return extendedKeyData; - } else { // BIP32 privkey - byte[][] extendedPrivKeyData = parser.parseBip32GetExtendedKey(respApdu); - extendedPrivKey = extendedPrivKeyData[0]; - extendedPrivKeyHex = parser.toHexString(extendedPrivKey); - return extendedPrivKeyData; + 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; } } } - // todo: only for testing testCardBip32GetExtendedkeyBip85 - public byte[] getExtendedKey() { - return extendedKey; - } - /* * Get the BIP32 xpub for given path. * @@ -739,19 +354,18 @@ public byte[] getExtendedKey() { * Return: * xpub (str): the corresponding xpub value */ - public String cardBip32GetXpub(String path, int xtype, Integer sid) throws Exception { + public String cardBip32GetXpub(String path, int xtype) throws Exception { Bip32Path bip32Path = parser.parseBip32PathToBytes(path); - return cardBip32GetXpub(bip32Path, xtype, sid); + return cardBip32GetXpub(bip32Path, xtype); } - public String cardBip32GetXpub(Bip32Path bip32Path, int xtype, Integer sid) throws Exception { + public String cardBip32GetXpub(Bip32Path bip32Path, int xtype) throws Exception { logger.warning("SATOCHIPLIB: cardBip32GetXpub"); byte[] childPubkey, childChaincode; - byte optionFlags = (byte) 0x40; // Get extended key logger.warning("SATOCHIPLIB: cardBip32GetXpub: getting card cardBip32GetExtendedKey"); - cardBip32GetExtendedKey(bip32Path, optionFlags, sid); + cardBip32GetExtendedKey(bip32Path); logger.warning("SATOCHIPLIB: cardBip32GetXpub: got it "+ extendedKey.length); childPubkey = extendedKey; @@ -770,13 +384,10 @@ public String cardBip32GetXpub(Bip32Path bip32Path, int xtype, Integer sid) thro if (depth > 0) { // Get parent info - //String parentPath = parser.getBip32PathParentPath(path); - //logger.warning("SATOCHIPLIB: cardBip32GetXpub: parentPathString: "+ parentPath); byte[] parentBytePath = Arrays.copyOfRange(bytePath, 0, bytePath.length-4); - //parentBytePath = Arrays.copyOfRange(bytePath, 0, bytePath.length-1-4); Bip32Path parentBip32Path = new Bip32Path(depth-1, parentBytePath); - cardBip32GetExtendedKey(parentBip32Path, optionFlags, sid); + cardBip32GetExtendedKey(parentBip32Path); byte[] parentPubkeyBytes = extendedKey; // Pubkey should be in compressed form @@ -784,8 +395,7 @@ public String cardBip32GetXpub(Bip32Path bip32Path, int xtype, Integer sid) thro parentPubkeyBytes = parser.compressPubKey(parentPubkeyBytes); } - //fingerprintBytes = Arrays.copyOfRange(digestRipeMd160(Sha256Hash.hash(parentPubkeyBytes)), 0, 4); - fingerprintBytes = Arrays.copyOfRange(digestRipeMd160(Wally.sha256(parentPubkeyBytes)), 0, 4); // debug + fingerprintBytes = Arrays.copyOfRange(digestRipeMd160(Wally.sha256(parentPubkeyBytes)), 0, 4); childNumberBytes = Arrays.copyOfRange(bytePath, bytePath.length - 4, bytePath.length); } @@ -799,32 +409,11 @@ public String cardBip32GetXpub(Bip32Path bip32Path, int xtype, Integer sid) thro System.arraycopy(childChaincode, 0, xpubBytes, 13, 32); System.arraycopy(childPubkey, 0, xpubBytes, 45, 33); - if (xpubBytes.length != 78) { - throw new Exception("wrongXpubLength " + xpubBytes.length + " " + 78); - } - - //String xpub = encodeChecked(xpubBytes); String xpub = Wally.base58check_from_bytes(xpubBytes); - logger.warning("SATOCHIPLIB: cardBip32GetXpub() xpub: " + xpub); + logger.info("SATOCHIPLIB: cardBip32GetXpub() xpub: " + xpub); return xpub; } -// private String encodeChecked(byte[] bytes) { -// -// byte[] checksum = calculateChecksum(bytes); -// byte[] checksummedBytes = new byte[bytes.length + 4]; -// System.arraycopy(bytes, 0, checksummedBytes, 0, bytes.length); -// System.arraycopy(checksum, 0, checksummedBytes, bytes.length, 4); -// return Base58.encode(checksummedBytes); -// } - -// private byte[] calculateChecksum(byte[] bytes) { -// byte[] hash = Wally.sha256d(bytes); //Sha256Hash.hashTwice(bytes); -// byte[] checksum = new byte[4]; -// System.arraycopy(hash, 0, checksum, 0, 4); -// return checksum; -// } - public static byte[] digestRipeMd160(byte[] input) { RIPEMD160Digest digest = new RIPEMD160Digest(); digest.update(input, 0, input.length); @@ -833,9 +422,6 @@ public static byte[] digestRipeMd160(byte[] input) { return ripmemdHash; } - // public ApduResponse cardSignMessage(int keyNbr, byte[] pubkey, String message, byte[] hmac, String altcoin){ - // } - /**************************************** * SIGNATURES * ****************************************/ @@ -870,279 +456,4 @@ public ApduResponse cardSignHash(byte keynbr, byte[] txhash, byte[] chalresponse return respApdu; } - /**************************************** - * 2FA commands * - ****************************************/ - - - -// public String getCardLabel() { -// logger.info("SATOCHIPLIB: getCardLabel START"); -// -// ApduCommand plainApdu = new ApduCommand(0xB0, INS_CARD_LABEL, 0x00, 0x01, new byte[0]); -// logger.info("SATOCHIPLIB: C-APDU getCardLabel:"+ plainApdu.toHexString()); -// ApduResponse respApdu = this.cardTransmit(plainApdu); -// logger.info("SATOCHIPLIB: R-APDU getCardLabel:"+ respApdu.toHexString()); -// int sw = respApdu.getSw(); -// String label; -// if (sw == 0x9000){ -// byte labelSize = respApdu.getData()[0]; -// try { -// label = new String(respApdu.getData(), 1, labelSize, StandardCharsets.UTF_8); -// } catch (Exception e) { -// logger.warning("SATOCHIPLIB: getCardLabel UnicodeDecodeError while decoding card label!"); -// label = new String(respApdu.getData(), 1, respApdu.getData().length - 1, StandardCharsets.UTF_8); -// } -// } else if (sw == 0x6D00) { -// logger.info("SATOCHIPLIB: getCardLabel label not set:" + sw); -// label = "(none)"; -// } else { -// logger.warning("SATOCHIPLIB: getCardLabel Error while recovering card label:" + sw); -// label = "(unknown)"; -// } -// return label; -// } -// -// public Boolean setCardLabel(String label) { -// logger.info("SATOCHIPLIB: setCardLabel START"); -// -// byte[] labelData = label.getBytes(StandardCharsets.UTF_8); -// byte[] data = new byte[1 + labelData.length]; -// data[0] = (byte) labelData.length; -// System.arraycopy(labelData, 0, data, 1, labelData.length); -// -// ApduCommand plainApdu = new ApduCommand(0xB0, INS_CARD_LABEL, 0x00, 0x00, data); -// logger.info("SATOCHIPLIB: C-APDU setCardLabel:"+ plainApdu.toHexString()); -// ApduResponse respApdu = this.cardTransmit(plainApdu); -// logger.info("SATOCHIPLIB: R-APDU setCardLabel:"+ respApdu.toHexString()); -// int sw = respApdu.getSw(); -// return sw == 0x9000; -// } - - /**************************************** - * PKI commands * - ****************************************/ - -// public ApduResponse cardExportPersoPubkey(){ -// -// ApduCommand plainApdu = new ApduCommand(0xB0, INS_EXPORT_PKI_PUBKEY, 0x00, 0x00, new byte[0]); -// logger.info("SATOCHIPLIB: C-APDU cardExportPersoPubkey:"+ plainApdu.toHexString()); -// ApduResponse respApdu = this.cardTransmit(plainApdu); -// logger.info("SATOCHIPLIB: R-APDU cardExportPersoPubkey:"+ respApdu.toHexString()); -// -// return respApdu; -// } -// -// public String cardExportPersoCertificate() throws ApduException { -// -// // init -// byte p1 = 0x00; -// byte p2 = 0x01; // init -// ApduCommand plainApdu = new ApduCommand(0xB0, INS_EXPORT_PKI_CERTIFICATE, p1, p2, new byte[0]); -// logger.info("SATOCHIPLIB: C-APDU cardExportPersoCertificate - init:"+ plainApdu.toHexString()); -// ApduResponse respApdu = this.cardTransmit(plainApdu); -// logger.info("SATOCHIPLIB: R-APDU cardExportPersoCertificate - init:"+ respApdu.toHexString()); -// respApdu.checkOK(); -// int sw = respApdu.getSw(); -// byte[] response = null; -// int certificate_size = 0; -// if (sw == 0x9000){ -// response= respApdu.getData(); -// certificate_size= (response[0] & 0xFF) * 256 + (response[1] & 0xFF); -// logger.warning("SATOCHIPLIB: personalization certificate export: code:" + sw + "certificate size: " + certificate_size); -// } else if (sw == 0x6D00){ -// logger.warning("SATOCHIPLIB: Error during personalization certificate export: command unsupported(0x6D00)"); -// return "Error during personalization certificate export: command unsupported(0x6D00)"; -// } else if (sw == 0x0000){ -// logger.warning("SATOCHIPLIB: Error during personalization certificate export: no card present(0x0000)"); -// return "Error during personalization certificate export: no card present(0x0000)"; -// } -// -// if (certificate_size==0){ -// return ""; //new byte[0]; //"(empty)"; -// } -// -// // UPDATE apdu: certificate data in chunks -// p2= 0x02; //update -// byte[] certificate = new byte[certificate_size];//certificate_size*[0] -// short chunk_size = 128; -// byte[] chunk = new byte[chunk_size]; -// int remaining_size = certificate_size; -// int cert_offset = 0; -// byte[] data = new byte[4]; -// while(remaining_size > 128){ -// // data=[ chunk_offset(2b) | chunk_size(2b) ] -// data[0]= (byte) ((cert_offset>>8)&0xFF); -// data[1]= (byte) (cert_offset&0xFF); -// data[2]= (byte) ((chunk_size>>8)&0xFF);; -// data[3]= (byte) (chunk_size & 0xFF); -// plainApdu = new ApduCommand(0xB0, INS_EXPORT_PKI_CERTIFICATE, p1, p2, data); -// logger.warning("SATOCHIPLIB: C-APDU cardExportPersoCertificate - update:"+ plainApdu.toHexString()); -// respApdu = this.cardTransmit(plainApdu); -// logger.warning("SATOCHIPLIB: R-APDU cardExportPersoCertificate - update:"+ respApdu.toHexString()); -// respApdu.checkOK(); -// // update certificate -// response= respApdu.getData(); -// System.arraycopy(response, 0, certificate, cert_offset, chunk_size); -// remaining_size-=chunk_size; -// cert_offset+=chunk_size; -// } -// -// // last chunk -// data[0]= (byte) ((cert_offset>>8)&0xFF); -// data[1]= (byte) (cert_offset&0xFF); -// data[2]= (byte) ((remaining_size>>8)&0xFF);; -// data[3]= (byte) (remaining_size & 0xFF); -// plainApdu = new ApduCommand(0xB0, INS_EXPORT_PKI_CERTIFICATE, p1, p2, data); -// logger.warning("SATOCHIPLIB: C-APDU cardExportPersoCertificate - final:"+ plainApdu.toHexString()); -// respApdu = this.cardTransmit(plainApdu); -// logger.warning("SATOCHIPLIB: R-APDU cardExportPersoCertificate - final:"+ respApdu.toHexString()); -// respApdu.checkOK(); -// // update certificate -// response= respApdu.getData(); -// System.arraycopy(response, 0, certificate, cert_offset, remaining_size); -// cert_offset+=remaining_size; -// -// // parse and return raw certificate -// String cert_pem= parser.convertBytesToStringPem(certificate); -// logger.warning("SATOCHIPLIB: cardExportPersoCertificate checking certificate:" + Arrays.toString(certificate)); -// -// return cert_pem; -// } -// -// public ApduResponse cardChallengeResponsePerso(byte[] challenge_from_host){ -// -// ApduCommand plainApdu = new ApduCommand(0xB0, INS_CHALLENGE_RESPONSE_PKI, 0x00, 0x00, challenge_from_host); -// logger.info("SATOCHIPLIB: C-APDU cardChallengeResponsePerso:"+ plainApdu.toHexString()); -// ApduResponse respApdu = this.cardTransmit(plainApdu); -// logger.info("SATOCHIPLIB: R-APDU cardChallengeResponsePerso:"+ respApdu.toHexString()); -// -// return respApdu; -// } -// -// public String[] cardVerifyAuthenticity(){ -// -// String txt_error=""; -// String txt_ca="(empty)"; -// String txt_subca="(empty)"; -// String txt_device="(empty)"; -// final String FAIL= "FAIL"; -// final String OK= "OK"; -// -// // get certificate from device -// String cert_pem=""; -// try{ -// cert_pem = cardExportPersoCertificate(); -// logger.warning("SATOCHIPLIB: Cert PEM: "+ cert_pem); -// } catch (Exception e){ -// logger.warning("SATOCHIPLIB: Exception in cardVerifyAuthenticity:"+ e); -// txt_error= "Unable to retrieve device certificate!"; -// //String[] out = new String [5]; -// //out[0]={"a","b","c","d"}; -// String[] out = new String [] {FAIL, txt_ca, txt_subca, txt_device, txt_error}; -// return out; -// } -// -// // verify certificate chain -// boolean isValidated= false; -// PublicKey pubkeyDevice= null; -// try{ -// // load certs -// InputStream isCa = this.getClass().getClassLoader().getResourceAsStream("cert/ca.cert"); -// InputStream isSubca; -// if (cardType.equals("satochip")) { -// isSubca = this.getClass().getClassLoader().getResourceAsStream("cert/subca-satochip.cert"); -// } else if (cardType.equals("seedkeeper")) { -// isSubca = this.getClass().getClassLoader().getResourceAsStream("cert/subca-seedkeeper.cert"); -// } else { -// isSubca = this.getClass().getClassLoader().getResourceAsStream("cert/subca-satodime.cert"); -// } -// InputStream isDevice = new ByteArrayInputStream(cert_pem.getBytes(StandardCharsets.UTF_8)); -// // gen certs -// CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509", "BC"); // without BC provider, validation fails... -// Certificate certCa = certificateFactory.generateCertificate(isCa); -// txt_ca= certCa.toString(); -// logger.warning("SATOCHIPLIB: certCa: " + txt_ca); -// Certificate certSubca = certificateFactory.generateCertificate(isSubca); -// txt_subca= certSubca.toString(); -// logger.warning("SATOCHIPLIB: certSubca: " + txt_subca); -// Certificate certDevice = certificateFactory.generateCertificate(isDevice); -// logger.warning("SATOCHIPLIB: certDevice: " + certDevice); -// txt_device= certDevice.toString(); -// logger.warning("SATOCHIPLIB: txtCertDevice: " + txt_device); -// -// pubkeyDevice= certDevice.getPublicKey(); -// logger.warning("SATOCHIPLIB: certDevice pubkey: " + pubkeyDevice.toString()); -// -// // cert chain -// Certificate[] chain= new Certificate[2]; -// chain[0]= certDevice; -// chain[1]= certSubca; -// CertPath certPath = certificateFactory.generateCertPath(Arrays.asList(chain)); -// -// // keystore -// KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); -// ks.load(null, null); -// KeyStore.TrustedCertificateEntry tcEntry= new KeyStore.TrustedCertificateEntry(certCa); -// //KeyStore.TrustedCertificateEntry tcEntry= new KeyStore.TrustedCertificateEntry(certSubca); -// ks.setEntry("SatodimeCA", tcEntry, null); -// -// // validator -// PKIXParameters params = new PKIXParameters(ks); -// params.setRevocationEnabled(false); -// CertPathValidator certValidator = CertPathValidator.getInstance(CertPathValidator.getDefaultType()); // PKIX -// certValidator.validate(certPath, params); -// isValidated=true; -// logger.info("SATOCHIPLIB: Certificate chain validated!"); -// -// }catch (Exception e){ -// logger.warning("SATOCHIPLIB: Exception in cardVerifyAuthenticity:"+ e); -// e.printStackTrace(); -// isValidated=false; -// txt_error= "Failed to validate certificate chain! \r\n\r\n" + e.toString(); -// String[] out = new String [] {FAIL, txt_ca, txt_subca, txt_device, txt_error}; -// return out; -// } -// -// // perform challenge-response with the card to ensure that the key is correctly loaded in the device -// try{ -// SecureRandom random = new SecureRandom(); -// byte[] challenge_from_host= new byte[32]; -// random.nextBytes(challenge_from_host); -// ApduResponse rapduChalresp= cardChallengeResponsePerso(challenge_from_host); -// byte[][] parsedData= parser.parseVerifyChallengeResponsePerso(rapduChalresp); -// byte[] challenge_from_device= parsedData[0]; -// byte[] sig= parsedData[1]; -// -// // build challenge byte[] -// int offset=0; -// String chalHeaderString= "Challenge:"; -// byte[] chalHeaderBytes= chalHeaderString.getBytes(StandardCharsets.UTF_8); -// byte[] chalFullBytes= new byte[chalHeaderBytes.length + 32 + 32]; -// System.arraycopy(chalHeaderBytes, 0, chalFullBytes, offset, chalHeaderBytes.length); -// offset+= chalHeaderBytes.length; -// System.arraycopy(challenge_from_device, 0, chalFullBytes, offset, 32); -// offset+= 32; -// System.arraycopy(challenge_from_host, 0, chalFullBytes, offset, 32); -// -// // verify sig with pubkeyDevice -// byte[] pubkey= new byte[65]; -// byte[] pubkeyEncoded= pubkeyDevice.getEncoded(); -// System.arraycopy(pubkeyEncoded, (pubkeyEncoded.length-65), pubkey, 0, 65); // extract pubkey from ASN1 encoding -// boolean isChalrespOk= parser.verifySig(chalFullBytes, sig, pubkey); -// if (!isChalrespOk){ -// throw new RuntimeException("Failed to verify challenge-response signature!"); -// } -// // TODO: pubkeyDevice should be equal to authentikey -// }catch (Exception e){ -// logger.warning("SATOCHIPLIB: Exception in cardVerifyAuthenticity:"+ e); -// e.printStackTrace(); -// txt_error= "Failed to verify challenge-response! \r\n\r\n" + e.toString(); -// String[] out = new String [] {FAIL, txt_ca, txt_subca, txt_device, txt_error}; -// return out; -// } -// -// String[] out = new String [] {OK, txt_ca, txt_subca, txt_device, txt_error}; -// return out; -// } } diff --git a/hardware/src/main/java/com/satochip/SatochipParser.java b/hardware/src/main/java/com/satochip/SatochipParser.java index 4491b09fd..a834826af 100644 --- a/hardware/src/main/java/com/satochip/SatochipParser.java +++ b/hardware/src/main/java/com/satochip/SatochipParser.java @@ -17,10 +17,8 @@ import java.math.BigInteger; import java.io.IOException; import java.util.Arrays; -import java.util.HashMap; import java.util.ArrayList; import java.util.List; -import java.util.Base64; import java.util.logging.Logger; public class SatochipParser{ @@ -69,35 +67,6 @@ public byte[] compressPubKey(byte[] pubkey) throws Exception { * PARSER * ****************************************/ -// public String getBip32PathParentPath(String bip32path) throws Exception { -// System.out.println("In getBip32PathParentPath"); -// String[] splitPath = bip32path.split("/"); -// if (splitPath.length <= 1) { -// throw new Exception("Invalid BIP32 path: " + bip32path); -// } -// String[] parentPathArray = Arrays.copyOf(splitPath, splitPath.length - 1); -// String parentPath = String.join("/", parentPathArray); -// return parentPath; -// } - - public byte[][] parseBip85GetExtendedKey(ApduResponse rapdu){ - logger.warning("SATOCHIPLIB: parseBip85GetExtendedKey: Start "); - - try { - byte[] data = rapdu.getData(); - logger.warning("SATOCHIPLIB: parseBip85GetExtendedKey data: " + toHexString(data)); - - int entropySize = 256 * (data[0] & 0xFF) + (data[1] & 0xFF); - byte[] entropyBytes = Arrays.copyOfRange(data, 2, 2 + entropySize); - - return new byte[][] {entropyBytes, new byte[0]}; - } catch(Exception e) { - System.out.println("SATOCHIPLIB parseBip85GetExtendedKey() exception: "+e); - e.printStackTrace(); - throw new RuntimeException("Is BouncyCastle in the classpath?", e); - } - } - public Bip32Path parseBip32PathToBytes(String bip32path) throws Exception { logger.info("SATOCHIPLIB: parseBip32PathToBytes: Start "); @@ -266,19 +235,7 @@ public byte[] parseBip32GetAuthentikey(ApduResponse rapdu){ throw new RuntimeException("Exception during Authentikey recovery", e); } } - - public byte[] parseExportPkiPubkey(ApduResponse rapdu){ - try{ - byte[] data= rapdu.getData(); - logger.info("SATOCHIPLIB: parseExportPkiPubkey data: " + toHexString(data)); - // data: [autehntikey(65b) | sig_size(2b - option) | sig(option) ] - byte[] pubkey= new byte[65]; - System.arraycopy(data, 0, pubkey, 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{ @@ -325,25 +282,6 @@ public byte[][] parseBip32GetExtendedKey(ApduResponse rapdu){//todo: return a wr } } - - - public BigInteger[] parse_rsi_from_dersig(byte[] hash, byte[] dersig, byte[] coordx){ - - BigInteger[] sigBig = decodeFromDER(dersig); - - int recid= recoverRecId(hash, sigBig, coordx); - if (recid==-1){ - throw new RuntimeException("Exception in parse_rsv_from_dersig: could not recover recid"); - } - - BigInteger[] rsi= new BigInteger[3]; - rsi[0]= sigBig[0]; - rsi[1]= sigBig[1]; - rsi[2]= BigInteger.valueOf(recid); - - return rsi; - } - /**************************************** * recovery methods * ****************************************/ @@ -559,20 +497,7 @@ private ECPoint decompressKey(BigInteger xBN, boolean yBit) { compEnc[0] = (byte)(yBit ? 0x03 : 0x02); return CURVE.getCurve().decodePoint(compEnc); } - - // public static String toHexString(byte[] raw) { - - // 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))); - //hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F))).append(" "); - // } - // return hex.toString(); - // } - + //based on https://github.com/bitcoinj/bitcoinj/blob/master/core/src/main/java/org/bitcoinj/core/ECKey.java public BigInteger[] decodeFromDER(byte[] bytes) { @@ -644,63 +569,7 @@ public boolean verifySig(byte[] msg, byte[] dersig, byte[] pub) { return false; } } - - /** - * PKI PARSER - **/ - public String convertBytesToStringPem(byte[] certBytes){ - logger.info("SATOCHIPLIB: In convertBytesToStringPem"); - String certBase64Raw= Base64.getEncoder().encodeToString(certBytes); - logger.info("SATOCHIPLIB: certBase64Raw"+ certBase64Raw); - - // divide in fixed size chunk - int chunkSize=64; - String certBase64= "-----BEGIN CERTIFICATE-----\r\n"; - for (int offset=0; offset Date: Mon, 24 Feb 2025 22:33:24 +0100 Subject: [PATCH 16/25] remove unused code --- .../compose/devices/SatochipDevice.kt | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt b/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt index 4755ef56d..e9d37122c 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt @@ -44,31 +44,6 @@ class SatochipDevice constructor( ): Network? = interaction.requestNetwork() companion object : Loggable() { - //const val VENDOR_TREZOR = 0x534c - //const val VENDOR_TREZOR_V2 = 0x1209 - -// private fun hasSuportedVendorId(usbDevice: UsbDevice): Boolean { -// return false -//// val vId = usbDevice.vendorId -//// return (vId == VENDOR_TREZOR || -//// vId == VENDOR_TREZOR_V2) -// } - - // TODO remove -// fun fromUsbDevice( -// deviceManager: DeviceManagerAndroid, -// usbDevice: UsbDevice -// ): SatochipDevice? { -//// if (hasSuportedVendorId(usbDevice)) { -//// return TrezorDevice( -//// context = deviceManager.context, -//// deviceManager = deviceManager, -//// type = ConnectionType.USB, -//// usbDevice = usbDevice, -//// ) -//// } -// return null -// } fun fromNfcDevice( deviceManager: DeviceManagerAndroid, From aa6131c42bc050bb337e53005eb46aba0f5de196 Mon Sep 17 00:00:00 2001 From: Toporin Date: Tue, 25 Feb 2025 09:58:18 +0100 Subject: [PATCH 17/25] Make NFC support more generic Allows to easily add support for additional NFC devices... --- .../common/devices/DeviceManagerAndroid.kt | 53 +++++++++++-------- .../blockstream/common/devices/NfcDevice.kt | 10 ++++ .../common/devices/SatochipCommandSet.kt | 9 ++-- .../compose/devices/SatochipDevice.kt | 25 +++++---- .../com/blockstream/green/di/GreenModules.kt | 7 ++- 5 files changed, 63 insertions(+), 41 deletions(-) create mode 100644 common/src/androidMain/kotlin/com/blockstream/common/devices/NfcDevice.kt 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 b3b9f1c21..2cb9a1fa1 100644 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt @@ -40,6 +40,7 @@ class DeviceManagerAndroid constructor( bleService: Uuid?, peripheral: Peripheral?, isBonded: Boolean?, + nfcDevice: NfcDevice?, activityProvider: ActivityProvider?, ) -> AndroidDevice? ): CardListener, DeviceManager(scope, sessionManager, bluetoothManager, supportedBleDevices) { @@ -115,7 +116,7 @@ class DeviceManagerAndroid constructor( val peripheral = scope.peripheral(advertisement) val bleService = advertisement.uuids.firstOrNull() - deviceMapper.invoke(this, null, bleService, peripheral, advertisement.isBonded(), null) + deviceMapper.invoke(this, null, bleService, peripheral, advertisement.isBonded(), null,null) ?.also { addBluetoothDevice(it) } @@ -172,7 +173,7 @@ class DeviceManagerAndroid constructor( // Jade or UsbDeviceMapper (JadeUsbDevice.fromUsbDevice(deviceManager = this, usbDevice = usbDevice) - ?: deviceMapper.invoke(this, usbDevice, null, null, null, null))?.let { + ?: deviceMapper.invoke(this, usbDevice, null, null, null, null,null))?.let { newDevices += it } } @@ -209,30 +210,36 @@ class DeviceManagerAndroid constructor( override fun onConnected(channel: CardChannel) { println("SATODEBUG DeviceManagerAndroid onConnected: Card is connected") - try { - val cmdSet = SatochipCommandSet(channel) - - val rapduSelect = cmdSet.cardSelect("satochip").checkOK() - - // add device - val newDevices = mutableListOf() - deviceMapper.invoke(this, null, null, null, null, activityProvider)?.let { - newDevices += it - } - println("SATODEBUG DeviceManagerAndroid readCard newDevices: ${newDevices}") - nfcDevices.value = newDevices + for (nfcDeviceType in NfcDeviceType.entries) { + try { + val cmdSet = SatochipCommandSet(channel) + + // try to select applet according to device candidate + cmdSet.cardSelect(nfcDeviceType).checkOK() + + // add device + val nfcDevice = NfcDevice(NfcDeviceType.SATOCHIP) + val newDevices = mutableListOf() + deviceMapper.invoke(this, null, null, null, null, nfcDevice, activityProvider) + ?.let { + newDevices += it + } + println("SATODEBUG DeviceManagerAndroid readCard newDevices: ${newDevices}") + nfcDevices.value = newDevices - // disconnect card - println("SATODEBUG DeviceManagerAndroid onConnected: trigger disconnection!") - onDisconnected() + // disconnect card + println("SATODEBUG DeviceManagerAndroid onConnected: trigger disconnection!") + onDisconnected() - // stop polling - val activity = activityProvider.getCurrentActivity() - nfcAdapter?.disableReaderMode(activity) + // stop polling + val activity = activityProvider.getCurrentActivity() + nfcAdapter?.disableReaderMode(activity) - } catch (e: Exception) { - println("SATODEBUG DeviceManagerAndroid onConnected: an exception has been thrown during card init.") - onDisconnected() + // should probably avoid to connect to multiple NFC devices at the same time? + return + } catch (e: Exception) { + println("SATODEBUG DeviceManagerAndroid onConnected: failed to connect to device: $nfcDeviceType") + } } } 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..50a1ee123 --- /dev/null +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcDevice.kt @@ -0,0 +1,10 @@ +package com.blockstream.common.devices + + +enum class NfcDeviceType { + SATOCHIP +} + +class NfcDevice(val type: NfcDeviceType) { + +} \ 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 index e3047bb7d..f19bd4b63 100644 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.kt +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.kt @@ -29,17 +29,16 @@ class SatochipCommandSet(private val apduChannel: CardChannel) { } // Satochip or... - private var cardType: String? = null + private var cardType: NfcDeviceType = NfcDeviceType.SATOCHIP init { logger.level = Level.WARNING } @Throws(IOException::class) - fun cardSelect(cardType: String): ApduResponse { - val selectApplet = when (cardType) { - "satochip" -> ApduCommand(0x00, 0xA4, 0x04, 0x00, SATOCHIP_AID) - else -> ApduCommand(0x00, 0xA4, 0x04, 0x00, SATOCHIP_AID) + 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()}") diff --git a/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt b/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt index e9d37122c..4ad823b6d 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt @@ -8,6 +8,8 @@ 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 @@ -47,20 +49,21 @@ class SatochipDevice constructor( fun fromNfcDevice( deviceManager: DeviceManagerAndroid, + nfcDevice: NfcDevice, activityProvider: ActivityProvider?, - // TODO add params? - // add AID? ): SatochipDevice? { println("SATODEBUG SatochipDevice fromNfcDevice()") - // todo check AID - return SatochipDevice( - context = deviceManager.context, - deviceManager = deviceManager, - type = ConnectionType.NFC, - usbDevice = null, - activityProvider = activityProvider, - ) - //return null + if (nfcDevice.type == NfcDeviceType.SATOCHIP) { + return SatochipDevice( + context = deviceManager.context, + deviceManager = deviceManager, + type = ConnectionType.NFC, + usbDevice = null, + activityProvider = activityProvider, + ) + } else { + return null + } } 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 8cf5873c0..197d27064 100644 --- a/green/src/main/java/com/blockstream/green/di/GreenModules.kt +++ b/green/src/main/java/com/blockstream/green/di/GreenModules.kt @@ -11,6 +11,7 @@ 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 @@ -54,7 +55,7 @@ val greenModules = module { get(), get(), listOf(LedgerDeviceBLE.SERVICE_UUID.toString(), JadeBleConnection.JADE_SERVICE) - ) { deviceManagerAndroid: DeviceManagerAndroid, usbDevice: UsbDevice?, bleService: Uuid?, peripheral: Peripheral?, isBonded: Boolean?, activityProvider: ActivityProvider? -> + ) { deviceManagerAndroid: DeviceManagerAndroid, usbDevice: UsbDevice?, bleService: Uuid?, peripheral: Peripheral?, isBonded: Boolean?, nfcDevice: NfcDevice?, activityProvider: ActivityProvider? -> usbDevice?.let { TrezorDevice.fromUsbDevice(deviceManager = deviceManagerAndroid, usbDevice = usbDevice) ?: LedgerDevice.fromUsbDevice( @@ -63,7 +64,9 @@ val greenModules = module { ) } ?: peripheral?.let { LedgerDevice.fromScan(deviceManager = deviceManagerAndroid, bleService = bleService, peripheral = peripheral, isBonded = isBonded == true) - } ?: SatochipDevice.fromNfcDevice(deviceManager = deviceManagerAndroid, activityProvider = activityProvider) // SATODEBUG + } ?: nfcDevice?.let { + SatochipDevice.fromNfcDevice(deviceManager = deviceManagerAndroid, nfcDevice= nfcDevice, activityProvider = activityProvider) + } } } binds (arrayOf(DeviceManager::class, DeviceManagerAndroid::class)) single { From 0a185c300df375af4dd832ba74b5b0bb9083c8f6 Mon Sep 17 00:00:00 2001 From: Toporin Date: Wed, 26 Feb 2025 09:37:30 +0100 Subject: [PATCH 18/25] Remove debug traces --- .../common/devices/ConnectionType.kt | 2 +- .../blockstream/common/devices/DeviceBrand.kt | 1 - .../blockstream/common/devices/DeviceModel.kt | 2 +- .../blockstream/common/devices/GreenDevice.kt | 6 ++-- .../com/blockstream/common/gdk/GdkSession.kt | 32 ------------------- .../com/blockstream/common/gdk/data/Device.kt | 1 - .../common/gdk/device/GdkHardwareWallet.kt | 2 +- .../common/managers/DeviceManager.kt | 3 -- .../common/models/GreenViewModel.kt | 11 +------ .../models/devices/AbstractDeviceViewModel.kt | 1 - .../models/devices/DeviceInfoViewModel.kt | 23 ------------- .../models/devices/DeviceListViewModel.kt | 3 -- .../models/devices/DeviceScanViewModel.kt | 23 ------------- .../managers/DeviceConnectionManager.kt | 2 -- .../screens/devices/DeviceInfoScreen.kt | 2 -- 15 files changed, 6 insertions(+), 108 deletions(-) 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 696595fb1..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, NFC // SATODEBUG + 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 d69ffd264..07ea3619b 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/devices/DeviceBrand.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/devices/DeviceBrand.kt @@ -12,7 +12,6 @@ enum class DeviceBrand(val brand: String) { val isJade get() = this == Blockstream - // SATODEBUG val isSatochip get() = this == Satochip 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 0c154f674..a5d4b9249 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/devices/DeviceModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/devices/DeviceModel.kt @@ -12,7 +12,7 @@ enum class DeviceModel(val deviceModel: String) { LedgerGeneric("Ledger"), LedgerNanoS("Ledger Nano S"), LedgerNanoX("Ledger Nano X"), - SatochipGeneric("Satochip"), // SATODEBUG + SatochipGeneric("Satochip"), Generic("Generic Hardware Wallet"); val deviceBrand: DeviceBrand 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 73ac86823..d00045e4f 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/devices/GreenDevice.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/devices/GreenDevice.kt @@ -40,7 +40,7 @@ interface GreenDevice: DeviceOperatingNetwork { val isBonded: Boolean val isUsb: Boolean val isBle: Boolean - val isNfc: Boolean // SATODEBUG + val isNfc: Boolean val deviceState: StateFlow val firmwareState: StateFlow val name: String @@ -48,7 +48,7 @@ interface GreenDevice: DeviceOperatingNetwork { val isJade: Boolean val isTrezor: Boolean val isLedger: Boolean - val isSatochip: Boolean // SATODEBUG + val isSatochip: Boolean val isOffline: Boolean val isConnected: Boolean val heartbeat: Long @@ -107,7 +107,6 @@ abstract class GreenDeviceImpl constructor( override val isBle get() = type == ConnectionType.BLUETOOTH - // SATODEBUG override val isNfc get() = type == ConnectionType.NFC @@ -126,7 +125,6 @@ abstract class GreenDeviceImpl constructor( override val isLedger: Boolean get() = deviceBrand.isLedger - // SATODEBUG override val isSatochip: Boolean get() = deviceBrand.isSatochip 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 f12826b4b..771bfe8f1 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt @@ -1203,13 +1203,6 @@ class GdkSession constructor( isCreate: Boolean, isRestore: Boolean, ): LoginData { - println("SATODEBUG GdkSession loginWithMnemonic() wallet: " + wallet) - println("SATODEBUG GdkSession loginWithMnemonic() loginCredentialsParams: " + loginCredentialsParams) - println("SATODEBUG GdkSession loginWithMnemonic() START") - println("SATODEBUG GdkSession loginWithMnemonic() START") - println("SATODEBUG GdkSession loginWithMnemonic() START") - - return loginWithLoginCredentials( prominentNetwork = prominentNetwork(isTestnet), wallet = wallet, @@ -1289,13 +1282,6 @@ class GdkSession constructor( hardwareWalletResolver: HardwareWalletResolver, hwInteraction: HardwareWalletInteraction? = null, ): LoginData { - println("SATODEBUG GdkSession loginWithDevice() START") - println("SATODEBUG GdkSession loginWithDevice() START wallet: " + wallet) - println("SATODEBUG GdkSession loginWithDevice() START device: " + device) - println("SATODEBUG GdkSession loginWithDevice() START derivedLightningMnemonic: " + derivedLightningMnemonic) - println("SATODEBUG GdkSession loginWithDevice() START hardwareWalletResolver: " + hardwareWalletResolver) - - // If last used network is Lightning, change to bitcoin as the ln network can't be used for login val lastUsedNetwork = (wallet.activeNetwork .takeIf { !Network.isLightning(it) } ?: Network.ElectrumMainnet) @@ -1343,15 +1329,6 @@ class GdkSession constructor( hardwareWalletResolver: HardwareWalletResolver? = null, hwInteraction: HardwareWalletInteraction? = null, ): LoginData { - println("SATODEBUG GdkSession loginWithLoginCredentials() START") - println("SATODEBUG GdkSession loginWithLoginCredentials() START prominentNetwork: " + prominentNetwork) - println("SATODEBUG GdkSession loginWithLoginCredentials() START wallet: " + wallet) - println("SATODEBUG GdkSession loginWithLoginCredentials() START walletLoginCredentialsParams: " + walletLoginCredentialsParams) - println("SATODEBUG GdkSession loginWithLoginCredentials() START appGreenlightCredentials: " + appGreenlightCredentials) - println("SATODEBUG GdkSession loginWithLoginCredentials() START device: " + device) - println("SATODEBUG GdkSession loginWithLoginCredentials() START appGreenlightCredentials: " + appGreenlightCredentials) - println("SATODEBUG GdkSession loginWithLoginCredentials() START derivedLightningMnemonic: " + derivedLightningMnemonic) - isWatchOnly = walletLoginCredentialsParams.isWatchOnly isNoBlobWatchOnly = isWatchOnly && richWatchOnly == null isRichWatchOnly = isWatchOnly && richWatchOnly != null @@ -1660,8 +1637,6 @@ class GdkSession constructor( initAccount: Long?, initializeSession: Boolean ) { - println("SATODEBUG GdkSession onLoginSuccess() START") - _isConnectedState.value = true xPubHashId = if(isNoBlobWatchOnly) loginData.networkHashId else loginData.xpubHashId @@ -1675,8 +1650,6 @@ class GdkSession constructor( } private suspend fun initializeSessionData(initNetwork: String?, initAccount: Long?) { - println("SATODEBUG GdkSession initializeSessionData() START") - // Check if active account index was archived from 1) a different client (multisig) or 2) from cached Singlesig hww session // Expect refresh = true to be already called updateAccounts() @@ -1760,7 +1733,6 @@ class GdkSession constructor( connectionParams = createConnectionParams(network), loginCredentialsParams = loginCredentialsParams?.takeIf { !it.mnemonic.isNullOrBlank() } ?: (gdkHwWallet ?: this.gdkHwWallet)?.let { - println("SATODEBUG GdkSession getWalletIdentifier ") LoginCredentialsParams( masterXpub = it.getXpubs(network, listOf(listOf()), hwInteraction).first() ) @@ -2761,10 +2733,6 @@ class GdkSession constructor( suspend fun signTransaction(network: Network, createTransaction: CreateTransaction): CreateTransaction = if(network.isLightning){ createTransaction // no need to sign on gdk side }else{ - logger.d { "SATODEBUG GdkSession signTransaction() network: ${network}" } - logger.d { "SATODEBUG GdkSession signTransaction() createTransaction: ${createTransaction}" } - logger.d { "SATODEBUG GdkSession signTransaction() createTransaction.jsonElement: ${createTransaction.jsonElement}" } - authHandler( network, gdk.signTransaction(gdkSession(network), createTransaction = createTransaction.jsonElement!!) 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 04b1a71a8..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,7 +35,6 @@ data class Device constructor( val isLedger get() = name.lowercase() == "ledger" - // SATODEBUG val isSatochip get() = name.lowercase() == "satochip" 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 ad2c1064a..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,7 +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?)// satodebug + 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 99d0a8293..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,7 +55,6 @@ open class DeviceManager constructor( protected val usbDevices = MutableStateFlow>(listOf()) private val bleDevices = MutableStateFlow>(listOf()) - // TODO SATODEBUG protected val nfcDevices = MutableStateFlow>(listOf()) private val _status = MutableStateFlow(ScanStatus.Stopped) @@ -73,7 +72,6 @@ open class DeviceManager constructor( } } - // SATODEBUG val devices = combine(usbDevices, bleDevices, disconnectEvent, nfcDevices) { usb, ble, _, nfc-> ble.filter { it.deviceState.value == DeviceState.CONNECTED } + usb + nfc }.stateIn(scope, SharingStarted.Eagerly, emptyList()) @@ -109,7 +107,6 @@ open class DeviceManager constructor( } fun getDevice(deviceId: String?): GreenDevice? { - logger.d { "SATODEBUG DeviceManager getDevice Start deviceId: $deviceId" } return devices.value.find { it.connectionIdentifier == deviceId } // Check if device is already in a session ?: sessionManager.getConnectedHardwareWalletSessions() 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 fec938334..37ff633ef 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/GreenViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/GreenViewModel.kt @@ -238,7 +238,6 @@ open class GreenViewModel constructor( //private var _deviceRequest: CompletableDeferred? = null //satodebug use compagnon object - private var _bootstrapped: Boolean = false open val isLoginRequired: Boolean = greenWalletOrNull != null @@ -397,7 +396,6 @@ open class GreenViewModel constructor( } open suspend fun handleEvent(event: Event) { - println("SATODEBUG GreenViewModel handleEvent() event: $event") when(event){ is Events.ProvideCipher -> { event.platformCipher?.also { @@ -510,13 +508,11 @@ open class GreenViewModel constructor( } is Events.DeviceRequestResponse -> { - println("SATODEBUG GreenViewModel handleEvent() DeviceRequestResponse event: $event, deviceRequest: $_deviceRequest") if(event.data == null){ _deviceRequest?.completeExceptionally(Exception("id_action_canceled")) }else{ _deviceRequest?.complete(event.data) } - println("SATODEBUG After completing deviceRequest: $_deviceRequest, isCompleted: ${_deviceRequest?.isCompleted}") } is Events.SelectDenomination -> { @@ -879,15 +875,10 @@ open class GreenViewModel constructor( } final override fun requestPassphrase(deviceBrand: DeviceBrand?): String { - println("SATODEBUG GreenViewModel requestPassphrase start") return CompletableDeferred().let { - println("SATODEBUG GreenViewModel requestPassphrase continue") _deviceRequest = it - println("SATODEBUG _deviceRequest before posting side effect: $_deviceRequest") postSideEffect(SideEffects.DeviceRequestPassphrase) - println("SATODEBUG _deviceRequest after posting side effect: $_deviceRequest") runBlocking { - println("SATODEBUG GreenViewModel requestPassphrase runblocking") it.await() } } @@ -1053,7 +1044,7 @@ open class GreenViewModel constructor( companion object: Loggable(){ fun preview() = object : GreenViewModel() { } - private var _deviceRequest: CompletableDeferred? = null //satodebug + private var _deviceRequest: CompletableDeferred? = null } } diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/AbstractDeviceViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/AbstractDeviceViewModel.kt index 874845467..7bdbdb675 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/AbstractDeviceViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/AbstractDeviceViewModel.kt @@ -94,7 +94,6 @@ abstract class AbstractDeviceViewModel constructor( override suspend fun handleEvent(event: Event) { super.handleEvent(event) - println("SATODEBUG AbstractDeviceViewModel handleEvent() event: $event") if(event is LocalEvents.Refresh) { deviceManager.refreshDevices() diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceInfoViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceInfoViewModel.kt index 44e55e9f2..746c24f23 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceInfoViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceInfoViewModel.kt @@ -60,21 +60,17 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs } init { - println("SATODEBUG DeviceInfoViewModel init()") deviceOrNull = deviceManager.getDevice(deviceId) if(deviceOrNull == null){ - println("SATODEBUG DeviceInfoViewModel init() device is null!") postSideEffect(SideEffects.NavigateBack()) }else { if (device.gdkHardwareWallet == null) { - println("SATODEBUG DeviceInfoViewModel init() gdkHardwareWallet is null") connectDevice() } device.deviceState.onEach { - println("SATODEBUG DeviceInfoViewModel init() deviceState loop: $it") // Device went offline if (it == DeviceState.DISCONNECTED) { postSideEffect(SideEffects.Snackbar(StringHolder(stringResource = Res.string.id_your_device_was_disconnected))) @@ -84,7 +80,6 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs } viewModelScope.launch { - println("SATODEBUG DeviceInfoViewModel init() viewModelScope.launch") _navData.value = NavData( title = deviceOrNull?.deviceBrand?.name, onBackPressed = { @@ -114,12 +109,10 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs override suspend fun handleEvent(event: Event) { super.handleEvent(event) - println("SATODEBUG DeviceInfoViewModel handleEvent() event: $event") if (event is LocalEvents.AuthenticateAndContinue) { authenticateAndContinue(event.updateFirmwareFromChannel) } else if (event is LocalEvents.SelectEnviroment) { - println("SATODEBUG DeviceInfoViewModel handleEvent() LocalEvents.SelectEnviroment") if (event.isTestnet == null) { requestNetworkEmitter?.completeExceptionally(Exception("id_action_canceled")) } else { @@ -131,14 +124,11 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs } private fun connectDevice() { - println("SATODEBUG DeviceInfoViewModel connectDevice()") doAsync({ - println("SATODEBUG DeviceInfoViewModel connectDevice() doAsync start") deviceConnectionManager.connectDevice(device, sessionManager.httpRequestHandler, this).also { countly.hardwareConnect(device) } }, onSuccess = { - println("SATODEBUG DeviceInfoViewModel connectDevice() doAsync onSuccess") deviceIsConnected.value = true countly.hardwareConnected(device) _jadeIsUninitialized.value = it.isJadeUninitialized == true @@ -155,7 +145,6 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs } }, onError = { - println("SATODEBUG DeviceInfoViewModel connectDevice() doAsync onError") it.printStackTrace() if (it is ConnectionLostException) { @@ -167,12 +156,9 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs } private fun authenticateAndContinue(updateFirmwareFromChannel: String? = null) { - println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() start updateFirmwareFromChannel: "+ updateFirmwareFromChannel) - println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() device.gdkHardwareWallet: " + device.gdkHardwareWallet) val gdkHardwareWallet = device.gdkHardwareWallet ?: return doAsync({ - println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() doAsync") // Authenticate device if needed deviceConnectionManager.authenticateDeviceIfNeeded( gdkHardwareWallet = gdkHardwareWallet, @@ -189,10 +175,8 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs ) val network = device.getOperatingNetwork(device, gdk, interaction = this)!! - println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() network: " + network) val isEphemeral = !settingsManager.appSettings.rememberHardwareDevices - println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() isEphemeral: " + isEphemeral) val previousSession = (if (device.isLedger) { sessionManager.getDeviceSessionForNetworkAllPolicies(device, network, isEphemeral) @@ -203,7 +187,6 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs isEphemeral ) }) - println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() previousSession: " + previousSession) if (previousSession != null) { // Session already setup @@ -216,14 +199,11 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs // Disconnect any previous hww connection it.disconnect() } - println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() session: " + session) val walletHashId = getWalletHashId(session, network, device) - println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() walletHashId: " + walletHashId) // Disable Jade wallet fingerprint, keep the device name // getWalletName(session, network, device) val walletName = device.name - println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() walletName: " + walletName) val wallet: GreenWallet if (isEphemeral) { @@ -275,7 +255,6 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs ).toSet().toList() // Make it unique wallet.deviceIdentifiers = combinedLoginCredentials - println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() wallet.deviceIdentifiers: " + wallet.deviceIdentifiers) if (isNewWallet) { database.insertWallet(wallet) @@ -284,7 +263,6 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs } session = sessionManager.getWalletSessionOrCreate(wallet) - println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() session: " + session) countly.importWallet(session) } @@ -293,7 +271,6 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs }, postAction = { onProgress.value = it == null }, onSuccess = { - println("SATODEBUG DeviceInfoViewModel authenticateAndContinue() onSuccess: ") disconnectDeviceOnCleared = false deviceManager.savedDevice = device diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceListViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceListViewModel.kt index 8699270f0..3fed30de4 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceListViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceListViewModel.kt @@ -42,7 +42,6 @@ class DeviceListViewModel(isJade: Boolean = true) : }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), listOf()) init { - println("SATODEBUG DeviceListViewModel init()") viewModelScope.launch { _navData.value = NavData(title = if (isJade) "Blockstream Jade" else "", actions = listOfNotNull( @@ -57,7 +56,6 @@ class DeviceListViewModel(isJade: Boolean = true) : override suspend fun handleEvent(event: Event) { super.handleEvent(event) - println("SATODEBUG DeviceListViewModel handleEvent() event: $event") if(event is LocalEvents.ConnectViaQR){ if(isJade) { @@ -72,7 +70,6 @@ class DeviceListViewModel(isJade: Boolean = true) : postSideEffect(SideEffects.NavigateTo(NavigateDestinations.ImportPubKey(deviceModel = DeviceModel.Generic))) } }else if (event is LocalEvents.SelectDevice) { - println("SATODEBUG DeviceListViewModel handleEvent() event is SelectDevice") val navigateTo = SideEffects.NavigateTo(NavigateDestinations.DeviceInfo(deviceId = event.device.connectionIdentifier)) diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceScanViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceScanViewModel.kt index 228237585..1367ab7d0 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceScanViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceScanViewModel.kt @@ -43,14 +43,9 @@ class DeviceScanViewModel(greenWallet: GreenWallet) : } init { - println("SATODEBUG DeviceScanViewModel init") - println("SATODEBUG DeviceScanViewModel init session: $session") - println("SATODEBUG DeviceScanViewModel init session.device: ${session.device}") - println("SATODEBUG DeviceScanViewModel init session.isConnected: ${session.isConnected}") session.device.takeIf { session.isConnected }?.also { device -> deviceManager.savedDevice = device - println("SATODEBUG DeviceScanViewModel init deviceManager.savedDevice: ${deviceManager.savedDevice}") postSideEffect( SideEffects.NavigateTo( @@ -60,29 +55,23 @@ class DeviceScanViewModel(greenWallet: GreenWallet) : ) ) ) - println("SATODEBUG DeviceScanViewModel init after postSideEffect") } ?: run { combine(deviceFlow, deviceManager.devices) { _, devices -> - println("SATODEBUG DeviceScanViewModel init run combine devices: $devices") if(deviceFlow.value == null) { var foundDevice = devices.firstOrNull { device -> - println("SATODEBUG DeviceScanViewModel init run combine step1 device: $device") greenWallet.deviceIdentifiers?.any { it.uniqueIdentifier == device.uniqueIdentifier } == true } - println("SATODEBUG DeviceScanViewModel init run combine step2 foundDevice: $foundDevice") // Find a BLE device or request a usb authentication foundDevice = foundDevice ?: devices.firstOrNull { it.needsUsbPermissionsToIdentify() } - println("SATODEBUG DeviceScanViewModel init run combine step3 foundDevice: $foundDevice") // TODO if device is disconnected, do not rescan it!!!! foundDevice?.also { - println("SATODEBUG DeviceScanViewModel init run combine step4 foundDevice: $foundDevice") selectDevice(it) } } @@ -103,15 +92,12 @@ class DeviceScanViewModel(greenWallet: GreenWallet) : } private fun selectDevice(device: GreenDevice) { - println("SATODEBUG DeviceScanViewModel selectDevice() device: $device") _deviceFlow.value = device if (device.hasPermissions()) { doAsync({ - println("SATODEBUG DeviceScanViewModel selectDevice() doAsync START") if (device.gdkHardwareWallet == null) { - println("SATODEBUG DeviceScanViewModel selectDevice() doAsync device.gdkHardwareWallet is NULL") session.disconnect() deviceConnectionManager.connectDevice( device, @@ -121,16 +107,12 @@ class DeviceScanViewModel(greenWallet: GreenWallet) : countly.hardwareConnect(device) } - println("SATODEBUG DeviceScanViewModel selectDevice() doAsync device.gdkHardwareWallet: ${device.gdkHardwareWallet}") val gdkHardwareWallet = device.gdkHardwareWallet ?: throw Exception("Not HWWallet initiated") - println("SATODEBUG DeviceScanViewModel selectDevice() doAsync BEFORE deviceConnectionManager.authenticateDeviceIfNeeded()") deviceConnectionManager.authenticateDeviceIfNeeded(httpRequestHandler = sessionManager.httpRequestHandler, interaction = this, gdkHardwareWallet = gdkHardwareWallet) - println("SATODEBUG DeviceScanViewModel selectDevice() doAsync AFTER deviceConnectionManager.authenticateDeviceIfNeeded()") val network = device.getOperatingNetworkForEnviroment(device, gdk, greenWallet.isTestnet) ?: throw Exception("No network is available") - println("SATODEBUG DeviceScanViewModel selectDevice() doAsync network: $network") if(greenWallet.isTestnet != network.isTestnet){ throw Exception("The device is operating on the wrong Environment") @@ -142,7 +124,6 @@ class DeviceScanViewModel(greenWallet: GreenWallet) : } val walletHashId = getWalletHashId(session, network, device) - println("SATODEBUG DeviceScanViewModel selectDevice() doAsync walletHashId: $walletHashId") if (greenWallet.xPubHashId.isNotBlank() && greenWallet.xPubHashId != walletHashId) { // Disconnect only if there are no other connected sessions @@ -191,18 +172,15 @@ class DeviceScanViewModel(greenWallet: GreenWallet) : greenWallet to device }, onError = { - println("SATODEBUG DeviceScanViewModel selectDevice() onError : ${it}") if (it !is ConnectionLostException) { postSideEffect(SideEffects.ErrorSnackbar(it)) } _deviceFlow.value = null }, onSuccess = { - println("SATODEBUG DeviceScanViewModel selectDevice() onSuccess : ${it}") disconnectDeviceOnCleared = false deviceManager.savedDevice = it.second - println("SATODEBUG DeviceScanViewModel selectDevice() onSuccess BEFORE postSideEffect NavigateTo(NavigateDestinations.Login)") postSideEffect( SideEffects.NavigateTo( NavigateDestinations.Login( @@ -212,7 +190,6 @@ class DeviceScanViewModel(greenWallet: GreenWallet) : ) ) countly.hardwareConnected(device) - println("SATODEBUG DeviceScanViewModel selectDevice() onSuccess END") }) } else { askForPermission(device) diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManager.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManager.kt index 041f7c934..d61f2b679 100644 --- a/compose/src/commonMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManager.kt +++ b/compose/src/commonMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManager.kt @@ -28,7 +28,6 @@ open class DeviceConnectionManager( @Throws(Exception::class) override suspend fun connectDevice(device: GreenDevice, httpRequestHandler: HttpRequestHandler, interaction: HardwareConnectInteraction): ConnectionResult { - println("SATODEBUG DeviceConnectionManager connectDevice() device: $device") device.frozeHeartbeat() try { @@ -106,7 +105,6 @@ open class DeviceConnectionManager( gdkHardwareWallet: GdkHardwareWallet, jadeFirmwareManager: JadeFirmwareManager? ) { - println("SATODEBUG DeviceConnectionManager authenticateDeviceIfNeeded() start") if (gdkHardwareWallet is JadeHWWallet && gdkHardwareWallet.getVersionInfo().jadeState != JadeState.READY) { try { gdkHardwareWallet.authenticate(interaction, jadeFirmwareManager ?: JadeFirmwareManager( diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceInfoScreen.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceInfoScreen.kt index 94d198e86..bd3b0d0c2 100644 --- a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceInfoScreen.kt +++ b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceInfoScreen.kt @@ -68,8 +68,6 @@ data class DeviceInfoScreen(val deviceId: String) : Screen, Parcelable { @Composable override fun Content() { - println("SATODEBUG DeviceInfoScreen Content() Start deviceId: $deviceId") - val viewModel = koinScreenModel { parametersOf(deviceId) } From 234bf437bddcec78653ba7ec11cdfdf859165399 Mon Sep 17 00:00:00 2001 From: Toporin Date: Wed, 26 Feb 2025 09:39:33 +0100 Subject: [PATCH 19/25] Remove more debug traces --- .../common/devices/DeviceManagerAndroid.kt | 38 +--- .../compose/devices/SatochipDevice.kt | 1 - .../DeviceConnectionManagerAndroid.kt | 8 +- .../compose/extensions/Resources.kt | 2 +- .../managers/DeviceConnectionManager.kt | 1 - .../screens/devices/DeviceListScreen.kt | 2 - .../blockstream/compose/utils/SideEffects.kt | 17 +- .../greenbits/wallets/SatochipHWWallet.java | 169 ++++++------------ 8 files changed, 65 insertions(+), 173 deletions(-) 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 2cb9a1fa1..7c8282f68 100644 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt @@ -82,7 +82,6 @@ class DeviceManagerAndroid constructor( } init { - logger.i { "SATODEBUG DeviceManagerAndroid init() start" } val intentFilter = IntentFilter().also { it.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED) it.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED) @@ -98,15 +97,11 @@ class DeviceManagerAndroid constructor( ) - scanNfcDevices() // SATODEBUG + scanNfcDevices() scanUsbDevices() - - logger.i { "SATODEBUG DeviceManagerAndroid init() end" } } - // SATODEBUG: for bluetooth? override fun advertisedDevice(advertisement: PlatformAdvertisement) { - logger.i { "SATODEBUG advertisedDevice() start" } val isJade = advertisement.isJade // Jade is added in Common code @@ -121,13 +116,11 @@ class DeviceManagerAndroid constructor( addBluetoothDevice(it) } } - logger.i { "SATODEBUG advertisedDevice() end" } } fun hasPermissions(device: UsbDevice) = usbManager.hasPermission(device) fun askForUsbPermissions(device: UsbDevice, onSuccess: (() -> Unit), onError: ((throwable: Throwable?) -> Unit)? = null) { - logger.i { "SATODEBUG askForUsbPermissions() start" } onPermissionSuccess = WeakReference(onSuccess) onPermissionError = onError?.let { WeakReference(it) } val permissionIntent = PendingIntent.getBroadcast(context, 748, Intent(ACTION_USB_PERMISSION).also { @@ -135,28 +128,21 @@ class DeviceManagerAndroid constructor( it.putExtra(UsbManager.EXTRA_DEVICE, device) }, FLAG_IMMUTABLE) usbManager.requestPermission(device, permissionIntent) - logger.i { "SATODEBUG askForUsbPermissions() end" } } override fun refreshDevices(){ super.refreshDevices() - logger.i { "SATODEBUG refreshDevices() start" } - scanNfcDevices() // SATODEBUG + scanNfcDevices() scanUsbDevices() - logger.i { "SATODEBUG refreshDevices() end" } } fun scanUsbDevices() { - logger.i { "SATODEBUG scanUsbDevices() start" } logger.i { "Scan for USB devices" } val newUsbDevices = usbManager.deviceList.values - logger.i { "SATODEBUG scanUsbDevices() newUsbDevices: $newUsbDevices" } - // Disconnect devices val oldDevices = usbDevices.value.filter { - logger.i { "SATODEBUG scanUsbDevices() usbDevice: $it" } if(newUsbDevices.contains(it.toAndroidDevice()?.usbDevice)){ true }else{ @@ -164,11 +150,9 @@ class DeviceManagerAndroid constructor( false } } - logger.i { "SATODEBUG scanUsbDevices() oldDevices: $oldDevices" } val newDevices = mutableListOf() for (usbDevice in newUsbDevices){ - logger.i { "SATODEBUG scanUsbDevices() usbDevice: $usbDevice" } if(oldDevices.find { it.toAndroidDevice()?.usbDevice == usbDevice } == null) { // Jade or UsbDeviceMapper @@ -178,20 +162,16 @@ class DeviceManagerAndroid constructor( } } } - logger.i { "SATODEBUG scanUsbDevices() newDevices: $newDevices" } usbDevices.value = oldDevices + newDevices - logger.i { "SATODEBUG scanUsbDevices() end" } } fun scanNfcDevices() { - logger.i { "SATODEBUG scanNfcDevices() start" } logger.i { "Scan for NFC devices" } val cardManager = NfcCardManager() cardManager.setCardListener(this) cardManager.start() - logger.i { "SATODEBUG scanNfcDevices() after cardManager start" } val activity = activityProvider.getCurrentActivity() @@ -201,15 +181,10 @@ class DeviceManagerAndroid constructor( NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_NFC_B or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, null ) - logger.d { "SATODEBUG scanNfcDevices() after nfcAdapter" } - - - logger.i { "SATODEBUG scanNfcDevices() end" } } override fun onConnected(channel: CardChannel) { - - println("SATODEBUG DeviceManagerAndroid onConnected: Card is connected") + logger.i { "SATODEBUG DeviceManagerAndroid onConnected" } for (nfcDeviceType in NfcDeviceType.entries) { try { val cmdSet = SatochipCommandSet(channel) @@ -224,11 +199,10 @@ class DeviceManagerAndroid constructor( ?.let { newDevices += it } - println("SATODEBUG DeviceManagerAndroid readCard newDevices: ${newDevices}") + logger.i { "SATODEBUG DeviceManagerAndroid onConnected newDevices: ${newDevices}" } nfcDevices.value = newDevices // disconnect card - println("SATODEBUG DeviceManagerAndroid onConnected: trigger disconnection!") onDisconnected() // stop polling @@ -238,13 +212,13 @@ class DeviceManagerAndroid constructor( // should probably avoid to connect to multiple NFC devices at the same time? return } catch (e: Exception) { - println("SATODEBUG DeviceManagerAndroid onConnected: failed to connect to device: $nfcDeviceType") + logger.i { "SATODEBUG DeviceManagerAndroid onConnected: failed to connect to device: $nfcDeviceType" } } } } override fun onDisconnected() { - println("SATODEBUG DeviceManagerAndroid onDisconnected: Card disconnected!") + logger.i { "SATODEBUG DeviceManagerAndroid onDisconnected: Card disconnected!" } } diff --git a/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt b/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt index 4ad823b6d..180f94fdf 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt @@ -52,7 +52,6 @@ class SatochipDevice constructor( nfcDevice: NfcDevice, activityProvider: ActivityProvider?, ): SatochipDevice? { - println("SATODEBUG SatochipDevice fromNfcDevice()") if (nfcDevice.type == NfcDeviceType.SATOCHIP) { return SatochipDevice( context = deviceManager.context, 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 bb7d92762..ddcbfd4cc 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt @@ -62,7 +62,7 @@ class DeviceConnectionManagerAndroid constructor( } ?: (device as? LedgerDevice)?.let { connectLedgerDevice(it, interaction) } ?: (device as? SatochipDevice)?.let { - connectSatochipDevice(it, interaction) // SATODEBUG + connectSatochipDevice(it, interaction) } ?: super.connectDevice(device, httpRequestHandler, interaction)) } @@ -153,7 +153,6 @@ class DeviceConnectionManagerAndroid constructor( } private suspend fun connectSatochipDevice(device: SatochipDevice, interaction: HardwareConnectInteraction): ConnectionResult { - logger.i {"SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() start device: $device"} val satoDevice = com.blockstream.common.gdk.data.Device( name = "Satochip", @@ -167,16 +166,11 @@ class DeviceConnectionManagerAndroid constructor( val pin: String? = null; //val pin: String? = satochipInteraction?.requestPassphrase(DeviceBrand.Satochip) - println("SATODEBUG DeviceConnectionManagerAndroid onConnected(): PIN: " + pin) // provide activity and context needed for NFC val activity: Activity? = device.activityProvider?.getCurrentActivity() - println("SATODEBUG DeviceConnectionManagerAndroid onConnected() creating gdkHardwareWallet") device.gdkHardwareWallet = SatochipHWWallet(satoDevice, pin, activity, device.context) - println("SATODEBUG DeviceConnectionManagerAndroid onConnected() created gdkHardwareWallet!") - - logger.i { "SATODEBUG DeviceConnectionManagerAndroid connectSatochipDevice() end" } return ConnectionResult() } 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 3a14f2a78..61849f6d2 100644 --- a/compose/src/commonMain/kotlin/com/blockstream/compose/extensions/Resources.kt +++ b/compose/src/commonMain/kotlin/com/blockstream/compose/extensions/Resources.kt @@ -125,7 +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 // todo SATODEBUG + DeviceModel.SatochipGeneric -> Res.drawable.nfc_scan } fun DeviceModel.actionIcon(): DrawableResource = when (this) { diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManager.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManager.kt index d61f2b679..573031490 100644 --- a/compose/src/commonMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManager.kt +++ b/compose/src/commonMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManager.kt @@ -139,6 +139,5 @@ open class DeviceConnectionManager( // force update if needed jadeFirmwareManager.checkFirmware(jade = gdkHardwareWallet.jade) } - println("SATODEBUG DeviceConnectionManager authenticateDeviceIfNeeded() end") } } \ No newline at end of file diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceListScreen.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceListScreen.kt index f564ca83a..7f3e249ba 100644 --- a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceListScreen.kt +++ b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceListScreen.kt @@ -92,7 +92,6 @@ data class DeviceListScreen(val isJade: Boolean) : Screen, Parcelable { @Composable override fun Content() { - println("SATODEBUG DeviceListScreen Content() Start isJade: $isJade") val viewModel = koinScreenModel { parametersOf(isJade) @@ -116,7 +115,6 @@ data class DeviceListScreen(val isJade: Boolean) : Screen, Parcelable { @Composable fun DeviceListItem(device: GreenDevice, modifier: Modifier, onClick: () -> Unit) { - println("SATODEBUG DeviceListScreen DeviceListItem() Start device: $device") GreenCard(onClick = onClick, padding = 0, modifier = modifier) { Image( painter = painterResource(device.icon()), 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 11456f28d..a0dd6db70 100644 --- a/compose/src/commonMain/kotlin/com/blockstream/compose/utils/SideEffects.kt +++ b/compose/src/commonMain/kotlin/com/blockstream/compose/utils/SideEffects.kt @@ -200,8 +200,6 @@ fun HandleSideEffect( val biometricsState = LocalBiometricState.current var twoFactorResolverData by remember { mutableStateOf(null) } - println("SATODEBUG SideEffects HandleSideEffect() START") - twoFactorResolverData?.also { resolverData -> resolverData.methods?.also { methods -> SingleChoiceDialog( @@ -262,7 +260,6 @@ fun HandleSideEffect( // Device Passphrase PassphraseBottomSheet.getResult { - println("SATODEBUG SideEffects HandleSideEffect() PassphraseBottomSheet.getResult: " + it) viewModel.postEvent(Events.DeviceRequestResponse(it)) } @@ -277,13 +274,10 @@ fun HandleSideEffect( // Handle sideEffect only on resumed state LaunchedEffect(state) { - println("SATODEBUG SideEffects HandleSideEffect() LaunchedEffect(state) state: " + state) - if(state != Lifecycle.State.RESUMED) return@LaunchedEffect viewModel.sideEffect.onEach { - println("SATODEBUG SideEffects HandleSideEffect() LaunchedEffect(state) onEach sideEffect: " + it) handler.invoke(this, it) when (it) { @@ -555,16 +549,7 @@ fun HandleSideEffect( } is SideEffects.DeviceRequestPassphrase -> { - - println("SATODEBUG HandleSideEffect received DeviceRequestPassphrase") - bottomSheetNavigator?.also { - println("SATODEBUG bottomSheetNavigator is not null, showing sheet") - it.show(PassphraseBottomSheet) - } ?: run { - println("SATODEBUG bottomSheetNavigator is null!") - } - - //bottomSheetNavigator?.show(PassphraseBottomSheet) + bottomSheetNavigator?.show(PassphraseBottomSheet) } is SideEffects.DeviceRequestPin -> { diff --git a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java index c42a84cd6..df88a67aa 100644 --- a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java +++ b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java @@ -79,19 +79,12 @@ public class SatochipHWWallet extends GdkHardwareWallet implements CardListener private final Activity activity; private final Context context; private final CardChannel channel = null; - //private final SatochipCommandSet cmdSet; private NfcAdapter nfcAdapter = null; - - //private NfcActionType actionType = NfcActionType.none; - //private NfcActionResult actionResult = null; private NfcActionObject actionObject = new NfcActionObject(); public SatochipHWWallet(Device device, String pin, Activity activity, Context context){ - Log.i(TAG, "SATODEBUG SatochipHWWallet constructor start"); - //mTrezor = t; this.device = device; - //String model = "Satochip"; this.model = DeviceModel.SatochipGeneric; this.pin = pin; this.activity = activity; @@ -100,7 +93,6 @@ public SatochipHWWallet(Device device, String pin, Activity activity, Context c NfcCardManager cardManager = new NfcCardManager(); cardManager.setCardListener(this); cardManager.start(); - Log.i(TAG, "SATODEBUG SatochipHWWallet constructor after cardManager start"); nfcAdapter = NfcAdapter.getDefaultAdapter(this.context); nfcAdapter.enableReaderMode( @@ -109,7 +101,6 @@ public SatochipHWWallet(Device device, String pin, Activity activity, Context c NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_NFC_B | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, null ); - Log.i(TAG, "SATODEBUG SatochipHWWallet constructor after NfcAdapter.enableReaderMode"); } @@ -132,9 +123,6 @@ public void onConnected(CardChannel channel) { } SatochipCommandSet cmdSet = new SatochipCommandSet(channel); - Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() cmdSet created"); - // start to interact with card - //NfcCardService.initialize(cmdSet) ApduResponse rapduSelect = cmdSet.cardSelect("satochip").checkOK(); Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() applet selected"); @@ -142,22 +130,16 @@ public void onConnected(CardChannel channel) { 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"); - Log.i(TAG, "SATODEBUG SatochipHWWallet readCard cardStatus: ${cardStatus.toString()}"); - // verify PIN if (this.pin != null) { - //String pinStr = "123456"; //todo byte[] pinBytes = this.pin.getBytes(Charset.forName("UTF-8")); ApduResponse rapduPin = cmdSet.cardVerifyPIN(pinBytes); - Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() pin verified: " + rapduPin.toHexString()); - // todo: wrong pin/blockecd pin... } 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); - Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() pin verified: " + rapduPin.toHexString()); } else { throw new SatochipException(SatochipException.ExceptionReason.PIN_UNDEFINED); } @@ -172,7 +154,7 @@ public void onConnected(CardChannel channel) { onConnectedSignTransaction(cmdSet); } - // TODO: disconnect? + // disconnect Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() trigger disconnection!"); onDisconnected(); @@ -183,7 +165,6 @@ public void onConnected(CardChannel channel) { if (actionObject.hwInteraction != null){ actionObject.hwInteraction.interactionRequest(this, "No PIN available", false, null); } - //throw new RuntimeException(e); // todo: msg user instead!! } catch (WrongPINException e) { this.pin = null; Log.e(TAG, "SATODEBUG onConnected: WRONG PIN! "+ e); @@ -192,7 +173,6 @@ public void onConnected(CardChannel channel) { String message = "WRONG PIN!\n"+e.getRetryAttempts() + " tries remaining"; actionObject.hwInteraction.interactionRequest(this, message, false, null); } - //throw new RuntimeException(e); // todo: msg user instead!! } catch (WrongPINLegacyException e) { this.pin = null; Log.e(TAG, "SATODEBUG onConnected: WRONG PIN LEGACY! "+ e); @@ -201,28 +181,16 @@ public void onConnected(CardChannel channel) { String message = "WRONG PIN!\n"; actionObject.hwInteraction.interactionRequest(this, message, false, null); } - //throw new RuntimeException(e); // todo: msg user instead!! } catch (BlockedPINException e) { this.pin = null; - Log.e(TAG, "SATODEBUG onConnected: A an exception has been thrown during card init: "+ e); + 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); } - //throw new RuntimeException(e); // todo: msg user instead!! - } catch (ApduException e) { - //throw new RuntimeException(e); - Log.e(TAG, "SATODEBUG onConnected: A an exception has been thrown during card init: "+ e); - e.printStackTrace(); - onDisconnected(); - } catch (IOException e) { - //throw new RuntimeException(e); - Log.e(TAG, "SATODEBUG onConnected: B an exception has been thrown during card init: " + e); - e.printStackTrace(); - onDisconnected(); } catch (Exception e) { - Log.e(TAG, "SATODEBUG onConnected: C an exception has been thrown during card init.: " + e); + Log.e(TAG, "SATODEBUG onConnected exception: " + e); e.printStackTrace(); onDisconnected(); } @@ -237,17 +205,15 @@ public void onConnectedGetXpubs(SatochipCommandSet cmdSet) throws Exception { for (List path : paths) { Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedGetXpubs() path: " + path); final String key = Joiner.on("/").join(path); -// Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedGetXpubs() key: " + key + "."); 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 +// 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 - //pathStr = "m/44'/0'/0'/0"; - int xtype = this.actionObject.networkParam.getVerPublic(); //0x0488B21E; + int xtype = this.actionObject.networkParam.getVerPublic(); String xpub = cmdSet.cardBip32GetXpub(bip32path, xtype); Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedGetXpubs() xpub: " + xpub); @@ -261,9 +227,8 @@ public void onConnectedGetXpubs(SatochipCommandSet cmdSet) throws Exception { xpubs.add(mUserXPubs.get(key)); } - this.actionObject.xpubsResult = xpubs; - // action finished + this.actionObject.xpubsResult = xpubs; this.actionObject.actionStatus = NfcActionStatus.finished; } @@ -273,19 +238,13 @@ public void onConnectedSignMessage(SatochipCommandSet cmdSet) throws Exception { List path = this.actionObject.pathParam; Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() path: " + path); - // derive key DEBUG -// String pathStr = "m/84'/0'/0'/0/0"; -// Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() pathStr: " + pathStr); //debug -// byte optionFlags = (byte) 0x40; -// byte[][] extendedKey = cmdSet.cardBip32GetExtendedKey(pathStr, optionFlags, null); - // 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 + //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 + //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; @@ -298,44 +257,40 @@ public void onConnectedSignMessage(SatochipCommandSet cmdSet) throws Exception { ByteArrayOutputStream bos = new ByteArrayOutputStream(); bos.write(BITCOIN_SIGNED_MESSAGE_HEADER_BYTES.length); bos.write(BITCOIN_SIGNED_MESSAGE_HEADER_BYTES); -// VarInt size = new VarInt(messageBytes.length); -// bos.write(size.encode()); VarintUtils.write(bos, messageBytes.length); bos.write(messageBytes); formatedMessageBytes = bos.toByteArray(); - Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() formatedMessageBytes: " + hex(formatedMessageBytes)); //debug + //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 - //byte[] hashBytes = Wally.sha256(formatedMessageBytes); // single hash debug? - Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() hashBytes: " + hex(hashBytes)); //debug + //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 +// // 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 + //Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignMessage() sigBytes: " + hex(sigBytes)); //debug // verify sig SatochipParser parser = new SatochipParser(); - //boolean isOk = parser.verifySig(formatedMessageBytes, sigBytes, extendedKey[0]); 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 - this.actionObject.signatureResult = sigHex; // action finished + this.actionObject.signatureResult = sigHex; this.actionObject.actionStatus = NfcActionStatus.finished; } @@ -350,18 +305,17 @@ public void onConnectedSignTransaction(SatochipCommandSet cmdSet) throws Excepti // get path List path = paths.get(i); - Log.i(TAG, "SATODEBUG SatochipHWWallet onConnectedSignTransaction() path: " + path); + //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 optionFlags = (byte) 0x40; + //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 + //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 card + // 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; @@ -413,9 +367,7 @@ public void onConnectedSignTransaction(SatochipCommandSet cmdSet) throws Excepti this.actionObject.actionStatus = NfcActionStatus.finished; } - // SATODEBUG public void onDisconnected() { - //NfcCardService.isConnected.postValue(false) Log.i(TAG, "SATODEBUG SatochipHWWallet onDisconnected: Card disconnected!"); } @@ -424,14 +376,13 @@ public void onDisconnected() { public synchronized List getXpubs(@NonNull Network network, @NonNull List> paths, @Nullable HardwareWalletInteraction hwInteraction) { Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getXpubs start"); Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getXpubs paths: " + paths); - Log.i("SatochipHWWallet", "SATODEBUG SatochipHWWallet getXpubs HardwareWalletInteraction: " + hwInteraction); // 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); + //Log.i(TAG, "SATODEBUG SatochipHWWallet getXpubs() path: " + path); final String key = Joiner.on("/").join(path); if (mUserXPubs.containsKey(key)) { cachedXpubs.add(mUserXPubs.get(key)); @@ -471,7 +422,7 @@ public synchronized List getXpubs(@NonNull Network network, @NonNull Lis return xpubs; } catch (Exception e) { - Log.i("SatochipHWWallet", "getXpubs exception: " + e); + Log.e("SatochipHWWallet", "getXpubs exception: " + e); } finally { completable.complete(true); } @@ -484,11 +435,7 @@ public synchronized List getXpubs(@NonNull Network network, @NonNull Lis 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); - Log.i("SatochipHWWallet", "signMessage start useAeProtocol: " + useAeProtocol); - Log.i("SatochipHWWallet", "signMessage start aeHostCommitment: " + aeHostCommitment); - Log.i("SatochipHWWallet", "signMessage start aeHostEntropy: " + aeHostEntropy); - Log.i("SatochipHWWallet", "signMessage start hwInteraction: " + hwInteraction); + //Log.i("SatochipHWWallet", "signMessage start message: " + message); if (useAeProtocol) { throw new RuntimeException("Hardware Wallet does not support the Anti-Exfil protocol"); @@ -521,7 +468,7 @@ public SignMessageResult signMessage(@NonNull List path, @NonNull Strin Log.i(TAG, "SATODEBUG SatochipHWWallet signMessage() signatureResult: " + signatureResult); return new SignMessageResult(signatureResult, null); } catch (Exception e) { - Log.e("SatochipHWWallet", "signMessage exception: " + e); + Log.e(TAG, "signMessage exception: " + e); } finally { completable.complete(true); } @@ -535,16 +482,16 @@ public SignMessageResult signMessage(@NonNull List path, @NonNull Strin @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"); - Log.i("SatochipHWWallet", "signTransaction start network: " + network); - Log.i("SatochipHWWallet", "signTransaction start transaction: " + transaction); - Log.i("SatochipHWWallet", "signTransaction start inputs: " + inputs); - Log.i("SatochipHWWallet", "signTransaction start outputs: " + outputs); - Log.i("SatochipHWWallet", "signTransaction start transactions: " + transactions); + //Log.i("SatochipHWWallet", "signTransaction start network: " + network); + //Log.i("SatochipHWWallet", "signTransaction start transaction: " + transaction); + //Log.i("SatochipHWWallet", "signTransaction start inputs: " + inputs); + //Log.i("SatochipHWWallet", "signTransaction start outputs: " + outputs); + //Log.i("SatochipHWWallet", "signTransaction start transactions: " + transactions); CompletableDeferred completable = CompletableDeferredKt.CompletableDeferred(null); final byte[] txBytes = Wally.hex_to_bytes(transaction); - Log.i("SatochipHWWallet", "signTransaction txBytes: " + Wally.hex_from_bytes(txBytes)); + //Log.i("SatochipHWWallet", "signTransaction txBytes: " + Wally.hex_from_bytes(txBytes)); if(network.isLiquid()){ throw new RuntimeException(network.getCanonicalName() + " is not supported"); @@ -561,25 +508,25 @@ public SignTransactionResult signTransaction(@NonNull Network network, @NonNull } final Object wallyTx = Wally.tx_from_bytes(txBytes, Wally.WALLY_TX_FLAG_USE_WITNESS); - Log.i("SatochipHWWallet", "signTransaction wallyTx: " + wallyTx); - - boolean sw = false; - boolean p2sh = false; - for (final InputOutput in : inputs) { - Log.i("SatochipHWWallet", "signTransaction inputs[i]: " + in); - if (in.isSegwit()) { - sw = true; - } else { - p2sh = true; - } - } - Log.i("SatochipHWWallet", "signTransaction sw: " + sw); - Log.i("SatochipHWWallet", "signTransaction p2sh: " + p2sh); - - // debug - for (final InputOutput out : outputs) { - Log.i("SatochipHWWallet", "signTransaction outputs[i]: " + out); - } + //Log.i("SatochipHWWallet", "signTransaction wallyTx: " + wallyTx); + +// boolean sw = false; +// boolean p2sh = false; +// for (final InputOutput in : inputs) { +// //Log.i("SatochipHWWallet", "signTransaction inputs[i]: " + in); +// if (in.isSegwit()) { +// sw = true; +// } else { +// p2sh = true; +// } +// } +// Log.i("SatochipHWWallet", "signTransaction sw: " + sw); +// Log.i("SatochipHWWallet", "signTransaction p2sh: " + p2sh); + +// // debug +// for (final InputOutput out : outputs) { +// Log.i("SatochipHWWallet", "signTransaction outputs[i]: " + out); +// } // get tx hash signature for each inputs final int inputSize = inputs.size(); @@ -598,7 +545,6 @@ public SignTransactionResult signTransaction(@NonNull Network network, @NonNull final long flags = Wally.WALLY_TX_FLAG_USE_WITNESS; byte[] hash_out = new byte[32]; - //tx_get_btc_signature_hash(Object jarg1, long jarg2, byte[] jarg3, long jarg5, long jarg6, long jarg7, byte[] jarg8); byte[] hash = Wally.tx_get_btc_signature_hash( wallyTx, i, @@ -609,14 +555,12 @@ public SignTransactionResult signTransaction(@NonNull Network network, @NonNull hash_out ); Log.i("SatochipHWWallet", "signTransaction() input hash_out[i]: " + Wally.hex_from_bytes(hash_out)); - //Log.i("SatochipHWWallet", "signTransaction() input hash[i]: " + hash.getClass().getName()); hashesParam.add(hash_out); - // derive key for path + // get derivation path for each input List path = in.getUserPathAsInts(); Log.i("SatochipHWWallet", "signTransaction() input path[i]: " + path); pathsParam.add(in.getUserPathAsInts()); - } // create action @@ -672,7 +616,6 @@ public synchronized BlindingFactorsResult getBlindingFactors(final List path, final long csvBlocks, HardwareWalletInteraction hwInteraction) { - Log.i("SatochipHWWallet", "getGreenAddress start"); if (network.isMultisig()) { throw new RuntimeException("Hardware Wallet does not support displaying Green Multisig Shield addresses"); } From 66eac1de7da00a3ef150fbd4afd0af44e807a1a9 Mon Sep 17 00:00:00 2001 From: Toporin Date: Wed, 26 Feb 2025 10:49:46 +0100 Subject: [PATCH 20/25] (minor) code cleaning --- .../kotlin/com/blockstream/common/devices/GreenDevice.kt | 2 +- .../kotlin/com/blockstream/common/gdk/GdkSession.kt | 1 - .../com/blockstream/common/models/GreenViewModel.kt | 8 +------- .../common/models/devices/AbstractDeviceViewModel.kt | 6 ------ .../common/models/devices/DeviceInfoViewModel.kt | 3 +-- .../common/models/devices/DeviceListViewModel.kt | 1 - .../common/models/devices/DeviceScanViewModel.kt | 2 ++ .../compose/managers/DeviceConnectionManagerAndroid.kt | 1 - .../compose/screens/devices/DeviceListScreen.kt | 3 +-- .../kotlin/com/blockstream/compose/utils/SideEffects.kt | 4 +--- gradle/libs.versions.toml | 3 --- .../src/main/java/com/blockstream/green/GreenActivity.kt | 1 - .../main/java/com/blockstream/green/di/GreenModules.kt | 3 --- hardware/build.gradle.kts | 2 -- 14 files changed, 7 insertions(+), 33 deletions(-) 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 d00045e4f..735e770e6 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/devices/GreenDevice.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/devices/GreenDevice.kt @@ -92,7 +92,7 @@ abstract class GreenDeviceImpl constructor( get() = name override val deviceModel: DeviceModel? - get() = gdkHardwareWallet?.model // todo + get() = gdkHardwareWallet?.model final override var heartbeat: Long = Clock.System.now().toEpochMilliseconds() private set 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 771bfe8f1..4cd2eed22 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt @@ -155,7 +155,6 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import kotlinx.datetime.Clock import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.add import kotlinx.serialization.json.buildJsonObject 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 37ff633ef..5e337494e 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/GreenViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/GreenViewModel.kt @@ -339,7 +339,6 @@ open class GreenViewModel constructor( } viewModelScope.coroutineScope.launch { - logger.d { "postSideEffect: launch/send $sideEffect" } _sideEffect.send(sideEffect) } } @@ -506,7 +505,6 @@ open class GreenViewModel constructor( countly.renameWallet() }) } - is Events.DeviceRequestResponse -> { if(event.data == null){ _deviceRequest?.completeExceptionally(Exception("id_action_canceled")) @@ -514,7 +512,6 @@ open class GreenViewModel constructor( _deviceRequest?.complete(event.data) } } - is Events.SelectDenomination -> { viewModelScope.coroutineScope.launch { denominatedValue()?.also { @@ -878,9 +875,7 @@ open class GreenViewModel constructor( return CompletableDeferred().let { _deviceRequest = it postSideEffect(SideEffects.DeviceRequestPassphrase) - runBlocking { - it.await() - } + runBlocking { it.await() } } } @@ -1046,5 +1041,4 @@ open class GreenViewModel constructor( fun preview() = object : GreenViewModel() { } private var _deviceRequest: CompletableDeferred? = null } - } diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/AbstractDeviceViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/AbstractDeviceViewModel.kt index 7bdbdb675..e0c4e0cac 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/AbstractDeviceViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/AbstractDeviceViewModel.kt @@ -217,17 +217,12 @@ abstract class AbstractDeviceViewModel constructor( } override fun requestPinBlocking(deviceBrand: DeviceBrand): String { - logger.i { "AbstractDeviceViewModel requestPinBlocking deviceBrand: ${deviceBrand}" } postSideEffect(LocalSideEffects.RequestPin(deviceBrand)) return CompletableDeferred().also { - logger.i { "AbstractDeviceViewModel requestPinBlocking requestPinEmitter before" } requestPinEmitter = it - logger.i { "AbstractDeviceViewModel requestPinBlocking requestPinEmitter after" } }.let { - logger.i { "AbstractDeviceViewModel requestPinBlocking runBlocking before" } runBlocking { it.await() } - } } @@ -238,7 +233,6 @@ abstract class AbstractDeviceViewModel constructor( override fun requestNetwork(): Network? = greenWalletOrNull?.let { if (it.isMainnet) gdk.networks().bitcoinElectrum else gdk.networks().testnetBitcoinElectrum } ?: if (settingsManager.appSettings.testnet) { - logger.i { "AbstractDeviceViewModel requestNetwork " } requestNetworkEmitter = CompletableDeferred() postSideEffect(SideEffects.SelectEnvironment) runBlocking { requestNetworkEmitter!!.await() } diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceInfoViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceInfoViewModel.kt index 746c24f23..fc4fe232e 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceInfoViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceInfoViewModel.kt @@ -113,6 +113,7 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs if (event is LocalEvents.AuthenticateAndContinue) { authenticateAndContinue(event.updateFirmwareFromChannel) } else if (event is LocalEvents.SelectEnviroment) { + if (event.isTestnet == null) { requestNetworkEmitter?.completeExceptionally(Exception("id_action_canceled")) } else { @@ -175,7 +176,6 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs ) val network = device.getOperatingNetwork(device, gdk, interaction = this)!! - val isEphemeral = !settingsManager.appSettings.rememberHardwareDevices val previousSession = (if (device.isLedger) { @@ -201,7 +201,6 @@ class DeviceInfoViewModel constructor(deviceId: String) : DeviceInfoViewModelAbs } val walletHashId = getWalletHashId(session, network, device) - // Disable Jade wallet fingerprint, keep the device name // getWalletName(session, network, device) val walletName = device.name diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceListViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceListViewModel.kt index 3fed30de4..294970d65 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceListViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceListViewModel.kt @@ -70,7 +70,6 @@ class DeviceListViewModel(isJade: Boolean = true) : postSideEffect(SideEffects.NavigateTo(NavigateDestinations.ImportPubKey(deviceModel = DeviceModel.Generic))) } }else if (event is LocalEvents.SelectDevice) { - val navigateTo = SideEffects.NavigateTo(NavigateDestinations.DeviceInfo(deviceId = event.device.connectionIdentifier)) if (event.device.hasPermissions()) { diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceScanViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceScanViewModel.kt index 1367ab7d0..bd0d57b6a 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceScanViewModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/models/devices/DeviceScanViewModel.kt @@ -59,6 +59,7 @@ class DeviceScanViewModel(greenWallet: GreenWallet) : } ?: run { combine(deviceFlow, deviceManager.devices) { _, devices -> + if(deviceFlow.value == null) { var foundDevice = devices.firstOrNull { device -> greenWallet.deviceIdentifiers?.any { it.uniqueIdentifier == device.uniqueIdentifier } == true @@ -97,6 +98,7 @@ class DeviceScanViewModel(greenWallet: GreenWallet) : if (device.hasPermissions()) { doAsync({ + if (device.gdkHardwareWallet == null) { session.disconnect() deviceConnectionManager.connectDevice( 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 ddcbfd4cc..981dd3656 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt @@ -64,7 +64,6 @@ class DeviceConnectionManagerAndroid constructor( } ?: (device as? SatochipDevice)?.let { connectSatochipDevice(it, interaction) } ?: super.connectDevice(device, httpRequestHandler, interaction)) - } override suspend fun disconnectDevice(device: GreenDevice) { diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceListScreen.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceListScreen.kt index 7f3e249ba..07c775736 100644 --- a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceListScreen.kt +++ b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/devices/DeviceListScreen.kt @@ -92,7 +92,6 @@ data class DeviceListScreen(val isJade: Boolean) : Screen, Parcelable { @Composable override fun Content() { - val viewModel = koinScreenModel { parametersOf(isJade) } @@ -129,7 +128,7 @@ fun DeviceListItem(device: GreenDevice, modifier: Modifier, onClick: () -> Unit) modifier = Modifier.align(Alignment.CenterStart) ) { Image( - painter = painterResource(if (device.isUsb) Res.drawable.usb else Res.drawable.ble), // TODO add NFC icon + painter = painterResource(if (device.isUsb) Res.drawable.usb else Res.drawable.ble), modifier = Modifier.size(24.dp), contentDescription = null ) 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 a0dd6db70..70e07941e 100644 --- a/compose/src/commonMain/kotlin/com/blockstream/compose/utils/SideEffects.kt +++ b/compose/src/commonMain/kotlin/com/blockstream/compose/utils/SideEffects.kt @@ -198,8 +198,8 @@ fun HandleSideEffect( val drawer = LocalDrawer.current val scope = rememberCoroutineScope() val biometricsState = LocalBiometricState.current - var twoFactorResolverData by remember { mutableStateOf(null) } + var twoFactorResolverData by remember { mutableStateOf(null) } twoFactorResolverData?.also { resolverData -> resolverData.methods?.also { methods -> SingleChoiceDialog( @@ -273,11 +273,9 @@ fun HandleSideEffect( // Handle sideEffect only on resumed state LaunchedEffect(state) { - if(state != Lifecycle.State.RESUMED) return@LaunchedEffect viewModel.sideEffect.onEach { - handler.invoke(this, it) when (it) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e6f639aa5..61d66ae34 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -73,7 +73,6 @@ protobuf-java = "3.4.0" media3 = "1.4.0" bouncycastle = "1.69" -spongycastle = "1.58.0.0" [libraries] accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } @@ -173,8 +172,6 @@ androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", versi androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3" } org-bouncycastle = { module = "org.bouncycastle:bcprov-jdk15to18", version.ref = "bouncycastle" } -com-spongycastle = { module = "com.madgag.spongycastle:core", version.ref = "spongycastle" } - [plugins] kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } diff --git a/green/src/main/java/com/blockstream/green/GreenActivity.kt b/green/src/main/java/com/blockstream/green/GreenActivity.kt index 636d9bc91..20d19a8f8 100644 --- a/green/src/main/java/com/blockstream/green/GreenActivity.kt +++ b/green/src/main/java/com/blockstream/green/GreenActivity.kt @@ -48,7 +48,6 @@ 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 activityProvider: AndroidActivityProvider by inject() private val mainViewModel: MainViewModel by viewModel() 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 197d27064..1d4108a50 100644 --- a/green/src/main/java/com/blockstream/green/di/GreenModules.kt +++ b/green/src/main/java/com/blockstream/green/di/GreenModules.kt @@ -5,8 +5,6 @@ package com.blockstream.green.di import android.content.Context import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager -import androidx.fragment.app.FragmentActivity -//import androidx.fragment.app.FragmentActivity import com.benasher44.uuid.Uuid import com.blockstream.common.devices.ActivityProvider import com.blockstream.common.devices.AndroidActivityProvider @@ -21,7 +19,6 @@ import com.blockstream.compose.devices.SatochipDevice import com.blockstream.compose.devices.TrezorDevice import com.blockstream.compose.managers.DeviceConnectionManager import com.blockstream.compose.managers.DeviceConnectionManagerAndroid -import com.blockstream.green.GreenActivity import com.blockstream.green.managers.FcmAndroid import com.blockstream.green.managers.NotificationManagerAndroid import com.blockstream.jade.connection.JadeBleConnection diff --git a/hardware/build.gradle.kts b/hardware/build.gradle.kts index 77c07f814..b7bba24a0 100644 --- a/hardware/build.gradle.kts +++ b/hardware/build.gradle.kts @@ -44,8 +44,6 @@ dependencies { /** For satochip */ implementation(libs.org.bouncycastle) - //implementation(libs.com.spongycastle) - //implementation(libs.org.bitcoinj) testImplementation(libs.junit) testImplementation(libs.androidx.core.testing) From 229e79eda8b46f038834a29113fd42f7a09306b8 Mon Sep 17 00:00:00 2001 From: Toporin Date: Thu, 27 Feb 2025 11:03:33 +0100 Subject: [PATCH 21/25] Add translation (wip) TODO: some configuration is required to actually add the translation in the app... --- .../src/commonMain/composeResources/values-cs/strings.xml | 1 + .../src/commonMain/composeResources/values-de/strings.xml | 1 + .../src/commonMain/composeResources/values-es/strings.xml | 1 + .../src/commonMain/composeResources/values-fr/strings.xml | 1 + .../src/commonMain/composeResources/values-he/strings.xml | 1 + .../src/commonMain/composeResources/values-it/strings.xml | 1 + .../src/commonMain/composeResources/values-ja/strings.xml | 1 + .../src/commonMain/composeResources/values-ko/strings.xml | 1 + .../src/commonMain/composeResources/values-nl/strings.xml | 1 + .../commonMain/composeResources/values-pt-rBR/strings.xml | 1 + .../src/commonMain/composeResources/values-ro/strings.xml | 1 + .../src/commonMain/composeResources/values-ru/strings.xml | 1 + .../src/commonMain/composeResources/values-uk/strings.xml | 1 + .../src/commonMain/composeResources/values-vi/strings.xml | 1 + .../src/commonMain/composeResources/values-zh/strings.xml | 1 + common/src/commonMain/composeResources/values/strings.xml | 1 + .../compose/sheets/DeviceInteractionBottomSheet.kt | 2 +- .../com/blockstream/compose/sheets/NfcToastBottomSheet.kt | 2 +- .../greenaddress/greenbits/wallets/SatochipHWWallet.java | 6 +++--- 19 files changed, 21 insertions(+), 5 deletions(-) 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 d6385b450..1b8aabd6b 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/compose/src/commonMain/kotlin/com/blockstream/compose/sheets/DeviceInteractionBottomSheet.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/sheets/DeviceInteractionBottomSheet.kt index 261bff6ea..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 -> if (viewModel.device?.isNfc == true) "Tap your card to sign" else 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 index 7671d9ccb..02b78fd87 100644 --- a/compose/src/commonMain/kotlin/com/blockstream/compose/sheets/NfcToastBottomSheet.kt +++ b/compose/src/commonMain/kotlin/com/blockstream/compose/sheets/NfcToastBottomSheet.kt @@ -79,7 +79,7 @@ fun NfcToastBottomSheet( onDismissRequest: () -> Unit, ) { - val title = "Please scan your card" + val title = "Scan your card" //stringResource(Res.string.id_scan_your_card) val deviceIcon = viewModel.device?.icon() diff --git a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java index df88a67aa..74d4d06c3 100644 --- a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java +++ b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java @@ -400,7 +400,7 @@ public synchronized List getXpubs(@NonNull Network network, @NonNull Lis // request to card if not cached already try { if(hwInteraction != null) { - hwInteraction.requestNfcToast(DeviceBrand.Satochip, "Scan card to get xpub...", completable); + hwInteraction.requestNfcToast(DeviceBrand.Satochip, "Exporting xpub...", completable); } this.actionObject.actionStatus = NfcActionStatus.busy; @@ -445,7 +445,7 @@ public SignMessageResult signMessage(@NonNull List path, @NonNull Strin try { if(hwInteraction != null) { - hwInteraction.requestNfcToast(DeviceBrand.Satochip, "Scan card to sign message...", completable); + hwInteraction.requestNfcToast(DeviceBrand.Satochip, "Signing login message...", completable); } this.actionObject.actionStatus = NfcActionStatus.busy; @@ -504,7 +504,7 @@ public SignTransactionResult signTransaction(@NonNull Network network, @NonNull } if(hwInteraction != null) { - hwInteraction.requestNfcToast(DeviceBrand.Satochip, "Scan card to sign transaction...", completable); + hwInteraction.requestNfcToast(DeviceBrand.Satochip, "Signing transaction...", completable); } final Object wallyTx = Wally.tx_from_bytes(txBytes, Wally.WALLY_TX_FLAG_USE_WITNESS); From 7573385960b826d051cbfdfc4ba2495994e599a1 Mon Sep 17 00:00:00 2001 From: Toporin Date: Thu, 27 Feb 2025 11:31:26 +0100 Subject: [PATCH 22/25] Add satochip in DeviceModel.zendeskValue --- .../kotlin/com/blockstream/common/devices/DeviceModel.kt | 1 + 1 file changed, 1 insertion(+) 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 67050fb12..be07d5186 100644 --- a/common/src/commonMain/kotlin/com/blockstream/common/devices/DeviceModel.kt +++ b/common/src/commonMain/kotlin/com/blockstream/common/devices/DeviceModel.kt @@ -40,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 From ef8185acef8872ab3373d9f1f7477b9c5e876424 Mon Sep 17 00:00:00 2001 From: Toporin Date: Fri, 28 Feb 2025 11:55:36 +0100 Subject: [PATCH 23/25] Update NfcActionStatus to none when no action requested --- .../com/greenaddress/greenbits/wallets/SatochipHWWallet.java | 1 + 1 file changed, 1 insertion(+) diff --git a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java index 74d4d06c3..fc3b5b5bf 100644 --- a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java +++ b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java @@ -117,6 +117,7 @@ public void onConnected(CardChannel channel) { try { if (this.actionObject.actionType == NfcActionType.none) { + this.actionObject.actionStatus = NfcActionStatus.none; Log.i(TAG, "SATODEBUG SatochipHWWallet onConnected() nothing to do => disconnection!"); onDisconnected(); return; From 0c83288219f33bdba691be268f90eac90cd4cd65 Mon Sep 17 00:00:00 2001 From: Toporin Date: Mon, 10 Mar 2025 16:36:54 +0100 Subject: [PATCH 24/25] Add support for Liquid-Bitcoin with Satochip (firmware v0.14-0.5+) --- .../common/devices/DeviceManagerAndroid.kt | 32 +++- .../blockstream/common/devices/NfcDevice.kt | 3 + .../common/devices/SatochipCommandSet.kt | 22 ++- .../compose/devices/SatochipDevice.kt | 3 + .../DeviceConnectionManagerAndroid.kt | 6 +- .../greenbits/wallets/SatochipHWWallet.java | 140 ++++++++++++++---- .../java/com/satochip/NfcActionObject.java | 2 + .../main/java/com/satochip/NfcActionType.java | 1 + .../java/com/satochip/SatochipCommandSet.java | 34 +++++ 9 files changed, 208 insertions(+), 35 deletions(-) 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 7c8282f68..bd739fa83 100644 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt @@ -8,15 +8,16 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.nfc.NfcAdapter 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 import com.blockstream.common.di.ApplicationScope import com.blockstream.common.extensions.isBonded import com.blockstream.common.extensions.isJade +import com.blockstream.common.gdk.Wally import com.blockstream.common.managers.BluetoothManager import com.blockstream.common.managers.DeviceManager import com.blockstream.common.managers.SessionManager @@ -192,8 +193,35 @@ class DeviceManagerAndroid constructor( // 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: statusBytes: ${statusBytes}" } + logger.i { "SATODEBUG DeviceManagerAndroid onConnected: statusSize: $statusSize" } + + // check if card is seeded + val isSeeded = if ((statusBytes[9].toInt() == 0X00)) false else true + 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" } + + // todo: error messsage if not seeded/setup + } + // add device - val nfcDevice = NfcDevice(NfcDeviceType.SATOCHIP) val newDevices = mutableListOf() deviceMapper.invoke(this, null, null, null, null, nfcDevice, activityProvider) ?.let { diff --git a/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcDevice.kt b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcDevice.kt index 50a1ee123..1f4955d90 100644 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcDevice.kt +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/NfcDevice.kt @@ -7,4 +7,7 @@ enum class NfcDeviceType { 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 index f19bd4b63..b5414b9eb 100644 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.kt +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/SatochipCommandSet.kt @@ -1,8 +1,8 @@ package com.blockstream.common.devices -import java.util.logging.Logger -import java.util.logging.Level 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 @@ -52,4 +52,22 @@ class SatochipCommandSet(private val apduChannel: CardChannel) { 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/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt b/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt index 180f94fdf..1b40373ba 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/devices/SatochipDevice.kt @@ -23,6 +23,7 @@ class SatochipDevice constructor( type: ConnectionType, peripheral: Peripheral? = null, isBonded: Boolean = false, + nfcDevice: NfcDevice? = null, activityProvider: ActivityProvider? = null ) : AndroidDevice( context = context, @@ -34,6 +35,7 @@ class SatochipDevice constructor( isBonded = isBonded ) { + val nfcDevice: NfcDevice? = nfcDevice val activityProvider: ActivityProvider? = activityProvider override suspend fun getOperatingNetworkForEnviroment(greenDevice: GreenDevice, gdk: Gdk, isTestnet: Boolean): Network = @@ -58,6 +60,7 @@ class SatochipDevice constructor( deviceManager = deviceManager, type = ConnectionType.NFC, usbDevice = null, + nfcDevice = nfcDevice, activityProvider = activityProvider, ) } else { 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 981dd3656..0bc917a87 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt @@ -155,11 +155,11 @@ class DeviceConnectionManagerAndroid constructor( val satoDevice = com.blockstream.common.gdk.data.Device( name = "Satochip", - supportsArbitraryScripts = false, + supportsArbitraryScripts = true, supportsLowR = false, - supportsHostUnblinding = false, + supportsHostUnblinding = true, supportsExternalBlinding = false, - supportsLiquid = DeviceSupportsLiquid.None, + supportsLiquid = if (device.nfcDevice?.supportsLiquid == true) DeviceSupportsLiquid.Lite else DeviceSupportsLiquid.None, supportsAntiExfilProtocol = DeviceSupportsAntiExfilProtocol.None ) diff --git a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java index fc3b5b5bf..f3ba54ba1 100644 --- a/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java +++ b/hardware/src/main/java/com/greenaddress/greenbits/wallets/SatochipHWWallet.java @@ -30,7 +30,6 @@ import com.btchip.utils.BufferUtils; import com.btchip.utils.VarintUtils; import com.google.common.base.Joiner; -import com.satochip.ApduException; import com.satochip.ApduResponse; import com.satochip.ApplicationStatus; import com.satochip.Bip32Path; @@ -153,6 +152,8 @@ public void onConnected(CardChannel channel) { onConnectedSignMessage(cmdSet); } else if (this.actionObject.actionType == NfcActionType.signTransaction){ onConnectedSignTransaction(cmdSet); + } else if (this.actionObject.actionType == NfcActionType.getMasterBlindingKey){ + onConnectedGetMasterBlindingKey(cmdSet); } // disconnect @@ -368,6 +369,16 @@ public void onConnectedSignTransaction(SatochipCommandSet cmdSet) throws Excepti 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!"); } @@ -483,19 +494,81 @@ public SignMessageResult signMessage(@NonNull List path, @NonNull Strin @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"); - //Log.i("SatochipHWWallet", "signTransaction start network: " + network); - //Log.i("SatochipHWWallet", "signTransaction start transaction: " + transaction); - //Log.i("SatochipHWWallet", "signTransaction start inputs: " + inputs); - //Log.i("SatochipHWWallet", "signTransaction start outputs: " + outputs); - //Log.i("SatochipHWWallet", "signTransaction start transactions: " + transactions); CompletableDeferred completable = CompletableDeferredKt.CompletableDeferred(null); final byte[] txBytes = Wally.hex_to_bytes(transaction); - //Log.i("SatochipHWWallet", "signTransaction txBytes: " + Wally.hex_from_bytes(txBytes)); if(network.isLiquid()){ - throw new RuntimeException(network.getCanonicalName() + " is not supported"); + + 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 { @@ -509,25 +582,6 @@ public SignTransactionResult signTransaction(@NonNull Network network, @NonNull } final Object wallyTx = Wally.tx_from_bytes(txBytes, Wally.WALLY_TX_FLAG_USE_WITNESS); - //Log.i("SatochipHWWallet", "signTransaction wallyTx: " + wallyTx); - -// boolean sw = false; -// boolean p2sh = false; -// for (final InputOutput in : inputs) { -// //Log.i("SatochipHWWallet", "signTransaction inputs[i]: " + in); -// if (in.isSegwit()) { -// sw = true; -// } else { -// p2sh = true; -// } -// } -// Log.i("SatochipHWWallet", "signTransaction sw: " + sw); -// Log.i("SatochipHWWallet", "signTransaction p2sh: " + p2sh); - -// // debug -// for (final InputOutput out : outputs) { -// Log.i("SatochipHWWallet", "signTransaction outputs[i]: " + out); -// } // get tx hash signature for each inputs final int inputSize = inputs.size(); @@ -597,7 +651,37 @@ public SignTransactionResult signTransaction(@NonNull Network network, @NonNull @NonNull @Override public synchronized String getMasterBlindingKey(@Nullable HardwareWalletInteraction hwInteraction) { - throw new RuntimeException("Master Blinding Key is not supported"); + + 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 diff --git a/hardware/src/main/java/com/satochip/NfcActionObject.java b/hardware/src/main/java/com/satochip/NfcActionObject.java index 30782ad9f..fad8277fb 100644 --- a/hardware/src/main/java/com/satochip/NfcActionObject.java +++ b/hardware/src/main/java/com/satochip/NfcActionObject.java @@ -34,5 +34,7 @@ public class NfcActionObject { 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/NfcActionType.java b/hardware/src/main/java/com/satochip/NfcActionType.java index 3d14e950a..458f3b112 100644 --- a/hardware/src/main/java/com/satochip/NfcActionType.java +++ b/hardware/src/main/java/com/satochip/NfcActionType.java @@ -5,4 +5,5 @@ public enum NfcActionType { getXpubs, signMessage, signTransaction, + getMasterBlindingKey, } diff --git a/hardware/src/main/java/com/satochip/SatochipCommandSet.java b/hardware/src/main/java/com/satochip/SatochipCommandSet.java index ceef0e2cc..29d091938 100644 --- a/hardware/src/main/java/com/satochip/SatochipCommandSet.java +++ b/hardware/src/main/java/com/satochip/SatochipCommandSet.java @@ -414,6 +414,40 @@ public String cardBip32GetXpub(Bip32Path bip32Path, int xtype) throws Exception 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); From 35d67a3467c9f2690b2480270a4ecfb5a9840076 Mon Sep 17 00:00:00 2001 From: Toporin Date: Mon, 10 Mar 2025 19:47:03 +0100 Subject: [PATCH 25/25] Show error message if Satochip card is not seeded --- .../com/blockstream/common/devices/DeviceManagerAndroid.kt | 5 +---- .../compose/managers/DeviceConnectionManagerAndroid.kt | 5 +++++ 2 files changed, 6 insertions(+), 4 deletions(-) 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 bd739fa83..2e472b04d 100644 --- a/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt +++ b/common/src/androidMain/kotlin/com/blockstream/common/devices/DeviceManagerAndroid.kt @@ -17,7 +17,6 @@ import com.benasher44.uuid.Uuid import com.blockstream.common.di.ApplicationScope import com.blockstream.common.extensions.isBonded import com.blockstream.common.extensions.isJade -import com.blockstream.common.gdk.Wally import com.blockstream.common.managers.BluetoothManager import com.blockstream.common.managers.DeviceManager import com.blockstream.common.managers.SessionManager @@ -202,11 +201,11 @@ class DeviceManagerAndroid constructor( val statusBytes = statusApdu.getData() val statusSize = statusBytes.size - logger.i { "SATODEBUG DeviceManagerAndroid onConnected: statusBytes: ${statusBytes}" } 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 @@ -217,8 +216,6 @@ class DeviceManagerAndroid constructor( val supportsLiquid = if ((statusSize>15) && (statusBytes[15].toInt() == 0X00)) true else false nfcDevice.supportsLiquid = supportsLiquid logger.i { "SATODEBUG DeviceManagerAndroid onConnected: supportsLiquid: $supportsLiquid" } - - // todo: error messsage if not seeded/setup } // add device 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 0bc917a87..cc700b490 100644 --- a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt +++ b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/DeviceConnectionManagerAndroid.kt @@ -153,6 +153,11 @@ class DeviceConnectionManagerAndroid constructor( 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,