Updated on 2026-08-14

This commit is contained in:
Tangem 2020-02-26 13:34:42 +03:00
commit 78f819cf01
25 changed files with 839 additions and 69 deletions

View file

@ -26,5 +26,5 @@ public interface AdaliteApi {
@Headers("Content-Type: application/json")
@POST(ServerApiAdalite.ADALITE_SEND)
Call<List> adaliteSend(@Body AdaliteBody adaliteBody);
Call<String> adaliteSend(@Body AdaliteBody adaliteBody);
}

View file

@ -27,7 +27,7 @@ public class ServerApiAdalite {
private int requestsCount = 0;
private final String adaliteURL1 = "https://explorer2.adalite.io"; //TODO: make random selection, add more?, move
private final String adaliteURL1 = "https://explorer3.adalite.io"; //TODO: make random selection, add more?, move
private final String adaliteURL2 = "https://nodes.southeastasia.cloudapp.azure.com";
private String currentURL = adaliteURL1;
@ -48,7 +48,7 @@ public class ServerApiAdalite {
void onSuccess(String method, AdaliteResponseUtxo adaliteResponseUtxo);
void onSuccess(String method, List listResponse);
void onSuccess(String method, String stringResponse);
void onFail(String method, String message);
}
@ -145,10 +145,10 @@ public class ServerApiAdalite {
break;
case ADALITE_SEND:
Call<List> sendCall = adaliteApi.adaliteSend(new AdaliteBody(tx));
sendCall.enqueue(new Callback<List>() {
Call<String> sendCall = adaliteApi.adaliteSend(new AdaliteBody(tx));
sendCall.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull Call<List> call, @NonNull Response<List> response) {
public void onResponse(@NonNull Call<String> call, @NonNull Response<String> response) {
requestsCount--;
if (response.code() == 200) {
@ -166,7 +166,7 @@ public class ServerApiAdalite {
}
@Override
public void onFailure(@NonNull Call<List> call, @NonNull Throwable t) {
public void onFailure(@NonNull Call<String> call, @NonNull Throwable t) {
Log.e(TAG, "requestData " + method + " onFailure " + t.getMessage());
requestsCount--;

View file

@ -671,7 +671,7 @@ public class CardanoEngine extends CoinEngine {
}
@Override
public void onSuccess(String method, List listResponse) {
public void onSuccess(String method, String stringResponse) {
Log.e(TAG, "Wrong response type for requestBalanceAndUnspentTransactions");
ctx.setError("Wrong response type for requestBalanceAndUnspentTransactions");
blockchainRequestsCallbacks.onComplete(false);
@ -731,30 +731,19 @@ public class CardanoEngine extends CoinEngine {
}
@Override
public void onSuccess(String method, List listResponse) {
public void onSuccess(String method, String stringResponse) {
if (method.equals(ServerApiAdalite.ADALITE_SEND)) {
String resultString = listResponse.toString();
try {
if (resultString.isEmpty()) {
ctx.setError("No response from node");
blockchainRequestsCallbacks.onComplete(false);
} else { // TODO: Make check for a valid send response
ctx.setError(null);
blockchainRequestsCallbacks.onComplete(true);
}
} catch (Exception e) {
if (e.getMessage() != null) {
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
} else {
ctx.setError(e.getClass().getName());
blockchainRequestsCallbacks.onComplete(false);
Log.e(TAG, resultString);
}
if (stringResponse.equals("\"Transaction sent successfully!\"")) {
ctx.setError(null);
blockchainRequestsCallbacks.onComplete(true);
} else { // TODO: Make check for a valid send response
ctx.setError(stringResponse);
blockchainRequestsCallbacks.onComplete(false);
}
}
}
@Override
public void onFail(String method, String message) {
Log.e(TAG, "onFail: " + method + " " + message);

View file

@ -96,8 +96,23 @@ class CardManager(
*/
fun readIssuerData(cardId: String,
callback: (result: TaskEvent<ReadIssuerDataResponse>) -> Unit) {
val getIssuerDataCommand = ReadIssuerDataCommand()
val task = SingleCommandTask(getIssuerDataCommand)
val task = ReadIssuerDataTask(config.issuerPublicKey)
runTask(task, cardId, callback)
}
/**
* This task retrieves Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. . For example, this field may contain photo or
* biometric information for ID card product. Because of the large size of Issuer_Extra_Data,
* a series of these commands have to be executed to read the entire Issuer_Extra_Data.
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [ReadIssuerExtraDataTask],
* provides card response in the form of [ReadIssuerExtraDataResponse].
*/
fun readIssuerExtraData(cardId: String,
callback: (result: TaskEvent<ReadIssuerExtraDataResponse>) -> Unit) {
val task = ReadIssuerExtraDataTask(config.issuerPublicKey)
runTask(task, cardId, callback)
}
@ -108,7 +123,7 @@ class CardManager(
* wallet balance signed by the issuer or additional issuers attestation data.
* @param cardId CID, Unique Tangem card ID number.
* @param issuerData Data provided by issuer.
* @param issuerDataSignature Issuers signature of [issuerData] with Issuer Data Private Key (which is kept on card).
* @param issuerDataSignature Issuers signature of [issuerData] with Issuer Data Private Key.
* @param issuerDataCounter An optional counter that protect issuer data against replay attack.
* @param callback is triggered on the completion of the [WriteIssuerDataCommand],
* provides card response in the form of [WriteIssuerDataResponse].
@ -118,12 +133,46 @@ class CardManager(
issuerDataSignature: ByteArray,
issuerDataCounter: Int? = null,
callback: (result: TaskEvent<WriteIssuerDataResponse>) -> Unit) {
val writeIssuerDataCommand = WriteIssuerDataCommand(
val task = WriteIssuerDataTask(
issuerData,
issuerDataSignature,
issuerDataCounter,
config.issuerPublicKey
)
runTask(task, cardId, callback)
}
/**
* This task writes Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS.
* The issuer defines purpose of use, format and payload of Issuer Data.
* For example, this field may contain a photo or biometric information for ID card products.
* Because of the large size of Issuer_Extra_Data, a series of these commands have to be executed
* to write entire Issuer_Extra_Data.
* @param cardId CID, Unique Tangem card ID number.
* @param issuerData Data provided by issuer.
* @param startingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerDataCounter] (if flags Protect_Issuer_Data_Against_Replay and
* Restrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]) and size of [issuerData].
* @param finalizingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerData] and [issuerDataCounter] (the latter one only if flags Protect_Issuer_Data_Against_Replay
* andRestrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]).
* @param issuerDataCounter An optional counter that protect issuer data against replay attack.
* @param callback is triggered on the completion of the [WriteIssuerDataCommand],
* provides card response in the form of [WriteIssuerDataResponse].
*/
fun writeIssuerExtraData(cardId: String,
issuerData: ByteArray,
startingSignature: ByteArray,
finalizingSignature: ByteArray,
issuerDataCounter: Int? = null,
callback: (result: TaskEvent<WriteIssuerDataResponse>) -> Unit) {
val task = WriteIssuerExtraDataTask(
issuerData,
startingSignature, finalizingSignature,
config.issuerPublicKey,
issuerDataCounter
)
val task = SingleCommandTask(writeIssuerDataCommand)
runTask(task, cardId, callback)
}

View file

@ -21,6 +21,12 @@ interface CardManagerDelegate {
*/
fun onSecurityDelay(ms: Int, totalDurationSeconds: Int)
/**
* It is called when long tasks are performed.
* A user is expected to hold the card until the task is complete.
*/
fun onDelay(total: Int, current: Int, step: Int)
/**
* It is called when user takes the card away from the Android device during the scanning
* (for example when security delay is in progress) and the TagLostException is received.

View file

@ -1,5 +1,6 @@
package com.tangem
data class Config(
val linkedTerminal: Boolean = true
class Config(
val linkedTerminal: Boolean = true,
val issuerPublicKey: ByteArray? = null
)

View file

@ -1,12 +1,12 @@
package com.tangem.commands
import com.tangem.commands.common.DefaultIssuerDataVerifier
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataVerifier
import com.tangem.common.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
@ -50,12 +50,16 @@ class ReadIssuerDataResponse(
* wallet balance signed by the issuer or additional issuers attestation data.
* @property cardId CID, Unique Tangem card ID number.
*/
class ReadIssuerDataCommand : CommandSerializer<ReadIssuerDataResponse>() {
class ReadIssuerDataCommand(
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : CommandSerializer<ReadIssuerDataResponse>(),
IssuerDataVerifier by verifier {
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadData)
return CommandApdu(Instruction.ReadIssuerData, tlvBuilder.serialize())
}

View file

@ -0,0 +1,97 @@
package com.tangem.commands
import com.tangem.commands.common.DefaultIssuerDataVerifier
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataVerifier
import com.tangem.common.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.tasks.TaskError
class ReadIssuerExtraDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Size of all Issuer_Extra_Data field.
*/
val size: Int?,
/**
* Data defined by issuer.
*/
val issuerData: ByteArray,
/**
* Issuers signature of [issuerData] with Issuer Data Private Key (which is kept on card).
* Issuers signature of SHA256-hashed [cardId] concatenated with [issuerData]:
* SHA256([cardId] | [issuerData]).
* When flag [SettingsMask.protectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
*/
val issuerDataSignature: ByteArray?,
/**
* An optional counter that protect issuer data against replay attack.
* When flag [SettingsMask.protectIssuerDataAgainstReplay] set in [SettingsMask]
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
*/
val issuerDataCounter: Int?
) : CommandResponse
/**
* This command retrieves Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. . For example, this field may contain photo or
* biometric information for ID card product. Because of the large size of Issuer_Extra_Data,
* a series of these commands have to be executed to read the entire Issuer_Extra_Data.
*/
class ReadIssuerExtraDataCommand(
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : CommandSerializer<ReadIssuerExtraDataResponse>(), IssuerDataVerifier by verifier {
var offset: Int = 0
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadExtraData)
tlvBuilder.append(TlvTag.Offset, offset)
return CommandApdu(Instruction.ReadIssuerData, tlvBuilder.serialize())
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): ReadIssuerExtraDataResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
return try {
val mapper = TlvMapper(tlvData)
ReadIssuerExtraDataResponse(
cardId = mapper.map(TlvTag.CardId),
size = mapper.mapOptional(TlvTag.Size),
issuerData = mapper.mapOptional(TlvTag.IssuerData) ?: byteArrayOf(),
issuerDataSignature = mapper.mapOptional(TlvTag.IssuerDataSignature),
issuerDataCounter = mapper.mapOptional(TlvTag.IssuerDataCounter)
)
} catch (exception: Exception) {
throw TaskError.SerializeCommandError()
}
}
companion object {
/**
* This mode value specifies that this command retrieves Issuer EXTRA data from the card
* (with value 0 the command will get instead simple Issuer Data from the card).
*/
const val EXTRA_DATA_MODE = 1
}
}

View file

@ -1,13 +1,12 @@
package com.tangem.commands
import com.tangem.commands.common.DefaultIssuerDataVerifier
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataVerifier
import com.tangem.common.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toByteArray
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
@ -33,13 +32,15 @@ class WriteIssuerDataResponse(
class WriteIssuerDataCommand(
private val issuerData: ByteArray,
private val issuerDataSignature: ByteArray,
private val issuerDataCounter: Int? = null
) : CommandSerializer<WriteIssuerDataResponse>() {
private val issuerDataCounter: Int? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : CommandSerializer<WriteIssuerDataResponse>(), IssuerDataVerifier by verifier {
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.WriteData)
tlvBuilder.append(TlvTag.IssuerData, issuerData)
tlvBuilder.append(TlvTag.IssuerDataSignature, issuerDataSignature)
tlvBuilder.append(TlvTag.IssuerDataCounter, issuerDataCounter)

View file

@ -0,0 +1,90 @@
package com.tangem.commands
import com.tangem.commands.common.DefaultIssuerDataVerifier
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataVerifier
import com.tangem.common.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.tasks.TaskError
/**
* This command writes Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS.
* The issuer defines purpose of use, format and payload of Issuer Data.
* For example, this field may contain a photo or biometric information for ID card products.
* Because of the large size of Issuer_Extra_Data, a series of these commands have to be executed
* to write entire Issuer_Extra_Data.
* @param issuerData Data provided by issuer.
* @param startingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerDataCounter] (if flags Protect_Issuer_Data_Against_Replay and
* Restrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]) and size of [issuerData].
* @param finalizingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerData] and [issuerDataCounter] (the latter one only if flags Protect_Issuer_Data_Against_Replay
* andRestrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]).
* @param issuerDataCounter An optional counter that protect issuer data against replay attack.
*/
class WriteIssuerExtraDataCommand(
private val issuerData: ByteArray,
private val startingSignature: ByteArray,
private val finalizingSignature: ByteArray,
private val issuerDataCounter: Int? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : CommandSerializer<WriteIssuerDataResponse>(), IssuerDataVerifier by verifier {
var mode: IssuerDataMode = IssuerDataMode.InitializeWritingExtraData
var offset: Int = 0
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
tlvBuilder.append(TlvTag.Mode, mode)
when (mode) {
IssuerDataMode.InitializeWritingExtraData -> {
tlvBuilder.append(TlvTag.Size, issuerData.size)
tlvBuilder.append(TlvTag.IssuerDataSignature, startingSignature)
tlvBuilder.append(TlvTag.IssuerDataCounter, issuerDataCounter)
}
IssuerDataMode.WriteExtraData -> {
tlvBuilder.append(TlvTag.IssuerData, getDataToWrite())
tlvBuilder.append(TlvTag.Offset, offset)
}
IssuerDataMode.FinalizeExtraData -> {
tlvBuilder.append(TlvTag.IssuerDataSignature, finalizingSignature)
}
}
return CommandApdu(Instruction.WriteIssuerData, tlvBuilder.serialize())
}
private fun getDataToWrite(): ByteArray =
issuerData.copyOfRange(offset, offset + calculatePartSize())
private fun calculatePartSize(): Int {
val bytesLeft = issuerData.size - offset
return if (bytesLeft < SINGLE_WRITE_SIZE) bytesLeft else SINGLE_WRITE_SIZE
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): WriteIssuerDataResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
return try {
val mapper = TlvMapper(tlvData)
WriteIssuerDataResponse(
cardId = mapper.map(TlvTag.CardId)
)
} catch (exception: Exception) {
throw TaskError.SerializeCommandError()
}
}
companion object {
const val SINGLE_WRITE_SIZE = 1524
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.commands.common
import com.tangem.commands.WriteIssuerExtraDataCommand
/**
* This enum specifies modes for [WriteIssuerExtraDataCommand].
*/
enum class IssuerDataMode(val code: Byte) {
/**
* This mode is required to read issuer data from the card.
*/
ReadData(0),
/**
* This mode is required to write issuer data to the card.
*/
WriteData(0),
/**
* This mode is required to read issuer extra data from the card.
*/
ReadExtraData(1),
/**
* This mode is required to initiate writing issuer extra data to the card.
*/
InitializeWritingExtraData(1),
/**
* With this mode, the command writes part of issuer extra data
* (block of a size [WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE]) to the card.
*/
WriteExtraData(2),
/**
* This mode is used after the issuer extra data was fully written to the card.
* Under this mode the command provides the issuer signature
* to confirm the validity of data that was written to card.
*/
FinalizeExtraData(3);
companion object {
private val values = values()
fun byCode(code: Byte): IssuerDataMode? = values.find { it.code == code }
}
}

View file

@ -0,0 +1,40 @@
package com.tangem.commands.common
import com.tangem.common.tlv.TlvEncoder
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.CryptoUtils
import java.io.ByteArrayOutputStream
interface IssuerDataVerifier {
fun verify(
issuerPublicKey: ByteArray, signature: ByteArray, issuerDataToVerify: IssuerDataToVerify
): Boolean
}
class IssuerDataToVerify(
val cardId: String,
val issuerData: ByteArray?,
val issuerDataCounter: Int? = null,
val issuerExtraDataSize: Int? = null
)
class DefaultIssuerDataVerifier : IssuerDataVerifier {
override fun verify(
issuerPublicKey: ByteArray,
signature: ByteArray,
issuerDataToVerify: IssuerDataToVerify
): Boolean {
val tlvEncoder = TlvEncoder()
val dataToVerify = ByteArrayOutputStream()
dataToVerify.write(tlvEncoder.encodeValue(TlvTag.CardId, issuerDataToVerify.cardId))
issuerDataToVerify.issuerData?.let { dataToVerify.write(it) }
issuerDataToVerify.issuerDataCounter?.let { counter ->
dataToVerify.write(tlvEncoder.encodeValue(TlvTag.IssuerDataCounter, counter))
}
issuerDataToVerify.issuerExtraDataSize?.let {
dataToVerify.write(tlvEncoder.encodeValue(TlvTag.Size, it))
}
return CryptoUtils.verify(issuerPublicKey, dataToVerify.toByteArray(), signature)
}
}

View file

@ -2,9 +2,15 @@ package com.tangem.common.extensions
import java.nio.ByteBuffer
fun Int.toByteArray(): ByteArray {
val bufferSize = Int.SIZE_BYTES
val buffer = ByteBuffer.allocate(bufferSize)
buffer.putInt(this)
return buffer.array()
fun Int.toByteArray(size: Int = Int.SIZE_BYTES): ByteArray {
if (size == Int.SIZE_BYTES) {
val buffer = ByteBuffer.allocate(size)
buffer.putInt(this)
return buffer.array()
} else if (size == Short.SIZE_BYTES){
return byteArrayOf(
(this ushr 8).toByte(),
this.toByte())
}
return byteArrayOf()
}

View file

@ -2,22 +2,33 @@ package com.tangem.common.tlv
import com.tangem.Log
import com.tangem.commands.*
import com.tangem.common.extensions.*
import com.tangem.commands.common.IssuerDataMode
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toByteArray
import com.tangem.tasks.TaskError
import java.time.Year
import java.util.*
/**
* Encodes information that is to be written on the card from parsed classes into [ByteArray]
* (according to the provided [TlvTag] and corresponding [TlvValueType])
* and then forms [Tlv] with the encoded values.
*/
class TlvEncoder {
/**
* @param value information that is to be encoded into [Tlv].
*/
internal inline fun <reified T> encode(tag: TlvTag, value: T?): Tlv {
if (value != null) {
return Tlv(tag, encodeValue(value, tag))
return Tlv(tag, encodeValue(tag, value))
} else {
Log.e(this::class.simpleName!!, "Encoding error. Value for tag $tag is null")
throw TaskError.SerializeCommandError()
}
}
private inline fun <reified T> encodeValue(value: T, tag: TlvTag): ByteArray {
internal inline fun <reified T> encodeValue(tag: TlvTag, value: T): ByteArray {
return when (tag.valueType()) {
TlvValueType.HexString -> {
typeCheck<T, String>(tag)
@ -31,7 +42,11 @@ class TlvEncoder {
typeCheck<T, String>(tag)
(value as String).toByteArray()
}
TlvValueType.IntValue -> {
TlvValueType.Uint16 -> {
typeCheck<T, Int>(tag)
(value as Int).toByteArray(2)
}
TlvValueType.Uint32 -> {
typeCheck<T, Int>(tag)
(value as Int).toByteArray()
}
@ -74,6 +89,10 @@ class TlvEncoder {
typeCheck<T, SigningMethod>(tag)
(value as SigningMethod).rawValue.toByteArray()
}
TlvValueType.IssuerDataMode -> {
typeCheck<T, IssuerDataMode>(tag)
byteArrayOf((value as IssuerDataMode).code)
}
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.common.tlv
import com.tangem.Log
import com.tangem.commands.*
import com.tangem.commands.common.IssuerDataMode
import com.tangem.common.extensions.toDate
import com.tangem.common.extensions.toHexString
import com.tangem.common.extensions.toInt
@ -34,7 +35,7 @@ class TlvMapper(val tlvList: List<Tlv>) {
/**
* Finds [Tlv] by its [TlvTag].
* Throws [MissingTagException] if [Tlv] is not found,
* Throws [TaskError.MissingTag] if [Tlv] is not found,
* otherwise converts [Tlv] value to [T].
*
* @param tag [TlvTag] of a [Tlv] which value is to be returned.
@ -61,7 +62,7 @@ class TlvMapper(val tlvList: List<Tlv>) {
typeCheck<T, String>(tag)
tlvValue.toUtf8() as T
}
TlvValueType.IntValue -> {
TlvValueType.Uint16, TlvValueType.Uint32 -> {
typeCheck<T, Int>(tag)
try {
tlvValue.toInt() as T
@ -129,6 +130,15 @@ class TlvMapper(val tlvList: List<Tlv>) {
throw TaskError.ConvertError()
}
}
TlvValueType.IssuerDataMode -> {
typeCheck<T, IssuerDataMode>(tag)
try {
IssuerDataMode.byCode(tlvValue.toInt().toByte()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.toInt().toString(), exception)
throw TaskError.ConvertError()
}
}
}
}

View file

@ -6,7 +6,8 @@ package com.tangem.common.tlv
enum class TlvValueType {
HexString,
Utf8String,
IntValue,
Uint16,
Uint32,
BoolValue,
ByteArray,
EllipticCurve,
@ -14,7 +15,8 @@ enum class TlvValueType {
ProductMask,
SettingsMask,
CardStatus,
SigningMethod
SigningMethod,
IssuerDataMode
}
/**
@ -60,6 +62,10 @@ enum class TlvTag(val code: Int) {
IssuerDataSignature(0x33),
IssuerTransactionSignature(0x34),
IssuerDataCounter(0x35),
Size(0x25),
Mode(0x23),
Offset(0x24),
IsActivated(0x3A),
ActivationSeed(0x3B),
@ -113,13 +119,16 @@ enum class TlvTag(val code: Int) {
TlvValueType.Utf8String
CurveId -> TlvValueType.EllipticCurve
MaxSignatures, PauseBeforePin2, RemainingSignatures,
SignedHashes, Health, TokenDecimal, UserCounter, IssuerDataCounter -> TlvValueType.IntValue
SignedHashes, Health, TokenDecimal,
Offset, Size -> TlvValueType.Uint16
UserCounter, IssuerDataCounter -> TlvValueType.Uint32
IsActivated, TerminalIsLinked -> TlvValueType.BoolValue
ManufactureDateTime -> TlvValueType.DateTime
ProductMask -> TlvValueType.ProductMask
SettingsMask -> TlvValueType.SettingsMask
Status -> TlvValueType.CardStatus
SigningMethod -> TlvValueType.SigningMethod
Mode -> TlvValueType.IssuerDataMode
else -> TlvValueType.ByteArray
}
}

View file

@ -0,0 +1,53 @@
package com.tangem.tasks
import com.tangem.commands.Card
import com.tangem.commands.ReadIssuerDataCommand
import com.tangem.commands.ReadIssuerDataResponse
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.common.CardEnvironment
import com.tangem.common.CompletionResult
class ReadIssuerDataTask(private val issuerPublicKey: ByteArray? = null) : Task<ReadIssuerDataResponse>() {
override fun onRun(
cardEnvironment: CardEnvironment,
currentCard: Card?,
callback: (result: TaskEvent<ReadIssuerDataResponse>) -> Unit) {
val command = ReadIssuerDataCommand()
sendCommand(command, cardEnvironment) { result ->
when (result) {
is CompletionResult.Success -> {
val data = result.data
if (data.issuerData.isEmpty()) {
completeNfcSession()
callback(TaskEvent.Event(result.data))
callback(TaskEvent.Completion())
return@sendCommand
}
val publicKey = issuerPublicKey ?: currentCard!!.issuerPublicKey!!
val issuerDataToVerify = IssuerDataToVerify(
cardEnvironment.cardId!!,
data.issuerData,
data.issuerDataCounter
)
if (command.verify(publicKey, data.issuerDataSignature, issuerDataToVerify)) {
completeNfcSession()
callback(TaskEvent.Event(result.data))
callback(TaskEvent.Completion())
} else {
completeNfcSession(TaskError.VerificationFailed())
callback(TaskEvent.Completion(TaskError.VerificationFailed()))
}
}
is CompletionResult.Failure -> {
if (result.error !is TaskError.UserCancelled) {
completeNfcSession(result.error)
}
callback(TaskEvent.Completion(result.error))
}
}
}
}
}

View file

@ -0,0 +1,100 @@
package com.tangem.tasks
import com.tangem.commands.Card
import com.tangem.commands.ReadIssuerExtraDataCommand
import com.tangem.commands.ReadIssuerExtraDataResponse
import com.tangem.commands.WriteIssuerExtraDataCommand
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.common.CardEnvironment
import com.tangem.common.CompletionResult
import java.io.ByteArrayOutputStream
/**
* This task performs [ReadIssuerExtraDataCommand] repeatedly until
* the issuer extra data is fully retrieved.
*/
internal class ReadIssuerExtraDataTask(
private val issuerPublicKey: ByteArray?) : Task<ReadIssuerExtraDataResponse>() {
private val issuerData = ByteArrayOutputStream()
private var issuerDataSize = 0
private lateinit var card: Card
private lateinit var cardEnvironment: CardEnvironment
override fun onRun(
cardEnvironment: CardEnvironment,
currentCard: Card?,
callback: (result: TaskEvent<ReadIssuerExtraDataResponse>) -> Unit
) {
card = currentCard!!
this.cardEnvironment = cardEnvironment
val command = ReadIssuerExtraDataCommand()
readIssuerData(command, callback)
}
private fun readIssuerData(
command: ReadIssuerExtraDataCommand,
callback: (result: TaskEvent<ReadIssuerExtraDataResponse>) -> Unit) {
if (issuerDataSize != 0) {
delegate?.onDelay(
issuerDataSize, command.offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE
)
}
sendCommand(command, cardEnvironment) { result ->
when (result) {
is CompletionResult.Success -> {
if (result.data.size != null) {
if (result.data.size == 0) {
completeNfcSession()
callback(TaskEvent.Event(result.data))
callback(TaskEvent.Completion())
return@sendCommand
}
issuerDataSize = result.data.size
}
issuerData.write(result.data.issuerData)
if (result.data.issuerDataSignature == null) {
command.offset = issuerData.size()
readIssuerData(command, callback)
} else {
completeTask(result.data, command, callback)
}
}
is CompletionResult.Failure -> {
if (result.error !is TaskError.UserCancelled) {
completeNfcSession(result.error)
}
callback(TaskEvent.Completion(result.error))
}
}
}
}
private fun completeTask(data: ReadIssuerExtraDataResponse, command: ReadIssuerExtraDataCommand,
callback: (result: TaskEvent<ReadIssuerExtraDataResponse>) -> Unit) {
val publicKey = issuerPublicKey ?: card.issuerPublicKey!!
val dataToVerify = IssuerDataToVerify(
cardEnvironment.cardId!!,
issuerData.toByteArray(),
data.issuerDataCounter
)
if (command.verify(publicKey, data.issuerDataSignature!!, dataToVerify)) {
completeNfcSession()
val finalResult = ReadIssuerExtraDataResponse(
data.cardId,
issuerDataSize,
issuerData.toByteArray(),
data.issuerDataSignature,
data.issuerDataCounter
)
callback(TaskEvent.Event(finalResult))
callback(TaskEvent.Completion())
} else {
completeNfcSession(TaskError.VerificationFailed())
callback(TaskEvent.Completion(TaskError.VerificationFailed()))
}
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.tasks
import com.tangem.common.CardEnvironment
import com.tangem.CardManagerDelegate
import com.tangem.CardReader
import com.tangem.Log
@ -8,6 +7,7 @@ import com.tangem.commands.Card
import com.tangem.commands.CommandResponse
import com.tangem.commands.CommandSerializer
import com.tangem.commands.ReadCommand
import com.tangem.common.CardEnvironment
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.StatusWord
@ -51,6 +51,9 @@ sealed class TaskError(val code: Int): Exception() {
class TagLost: TaskError(5003)
class UnknownError: TaskError(6000)
//Issuer Data Errors
class MissingCounter: TaskError(7001)
}
/**
@ -105,7 +108,6 @@ abstract class Task<T> {
/**
* Should be called on [Task] completion, whether it was successful or with failure.
*
* @param withError True when there is an error
* @param taskError The error to be shown by [CardManagerDelegate]
*/
protected fun completeNfcSession(taskError: TaskError? = null) {

View file

@ -0,0 +1,67 @@
package com.tangem.tasks
import com.tangem.commands.Card
import com.tangem.commands.SettingsMask
import com.tangem.commands.WriteIssuerDataCommand
import com.tangem.commands.WriteIssuerDataResponse
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.common.CardEnvironment
import com.tangem.common.CompletionResult
class WriteIssuerDataTask(
private val issuerData: ByteArray,
private val issuerDataSignature: ByteArray,
private val issuerDataCounter: Int? = null,
private val issuerPublicKey: ByteArray? = null
) : Task<WriteIssuerDataResponse>() {
private lateinit var card: Card
override fun onRun(
cardEnvironment: CardEnvironment,
currentCard: Card?,
callback: (result: TaskEvent<WriteIssuerDataResponse>) -> Unit) {
card = currentCard!!
val command = WriteIssuerDataCommand(
issuerData, issuerDataSignature, issuerDataCounter
)
if (!isCounterValid(issuerDataCounter)) {
completeNfcSession(TaskError.MissingCounter())
callback(TaskEvent.Completion(TaskError.MissingCounter()))
} else if (!verifySignature(command, cardEnvironment.cardId!!)) {
completeNfcSession(TaskError.VerificationFailed())
callback(TaskEvent.Completion(TaskError.VerificationFailed()))
}
sendCommand(command, cardEnvironment) { result ->
when (result) {
is CompletionResult.Success -> {
completeNfcSession()
callback(TaskEvent.Event(result.data))
callback(TaskEvent.Completion())
}
is CompletionResult.Failure -> {
if (result.error !is TaskError.UserCancelled) {
completeNfcSession(result.error)
}
callback(TaskEvent.Completion(result.error))
}
}
}
}
private fun isCounterValid(issuerDataCounter: Int?): Boolean =
if (isCounterRequired()) issuerDataCounter != null else true
private fun isCounterRequired(): Boolean =
card.settingsMask?.contains(SettingsMask.protectIssuerDataAgainstReplay) != false
private fun verifySignature(command: WriteIssuerDataCommand, cardId: String): Boolean {
return command.verify(
issuerPublicKey ?: card.issuerPublicKey!!,
issuerDataSignature,
IssuerDataToVerify(cardId, issuerData, issuerDataCounter)
)
}
}

View file

@ -0,0 +1,113 @@
package com.tangem.tasks
import com.tangem.commands.Card
import com.tangem.commands.SettingsMask
import com.tangem.commands.WriteIssuerDataResponse
import com.tangem.commands.WriteIssuerExtraDataCommand
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.common.CardEnvironment
import com.tangem.common.CompletionResult
/**
* This task performs [WriteIssuerExtraDataCommand] repeatedly until the issuer extra data is fully
* written on the card.
* @param issuerData Data provided by issuer.
* @param startingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerDataCounter] (if flags Protect_Issuer_Data_Against_Replay and
* Restrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]) and size of [issuerData].
* @param finalizingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerData] and [issuerDataCounter] (the latter one only if flags Protect_Issuer_Data_Against_Replay
* andRestrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]).
* @param issuerDataCounter An optional counter that protect issuer data against replay attack.
*/
internal class WriteIssuerExtraDataTask(
private val issuerData: ByteArray,
private val startingSignature: ByteArray,
private val finalizingSignature: ByteArray,
private val issuerPublicKey: ByteArray? = null,
private val issuerDataCounter: Int? = null
) : Task<WriteIssuerDataResponse>() {
private lateinit var card: Card
private lateinit var cardEnvironment: CardEnvironment
override fun onRun(cardEnvironment: CardEnvironment,
currentCard: Card?,
callback: (result: TaskEvent<WriteIssuerDataResponse>) -> Unit) {
card = currentCard!!
this.cardEnvironment = cardEnvironment
val command = WriteIssuerExtraDataCommand(
issuerData, startingSignature, finalizingSignature, issuerDataCounter
)
if (!isCounterValid(issuerDataCounter)) {
completeNfcSession(TaskError.MissingCounter())
callback(TaskEvent.Completion(TaskError.MissingCounter()))
} else if (!verifySignatures(command)) {
completeNfcSession(TaskError.VerificationFailed())
callback(TaskEvent.Completion(TaskError.VerificationFailed()))
}
writeIssuerData(command, callback)
}
private fun writeIssuerData(
command: WriteIssuerExtraDataCommand,
callback: (result: TaskEvent<WriteIssuerDataResponse>) -> Unit) {
if (command.mode == IssuerDataMode.WriteExtraData) {
delegate?.onDelay(issuerData.size, command.offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE)
}
sendCommand(command, cardEnvironment) { result ->
when (result) {
is CompletionResult.Success -> {
when (command.mode) {
IssuerDataMode.InitializeWritingExtraData -> {
command.mode = IssuerDataMode.WriteExtraData
writeIssuerData(command, callback)
return@sendCommand
}
IssuerDataMode.WriteExtraData -> {
command.offset += WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE
if (command.offset >= issuerData.size) {
command.mode = IssuerDataMode.FinalizeExtraData
}
writeIssuerData(command, callback)
return@sendCommand
}
IssuerDataMode.FinalizeExtraData -> {
completeNfcSession()
callback(TaskEvent.Event(result.data))
callback(TaskEvent.Completion())
}
}
}
is CompletionResult.Failure -> {
if (result.error !is TaskError.UserCancelled) {
completeNfcSession(result.error)
}
callback(TaskEvent.Completion(result.error))
}
}
}
}
private fun isCounterValid(issuerDataCounter: Int?): Boolean =
if (isCounterRequired()) issuerDataCounter != null else true
private fun isCounterRequired(): Boolean =
card.settingsMask?.contains(SettingsMask.protectIssuerDataAgainstReplay) != false
private fun verifySignatures(command: WriteIssuerExtraDataCommand): Boolean {
val publicKey = issuerPublicKey ?: card.issuerPublicKey!!
val cardId = cardEnvironment.cardId!!
val firstData = IssuerDataToVerify(cardId, null, issuerDataCounter, issuerData.size)
val secondData = IssuerDataToVerify(cardId, issuerData, issuerDataCounter)
return command.verify(publicKey, startingSignature, firstData) &&
command.verify(publicKey, finalizingSignature, secondData)
}
}

View file

@ -3,6 +3,11 @@ package com.tangem.tangemtest
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.tangem.CardManager
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toByteArray
import com.tangem.common.extensions.toHexString
import com.tangem.crypto.CryptoUtils
import com.tangem.crypto.sign
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tasks.ScanEvent
import com.tangem.tasks.TaskError
@ -15,6 +20,7 @@ class MainActivity : AppCompatActivity() {
private lateinit var cardId: String
private lateinit var issuerData: ByteArray
private lateinit var issuerDataSignature: ByteArray
private var issuerDataCounter: Int = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -41,6 +47,8 @@ class MainActivity : AppCompatActivity() {
tv_card_cid?.text = cardId
btn_sign.isEnabled = true
btn_read_issuer_data.isEnabled = true
btn_read_issuer_extra_data.isEnabled = true
btn_write_issuer_data.isEnabled = true
btn_purge_wallet.isEnabled = true
btn_create_wallet.isEnabled = true
}
@ -101,6 +109,20 @@ class MainActivity : AppCompatActivity() {
}
}
}
btn_read_issuer_extra_data?.setOnClickListener { _ ->
cardManager.readIssuerExtraData(cardId) {
when (it) {
is TaskEvent.Completion -> {
if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName }
}
is TaskEvent.Event -> runOnUiThread {
issuerDataCounter = (it.data.issuerDataCounter ?: 0) + 1
btn_write_issuer_data.isEnabled = true
tv_card_cid?.text = "Read ${it.data.issuerData.size} bytes of data."
}
}
}
}
btn_purge_wallet?.setOnClickListener { _ ->
cardManager.purgeWallet(
cardId) {

View file

@ -33,7 +33,7 @@
<Button
android:id="@+id/btn_sign"
android:layout_marginTop="24dp"
android:layout_marginTop="16dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Sign transaction"
@ -44,7 +44,7 @@
<Button
android:id="@+id/btn_read_issuer_data"
android:layout_marginTop="24dp"
android:layout_marginTop="16dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Read issuer data"
@ -55,7 +55,7 @@
<Button
android:id="@+id/btn_write_issuer_data"
android:layout_marginTop="24dp"
android:layout_marginTop="16dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Write issuer data"
@ -65,19 +65,30 @@
android:enabled="false"/>
<Button
android:id="@+id/btn_purge_wallet"
android:layout_marginTop="24dp"
android:id="@+id/btn_read_issuer_extra_data"
android:layout_marginTop="16dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Purge wallet"
android:text="Read issuer extra data"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_write_issuer_data"
android:enabled="false"/>
<Button
android:id="@+id/btn_purge_wallet"
android:layout_marginTop="16dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Purge wallet"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_read_issuer_extra_data"
android:enabled="false"/>
<Button
android:id="@+id/btn_create_wallet"
android:layout_marginTop="24dp"
android:layout_marginTop="16dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Create wallet"

View file

@ -1,6 +1,7 @@
package com.tangem.tangem_sdk_new
import android.animation.ObjectAnimator
import android.view.HapticFeedbackConstants
import android.view.View
import android.view.animation.DecelerateInterpolator
import androidx.fragment.app.FragmentActivity
@ -77,7 +78,8 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_security_delay)
readingDialog?.tvTaskText?.text =
activity.getText(R.string.dialog_security_delay_description)
// readingDialog?.pbSecurityDelay?.isIndeterminate = false
performHapticFeedback()
if (readingDialog?.pbSecurityDelay?.max != totalDurationSeconds) {
readingDialog?.pbSecurityDelay?.max = totalDurationSeconds
@ -95,6 +97,32 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
}
}
override fun onDelay(total: Int, current: Int, step: Int) {
postUI {
readingDialog?.lTouchCard?.hide()
readingDialog?.flSecurityDelay?.show()
readingDialog?.tvRemainingTime?.text = (((total - current) / step) + 1).toString()
readingDialog?.tvTaskTitle?.text = "Operation in process"
readingDialog?.tvTaskText?.text = "Please hold the card firmly until the operation is completed…"
performHapticFeedback()
if (readingDialog?.pbSecurityDelay?.max != total) {
readingDialog?.pbSecurityDelay?.max = total
}
readingDialog?.pbSecurityDelay?.progress = current
val animation = ObjectAnimator.ofInt(
readingDialog?.pbSecurityDelay,
"progress",
current,
current + step)
animation.duration = 300
animation.interpolator = DecelerateInterpolator()
animation.start()
}
}
override fun onTagLost() {
postUI {
readingDialog?.lTouchCard?.show()
@ -110,6 +138,7 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
readingDialog?.flSecurityDelay?.hide()
readingDialog?.flCompletion?.show()
readingDialog?.ivCompletion?.setImageDrawable(activity.getDrawable(R.drawable.ic_done_135dp))
performHapticFeedback()
}
postUI(300) { readingDialog?.dismiss() }
}
@ -122,6 +151,7 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
readingDialog?.flError?.show()
readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_error)
readingDialog?.tvTaskText?.text = "${error::class.simpleName}: ${error.code}"
performHapticFeedback()
}
}
@ -129,6 +159,12 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
private fun performHapticFeedback() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
readingDialog?.llHeader?.isHapticFeedbackEnabled = true
readingDialog?.llHeader?.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
}
}
private fun setLogger() {
Log.setLogger(

View file

@ -8,6 +8,7 @@
app:layout_behavior="@string/bottom_sheet_behavior">
<LinearLayout
android:id="@+id/llHeader"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="#e2e5e6"