Updated on 2026-08-14

This commit is contained in:
Tangem 2020-06-11 12:53:54 +00:00
commit 19d44f47d4
40 changed files with 1210 additions and 1112 deletions

View file

@ -11,6 +11,8 @@ dependencies {
// kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.7"
// crypto
implementation "com.madgag.spongycastle:core:1.58.0.0"
@ -21,8 +23,8 @@ dependencies {
implementation 'com.google.code.gson:gson:2.8.6'
// tests
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
testImplementation "com.google.truth:truth:1.0"
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.2'
testImplementation "com.google.truth:truth:1.0.1"
}
sourceCompatibility = "8"

View file

@ -3,6 +3,8 @@ package com.tangem
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BroadcastChannel
/**
* Allows interaction between the phone or any other terminal and Tangem card.
@ -11,6 +13,18 @@ import com.tangem.common.apdu.ResponseApdu
*/
interface CardReader {
val tag: BroadcastChannel<TagType?>
var scope: CoroutineScope?
/**
* Sends data to the card and receives the reply in an asynchronous way using coroutines.
*
* @param apdu Data to be sent. [CommandApdu] serializes it to a [ByteArray]
* @param callback Returns response from the card,
* [ResponseApdu] Allows to convert raw data to [Tlv]
*/
suspend fun transceiveApdu(apdu: CommandApdu): CompletionResult<ResponseApdu>
/**
* Sends data to the card and receives the reply.
*
@ -23,10 +37,17 @@ interface CardReader {
/**
* Signals to [CardReader] to become ready to transceive data.
*/
fun openSession()
fun startSession()
/**
* Signals to [CardReader] that no further NFC transition is expected.
*/
fun closeSession()
fun stopSession(cancelled: Boolean = false)
fun readSlixTag(callback: (result: CompletionResult<ResponseApdu>) -> Unit)
}
interface ReadingActiveListener {
var readingIsActive: Boolean
}

View file

@ -1,6 +1,5 @@
package com.tangem
import com.tangem.commands.Card
import com.tangem.commands.CommandResponse
import com.tangem.commands.OpenSessionCommand
import com.tangem.commands.ReadCommand
@ -10,9 +9,9 @@ import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.getType
import com.tangem.crypto.EncryptionHelper
import com.tangem.crypto.FastEncryptionHelper
import com.tangem.crypto.StrongEncryptionHelper
import com.tangem.crypto.pbkdf2Hash
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
/**
* Basic interface for running tasks and [com.tangem.commands.Command] in a [CardSession]
@ -30,6 +29,16 @@ interface CardSessionRunnable<T : CommandResponse> {
fun run(session: CardSession, callback: (result: CompletionResult<T>) -> Unit)
}
enum class CardSessionState {
Inactive,
Active
}
enum class TagType {
Nfc,
Slix
}
/**
* Allows interaction with Tangem cards. Should be opened before sending commands.
*
@ -44,20 +53,25 @@ interface CardSessionRunnable<T : CommandResponse> {
* If null, a default header and text body will be used.
*/
class CardSession(
val environment: SessionEnvironment,
private val reader: CardReader,
val viewDelegate: SessionViewDelegate,
private var cardId: String? = null,
private val initialMessage: Message? = null
val environment: SessionEnvironment,
private val reader: CardReader,
val viewDelegate: SessionViewDelegate,
private var cardId: String? = null,
private val initialMessage: Message? = null
) {
private val tag = this.javaClass.simpleName
var connectedTag: TagType? = null
/**
* True if some operation is still in progress.
*/
private var isBusy = false
private var state = CardSessionState.Inactive
private var performPreflightRead = true
val scope = CoroutineScope(Dispatchers.IO) + CoroutineExceptionHandler { _, ex ->
throw ex
}
private val tag = this.javaClass.simpleName
/**
* This metod starts a card session, performs preflight [ReadCommand],
@ -66,19 +80,14 @@ class CardSession(
* @param callback will be triggered with a [CompletionResult] of a session.
*/
fun <T : CardSessionRunnable<R>, R : CommandResponse> startWithRunnable(
runnable: T, callback: (result: CompletionResult<R>) -> Unit) {
runnable: T, callback: (result: CompletionResult<R>) -> Unit
) {
performPreflightRead = runnable.performPreflightRead
start { session, error ->
start(runnable.performPreflightRead) { session, error ->
if (error != null) {
callback(CompletionResult.Failure(error))
return@start
}
if (runnable is ReadCommand) {
callback(CompletionResult.Success(environment.card as R))
return@start
}
runnable.run(this) { result ->
when (result) {
@ -103,72 +112,77 @@ class CardSession(
* Starts a card session and performs preflight [ReadCommand].
* @param callback: callback with the card session. Can contain [TangemSdkError] if something goes wrong.
*/
fun start(callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
try {
startSession()
} catch (error: TangemSdkError) {
callback(this, error)
}
fun start(
performPreflightRead: Boolean = true,
callback: (session: CardSession, error: TangemSdkError?) -> Unit
) {
if (!performPreflightRead) {
callback(this, null)
if (state != CardSessionState.Inactive) {
callback(this, TangemSdkError.Busy())
return
}
state = CardSessionState.Active
viewDelegate.onSessionStarted(cardId)
preflightRead() { result ->
scope.launch {
reader.tag
.asFlow()
.collect { tagType ->
if (tagType == null && connectedTag != null) {
handleTagLost()
} else if (tagType != null) {
connectedTag = tagType
viewDelegate.onTagConnected()
if (tagType == TagType.Nfc && performPreflightRead) {
preflightCheck(callback)
} else {
callback(this@CardSession, null)
}
}
}
}
reader.scope = scope
reader.startSession()
}
private fun handleTagLost() {
connectedTag = null
environment.encryptionKey = null
viewDelegate.onTagLost()
}
private fun preflightCheck(callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
val readCommand = ReadCommand()
readCommand.run(this) { result ->
when (result) {
is CompletionResult.Failure -> {
callback(this, result.error)
stopWithError(result.error)
callback(this, result.error)
}
is CompletionResult.Success -> {
val receivedCardId = result.data.cardId
if (cardId != null && receivedCardId != cardId) {
viewDelegate.onWrongCard()
preflightCheck(callback)
return@run
}
val allowedCardTypes = environment.cardFilter.allowedCardTypes
if (!allowedCardTypes.contains(result.data.getType())) {
stopWithError(TangemSdkError.WrongCardType())
callback(this, TangemSdkError.WrongCardType())
return@run
}
environment.card = result.data
cardId = receivedCardId
callback(this, null)
}
}
}
}
private fun startSession() {
if (isBusy) throw TangemSdkError.Busy()
isBusy = true
viewDelegate.onNfcSessionStarted(cardId, initialMessage)
reader.openSession()
}
private fun preflightRead(callback: (result: CompletionResult<Card>) -> Unit) {
val readCommand = ReadCommand()
readCommand.run(this) { result ->
when (result) {
is CompletionResult.Failure -> {
tryHandleError(result.error) { handleErrorResult ->
when (handleErrorResult) {
is CompletionResult.Success -> preflightRead(callback)
is CompletionResult.Failure -> {
stopWithError(result.error)
callback(CompletionResult.Failure(result.error))
}
}
}
}
is CompletionResult.Success -> {
val receivedCardId = result.data.cardId
if (cardId != null && receivedCardId != cardId) {
stopWithError(TangemSdkError.WrongCardNumber())
callback(CompletionResult.Failure(TangemSdkError.WrongCardNumber()))
return@run
}
val allowedCardTypes = environment.cardFilter.allowedCardTypes
if (!allowedCardTypes.contains(result.data.getType())) {
stopWithError(TangemSdkError.WrongCardType())
callback(CompletionResult.Failure(TangemSdkError.WrongCardType()))
return@run
}
environment.card = result.data
cardId = receivedCardId
callback(CompletionResult.Success(result.data))
}
}
}
fun readSlixTag(callback: (result: CompletionResult<ResponseApdu>) -> Unit) {
reader.readSlixTag(callback)
}
/**
@ -176,9 +190,8 @@ class CardSession(
* @param message If null, the default message will be shown.
*/
private fun stop(message: Message? = null) {
reader.closeSession()
viewDelegate.onNfcSessionCompleted(message)
isBusy = false
stopSession()
viewDelegate.onSessionStopped(message)
}
/**
@ -186,75 +199,80 @@ class CardSession(
* @param error An error that will be shown.
*/
private fun stopWithError(error: TangemSdkError) {
if (!isBusy) return
reader.closeSession()
isBusy = false
val errorMessage = if (error is TangemSdkError) {
"${error::class.simpleName}: ${error.code}"
} else {
error.localizedMessage
}
stopSession()
if (error !is TangemSdkError.UserCancelled) {
Log.e(tag, "Finishing with error: $errorMessage")
Log.e(tag, "Finishing with error: ${error::class.simpleName}: ${error.code}")
viewDelegate.onError(error)
} else {
Log.i(tag, "User cancelled NFC session")
}
}
private fun stopSession() {
reader.stopSession()
state = CardSessionState.Inactive
scope.cancel()
}
fun send(apdu: CommandApdu, callback: (result: CompletionResult<ResponseApdu>) -> Unit) {
reader.transceiveApdu(apdu, callback)
}
private fun tryHandleError(
error: TangemSdkError, callback: (result: CompletionResult<Boolean>) -> Unit) {
when (error) {
is TangemSdkError.NeedEncryption -> {
Log.i(tag, "Establishing encryption")
when (environment.encryptionMode) {
EncryptionMode.NONE -> {
environment.encryptionKey = null
environment.encryptionMode = EncryptionMode.FAST
}
EncryptionMode.FAST -> {
environment.encryptionKey = null
environment.encryptionMode = EncryptionMode.STRONG
}
EncryptionMode.STRONG -> {
Log.e(tag, "Encryption doesn't work")
callback(CompletionResult.Failure(TangemSdkError.NeedEncryption()))
}
val subscription = reader.tag.openSubscription()
scope.launch {
subscription.consumeAsFlow()
.filterNotNull()
.map { establishEncryptionIfNeeded() }
.map { apdu.encrypt(environment.encryptionMode, environment.encryptionKey) }
.map { encryptedApdu -> reader.transceiveApdu(encryptedApdu) }
.map { responseApdu -> decrypt(responseApdu) }
.catch { if (it is TangemSdkError) callback(CompletionResult.Failure(it)) }
.collect { result ->
subscription.cancel()
callback(result)
}
return establishEncryption(callback)
}
else -> callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
}
}
private fun establishEncryption(callback: (result: CompletionResult<Boolean>) -> Unit) {
val encryptionHelper: EncryptionHelper =
if (environment.encryptionMode == EncryptionMode.STRONG) {
StrongEncryptionHelper()
} else {
FastEncryptionHelper()
}
private suspend fun establishEncryptionIfNeeded(): CompletionResult<Boolean> {
if (environment.encryptionMode == EncryptionMode.NONE || environment.encryptionKey != null) {
return CompletionResult.Success(true)
}
val encryptionHelper = EncryptionHelper.create(environment.encryptionMode)
?: return CompletionResult.Success(true)
val openSesssionCommand = OpenSessionCommand(encryptionHelper.keyA)
openSesssionCommand.run(this) { result ->
when (result) {
is CompletionResult.Success -> {
val uid = result.data.uid
val protocolKey = environment.pin1.pbkdf2Hash(uid, 50)
val secret = encryptionHelper.generateSecret(result.data.sessionKeyB)
val sessionKey = (secret + protocolKey).calculateSha256()
environment.encryptionKey = sessionKey
callback(CompletionResult.Success(true))
val apdu = openSesssionCommand.serialize(environment)
val response = reader.transceiveApdu(apdu)
when (response) {
is CompletionResult.Success -> {
val result = try {
openSesssionCommand.deserialize(environment, response.data)
} catch (error: TangemSdkError) {
return CompletionResult.Failure(error)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
val uid = result.uid
val protocolKey = environment.pin1.pbkdf2Hash(uid, 50)
val secret = encryptionHelper.generateSecret(result.sessionKeyB)
val sessionKey = (secret + protocolKey).calculateSha256()
environment.encryptionKey = sessionKey
return CompletionResult.Success(true)
}
is CompletionResult.Failure -> return CompletionResult.Failure(response.error)
}
}
private fun decrypt(result: CompletionResult<ResponseApdu>): CompletionResult<ResponseApdu> {
return when (result) {
is CompletionResult.Success -> {
try {
CompletionResult.Success(
result.data.decrypt(environment.encryptionKey)
)
} catch (error: TangemSdkError) {
return CompletionResult.Failure(error)
}
}
is CompletionResult.Failure -> result
}
}
}

View file

@ -42,7 +42,7 @@ data class SessionEnvironment(
/**
* All possible encryption modes.
*/
enum class EncryptionMode(val code: Byte) {
enum class EncryptionMode(val code: Int) {
NONE(0x0),
FAST(0x1),
STRONG(0x2)

View file

@ -12,7 +12,7 @@ interface SessionViewDelegate {
/**
* It is called when user is expected to scan a Tangem Card with an Android device.
*/
fun onNfcSessionStarted(cardId: String?, message: Message? = null)
fun onSessionStarted(cardId: String?, message: Message? = null)
/**
* It is called when security delay is triggered by the card.
@ -32,10 +32,14 @@ interface SessionViewDelegate {
*/
fun onTagLost()
fun onTagConnected()
fun onWrongCard()
/**
* It is called when NFC session was completed and a user can take the card away from the Android device.
*/
fun onNfcSessionCompleted(message: Message? = null)
fun onSessionStopped(message: Message? = null)
/**
* It is called when some error occur during NFC session.

View file

@ -374,7 +374,7 @@ class TangemSdk(
fun startSession(cardId: String? = null, initialMessage: Message? = null,
callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
val cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId, initialMessage)
Thread().run { cardSession.start(callback) }
Thread().run { cardSession.start(callback = callback) }
}
/**

View file

@ -29,6 +29,7 @@ sealed class TangemSdkError(val code: Int) : Exception(code.toString()) {
class DecodingFailedMissingTag : TangemSdkError(20005)
class DecodingFailedTypeMismatch : TangemSdkError(20006)
class DecodingFailed : TangemSdkError(20007)
class InvalidResponse : TangemSdkError(20008)
/**
* This error is returned when unknown [StatusWord] is received from a card.
@ -104,7 +105,7 @@ sealed class TangemSdkError(val code: Int) : Exception(code.toString()) {
class TooManyHashesInOneTransaction : TangemSdkError(40906)
//Write Extra Issuer Data Errors
class ExendedDataSizeTooLarge : TangemSdkError(41101)
class ExtendedDataSizeTooLarge : TangemSdkError(41101)
//General Errors
class NotPersonalized() : TangemSdkError(40001)

View file

@ -70,19 +70,14 @@ class CheckWalletCommand(
}
}
override fun performPreCheck(
session: CardSession,
callback: (result: CompletionResult<CheckWalletResponse>) -> Unit
): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.status == CardStatus.NotPersonalized) {
return TangemSdkError.NotPersonalized()
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
if (card.isActivated) {
return TangemSdkError.NotActivated()
}
return false
return null
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
@ -90,15 +85,11 @@ class CheckWalletCommand(
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Challenge, challenge)
return CommandApdu(
Instruction.CheckWallet, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
return CommandApdu(Instruction.CheckWallet, tlvBuilder.serialize())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): CheckWalletResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return CheckWalletResponse(

View file

@ -2,13 +2,31 @@ package com.tangem.commands
import com.tangem.*
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.apdu.StatusWord
import com.tangem.common.apdu.toTangemSdkError
import com.tangem.common.apdu.*
import com.tangem.common.extensions.toInt
import com.tangem.common.tlv.TlvTag
interface ApduSerializable<T : CommandResponse> {
/**
* Serializes data into an array of [com.tangem.common.tlv.Tlv],
* then creates [CommandApdu] with this data.
* @param environment [SessionEnvironment] of the current card
* @return command data converted to [CommandApdu] that allows to convert it to [ByteArray]
* that can be sent to a Tangem card
*/
fun serialize(environment: SessionEnvironment): CommandApdu
/**
* Deserializes data received from a card and stored in [ResponseApdu]
* into an array of [com.tangem.common.tlv.Tlv]. Then maps it into a [CommandResponse].
* @param environment [SessionEnvironment] of the current card.
* @param apdu received data.
* @return Card response converted to a [CommandResponse] of a type [T]
*/
fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): T
}
/**
* Basic interface for a parsed response from [Command].
*/
@ -17,88 +35,105 @@ interface CommandResponse
/**
* Basic class for Tangem card commands
*/
abstract class Command<T : CommandResponse> : CardSessionRunnable<T> {
abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRunnable<T> {
override val performPreflightRead: Boolean = true
/**
* Serializes data into an array of [com.tangem.common.tlv.Tlv],
* then creates [CommandApdu] with this data.
* @param environment [SessionEnvironment] of the current card
* @return command data converted to [CommandApdu] that allows to convert it to [ByteArray]
* that can be sent to a Tangem card
*/
abstract fun serialize(environment: SessionEnvironment): CommandApdu
/**
* Deserializes data received from a card and stored in [ResponseApdu]
* into an array of [com.tangem.common.tlv.Tlv]. Then maps it into a [CommandResponse].
* @param environment [SessionEnvironment] of the current card.
* @param apdu received data.
* @return Card response converted to a [CommandResponse] of a type [T]
*/
abstract fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): T
override fun run(session: CardSession, callback: (result: CompletionResult<T>) -> Unit) {
Log.i("Command", "Initializing ${this::class.java.simpleName}")
if (session.environment.handleErrors) {
if (performPreCheck(session, callback)) return
}
transceive(session) { result ->
if (session.environment.handleErrors) {
if (performAfterCheck(session, result, callback)) return@transceive
}
callback(result)
}
transceive(session, callback)
}
open fun performPreCheck(session: CardSession,
callback: (result: CompletionResult<T>) -> Unit): Boolean {
return false
}
open fun performPreCheck(card: Card): TangemSdkError? = null
open fun performAfterCheck(session: CardSession,
result: CompletionResult<T>,
callback: (result: CompletionResult<T>) -> Unit): Boolean {
return false
}
open fun mapError(card: Card?, error: TangemSdkError): TangemSdkError = error
fun transceive(session: CardSession, callback: (result: CompletionResult<T>) -> Unit) {
try {
val apdu = serialize(session.environment)
transceiveApdu(apdu, session) { result ->
when (result) {
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
is CompletionResult.Success -> {
val card = session.environment.card
if (session.environment.handleErrors && card != null) {
performPreCheck(card)?.let { error ->
callback(CompletionResult.Failure(error))
return
}
}
val apdu = serialize(session.environment)
transceiveApdu(apdu, session) { result ->
when (result) {
is CompletionResult.Failure -> {
if (session.environment.handleErrors) {
val error = mapError(session.environment.card, result.error)
callback(CompletionResult.Failure(error))
return@transceiveApdu
}
callback(CompletionResult.Failure(result.error))
}
is CompletionResult.Success -> {
try {
val response = deserialize(session.environment, result.data)
callback(CompletionResult.Success(response))
} catch (error: TangemSdkError) {
callback(CompletionResult.Failure(error))
}
}
}
} catch (error: TangemSdkError) {
callback(CompletionResult.Failure(error))
}
}
private fun transceiveApdu(apdu: CommandApdu, session: CardSession, callback: (result: CompletionResult<ResponseApdu>) -> Unit) {
session.send(apdu) { result ->
private fun transceiveApdu(
apdu: CommandApdu,
session: CardSession,
callback: (result: CompletionResult<ResponseApdu>) -> Unit
) {
Log.i(this::class.simpleName!!, "transieve: ${Instruction.byCode(apdu.ins)}")
session.send(apdu) { result ->
when (result) {
is CompletionResult.Success -> {
val responseApdu = result.data
when (responseApdu.statusWord) {
StatusWord.ProcessCompleted, StatusWord.Pin1Changed, StatusWord.Pin2Changed, StatusWord.PinsChanged
-> callback(CompletionResult.Success(responseApdu))
StatusWord.ProcessCompleted, StatusWord.Pin1Changed,
StatusWord.Pin2Changed, StatusWord.PinsChanged -> {
callback(CompletionResult.Success(responseApdu))
}
StatusWord.NeedPause -> {
// NeedPause is returned from the card whenever security delay is triggered.
val remainingTime = deserializeSecurityDelay(responseApdu, session.environment)
val remainingTime =
deserializeSecurityDelay(responseApdu)
if (remainingTime != null) {
session.viewDelegate.onSecurityDelay(
remainingTime,
session.environment.card?.pauseBeforePin2 ?: 0)
remainingTime,
session.environment.card?.pauseBeforePin2 ?: 0
)
}
Log.i(
this::class.simpleName!!,
"Nfc command ${this::class.simpleName!!} " +
"triggered security delay of $remainingTime milliseconds"
)
transceiveApdu(apdu, session, callback)
}
StatusWord.NeedEncryption -> {
Log.i(this::class.simpleName!!, "Establishing encryption")
when (session.environment.encryptionMode) {
EncryptionMode.NONE -> {
session.environment.encryptionKey = null
session.environment.encryptionMode = EncryptionMode.FAST
}
EncryptionMode.FAST -> {
session.environment.encryptionKey = null
session.environment.encryptionMode = EncryptionMode.STRONG
}
EncryptionMode.STRONG -> {
Log.e(this::class.simpleName!!, "Encryption doesn't work")
callback(CompletionResult.Failure(TangemSdkError.NeedEncryption()))
return@send
}
}
Log.i(this::class.simpleName!!, "Nfc command ${this::class.simpleName!!} " +
"triggered security delay of $remainingTime milliseconds")
transceiveApdu(apdu, session, callback)
}
else -> {
@ -126,7 +161,9 @@ abstract class Command<T : CommandResponse> : CardSessionRunnable<T> {
*
* @return Remaining security delay in milliseconds.
*/
private fun deserializeSecurityDelay(responseApdu: ResponseApdu, environment: SessionEnvironment): Int? {
private fun deserializeSecurityDelay(
responseApdu: ResponseApdu
): Int? {
val tlv = responseApdu.getTlvData()
return tlv?.find { it.tag == TlvTag.Pause }?.value?.toInt()
}

View file

@ -1,9 +1,7 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
@ -12,18 +10,18 @@ import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class CreateWalletResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Current status of the card [1 - Empty, 2 - Loaded, 3- Purged]
*/
val status: CardStatus,
/**
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Current status of the card [1 - Empty, 2 - Loaded, 3- Purged]
*/
val status: CardStatus,
/**
*/
val walletPublicKey: ByteArray
*/
val walletPublicKey: ByteArray
) : CommandResponse
/**
@ -39,39 +37,25 @@ class CreateWalletResponse(
*/
class CreateWalletCommand : Command<CreateWalletResponse>() {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.isActivated) {
return TangemSdkError.NotActivated()
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
return when (card.status) {
CardStatus.Empty -> null
CardStatus.NotPersonalized -> TangemSdkError.NotPersonalized()
CardStatus.Loaded -> TangemSdkError.AlreadyCreated()
CardStatus.Purged -> TangemSdkError.CardIsPurged()
null -> TangemSdkError.CardError()
}
if (session.environment.card?.status == CardStatus.Purged) {
callback(CompletionResult.Failure(TangemSdkError.CardIsPurged()))
return true
}
if (session.environment.card?.status == CardStatus.Loaded) {
callback(CompletionResult.Failure(TangemSdkError.AlreadyCreated()))
return true
}
return false
}
override fun performAfterCheck(session: CardSession,
result: CompletionResult<CreateWalletResponse>,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams) {
callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired()))
return true
}
return false
}
else -> return false
override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError {
if (error is TangemSdkError.InvalidParams) {
return TangemSdkError.Pin2OrCvcRequired()
}
return error
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
@ -80,21 +64,21 @@ class CreateWalletCommand : Command<CreateWalletResponse>() {
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
tlvBuilder.append(TlvTag.Cvc, environment.cvc)
return CommandApdu(
Instruction.CreateWallet, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
return CommandApdu(Instruction.CreateWallet, tlvBuilder.serialize())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): CreateWalletResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
override fun deserialize(
environment: SessionEnvironment,
apdu: ResponseApdu
): CreateWalletResponse {
val tlvData = apdu.getTlvData()
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return CreateWalletResponse(
cardId = decoder.decode(TlvTag.CardId),
status = decoder.decode(TlvTag.Status),
walletPublicKey = decoder.decode(TlvTag.WalletPublicKey)
cardId = decoder.decode(TlvTag.CardId),
status = decoder.decode(TlvTag.Status),
walletPublicKey = decoder.decode(TlvTag.WalletPublicKey)
)
}
}

View file

@ -10,8 +10,8 @@ import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class OpenSessionResponse(
val sessionKeyB: ByteArray,
val uid: ByteArray
val sessionKeyB: ByteArray,
val uid: ByteArray
) : CommandResponse
/**
@ -25,19 +25,21 @@ class OpenSessionCommand(private val sessionKeyA: ByteArray) : Command<OpenSessi
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.SessionKeyA, sessionKeyA)
return CommandApdu(
Instruction.OpenSession, tlvBuilder.serialize(),
encryptionMode = environment.encryptionMode
Instruction.OpenSession.code, tlvBuilder.serialize(), 0, environment.encryptionMode.code
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): OpenSessionResponse {
override fun deserialize(
environment: SessionEnvironment,
apdu: ResponseApdu
): OpenSessionResponse {
val tlvData = apdu.getTlvData()
?: throw TangemSdkError.DeserializeApduFailed()
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return OpenSessionResponse(
sessionKeyB = decoder.decode(TlvTag.SessionKeyB),
uid = decoder.decode(TlvTag.Uid)
sessionKeyB = decoder.decode(TlvTag.SessionKeyB),
uid = decoder.decode(TlvTag.Uid)
)
}
}

View file

@ -1,9 +1,7 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
@ -31,51 +29,43 @@ class PurgeWalletResponse(
*/
class PurgeWalletCommand : Command<PurgeWalletResponse>() {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<PurgeWalletResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.status == CardStatus.NotPersonalized) {
return TangemSdkError.NotPersonalized()
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
if (card.isActivated) {
return TangemSdkError.NotActivated()
}
if (session.environment.card?.settingsMask?.contains(Settings.ProhibitPurgeWallet) == true) {
callback(CompletionResult.Failure(TangemSdkError.PurgeWalletProhibited()))
return true
if (card.settingsMask?.contains(Settings.ProhibitPurgeWallet) == true) {
return TangemSdkError.PurgeWalletProhibited()
}
return when (card.status) {
CardStatus.Loaded -> null
CardStatus.NotPersonalized -> TangemSdkError.NotPersonalized()
CardStatus.Empty, CardStatus.Purged -> TangemSdkError.CardIsEmpty()
null -> TangemSdkError.CardError()
}
return false
}
override fun performAfterCheck(session: CardSession,
result: CompletionResult<PurgeWalletResponse>,
callback: (result: CompletionResult<PurgeWalletResponse>) -> Unit): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams) {
callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired()))
return true
}
return false
}
else -> return false
override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError {
if (error is TangemSdkError.InvalidParams) {
return TangemSdkError.Pin2OrCvcRequired()
}
return error
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
return CommandApdu(
Instruction.PurgeWallet, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
return CommandApdu(Instruction.PurgeWallet, tlvBuilder.serialize())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): PurgeWalletResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return PurgeWalletResponse(

View file

@ -1,15 +1,12 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.commands.common.CardDeserializer
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import java.util.*
@ -318,6 +315,8 @@ class Card(
/**
* Whether the card requires issuers confirmation of activation.
* is "true" if the card requires activation,
* is 'false" if the card is activated or does not require activation
*/
val isActivated: Boolean,
@ -368,17 +367,11 @@ class Card(
*/
class ReadCommand : Command<Card>() {
override fun performAfterCheck(session: CardSession, result: CompletionResult<Card>, callback: (result: CompletionResult<Card>) -> Unit): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams) {
callback(CompletionResult.Failure(TangemSdkError.Pin1Required()))
return true
}
return false
}
else -> return false
override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError {
if (error is TangemSdkError.InvalidParams) {
return TangemSdkError.Pin1Required()
}
return error
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
@ -391,64 +384,10 @@ class ReadCommand : Command<Card>() {
*/
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.TerminalPublicKey, environment.terminalKeys?.publicKey)
return CommandApdu(
Instruction.Read, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
return CommandApdu(Instruction.Read, tlvBuilder.serialize())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): Card {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return Card(
cardId = decoder.decodeOptional(TlvTag.CardId) ?: "",
manufacturerName = decoder.decodeOptional(TlvTag.ManufactureId) ?: "",
status = decoder.decodeOptional(TlvTag.Status),
firmwareVersion = decoder.decodeOptional(TlvTag.Firmware),
cardPublicKey = decoder.decodeOptional(TlvTag.CardPublicKey),
settingsMask = decoder.decodeOptional(TlvTag.SettingsMask),
issuerPublicKey = decoder.decodeOptional(TlvTag.IssuerDataPublicKey),
curve = decoder.decodeOptional(TlvTag.CurveId),
maxSignatures = decoder.decodeOptional(TlvTag.MaxSignatures),
signingMethods = decoder.decodeOptional(TlvTag.SigningMethod),
pauseBeforePin2 = decoder.decodeOptional(TlvTag.PauseBeforePin2),
walletPublicKey = decoder.decodeOptional(TlvTag.WalletPublicKey),
walletRemainingSignatures = decoder.decodeOptional(TlvTag.RemainingSignatures),
walletSignedHashes = decoder.decodeOptional(TlvTag.SignedHashes),
health = decoder.decodeOptional(TlvTag.Health),
isActivated = decoder.decode(TlvTag.IsActivated),
activationSeed = decoder.decodeOptional(TlvTag.ActivationSeed),
paymentFlowVersion = decoder.decodeOptional(TlvTag.PaymentFlowVersion),
userCounter = decoder.decodeOptional(TlvTag.UserCounter),
userProtectedCounter = decoder.decodeOptional(TlvTag.UserProtectedCounter),
terminalIsLinked = decoder.decode(TlvTag.TerminalIsLinked),
cardData = deserializeCardData(tlvData)
)
}
private fun deserializeCardData(tlvData: List<Tlv>): CardData? {
val cardDataTlvs = tlvData.find { it.tag == TlvTag.CardData }?.let {
Tlv.deserialize(it.value)
}
if (cardDataTlvs.isNullOrEmpty()) return null
val decoder = TlvDecoder(cardDataTlvs)
return CardData(
batchId = decoder.decodeOptional(TlvTag.Batch),
manufactureDateTime = decoder.decodeOptional(TlvTag.ManufactureDateTime),
issuerName = decoder.decodeOptional(TlvTag.IssuerId),
blockchainName = decoder.decodeOptional(TlvTag.BlockchainId),
manufacturerSignature = decoder.decodeOptional(TlvTag.ManufacturerSignature),
productMask = decoder.decodeOptional(TlvTag.ProductMask),
tokenSymbol = decoder.decodeOptional(TlvTag.TokenSymbol),
tokenContractAddress = decoder.decodeOptional(TlvTag.TokenContractAddress),
tokenDecimal = decoder.decodeOptional(TlvTag.TokenDecimal)
)
return CardDeserializer.deserialize(apdu)
}
}

View file

@ -17,32 +17,32 @@ import com.tangem.common.tlv.TlvTag
class ReadIssuerDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Data defined by issuer.
*/
val issuerData: ByteArray,
/**
* 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 [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
*/
val issuerDataSignature: 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 [Settings.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 [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask]
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
*/
val issuerDataCounter: Int?
/**
* An optional counter that protect issuer data against replay attack.
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask]
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
*/
val issuerDataCounter: Int?
) : CommandResponse
@ -54,21 +54,17 @@ class ReadIssuerDataResponse(
* @property cardId CID, Unique Tangem card ID number.
*/
class ReadIssuerDataCommand(
val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : Command<ReadIssuerDataResponse>(), IssuerDataVerifier by verifier {
override fun run(session: CardSession, callback: (result: CompletionResult<ReadIssuerDataResponse>) -> Unit) {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
val publicKey = issuerPublicKey ?: card.issuerPublicKey
if (publicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey()))
return
}
override fun run(
session: CardSession,
callback: (result: CompletionResult<ReadIssuerDataResponse>) -> Unit
) {
val publicKey = issuerPublicKey ?: session.environment.card?.issuerPublicKey
super.run(session) { result ->
when (result) {
is CompletionResult.Failure -> callback(result)
@ -78,9 +74,9 @@ class ReadIssuerDataCommand(
return@run
}
val issuerDataToVerify = IssuerDataToVerify(
card.cardId, result.data.issuerData, result.data.issuerDataCounter
result.data.cardId, result.data.issuerData, result.data.issuerDataCounter
)
if (verify(publicKey, result.data.issuerDataSignature, issuerDataToVerify)) {
if (verify(publicKey!!, result.data.issuerDataSignature, issuerDataToVerify)) {
callback(result)
} else {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
@ -90,12 +86,12 @@ class ReadIssuerDataCommand(
}
}
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<ReadIssuerDataResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.status == CardStatus.NotPersonalized) {
return TangemSdkError.NotPersonalized()
}
return false
issuerPublicKey ?: card.issuerPublicKey ?: return TangemSdkError.MissingIssuerPubicKey()
return null
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
@ -103,22 +99,21 @@ class ReadIssuerDataCommand(
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadData)
return CommandApdu(
Instruction.ReadIssuerData, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
return CommandApdu(Instruction.ReadIssuerData, tlvBuilder.serialize())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): ReadIssuerDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
override fun deserialize(
environment: SessionEnvironment,
apdu: ResponseApdu
): ReadIssuerDataResponse {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return ReadIssuerDataResponse(
cardId = decoder.decode(TlvTag.CardId),
issuerData = decoder.decode(TlvTag.IssuerData),
issuerDataSignature = decoder.decode(TlvTag.IssuerDataSignature),
issuerDataCounter = decoder.decodeOptional(TlvTag.IssuerDataCounter)
cardId = decoder.decode(TlvTag.CardId),
issuerData = decoder.decode(TlvTag.IssuerData),
issuerDataSignature = decoder.decode(TlvTag.IssuerDataSignature),
issuerDataCounter = decoder.decodeOptional(TlvTag.IssuerDataCounter)
)
}
}

View file

@ -18,37 +18,37 @@ import java.io.ByteArrayOutputStream
class ReadIssuerExtraDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Size of all Issuer_Extra_Data field.
*/
val size: Int?,
/**
* Size of all Issuer_Extra_Data field.
*/
val size: Int?,
/**
* Data defined by issuer.
*/
val issuerData: ByteArray,
/**
* 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 [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
*/
val issuerDataSignature: 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 [Settings.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 protects issuer data against replay attack.
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask]
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
*/
val issuerDataCounter: Int?
/**
* An optional counter that protects issuer data against replay attack.
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask]
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
*/
val issuerDataCounter: Int?
) : CommandResponse
@ -60,42 +60,38 @@ class ReadIssuerExtraDataResponse(
* a series of these commands have to be executed to read the entire Issuer_Extra_Data.
*/
class ReadIssuerExtraDataCommand(
private val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
private val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : Command<ReadIssuerExtraDataResponse>(), IssuerDataVerifier by verifier {
private val issuerData = ByteArrayOutputStream()
private var offset: Int = 0
private var issuerDataSize: Int = 0
override fun run(session: CardSession, callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit) {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.status == CardStatus.NotPersonalized) {
return TangemSdkError.NotPersonalized()
}
val publicKey = issuerPublicKey ?: card.issuerPublicKey
if (publicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey()))
return
}
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return
}
readIssuerData(session, card.cardId, publicKey, callback)
issuerPublicKey ?: card.issuerPublicKey ?: return TangemSdkError.MissingIssuerPubicKey()
return null
}
override fun run(
session: CardSession,
callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit
) {
val publicKey = issuerPublicKey ?: session.environment.card?.issuerPublicKey
readIssuerData(session, publicKey!!, callback)
}
private fun readIssuerData(
session: CardSession,
cardId: String, publicKey: ByteArray,
callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit) {
session: CardSession, publicKey: ByteArray,
callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit
) {
if (issuerDataSize != 0) {
session.viewDelegate.onDelay(
issuerDataSize, offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE
issuerDataSize, offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE
)
}
@ -112,9 +108,9 @@ class ReadIssuerExtraDataCommand(
issuerData.write(result.data.issuerData)
if (result.data.issuerDataSignature == null) {
offset = issuerData.size()
readIssuerData(session, cardId, publicKey, callback)
readIssuerData(session, publicKey, callback)
} else {
completeTask(result.data, cardId, publicKey, callback)
completeTask(result.data, publicKey, callback)
}
}
is CompletionResult.Failure -> {
@ -124,21 +120,22 @@ class ReadIssuerExtraDataCommand(
}
}
private fun completeTask(data: ReadIssuerExtraDataResponse,
cardId: String, publicKey: ByteArray,
callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit) {
private fun completeTask(
data: ReadIssuerExtraDataResponse, publicKey: ByteArray,
callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit
) {
val dataToVerify = IssuerDataToVerify(
cardId,
issuerData.toByteArray(),
data.issuerDataCounter
data.cardId,
issuerData.toByteArray(),
data.issuerDataCounter
)
if (verify(publicKey, data.issuerDataSignature!!, dataToVerify)) {
val finalResult = ReadIssuerExtraDataResponse(
data.cardId,
issuerDataSize,
issuerData.toByteArray(),
data.issuerDataSignature,
data.issuerDataCounter
data.cardId,
issuerDataSize,
issuerData.toByteArray(),
data.issuerDataSignature,
data.issuerDataCounter
)
callback(CompletionResult.Success(finalResult))
} else {
@ -152,24 +149,22 @@ class ReadIssuerExtraDataCommand(
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadExtraData)
tlvBuilder.append(TlvTag.Offset, offset)
return CommandApdu(
Instruction.ReadIssuerData, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
return CommandApdu(Instruction.ReadIssuerData, tlvBuilder.serialize())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): ReadIssuerExtraDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
override fun deserialize(
environment: SessionEnvironment,
apdu: ResponseApdu
): ReadIssuerExtraDataResponse {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return ReadIssuerExtraDataResponse(
cardId = decoder.decode(TlvTag.CardId),
size = decoder.decodeOptional(TlvTag.Size),
issuerData = decoder.decodeOptional(TlvTag.IssuerData) ?: byteArrayOf(),
issuerDataSignature = decoder.decodeOptional(TlvTag.IssuerDataSignature),
issuerDataCounter = decoder.decodeOptional(TlvTag.IssuerDataCounter)
cardId = decoder.decode(TlvTag.CardId),
size = decoder.decodeOptional(TlvTag.Size),
issuerData = decoder.decodeOptional(TlvTag.IssuerData) ?: byteArrayOf(),
issuerDataSignature = decoder.decodeOptional(TlvTag.IssuerDataSignature),
issuerDataCounter = decoder.decodeOptional(TlvTag.IssuerDataCounter)
)
}

View file

@ -1,9 +1,7 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
@ -51,16 +49,14 @@ class ReadUserDataResponse(
*/
class ReadUserDataCommand : Command<ReadUserDataResponse>() {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<ReadUserDataResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.status == CardStatus.NotPersonalized) {
return TangemSdkError.NotPersonalized()
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
if (card.isActivated) {
return TangemSdkError.NotActivated()
}
return false
return null
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
@ -68,15 +64,11 @@ class ReadUserDataCommand : Command<ReadUserDataResponse>() {
builder.append(TlvTag.CardId, environment.card?.cardId)
builder.append(TlvTag.Pin, environment.pin1)
return CommandApdu(
Instruction.ReadUserData, builder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
return CommandApdu(Instruction.ReadUserData, builder.serialize())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): ReadUserDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return ReadUserDataResponse(

View file

@ -1,9 +1,7 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
@ -20,10 +18,10 @@ import com.tangem.crypto.sign
* Sums up array elements within all SIGN commands
*/
class SignResponse(
val cardId: String,
val signature: ByteArray,
val walletRemainingSignatures: Int,
val walletSignedHashes: Int
val cardId: String,
val signature: ByteArray,
val walletRemainingSignatures: Int,
val walletSignedHashes: Int
) : CommandResponse
/**
@ -32,60 +30,44 @@ class SignResponse(
* @property hashes Array of transaction hashes.
* @property cardId CID, Unique Tangem card ID number
*/
class SignCommand(private val hashes: Array<ByteArray>)
: Command<SignResponse>() {
class SignCommand(private val hashes: Array<ByteArray>) : Command<SignResponse>() {
//TODO: Allow signing more than 10 hashes
private val hashSizes = if (hashes.isNotEmpty()) hashes.first().size else 0
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<SignResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.isActivated) {
return TangemSdkError.NotActivated()
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
if (card.walletRemainingSignatures == 0) {
return TangemSdkError.NoRemainingSignatures()
}
if (session.environment.card?.status == CardStatus.Purged) {
callback(CompletionResult.Failure(TangemSdkError.CardIsPurged()))
return true
}
if (session.environment.card?.status == CardStatus.Empty) {
callback(CompletionResult.Failure(TangemSdkError.CardIsEmpty()))
return true
}
if (session.environment.card?.walletRemainingSignatures == 0) {
callback(CompletionResult.Failure(TangemSdkError.NoRemainingSignatures()))
return true
}
if (session.environment.card?.signingMethods?.contains(SigningMethod.SignHash) != true) {
callback(CompletionResult.Failure(TangemSdkError.SignHashesNotAvailable()))
return true
if (card.signingMethods?.contains(SigningMethod.SignHash) != true) {
return TangemSdkError.SignHashesNotAvailable()
}
if (hashSizes == 0) {
callback(CompletionResult.Failure(TangemSdkError.EmptyHashes()))
return true
return TangemSdkError.EmptyHashes()
}
if (hashes.any { it.size != hashSizes }) {
callback(CompletionResult.Failure(TangemSdkError.HashSizeMustBeEqual()))
return true
return TangemSdkError.HashSizeMustBeEqual()
}
return when (card.status) {
CardStatus.Loaded -> null
CardStatus.Empty -> TangemSdkError.
CardIsEmpty()
CardStatus.NotPersonalized -> TangemSdkError.NotPersonalized()
CardStatus.Purged -> TangemSdkError.CardIsPurged()
null -> TangemSdkError.CardError()
}
return false
}
override fun performAfterCheck(session: CardSession,
result: CompletionResult<SignResponse>,
callback: (result: CompletionResult<SignResponse>) -> Unit): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams) {
callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired()))
return true
}
return false
}
else -> return false
override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError {
if (error is TangemSdkError.InvalidParams) {
return TangemSdkError.Pin2OrCvcRequired()
}
return error
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
@ -99,23 +81,13 @@ class SignCommand(private val hashes: Array<ByteArray>)
tlvBuilder.append(TlvTag.Cvc, environment.cvc)
addTerminalSignature(environment, dataToSign, tlvBuilder)
return CommandApdu(
Instruction.Sign, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
return CommandApdu(Instruction.Sign, tlvBuilder.serialize())
}
private fun flattenHashes(): ByteArray {
checkForErrors()
return hashes.reduce { arr1, arr2 -> arr1 + arr2 }
}
private fun checkForErrors() {
if (hashes.isEmpty()) throw TangemSdkError.EmptyHashes()
if (hashes.size > 10) throw TangemSdkError.TooManyHashesInOneTransaction()
if (hashes.any { it.size != hashSizes }) throw TangemSdkError.HashSizeMustBeEqual()
}
/**
* Application can optionally submit a public key Terminal_PublicKey in [SignCommand].
* Submitted key is stored by the Tangem card if it differs from a previous submitted Terminal_PublicKey.
@ -124,7 +96,8 @@ class SignCommand(private val hashes: Array<ByteArray>)
* (this key should be generated and securily stored by the application).
*/
private fun addTerminalSignature(
environment: SessionEnvironment, dataToSign: ByteArray, tlvBuilder: TlvBuilder) {
environment: SessionEnvironment, dataToSign: ByteArray, tlvBuilder: TlvBuilder
) {
environment.terminalKeys?.let { terminalKeyPair ->
val signedData = dataToSign.sign(terminalKeyPair.privateKey)
tlvBuilder.append(TlvTag.TerminalTransactionSignature, signedData)
@ -133,15 +106,14 @@ class SignCommand(private val hashes: Array<ByteArray>)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): SignResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return SignResponse(
cardId = decoder.decode(TlvTag.CardId),
signature = decoder.decode(TlvTag.Signature),
walletRemainingSignatures = decoder.decode(TlvTag.RemainingSignatures),
walletSignedHashes = decoder.decode(TlvTag.SignedHashes)
cardId = decoder.decode(TlvTag.CardId),
signature = decoder.decode(TlvTag.Signature),
walletRemainingSignatures = decoder.decode(TlvTag.RemainingSignatures),
walletSignedHashes = decoder.decode(TlvTag.SignedHashes)
)
}
}

View file

@ -1,13 +1,11 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.common.DefaultIssuerDataVerifier
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.commands.common.IssuerDataVerifier
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
@ -16,10 +14,10 @@ import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class WriteIssuerDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String
) : CommandResponse
/**
@ -33,75 +31,53 @@ class WriteIssuerDataResponse(
* @property issuerDataCounter An optional counter that protect issuer data against replay attack.
*/
class WriteIssuerDataCommand(
private val issuerData: ByteArray,
private val issuerDataSignature: ByteArray,
private val issuerDataCounter: Int? = null,
private val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
private val issuerData: ByteArray,
private val issuerDataSignature: ByteArray,
private val issuerDataCounter: Int? = null,
private val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : Command<WriteIssuerDataResponse>(), IssuerDataVerifier by verifier {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit): Boolean {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return true
}
override fun performPreCheck(card: Card): TangemSdkError? {
val publicKey = issuerPublicKey ?: card.issuerPublicKey
if (publicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey()))
return true
?: return TangemSdkError.MissingIssuerPubicKey()
if (card.status == CardStatus.NotPersonalized) {
return TangemSdkError.NotPersonalized()
}
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
if (card.isActivated) {
return TangemSdkError.NotActivated()
}
if (issuerData.size > MAX_SIZE) {
callback(CompletionResult.Failure(TangemSdkError.DataSizeTooLarge()))
return true
return TangemSdkError.DataSizeTooLarge()
}
if (!isCounterValid(issuerDataCounter, card)) {
callback(CompletionResult.Failure(TangemSdkError.MissingCounter()))
return true
return TangemSdkError.MissingCounter()
}
if (!verifySignature(publicKey, card.cardId)) {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
return true
return TangemSdkError.VerificationFailed()
}
return false
return null
}
override fun performAfterCheck(session: CardSession,
result: CompletionResult<WriteIssuerDataResponse>,
callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit
): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams &&
isCounterRequired(session.environment.card)) {
callback(CompletionResult.Failure(TangemSdkError.DataCannotBeWritten()))
return true
}
return false
}
else -> return false
override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError {
if (error is TangemSdkError.InvalidParams && isCounterRequired(card)) {
return TangemSdkError.DataCannotBeWritten()
}
return error
}
private fun isCounterValid(issuerDataCounter: Int?, card: Card): Boolean =
if (isCounterRequired(card)) issuerDataCounter != null else true
if (isCounterRequired(card)) issuerDataCounter != null else true
private fun isCounterRequired(card: Card?): Boolean =
card?.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false
card?.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) == true
private fun verifySignature(publicKey: ByteArray, cardId: String): Boolean {
return verify(
publicKey,
issuerDataSignature,
IssuerDataToVerify(cardId, issuerData, issuerDataCounter)
publicKey,
issuerDataSignature,
IssuerDataToVerify(cardId, issuerData, issuerDataCounter)
)
}
@ -114,19 +90,18 @@ class WriteIssuerDataCommand(
tlvBuilder.append(TlvTag.IssuerDataSignature, issuerDataSignature)
tlvBuilder.append(TlvTag.IssuerDataCounter, issuerDataCounter)
return CommandApdu(
Instruction.WriteIssuerData, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
return CommandApdu(Instruction.WriteIssuerData, tlvBuilder.serialize())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): WriteIssuerDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
override fun deserialize(
environment: SessionEnvironment,
apdu: ResponseApdu
): WriteIssuerDataResponse {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return WriteIssuerDataResponse(
cardId = decoder.decode(TlvTag.CardId)
cardId = decoder.decode(TlvTag.CardId)
)
}

View file

@ -32,89 +32,64 @@ import com.tangem.common.tlv.TlvTag
* @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,
private val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
private val issuerData: ByteArray,
private val startingSignature: ByteArray,
private val finalizingSignature: ByteArray,
private val issuerDataCounter: Int? = null,
private val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : Command<WriteIssuerDataResponse>(), IssuerDataVerifier by verifier {
var mode: IssuerDataMode = IssuerDataMode.InitializeWritingExtraData
var offset: Int = 0
override fun run(session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit) {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
val publicKey = issuerPublicKey ?: card.issuerPublicKey
if (publicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey()))
return
}
writeIssuerData(session, card.cardId, publicKey) { response ->
when (response) {
is CompletionResult.Success -> callback(response)
is CompletionResult.Failure -> {
if (response.error is TangemSdkError.InvalidParams && isCounterRequired(card)) {
callback(CompletionResult.Failure(TangemSdkError.DataCannotBeWritten()))
return@writeIssuerData
}
if (response.error is TangemSdkError.InvalidState &&
card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false) {
callback(CompletionResult.Failure(TangemSdkError.OverwritingDataIsProhibited()))
return@writeIssuerData
}
}
}
}
override fun run(
session: CardSession,
callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit
) {
writeIssuerData(session, callback)
}
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit): Boolean {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return true
}
override fun performPreCheck(card: Card): TangemSdkError? {
val publicKey = issuerPublicKey ?: card.issuerPublicKey
if (publicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey()))
return true
}
?: return TangemSdkError.MissingIssuerPubicKey()
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
if (card.status == CardStatus.NotPersonalized) {
return TangemSdkError.NotPersonalized()
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
if (card.isActivated) {
return TangemSdkError.NotActivated()
}
if (issuerData.size > MAX_SIZE) {
callback(CompletionResult.Failure(TangemSdkError.ExendedDataSizeTooLarge()))
return true
return TangemSdkError.DataSizeTooLarge()
}
if (!isCounterValid(issuerDataCounter, card)) {
callback(CompletionResult.Failure(TangemSdkError.MissingCounter()))
return true
return TangemSdkError.MissingCounter()
}
if (!verifySignatures(card.cardId, publicKey)) {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
return true
if (!verifySignatures(publicKey, card.cardId)) {
return TangemSdkError.VerificationFailed()
}
return false
return null
}
override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError {
if (error is TangemSdkError.InvalidParams && isCounterRequired(card)) {
return TangemSdkError.DataCannotBeWritten()
}
if (error is TangemSdkError.InvalidState &&
card?.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) == true) {
return TangemSdkError.OverwritingDataIsProhibited()
}
return error
}
private fun isCounterValid(issuerDataCounter: Int?, card: Card): Boolean =
if (isCounterRequired(card)) issuerDataCounter != null else true
if (isCounterRequired(card)) issuerDataCounter != null else true
private fun isCounterRequired(card: Card): Boolean =
card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false
private fun isCounterRequired(card: Card?): Boolean =
card?.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) == true
private fun verifySignatures(cardId: String, publicKey: ByteArray): Boolean {
private fun verifySignatures(publicKey: ByteArray, cardId: String): Boolean {
val firstData = IssuerDataToVerify(cardId, null, issuerDataCounter, issuerData.size)
val secondData = IssuerDataToVerify(cardId, issuerData, issuerDataCounter)
@ -124,13 +99,16 @@ class WriteIssuerExtraDataCommand(
}
private fun writeIssuerData(
session: CardSession,
cardId: String, publicKey: ByteArray,
callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit
session: CardSession,
callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit
) {
if (mode == IssuerDataMode.WriteExtraData) {
session.viewDelegate.onDelay(issuerData.size, offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE)
session.viewDelegate.onDelay(
issuerData.size,
offset,
SINGLE_WRITE_SIZE
)
}
transceive(session) { result ->
when (result) {
@ -138,7 +116,7 @@ class WriteIssuerExtraDataCommand(
when (mode) {
IssuerDataMode.InitializeWritingExtraData -> {
mode = IssuerDataMode.WriteExtraData
writeIssuerData(session, cardId, publicKey, callback)
writeIssuerData(session, callback)
return@transceive
}
IssuerDataMode.WriteExtraData -> {
@ -146,7 +124,7 @@ class WriteIssuerExtraDataCommand(
if (offset >= issuerData.size) {
mode = IssuerDataMode.FinalizeExtraData
}
writeIssuerData(session, cardId, publicKey, callback)
writeIssuerData(session, callback)
return@transceive
}
IssuerDataMode.FinalizeExtraData -> {
@ -155,6 +133,11 @@ class WriteIssuerExtraDataCommand(
}
}
is CompletionResult.Failure -> {
if (session.environment.handleErrors) {
mapError(session.environment.card, result.error)?.let {
callback(CompletionResult.Failure(it))
}
}
callback(CompletionResult.Failure(result.error))
}
}
@ -184,25 +167,25 @@ class WriteIssuerExtraDataCommand(
tlvBuilder.append(TlvTag.IssuerDataSignature, finalizingSignature)
}
}
return CommandApdu(
Instruction.WriteIssuerData, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
return CommandApdu(Instruction.WriteIssuerData, tlvBuilder.serialize())
}
private fun getDataToWrite(): ByteArray =
issuerData.copyOfRange(offset, offset + calculatePartSize())
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(environment: SessionEnvironment, apdu: ResponseApdu): WriteIssuerDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
override fun deserialize(
environment: SessionEnvironment,
apdu: ResponseApdu
): WriteIssuerDataResponse {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
return WriteIssuerDataResponse(cardId = TlvDecoder(tlvData).decode(TlvTag.CardId)
return WriteIssuerDataResponse(
cardId = TlvDecoder(tlvData).decode(TlvTag.CardId)
)
}

View file

@ -1,9 +1,7 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
@ -34,36 +32,24 @@ class WriteUserDataCommand(private val userData: ByteArray? = null, private val
private val userCounter: Int? = null,
private val userProtectedCounter: Int? = null) : Command<WriteUserDataResponse>() {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<WriteUserDataResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.status == CardStatus.NotPersonalized) {
return TangemSdkError.NotPersonalized()
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
if (card.isActivated) {
return TangemSdkError.NotActivated()
}
if (userData?.size ?: 0 > MAX_SIZE || userProtectedData?.size ?: 0 > MAX_SIZE) {
callback(CompletionResult.Failure(TangemSdkError.DataSizeTooLarge()))
return true
return TangemSdkError.DataSizeTooLarge()
}
return false
return null
}
override fun performAfterCheck(session: CardSession,
result: CompletionResult<WriteUserDataResponse>,
callback: (result: CompletionResult<WriteUserDataResponse>) -> Unit
): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams) {
callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired()))
return true
}
return false
}
else -> return false
override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError {
if (error is TangemSdkError.InvalidParams) {
return TangemSdkError.Pin2OrCvcRequired()
}
return error
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
@ -77,15 +63,12 @@ class WriteUserDataCommand(private val userData: ByteArray? = null, private val
if (userProtectedCounter != null || userProtectedData != null)
builder.append(TlvTag.Pin2, environment.pin2)
return CommandApdu(
Instruction.WriteUserData, builder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
return CommandApdu(Instruction.WriteUserData, builder.serialize())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): WriteUserDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
return WriteUserDataResponse(TlvDecoder(tlvData).decode(TlvTag.CardId))
}

View file

@ -0,0 +1,67 @@
package com.tangem.commands.common
import com.tangem.TangemSdkError
import com.tangem.commands.Card
import com.tangem.commands.CardData
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class CardDeserializer() {
companion object {
fun deserialize(apdu: ResponseApdu): Card {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return Card(
cardId = decoder.decodeOptional(TlvTag.CardId) ?: "",
manufacturerName = decoder.decodeOptional(TlvTag.ManufactureId) ?: "",
status = decoder.decodeOptional(TlvTag.Status),
firmwareVersion = decoder.decodeOptional(TlvTag.Firmware),
cardPublicKey = decoder.decodeOptional(TlvTag.CardPublicKey),
settingsMask = decoder.decodeOptional(TlvTag.SettingsMask),
issuerPublicKey = decoder.decodeOptional(TlvTag.IssuerDataPublicKey),
curve = decoder.decodeOptional(TlvTag.CurveId),
maxSignatures = decoder.decodeOptional(TlvTag.MaxSignatures),
signingMethods = decoder.decodeOptional(TlvTag.SigningMethod),
pauseBeforePin2 = decoder.decodeOptional(TlvTag.PauseBeforePin2),
walletPublicKey = decoder.decodeOptional(TlvTag.WalletPublicKey),
walletRemainingSignatures = decoder.decodeOptional(TlvTag.RemainingSignatures),
walletSignedHashes = decoder.decodeOptional(TlvTag.SignedHashes),
health = decoder.decodeOptional(TlvTag.Health),
isActivated = decoder.decode(TlvTag.IsActivated),
activationSeed = decoder.decodeOptional(TlvTag.ActivationSeed),
paymentFlowVersion = decoder.decodeOptional(TlvTag.PaymentFlowVersion),
userCounter = decoder.decodeOptional(TlvTag.UserCounter),
userProtectedCounter = decoder.decodeOptional(TlvTag.UserProtectedCounter),
terminalIsLinked = decoder.decode(TlvTag.TerminalIsLinked),
cardData = deserializeCardData(tlvData)
)
}
private fun deserializeCardData(tlvData: List<Tlv>): CardData? {
val cardDataTlvs = tlvData.find { it.tag == TlvTag.CardData }?.let {
Tlv.deserialize(it.value)
}
if (cardDataTlvs.isNullOrEmpty()) return null
val decoder = TlvDecoder(cardDataTlvs)
return CardData(
batchId = decoder.decodeOptional(TlvTag.Batch),
manufactureDateTime = decoder.decodeOptional(TlvTag.ManufactureDateTime),
issuerName = decoder.decodeOptional(TlvTag.IssuerId),
blockchainName = decoder.decodeOptional(TlvTag.BlockchainId),
manufacturerSignature = decoder.decodeOptional(TlvTag.ManufacturerSignature),
productMask = decoder.decodeOptional(TlvTag.ProductMask),
tokenSymbol = decoder.decodeOptional(TlvTag.TokenSymbol),
tokenContractAddress = decoder.decodeOptional(TlvTag.TokenContractAddress),
tokenDecimal = decoder.decodeOptional(TlvTag.TokenDecimal)
)
}
}
}

View file

@ -1,12 +1,14 @@
package com.tangem.commands.personalization
import com.tangem.CardSession
import com.tangem.EncryptionMode
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.Card
import com.tangem.commands.CardData
import com.tangem.commands.CardStatus
import com.tangem.commands.Command
import com.tangem.commands.common.CardDeserializer
import com.tangem.commands.personalization.entities.*
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
@ -14,9 +16,7 @@ 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.TlvDecoder
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.sign
@ -39,74 +39,31 @@ class PersonalizeCommand(
private val acquirer: Acquirer? = null
) : Command<Card>() {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit): Boolean {
if (session.environment.card?.status != CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.AlreadyPersonalized()))
return true
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
val encryptionMode = session.environment.encryptionMode
val encryptionKey = session.environment.encryptionKey
session.environment.encryptionMode = EncryptionMode.NONE
session.environment.encryptionKey = devPersonalizationKey
super.run(session) { result ->
session.environment.encryptionMode = encryptionMode
session.environment.encryptionKey = encryptionKey
callback(result)
}
return false
}
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.status != CardStatus.NotPersonalized) {
return TangemSdkError.AlreadyPersonalized()
}
return null
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
return CommandApdu(
Instruction.Personalize,
serializePersonalizationData(config),
encryptionKey = devPersonalizationKey
)
return CommandApdu(Instruction.Personalize, serializePersonalizationData(config))
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): Card {
val tlvData = apdu.getTlvData(devPersonalizationKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return Card(
cardId = decoder.decodeOptional(TlvTag.CardId) ?: "",
manufacturerName = decoder.decodeOptional(TlvTag.ManufactureId) ?: "",
status = decoder.decodeOptional(TlvTag.Status),
firmwareVersion = decoder.decodeOptional(TlvTag.Firmware),
cardPublicKey = decoder.decodeOptional(TlvTag.CardPublicKey),
settingsMask = decoder.decodeOptional(TlvTag.SettingsMask),
issuerPublicKey = decoder.decodeOptional(TlvTag.IssuerDataPublicKey),
curve = decoder.decodeOptional(TlvTag.CurveId),
maxSignatures = decoder.decodeOptional(TlvTag.MaxSignatures),
signingMethods = decoder.decodeOptional(TlvTag.SigningMethod),
pauseBeforePin2 = decoder.decodeOptional(TlvTag.PauseBeforePin2),
walletPublicKey = decoder.decodeOptional(TlvTag.WalletPublicKey),
walletRemainingSignatures = decoder.decodeOptional(TlvTag.RemainingSignatures),
walletSignedHashes = decoder.decodeOptional(TlvTag.SignedHashes),
health = decoder.decodeOptional(TlvTag.Health),
isActivated = decoder.decode(TlvTag.IsActivated),
activationSeed = decoder.decodeOptional(TlvTag.ActivationSeed),
paymentFlowVersion = decoder.decodeOptional(TlvTag.PaymentFlowVersion),
userCounter = decoder.decodeOptional(TlvTag.UserCounter),
userProtectedCounter = decoder.decodeOptional(TlvTag.UserProtectedCounter),
terminalIsLinked = decoder.decode(TlvTag.TerminalIsLinked),
cardData = deserializeCardData(tlvData)
)
}
private fun deserializeCardData(tlvData: List<Tlv>): CardData? {
val cardDataTlvs = tlvData.find { it.tag == TlvTag.CardData }?.let {
Tlv.deserialize(it.value)
}
if (cardDataTlvs.isNullOrEmpty()) return null
val decoder = TlvDecoder(cardDataTlvs)
return CardData(
batchId = decoder.decodeOptional(TlvTag.Batch),
manufactureDateTime = decoder.decodeOptional(TlvTag.ManufactureDateTime),
issuerName = decoder.decodeOptional(TlvTag.IssuerId),
blockchainName = decoder.decodeOptional(TlvTag.BlockchainId),
manufacturerSignature = decoder.decodeOptional(TlvTag.ManufacturerSignature),
productMask = decoder.decodeOptional(TlvTag.ProductMask),
tokenSymbol = decoder.decodeOptional(TlvTag.TokenSymbol),
tokenContractAddress = decoder.decodeOptional(TlvTag.TokenContractAddress),
tokenDecimal = decoder.decodeOptional(TlvTag.TokenDecimal)
)
return CardDeserializer.deserialize(apdu)
}
private fun serializePersonalizationData(config: CardConfig): ByteArray {

View file

@ -15,42 +15,27 @@ import java.io.ByteArrayOutputStream
*/
class CommandApdu(
private val ins: Int,
private val tlvs: ByteArray,
val ins: Int,
private val tlvs: ByteArray,
private val le: Int = 0x00,
private val p1: Int,
private val p2: Int,
private val encryptionMode: EncryptionMode = EncryptionMode.NONE,
private val encryptionKey: ByteArray? = null,
private val le: Int = 0x00,
private val cla: Int = ISO_CLA) {
private val cla: Int = ISO_CLA
) {
constructor(
instruction: Instruction,
tlvs: ByteArray,
encryptionMode: EncryptionMode = EncryptionMode.NONE,
encryptionKey: ByteArray? = null
instruction: Instruction,
tlvs: ByteArray
) : this(
instruction.code,
tlvs,
encryptionMode = encryptionMode,
encryptionKey = encryptionKey
instruction.code,
tlvs,
0,
0
)
private val p1: Int
private val p2: Int
init {
if (ins == Instruction.OpenSession.code) {
p1 = 0x00
p2 = encryptionMode.code.toInt()
} else {
p1 = encryptionMode.code.toInt()
p2 = 0x00
}
}
/**
* Request converted to a raw data
*/
@ -62,7 +47,7 @@ class CommandApdu(
private fun toBytes(): ByteArray {
val data = if (encryptionKey != null) tlvs.encrypt() else tlvs
val data = tlvs
val byteStream = ByteArrayOutputStream()
byteStream.write(cla)
@ -83,14 +68,20 @@ class CommandApdu(
}
private fun ByteArray.encrypt(): ByteArray {
val crc: ByteArray = tlvs.calculateCrc16()
val stream = ByteArrayOutputStream()
stream.write(this.size.toByteArray(2))
stream.write(crc)
stream.write(this)
return stream.toByteArray().encrypt(encryptionKey!!)
fun encrypt(
encryptionMode: EncryptionMode,
encryptionKey: ByteArray?
): CommandApdu {
if (encryptionKey == null || p1 != EncryptionMode.NONE.code) {
return this
}
val crc: ByteArray = tlvs.calculateCrc16()
val dataToEncrypt = tlvs.size.toByteArray(2) + crc + tlvs
val encryptedData = dataToEncrypt.encrypt(encryptionKey)
return CommandApdu(ins, encryptedData, encryptionMode.code, p2, le, cla)
}
companion object {
const val ISO_CLA = 0x00

View file

@ -1,5 +1,6 @@
package com.tangem.common.apdu
import com.tangem.TangemSdkError
import com.tangem.common.extensions.calculateCrc16
import com.tangem.common.tlv.Tlv
import com.tangem.crypto.decrypt
@ -27,40 +28,37 @@ class ResponseApdu(private val data: ByteArray) {
* @param encryptionKey key to decrypt response.
* (Encryption / decryption functionality is not implemented yet.)
*/
fun getTlvData(encryptionKey: ByteArray? = null): List<Tlv>? {
fun getTlvData(): List<Tlv>? {
return if (data.size <= 2) {
null
} else {
val responseData = data.copyOf(data.size - 2)
return if (encryptionKey != null) {
if (data.size >= 18) {
val decryptedData = decrypt(responseData, encryptionKey)
Tlv.deserialize(decryptedData)
} else {
null
}
} else {
Tlv.deserialize(responseData)
}
Tlv.deserialize(data.copyOf(data.size - 2))
}
}
private fun decrypt(responseData: ByteArray, encryptionKey: ByteArray): ByteArray {
fun decrypt(encryptionKey: ByteArray?): ResponseApdu {
if (encryptionKey == null) return this
//nothing to decrypt
if (data.size < 18) return this
val responseData = data.copyOf(data.size - 2)
val decryptedData: ByteArray = responseData.decrypt(encryptionKey)
val inputStream = ByteArrayInputStream(decryptedData)
val baLength = ByteArray(2)
inputStream.read(baLength)
val length = (baLength[0].toInt() and 0xFF) * 256 + (baLength[1].toInt() and 0xFF)
if (length > decryptedData.size - 4) throw Exception("Can't decrypt - data size invalid")
if (length > decryptedData.size - 4) throw TangemSdkError.InvalidResponse()
val baCRC = ByteArray(2)
inputStream.read(baCRC)
val answerData = ByteArray(length)
inputStream.read(answerData)
val crc: ByteArray = answerData.calculateCrc16()
if (!baCRC.contentEquals(crc)) throw Exception("Can't decrypt - crc invalid")
if (!baCRC.contentEquals(crc)) throw TangemSdkError.InvalidResponse()
return answerData
return ResponseApdu(answerData + data[data.size - 2] + data[data.size - 1])
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.crypto
import com.tangem.EncryptionMode
import org.spongycastle.jce.interfaces.ECPublicKey
import java.security.KeyPair
import java.security.KeyPairGenerator
@ -11,6 +12,16 @@ interface EncryptionHelper {
val keyA: ByteArray
fun generateSecret(keyB: ByteArray): ByteArray
companion object {
fun create(encryptionMode: EncryptionMode): EncryptionHelper? {
return when (encryptionMode) {
EncryptionMode.NONE -> null
EncryptionMode.FAST -> FastEncryptionHelper()
EncryptionMode.STRONG -> StrongEncryptionHelper()
}
}
}
}
class StrongEncryptionHelper : EncryptionHelper {

View file

@ -2,8 +2,10 @@ package com.tangem.tasks
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.TagType
import com.tangem.TangemSdkError
import com.tangem.commands.*
import com.tangem.commands.common.CardDeserializer
import com.tangem.common.CompletionResult
/**
@ -13,31 +15,58 @@ import com.tangem.common.CompletionResult
*/
internal class ScanTask : CardSessionRunnable<Card> {
override val performPreflightRead = true
override val performPreflightRead = false
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
if (session.connectedTag == TagType.Slix) {
readSlixTag(session, callback)
return
}
} else if (card.cardData?.productMask?.contains(Product.Tag) != false) {
callback(CompletionResult.Success(card))
ReadCommand().run(session) { readResult ->
when(readResult) {
is CompletionResult.Failure -> callback(readResult)
is CompletionResult.Success -> {
val card = readResult.data
session.environment.card = card
if (card.cardData?.productMask?.contains(Product.Tag) != false) {
callback(CompletionResult.Success(card))
} else if (card.status != CardStatus.Loaded) {
callback(CompletionResult.Success(card))
} else if (card.status != CardStatus.Loaded) {
callback(CompletionResult.Success(card))
} else if (card.curve == null || card.walletPublicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
} else if (card.curve == null || card.walletPublicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
} else {
val checkWalletCommand = CheckWalletCommand(card.curve, card.walletPublicKey)
} else {
val checkWalletCommand = CheckWalletCommand(card.curve, card.walletPublicKey)
checkWalletCommand.run(session) { result ->
when (result) {
is CompletionResult.Success -> callback(CompletionResult.Success(card))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}
}
}
}
checkWalletCommand.run(session) { result ->
when (result) {
is CompletionResult.Success -> callback(CompletionResult.Success(card))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
private fun readSlixTag(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
session.readSlixTag { result ->
when (result) {
is CompletionResult.Success -> {
try {
val card = CardDeserializer.deserialize(result.data)
callback(CompletionResult.Success(card))
} catch (error: TangemSdkError) {
callback(CompletionResult.Failure(error))
}
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}

View file

@ -4,6 +4,10 @@ import com.google.common.truth.Truth.assertThat
import com.tangem.SessionEnvironment
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvTag
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collect
import org.junit.Test
@ -19,11 +23,12 @@ class CommandApduTest {
tlvBuilder.serialize()
)
val expected = byteArrayOf(0, -14, 0, 0, 0, 0, 34, 16, 32, -111, -76, -47, 66, -126, 63, 125,
32, -59, -16, -115, -10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, -10,
-45, 19, -124, -123, -55, -94, 3)
32, -59, -16, -115, -10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, -10,
-45, 19, -124, -123, -55, -94, 3)
assertThat(commandApdu.apduData)
.isEqualTo(expected)
assertThat(listOf(1, 2)).containsExactlyElementsIn(listOf(1,2))
}
@Test
@ -42,11 +47,11 @@ class CommandApduTest {
)
val expected = byteArrayOf(0, -14, 0, 0, 0, 0, 101, 16, 32, -111, -76, -47, 66, -126, 63,
125, 32, -59, -16, -115, -10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25,
-10, -45, 19, -124, -123, -55, -94, 3, 92, 65, 4, 80, -122, 58, -42, 74, -121, -82, -118,
47, -24, 60, 26, -15, -88, 64, 60, -75, 63, 83, -28, -122, -40, 81, 29, -83, -118, 4, -120,
126, 91, 35, 82, 44, -44, 112, 36, 52, 83, -94, -103, -6, -98, 119, 35, 119, 22, 16, 58,
-68, 17, -95, -33, 56, -123, 94, -42, -14, -18, 24, 126, -100, 88, 43, -90)
125, 32, -59, -16, -115, -10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25,
-10, -45, 19, -124, -123, -55, -94, 3, 92, 65, 4, 80, -122, 58, -42, 74, -121, -82, -118,
47, -24, 60, 26, -15, -88, 64, 60, -75, 63, 83, -28, -122, -40, 81, 29, -83, -118, 4, -120,
126, 91, 35, 82, 44, -44, 112, 36, 52, 83, -94, -103, -6, -98, 119, 35, 119, 22, 16, 58,
-68, 17, -95, -33, 56, -123, 94, -42, -14, -18, 24, 126, -100, 88, 43, -90)
assertThat(commandApdu.apduData)
.isEqualTo(expected)

View file

@ -181,7 +181,7 @@ class PersonalizationConfig {
signingMethodMaskBuilder.add(SigningMethod.SignRawValidateByIssuerWriteIssuerData)
}
if (from.SigningMethod6) {
signingMethodMaskBuilder.add(SigningMethod.SignHash)
signingMethodMaskBuilder.add(SigningMethod.SignPos)
}
return signingMethodMaskBuilder.build()
}

0
tangem-sdk-android-config/.circleci/config.yml Executable file → Normal file
View file

View file

@ -39,22 +39,31 @@ android {
}
dependencies {
// internal
implementation project(':tangem-core')
implementation fileTree(dir: 'libs', include: ['*.jar'])
// kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
// coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.7"
// android
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.skyfishjy.ripplebackground:library:1.0.1'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.core:core-ktx:1.3.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
// misc
implementation 'com.skyfishjy.ripplebackground:library:1.0.1'
implementation 'at.favre.lib:armadillo:0.9.0'
// testing
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

View file

@ -1,20 +1,10 @@
package com.tangem.tangem_sdk_new
import android.animation.ObjectAnimator
import android.app.Activity
import android.view.HapticFeedbackConstants
import android.view.View
import android.view.animation.DecelerateInterpolator
import com.google.android.material.bottomsheet.BottomSheetDialog
import androidx.fragment.app.FragmentActivity
import com.tangem.*
import com.tangem.common.CompletionResult
import com.tangem.tangem_sdk_new.extensions.hide
import com.tangem.tangem_sdk_new.extensions.localizedDescription
import com.tangem.tangem_sdk_new.extensions.show
import com.tangem.tangem_sdk_new.nfc.NfcReader
import com.tangem.tangem_sdk_new.ui.TouchCardAnimation
import kotlinx.android.synthetic.main.layout_touch_card.*
import kotlinx.android.synthetic.main.nfc_bottom_sheet.*
import com.tangem.tangem_sdk_new.ui.NfcSessionDialog
/**
* Default implementation of [SessionViewDelegate].
@ -22,148 +12,65 @@ import kotlinx.android.synthetic.main.nfc_bottom_sheet.*
*/
class DefaultSessionViewDelegate(private val reader: NfcReader) : SessionViewDelegate {
lateinit var activity: Activity
private var readingDialog: BottomSheetDialog? = null
lateinit var activity: FragmentActivity
private var readingDialog: NfcSessionDialog? = null
init {
setLogger()
}
override fun onNfcSessionStarted(cardId: String?, message: Message?) {
reader.readingCancelled = false
override fun onSessionStarted(cardId: String?, message: Message?) {
postUI { showReadingDialog(activity, cardId, message) }
}
private fun showReadingDialog(activity: Activity, cardId: String?, message: Message?) {
private fun showReadingDialog(activity: FragmentActivity, cardId: String?, message: Message?) {
val dialogView = activity.layoutInflater.inflate(R.layout.nfc_bottom_sheet, null)
readingDialog = BottomSheetDialog(activity)
readingDialog = NfcSessionDialog(activity)
readingDialog?.setContentView(dialogView)
readingDialog?.dismissWithAnimation = true
readingDialog?.create()
readingDialog?.setOnShowListener {
readingDialog?.rippleBackgroundNfc?.startRippleAnimation()
val nfcDeviceAntenna = TouchCardAnimation(
activity, readingDialog!!.ivHandCardHorizontal,
readingDialog!!.ivHandCardVertical, readingDialog!!.llHand, readingDialog!!.llNfc)
nfcDeviceAntenna.init()
if (cardId != null) {
readingDialog?.tvCard?.visibility = View.VISIBLE
readingDialog?.tvCardId?.visibility = View.VISIBLE
readingDialog?.tvCardId?.text = cardId
}
if (message != null) {
if (message.body != null) readingDialog?.tvTaskText?.text = message.body
if (message.header != null) readingDialog?.tvTaskTitle?.text = message.header
}
}
readingDialog?.setOnCancelListener {
reader.readingCancelled = true
reader.closeSession()
readingDialog?.show(SessionViewDelegateState.Ready(cardId, message))
}
readingDialog?.setOnCancelListener { reader.stopSession(true) }
readingDialog?.show()
}
override fun onSecurityDelay(ms: Int, totalDurationSeconds: Int) {
postUI {
readingDialog?.lTouchCard?.hide()
readingDialog?.tvRemainingTime?.text = ms.div(100).toString()
readingDialog?.flSecurityDelay?.show()
readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_security_delay)
readingDialog?.tvTaskText?.text =
activity.getText(R.string.dialog_security_delay_description)
performHapticFeedback()
if (readingDialog?.pbSecurityDelay?.max != totalDurationSeconds) {
readingDialog?.pbSecurityDelay?.max = totalDurationSeconds
}
readingDialog?.pbSecurityDelay?.progress = totalDurationSeconds - ms + 100
val animation = ObjectAnimator.ofInt(
readingDialog?.pbSecurityDelay,
"progress",
totalDurationSeconds - ms,
totalDurationSeconds - ms + 100)
animation.duration = 500
animation.interpolator = DecelerateInterpolator()
animation.start()
readingDialog?.show(SessionViewDelegateState.SecurityDelay(ms, totalDurationSeconds))
}
}
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()
readingDialog?.show(SessionViewDelegateState.Delay(total, current, step))
}
}
override fun onTagLost() {
postUI {
readingDialog?.lTouchCard?.show()
readingDialog?.flSecurityDelay?.hide()
readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_ready_to_scan)
readingDialog?.tvTaskText?.text = activity.getText(R.string.dialog_scan_text)
}
postUI { readingDialog?.show(SessionViewDelegateState.TagLost) }
}
override fun onNfcSessionCompleted(message: Message?) {
postUI {
readingDialog?.lTouchCard?.hide()
readingDialog?.flSecurityDelay?.hide()
readingDialog?.flCompletion?.show()
readingDialog?.ivCompletion?.setImageDrawable(activity.getDrawable(R.drawable.ic_done_135dp))
if (message != null) {
if (message.body != null) readingDialog?.tvTaskText?.text = message.body
if (message.header != null) readingDialog?.tvTaskTitle?.text = message.header
}
performHapticFeedback()
}
postUI(300) { readingDialog?.dismiss() }
override fun onTagConnected() {
postUI { readingDialog?.show(SessionViewDelegateState.TagConnected) }
}
override fun onWrongCard() {
postUI { readingDialog?.show(SessionViewDelegateState.WrongCard) }
}
override fun onSessionStopped(message: Message?) {
postUI { readingDialog?.show(SessionViewDelegateState.Success(message)) }
}
override fun onError(error: TangemSdkError) {
postUI {
readingDialog?.lTouchCard?.hide()
readingDialog?.flSecurityDelay?.hide()
readingDialog?.flCompletion?.hide()
readingDialog?.flError?.show()
readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_error)
readingDialog?.tvTaskText?.text = activity.getString(
R.string.error_message,
error.code.toString(), activity.getString(error.localizedDescription())
)
performHapticFeedback()
}
postUI { readingDialog?.show(SessionViewDelegateState.Error(error)) }
}
override fun onPinRequested(callback: (result: CompletionResult<String>) -> Unit) {
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)
}
postUI { readingDialog?.show(SessionViewDelegateState.PinRequested(callback)) }
}
private fun setLogger() {

View file

@ -0,0 +1,17 @@
package com.tangem.tangem_sdk_new
import com.tangem.Message
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
sealed class SessionViewDelegateState() {
data class Error(val error: TangemSdkError) : SessionViewDelegateState()
data class Success(val message: Message?) : SessionViewDelegateState()
data class SecurityDelay(val ms: Int, val totalDurationSeconds: Int) : SessionViewDelegateState()
data class Delay(val total: Int, val current: Int, val step: Int) : SessionViewDelegateState()
data class Ready(val cardId: String?, val message: Message?) : SessionViewDelegateState()
data class PinRequested(val callback: (result: CompletionResult<String>) -> Unit) : SessionViewDelegateState()
object TagLost : SessionViewDelegateState()
object TagConnected : SessionViewDelegateState()
object WrongCard : SessionViewDelegateState()
}

View file

@ -40,7 +40,7 @@ fun TangemSdkError.localizedDescription(): Int {
is TangemSdkError.Pin2OrCvcRequired -> R.string.error_operation
is TangemSdkError.VerificationFailed -> R.string.error_verification_failed
is TangemSdkError.DataSizeTooLarge -> R.string.error_data_size_too_large
is TangemSdkError.ExendedDataSizeTooLarge -> R.string.error_data_size_too_large_extended
is TangemSdkError.ExtendedDataSizeTooLarge -> R.string.error_data_size_too_large_extended
is TangemSdkError.MissingCounter -> R.string.error_missing_counter
is TangemSdkError.OverwritingDataIsProhibited -> R.string.error_data_cannot_be_written
is TangemSdkError.DataCannotBeWritten -> R.string.error_data_cannot_be_written
@ -52,5 +52,6 @@ fun TangemSdkError.localizedDescription(): Int {
is TangemSdkError.WrongCardNumber -> R.string.error_wrong_card_number
is TangemSdkError.WrongCardType -> R.string.error_wrong_card_type
is TangemSdkError.CardError -> R.string.error_card_error
is TangemSdkError.InvalidResponse -> R.string.error_invalid_response
}
}

View file

@ -8,4 +8,13 @@ internal fun View.show() {
internal fun View.hide() {
this.visibility = View.GONE
}
internal fun View.show(show: Boolean) {
if (show) {
this.visibility = View.VISIBLE
} else {
this.visibility = View.GONE
}
}

View file

@ -11,6 +11,7 @@ import android.nfc.tech.IsoDep
import android.os.Build
import android.os.Bundle
import com.tangem.Log
import com.tangem.ReadingActiveListener
import com.tangem.tangem_sdk_new.ui.NfcEnableDialog
/**
@ -18,7 +19,16 @@ import com.tangem.tangem_sdk_new.ui.NfcEnableDialog
* Launches [NfcAdapter], manages it with [Activity] lifecycle,
* enables and disables Nfc Reading Mode, receives NFC [Tag].
*/
class NfcManager : NfcAdapter.ReaderCallback {
class NfcManager : NfcAdapter.ReaderCallback, ReadingActiveListener {
override var readingIsActive: Boolean = false
set(value) {
if (value) {
disableReaderMode()
enableReaderMode()
}
field = value
}
val reader = NfcReader()
private var activity: Activity? = null
@ -42,7 +52,7 @@ class NfcManager : NfcAdapter.ReaderCallback {
override fun onTagDiscovered(tag: Tag?) {
Log.i(this::class.simpleName!!, "Nfc tag is discovered")
if (reader.readingActive) reader.onTagDiscovered(tag) else ignoreTag(tag)
if (readingIsActive) reader.onTagDiscovered(tag) else ignoreTag(tag)
}
@ -50,11 +60,10 @@ class NfcManager : NfcAdapter.ReaderCallback {
val filter = IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED)
activity?.registerReceiver(mBroadcastReceiver, filter)
handleNfcEnabled(nfcAdapter?.isEnabled == true)
reader.manager = this
reader.listener = this
}
private fun handleNfcEnabled(nfcEnabled: Boolean) {
reader.nfcEnabled = nfcEnabled
if (nfcEnabled) {
enableReaderMode()
nfcEnableDialog?.cancel()
@ -67,7 +76,7 @@ class NfcManager : NfcAdapter.ReaderCallback {
fun onPause() {
activity?.unregisterReceiver(mBroadcastReceiver)
disableReaderMode()
reader.manager = null
reader.listener = null
}
fun onDestroy() {
@ -75,11 +84,11 @@ class NfcManager : NfcAdapter.ReaderCallback {
nfcAdapter = null
}
fun enableReaderMode() {
private fun enableReaderMode() {
nfcAdapter?.enableReaderMode(activity, this, READER_FLAGS, Bundle())
}
fun disableReaderMode() {
private fun disableReaderMode() {
nfcAdapter?.disableReaderMode(activity)
}

View file

@ -4,127 +4,123 @@ import android.nfc.Tag
import android.nfc.TagLostException
import android.nfc.tech.IsoDep
import android.nfc.tech.NfcV
import com.tangem.CardReader
import com.tangem.Log
import com.tangem.TangemSdkError
import com.tangem.*
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.toHexString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
data class NfcTag(val type: TagType, val isoDep: IsoDep?, val nfcV: NfcV? = null)
/**
* Provides NFC communication between an Android application and Tangem card.
*/
class NfcReader : CardReader {
override val tag = ConflatedBroadcastChannel<TagType?>()
override var scope: CoroutineScope? = null
var readingActive = false
private set
var nfcEnabled = false
var manager: NfcManager? = null
private var isoDep: IsoDep? = null
set(value) {
// don't reassign when there's an active tag already
if (field == null) {
field = value
// if tag is received, call connect first before transceiving data
if (value != null) connect()
}
if (value == null) field = value
}
var listener: ReadingActiveListener? = null
var readingCancelled = false
private var nfcTag: NfcTag? = null
set(value) {
field = value
if (value) {
// Stops reading and sends failure callback to a task
// if reading is cancelled (when user closes nfc bottom sheet dialog).
closeSession()
callback?.invoke(CompletionResult.Failure(TangemSdkError.UserCancelled()))
}
scope?.launch { tag.send(value?.type) }
}
private var data: ByteArray? = null
private var callback: ((response: CompletionResult<ResponseApdu>) -> Unit)? = null
override fun openSession() {
override fun startSession() {
Log.i(this::class.simpleName!!, "NFC reader is starting NFC session")
readingActive = true
readingCancelled = false
manager?.disableReaderMode()
manager?.enableReaderMode()
}
override fun closeSession() {
isoDep = null
readingActive = false
}
override fun transceiveApdu(apdu: CommandApdu, callback: (response: CompletionResult<ResponseApdu>) -> Unit) {
data = apdu.apduData
this.callback = callback
if (isoDep != null) {
transceiveData()
}
}
private fun transceiveData() {
if (readingCancelled) {
callback?.invoke(CompletionResult.Failure(TangemSdkError.UserCancelled()))
return
}
if (data == null) return
val rawResponse: ByteArray?
try {
Log.i(this::class.simpleName!!, "Sending data to the card, size is ${data?.size}")
Log.v(this::class.simpleName!!, "Raw data that is to be sent to the card: ${data?.toHexString()}")
rawResponse = isoDep?.transceive(data)
Log.v(this::class.simpleName!!, "Raw data that was received from the card: ${rawResponse?.toHexString()}")
} catch (exception: TagLostException) {
callback?.invoke(CompletionResult.Failure(TangemSdkError.TagLost()))
isoDep = null
return
} catch (exception: Exception) {
Log.i(this::class.simpleName!!, exception.localizedMessage ?: "Error tranceiving data")
// The messages of errors can vary on different Android devices,
// but we try to identify it by parsing the message.
if (exception.message?.contains("length") == true) {
callback?.invoke(CompletionResult.Failure(TangemSdkError.ExtendedLengthNotSupported()))
}
isoDep = null
return
}
if (rawResponse != null) {
Log.i(this::class.simpleName!!, "Data from the card was received")
data = null
}
rawResponse?.let { callback?.invoke(CompletionResult.Success(ResponseApdu(it))) }
listener?.readingIsActive = true
}
fun onTagDiscovered(tag: Tag?) {
NfcV.get(tag)?.let { onNfcVDiscovered(it) }
isoDep = IsoDep.get(tag)
transceiveData()
NfcV.get(tag)?.let {
nfcTag = NfcTag(TagType.Slix, null, NfcV.get(tag))
return
}
IsoDep.get(tag)?.let { isoDep ->
connect(isoDep)
nfcTag = NfcTag(TagType.Nfc, isoDep)
}
}
private fun connect() {
isoDep?.connect()
isoDep?.close()
isoDep?.connect()
isoDep?.timeout = 240000
private fun connect(isoDep: IsoDep) {
isoDep.connect()
isoDep.close()
isoDep.connect()
isoDep.timeout = 240000
Log.i(this::class.simpleName!!, "NFC tag is connected")
}
private fun onNfcVDiscovered(nfcV: NfcV) {
override fun stopSession(cancelled: Boolean) {
nfcTag = null
listener?.readingIsActive = false
if (cancelled) scope?.cancel()
}
override suspend fun transceiveApdu(apdu: CommandApdu): CompletionResult<ResponseApdu> =
suspendCancellableCoroutine { continuation ->
transceiveApdu(apdu) { result ->
if (continuation.isActive) continuation.resume(result)
}
}
override fun transceiveApdu(apdu: CommandApdu, callback: (response: CompletionResult<ResponseApdu>) -> Unit) {
val rawResponse: ByteArray? = try {
transcieveAndLog(apdu.apduData)
} catch (exception: TagLostException) {
callback.invoke(CompletionResult.Failure(TangemSdkError.TagLost()))
nfcTag = null
return
} catch (exception: Exception) {
tryHandleNfcError(exception, callback)
nfcTag = null
return
}
if (rawResponse != null) {
Log.i(this::class.simpleName!!, "Data from the card was received")
}
rawResponse?.let { callback.invoke(CompletionResult.Success(ResponseApdu(it))) }
}
private fun transcieveAndLog(data: ByteArray): ByteArray? {
Log.i(this::class.simpleName!!, "Sending data to the card, size is ${data.size}")
Log.v(this::class.simpleName!!, "Raw data that is to be sent to the card: ${data.toHexString()}")
val rawResponse = nfcTag?.isoDep?.transceive(data)
Log.v(this::class.simpleName!!, "Raw data that was received from the card: ${rawResponse?.toHexString()}")
return rawResponse
}
private fun tryHandleNfcError(exception: Exception, callback: (response: CompletionResult<ResponseApdu>) -> Unit) {
Log.i(this::class.simpleName!!, exception.localizedMessage ?: "Error tranceiving data")
// The messages of errors can vary on different Android devices,
// but we try to identify it by parsing the message.
if (exception.message?.contains("length") == true) {
callback.invoke(CompletionResult.Failure(TangemSdkError.ExtendedLengthNotSupported()))
}
}
override fun readSlixTag(callback: (result: CompletionResult<ResponseApdu>) -> Unit) {
val nfcV = nfcTag?.nfcV
if (nfcV == null) {
callback.invoke(CompletionResult.Failure(TangemSdkError.ErrorProcessingCommand()))
return
}
val response = SlixTagReader().transceive(nfcV)
when (response) {
is SlixReadResult.Failure -> {
Log.e(this::class.simpleName!!, "${response.exception.message}")
callback?.invoke(CompletionResult.Failure(TangemSdkError.ErrorProcessingCommand()))
callback.invoke(CompletionResult.Failure(TangemSdkError.ErrorProcessingCommand()))
}
is SlixReadResult.Success -> {
callback?.invoke(CompletionResult.Success(ResponseApdu(response.data)))
callback.invoke(CompletionResult.Success(ResponseApdu(response.data)))
}
}
}

View file

@ -0,0 +1,171 @@
package com.tangem.tangem_sdk_new.ui
import android.animation.ObjectAnimator
import android.view.HapticFeedbackConstants
import android.view.View
import android.view.animation.DecelerateInterpolator
import androidx.fragment.app.FragmentActivity
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tangem_sdk_new.R
import com.tangem.tangem_sdk_new.SessionViewDelegateState
import com.tangem.tangem_sdk_new.extensions.localizedDescription
import com.tangem.tangem_sdk_new.extensions.show
import com.tangem.tangem_sdk_new.postUI
import kotlinx.android.synthetic.main.layout_touch_card.*
import kotlinx.android.synthetic.main.nfc_bottom_sheet.*
class NfcSessionDialog(val activity: FragmentActivity) : BottomSheetDialog(activity) {
private var currentState: SessionViewDelegateState? = null
fun show(state: SessionViewDelegateState) {
when (state) {
is SessionViewDelegateState.Ready -> onReady(state)
is SessionViewDelegateState.Success -> onSuccess(state)
is SessionViewDelegateState.Error -> onError(state)
is SessionViewDelegateState.SecurityDelay -> onSecurityDelay(state)
is SessionViewDelegateState.Delay -> onDelay(state)
is SessionViewDelegateState.PinRequested -> onPinRequested(state)
is SessionViewDelegateState.TagLost -> onTagLost()
is SessionViewDelegateState.TagConnected -> onTagConnected()
is SessionViewDelegateState.WrongCard -> onWrongCard()
}
currentState = state
}
private fun onReady(state: SessionViewDelegateState.Ready) {
show(lTouchCard)
rippleBackgroundNfc?.startRippleAnimation()
val nfcDeviceAntenna = TouchCardAnimation(
activity, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc
)
nfcDeviceAntenna.init()
state.cardId?.let { cardId ->
tvCard?.show()
tvCardId?.show()
tvCardId?.text = cardId
}
state.message?.let { message ->
if (message.body != null) tvTaskText?.text = message.body
if (message.header != null) tvTaskTitle?.text = message.header
}
}
private fun onSuccess(state: SessionViewDelegateState.Success) {
show(flCompletion)
ivCompletion?.setImageDrawable(activity.getDrawable(R.drawable.ic_done_135dp))
state.message?.let { message ->
if (message.body != null) tvTaskText?.text = message.body
if (message.header != null) tvTaskTitle?.text = message.header
}
performHapticFeedback()
postUI(300) { dismiss() }
}
private fun onError(state: SessionViewDelegateState.Error) {
show(flError)
tvTaskTitle?.text = activity.getText(R.string.dialog_error)
tvTaskText?.text = activity.getString(
R.string.error_message,
state.error.code.toString(), activity.getString(state.error.localizedDescription())
)
performHapticFeedback()
}
private fun onSecurityDelay(state: SessionViewDelegateState.SecurityDelay) {
show(flSecurityDelay)
tvRemainingTime?.text = state.ms.div(100).toString()
tvTaskTitle?.text = activity.getText(R.string.dialog_security_delay)
tvTaskText?.text =
activity.getText(R.string.dialog_security_delay_description)
performHapticFeedback()
if (pbSecurityDelay?.max != state.totalDurationSeconds) {
pbSecurityDelay?.max = state.totalDurationSeconds
}
pbSecurityDelay?.progress = state.totalDurationSeconds - state.ms + 100
val animation = ObjectAnimator.ofInt(
pbSecurityDelay,
"progress",
state.totalDurationSeconds - state.ms,
state.totalDurationSeconds - state.ms + 100)
animation.duration = 500
animation.interpolator = DecelerateInterpolator()
animation.start()
}
private fun onDelay(state: SessionViewDelegateState.Delay) {
show(flSecurityDelay)
tvRemainingTime?.text = (((state.total - state.current) / state.step) + 1).toString()
tvTaskTitle?.text = "Operation in process"
tvTaskText?.text = "Please hold the card firmly until the operation is completed…"
performHapticFeedback()
if (pbSecurityDelay?.max != state.total) {
pbSecurityDelay?.max = state.total
}
pbSecurityDelay?.progress = state.current
val animation = ObjectAnimator.ofInt(
pbSecurityDelay,
"progress",
state.current,
state.current + state.step)
animation.duration = 300
animation.interpolator = DecelerateInterpolator()
animation.start()
}
private fun onPinRequested(state: SessionViewDelegateState.PinRequested) {
TODO("To be implemented")
}
private fun onTagLost() {
if (currentState is SessionViewDelegateState.Success) return
show(lTouchCard)
tvTaskTitle?.text = activity.getText(R.string.dialog_ready_to_scan)
tvTaskText?.text = activity.getText(R.string.dialog_scan_text)
}
private fun onTagConnected() {
show(flReading)
}
private fun onWrongCard() {
if (currentState !is SessionViewDelegateState.WrongCard) {
show(flError)
tvTaskTitle?.text = activity.getText(R.string.dialog_error)
tvTaskText?.text = activity.getString(
R.string.error_message,
"Wrong Card", activity.getString(R.string.error_wrong_card_number)
)
performHapticFeedback()
postUI (2000){
show(lTouchCard)
tvTaskTitle?.text = activity.getText(R.string.dialog_ready_to_scan)
tvTaskText?.text = activity.getText(R.string.dialog_scan_text)
}
}
}
private fun show(view: View) {
lTouchCard?.show(view.id == lTouchCard.id)
flSecurityDelay?.show(view.id == flSecurityDelay.id)
flReading?.show(view.id == flReading.id)
flError?.show(view.id == flError.id)
flCompletion?.show(view.id == flCompletion.id)
}
private fun performHapticFeedback() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
llHeader?.isHapticFeedbackEnabled = true
llHeader?.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
}
}
}

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<rotate android:pivotX="50%" android:pivotY="50%" android:fromDegrees="0"
android:toDegrees="360"
xmlns:android="http://schemas.android.com/apk/res/android">
<shape
android:shape="ring"
android:thickness="5dp"
android:useLevel="true">
<gradient
android:angle="360"
android:endColor="@color/card_sdk_accent"
android:centerColor="@color/card_sdk_accent"
android:startColor="@color/card_sdk_accent"
android:type="sweep" />
</shape>
</rotate>

View file

@ -157,9 +157,24 @@
android:progressDrawable="@drawable/pb_circle"
android:progressTint="#FF9D30"
android:secondaryProgress="100" />
</FrameLayout>
<FrameLayout
android:id="@+id/flReading"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:layout_gravity="center"
android:visibility="gone">
<ProgressBar
android:id="@+id/pbReading"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"
android:indeterminateDrawable="@drawable/pb_simple_circle"
android:progressTint="#FF9D30"/>
</FrameLayout>
<TextView
android:id="@+id/tvTaskText"

View file

@ -29,4 +29,5 @@
<string name="error_wrong_card_number">You tapped a different card. Please match the in-app Card ID to the your physical Card ID to continue this process.</string>
<string name="error_wrong_card_type">This card is configured for a different app. Please see the information on your card and download the related app.</string>
<string name="error_card_error">Your card is missing essential data</string>
<string name="error_invalid_response">Invalid Response</string>
</resources>