Updated on 2026-08-14
This commit is contained in:
commit
19d44f47d4
40 changed files with 1210 additions and 1112 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 issuer’s 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
||||
/**
|
||||
* Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card).
|
||||
* Issuer’s 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,
|
||||
/**
|
||||
* Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card).
|
||||
* Issuer’s 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
||||
/**
|
||||
* Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card).
|
||||
* Issuer’s 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?,
|
||||
/**
|
||||
* Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card).
|
||||
* Issuer’s 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)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
0
tangem-sdk-android-config/.circleci/config.yml
Executable file → Normal 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'
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
21
tangem-sdk/src/main/res/drawable/pb_simple_circle.xml
Normal file
21
tangem-sdk/src/main/res/drawable/pb_simple_circle.xml
Normal 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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue