Updated on 2026-08-14

This commit is contained in:
Tangem 2020-06-05 12:35:29 +03:00
parent 0355c34514
commit 300e100a23
30 changed files with 264 additions and 275 deletions

View file

@ -1,4 +1,4 @@
ext.versions = [
kotlin : '1.3.72',
build_gradle: '3.6.3',
build_gradle: '4.0.0',
]

0
gradlew vendored Executable file → Normal file
View file

View file

@ -16,6 +16,15 @@ 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.
*

View file

@ -12,14 +12,8 @@ import com.tangem.crypto.EncryptionHelper
import com.tangem.crypto.FastEncryptionHelper
import com.tangem.crypto.StrongEncryptionHelper
import com.tangem.crypto.pbkdf2Hash
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
/**
* Basic interface for running tasks and [com.tangem.commands.Command] in a [CardSession]
@ -61,11 +55,11 @@ enum class TagType {
* 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
) {
var connectedTag: TagType? = null
@ -75,7 +69,9 @@ class CardSession(
*/
private var state = CardSessionState.Inactive
private val scope = CoroutineScope(Dispatchers.IO)
val scope = CoroutineScope(Dispatchers.IO) + CoroutineExceptionHandler { _, ex ->
throw ex
}
private val tag = this.javaClass.simpleName
@ -86,17 +82,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
) {
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) {
@ -121,8 +114,10 @@ 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(performPreflightRead: Boolean = true,
callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
fun start(
performPreflightRead: Boolean = true,
callback: (session: CardSession, error: TangemSdkError?) -> Unit
) {
if (state != CardSessionState.Inactive) {
callback(this, TangemSdkError.Busy())
@ -133,21 +128,21 @@ class CardSession(
scope.launch {
reader.tag
.asFlow()
.collect { tagType ->
if (tagType == null && connectedTag != null) {
handleTagLost()
} else if (tagType != null) {
connectedTag = tagType
viewDelegate.onTagConnected()
.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)
}
if (tagType == TagType.Nfc && performPreflightRead) {
preflightCheck(callback)
} else {
callback(this@CardSession, null)
}
}
}
}
reader.scope = scope
reader.startSession()
@ -164,15 +159,8 @@ class CardSession(
readCommand.run(this) { result ->
when (result) {
is CompletionResult.Failure -> {
tryHandleError(result.error) { handleErrorResult ->
when (handleErrorResult) {
is CompletionResult.Success -> preflightCheck(callback)
is CompletionResult.Failure -> {
stopWithError(result.error)
callback(this, result.error)
}
}
}
stopWithError(result.error)
callback(this, result.error)
}
is CompletionResult.Success -> {
val receivedCardId = result.data.cardId
@ -230,65 +218,53 @@ class CardSession(
fun send(apdu: CommandApdu, callback: (result: CompletionResult<ResponseApdu>) -> Unit) {
val subscription = reader.tag.openSubscription()
scope.launch {
subscription.consumeAsFlow()
.filterNotNull()
.collect {
reader.transceiveApdu(apdu) { result ->
subscription.cancel()
callback(result)
}
}
}
}
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()))
}
.filterNotNull()
.map { establishEncryption() }
.map { apdu.encrypt(environment.encryptionMode, environment.encryptionKey) }
.map { encryptedApdu -> reader.transceiveApdu(encryptedApdu) }
.catch { error ->
if (error is TangemSdkError) callback(
CompletionResult.Failure(
error
)
)
}
.collect { result ->
subscription.cancel()
callback(result)
}
return establishEncryption(callback)
}
else -> callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
}
}
private fun establishEncryption(callback: (result: CompletionResult<Boolean>) -> Unit) {
private suspend fun establishEncryption(): CompletionResult<Boolean> {
if (environment.encryptionKey != null) return CompletionResult.Success(true)
val encryptionHelper: EncryptionHelper =
if (environment.encryptionMode == EncryptionMode.STRONG) {
StrongEncryptionHelper()
} else {
FastEncryptionHelper()
}
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))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
when (environment.encryptionMode) {
EncryptionMode.NONE -> return CompletionResult.Success(true)
EncryptionMode.FAST -> FastEncryptionHelper()
EncryptionMode.STRONG -> StrongEncryptionHelper()
}
val openSesssionCommand = OpenSessionCommand(encryptionHelper.keyA)
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)
}
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)
}
}
}

View file

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

View file

@ -8,6 +8,8 @@ import com.tangem.commands.personalization.entities.Acquirer
import com.tangem.commands.personalization.entities.CardConfig
import com.tangem.commands.personalization.entities.Issuer
import com.tangem.commands.personalization.entities.Manufacturer
import com.tangem.commands.verifycard.VerifyCardCommand
import com.tangem.commands.verifycard.VerifyCardResponse
import com.tangem.common.CompletionResult
import com.tangem.common.TerminalKeysService
import com.tangem.crypto.CryptoUtils
@ -294,6 +296,11 @@ class TangemSdk(
startSessionWithRunnable(PurgeWalletCommand(), cardId, initialMessage, callback)
}
fun verify(cardId: String? = null, online: Boolean = true, initialMessage: Message? = null,
callback: (result: CompletionResult<VerifyCardResponse>) -> Unit) {
startSessionWithRunnable(VerifyCardCommand(online), cardId, initialMessage, callback)
}
/**
* Command available on SDK cards only
*

View file

@ -29,6 +29,7 @@ sealed class TangemSdkError(val code: Int) : Exception(code.toString()) {
class DecodingFailedMissingTag : TangemSdkError(20005)
class DecodingFailedTypeMismatch : TangemSdkError(20006)
class DecodingFailed : TangemSdkError(20007)
class InvalidResponse : TangemSdkError(20008)
/**
* This error is returned when unknown [StatusWord] is received from a card.

View file

@ -90,15 +90,11 @@ class CheckWalletCommand(
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Challenge, challenge)
return CommandApdu(
Instruction.CheckWallet, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
return CommandApdu(Instruction.CheckWallet, tlvBuilder.serialize())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): CheckWalletResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return CheckWalletResponse(

View file

@ -2,13 +2,31 @@ package com.tangem.commands
import com.tangem.*
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.apdu.StatusWord
import com.tangem.common.apdu.toTangemSdkError
import com.tangem.common.apdu.*
import com.tangem.common.extensions.toInt
import com.tangem.common.tlv.TlvTag
interface ApduSerializable<T : CommandResponse> {
/**
* Serializes data into an array of [com.tangem.common.tlv.Tlv],
* then creates [CommandApdu] with this data.
* @param environment [SessionEnvironment] of the current card
* @return command data converted to [CommandApdu] that allows to convert it to [ByteArray]
* that can be sent to a Tangem card
*/
fun serialize(environment: SessionEnvironment): CommandApdu
/**
* Deserializes data received from a card and stored in [ResponseApdu]
* into an array of [com.tangem.common.tlv.Tlv]. Then maps it into a [CommandResponse].
* @param environment [SessionEnvironment] of the current card.
* @param apdu received data.
* @return Card response converted to a [CommandResponse] of a type [T]
*/
fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): T
}
/**
* Basic interface for a parsed response from [Command].
*/
@ -17,28 +35,10 @@ 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) {
@ -52,14 +52,18 @@ abstract class Command<T : CommandResponse> : CardSessionRunnable<T> {
}
}
open fun performPreCheck(session: CardSession,
callback: (result: CompletionResult<T>) -> Unit): Boolean {
open fun performPreCheck(
session: CardSession,
callback: (result: CompletionResult<T>) -> Unit
): Boolean {
return false
}
open fun performAfterCheck(session: CardSession,
result: CompletionResult<T>,
callback: (result: CompletionResult<T>) -> Unit): Boolean {
open fun performAfterCheck(
session: CardSession,
result: CompletionResult<T>,
callback: (result: CompletionResult<T>) -> Unit
): Boolean {
return false
}
@ -80,25 +84,63 @@ abstract class Command<T : CommandResponse> : CardSessionRunnable<T> {
}
}
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 -> {
try {
val decryptedResponseApdu =
responseApdu.decrypt(session.environment.encryptionKey)
callback(CompletionResult.Success(decryptedResponseApdu))
} catch (error: TangemSdkError) {
callback(CompletionResult.Failure(error))
}
}
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 +168,9 @@ abstract class Command<T : CommandResponse> : CardSessionRunnable<T> {
*
* @return Remaining security delay in milliseconds.
*/
private fun deserializeSecurityDelay(responseApdu: ResponseApdu, environment: SessionEnvironment): Int? {
private fun deserializeSecurityDelay(
responseApdu: ResponseApdu
): Int? {
val tlv = responseApdu.getTlvData()
return tlv?.find { it.tag == TlvTag.Pause }?.value?.toInt()
}

View file

@ -80,14 +80,11 @@ 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)
val tlvData = apdu.getTlvData()
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)

View file

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

View file

@ -67,15 +67,11 @@ class PurgeWalletCommand : Command<PurgeWalletResponse>() {
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
return CommandApdu(
Instruction.PurgeWallet, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
return CommandApdu(Instruction.PurgeWallet, tlvBuilder.serialize())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): PurgeWalletResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return PurgeWalletResponse(

View file

@ -390,13 +390,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 {
return CardDeserializer.deserialize(apdu, environment)
return CardDeserializer.deserialize(apdu)
}
}

View file

@ -103,15 +103,11 @@ 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()
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return ReadIssuerDataResponse(

View file

@ -152,16 +152,11 @@ 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()
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return ReadIssuerExtraDataResponse(

View file

@ -68,15 +68,11 @@ class ReadUserDataCommand : Command<ReadUserDataResponse>() {
builder.append(TlvTag.CardId, environment.card?.cardId)
builder.append(TlvTag.Pin, environment.pin1)
return CommandApdu(
Instruction.ReadUserData, builder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
return CommandApdu(Instruction.ReadUserData, builder.serialize())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): ReadUserDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return ReadUserDataResponse(

View file

@ -99,10 +99,7 @@ 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 {
@ -133,8 +130,7 @@ 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(

View file

@ -114,15 +114,11 @@ 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()
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return WriteIssuerDataResponse(

View file

@ -184,10 +184,7 @@ 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 =
@ -199,8 +196,7 @@ class WriteIssuerExtraDataCommand(
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): WriteIssuerDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
return WriteIssuerDataResponse(cardId = TlvDecoder(tlvData).decode(TlvTag.CardId)
)

View file

@ -77,15 +77,12 @@ class WriteUserDataCommand(private val userData: ByteArray? = null, private val
if (userProtectedCounter != null || userProtectedData != null)
builder.append(TlvTag.Pin2, environment.pin2)
return CommandApdu(
Instruction.WriteUserData, builder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
return CommandApdu(Instruction.WriteUserData, builder.serialize())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): WriteUserDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
return WriteUserDataResponse(TlvDecoder(tlvData).decode(TlvTag.CardId))
}

View file

@ -1,6 +1,5 @@
package com.tangem.commands.common
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.Card
import com.tangem.commands.CardData
@ -11,9 +10,8 @@ import com.tangem.common.tlv.TlvTag
class CardDeserializer() {
companion object {
fun deserialize(apdu: ResponseApdu, environment: SessionEnvironment): Card {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
fun deserialize(apdu: ResponseApdu): Card {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)

View file

@ -47,15 +47,11 @@ class PersonalizeCommand(
}
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 {
return CardDeserializer.deserialize(apdu, environment)
return CardDeserializer.deserialize(apdu)
}
private fun serializePersonalizationData(config: CardConfig): ByteArray {

View file

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

View file

@ -1,5 +1,6 @@
package com.tangem.common.apdu
import com.tangem.TangemSdkError
import com.tangem.common.extensions.calculateCrc16
import com.tangem.common.tlv.Tlv
import com.tangem.crypto.decrypt
@ -27,40 +28,36 @@ 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
if (data.size < 18) throw TangemSdkError.InvalidResponse()
val responseData = data.copyOf(data.size - 2)
val decryptedData: ByteArray = responseData.decrypt(encryptionKey)
val inputStream = ByteArrayInputStream(decryptedData)
val baLength = ByteArray(2)
inputStream.read(baLength)
val length = (baLength[0].toInt() and 0xFF) * 256 + (baLength[1].toInt() and 0xFF)
if (length > decryptedData.size - 4) throw Exception("Can't decrypt - data size invalid")
if (length > decryptedData.size - 4) throw TangemSdkError.InvalidResponse()
val baCRC = ByteArray(2)
inputStream.read(baCRC)
val answerData = ByteArray(length)
inputStream.read(answerData)
val crc: ByteArray = answerData.calculateCrc16()
if (!baCRC.contentEquals(crc)) throw Exception("Can't decrypt - crc invalid")
if (!baCRC.contentEquals(crc)) throw TangemSdkError.InvalidResponse()
return answerData
return ResponseApdu(answerData + data[data.size - 2] + data[data.size - 1])
}
}

View file

@ -59,7 +59,7 @@ internal class ScanTask : CardSessionRunnable<Card> {
when (result) {
is CompletionResult.Success -> {
try {
val card = CardDeserializer.deserialize(result.data, session.environment)
val card = CardDeserializer.deserialize(result.data)
callback(CompletionResult.Success(card))
} catch (error: TangemSdkError) {
callback(CompletionResult.Failure(error))

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

View file

@ -52,5 +52,6 @@ fun TangemSdkError.localizedDescription(): Int {
is TangemSdkError.WrongCardNumber -> R.string.error_wrong_card_number
is TangemSdkError.WrongCardType -> R.string.error_wrong_card_type
is TangemSdkError.CardError -> R.string.error_card_error
is TangemSdkError.InvalidResponse -> R.string.error_invalid_response
}
}

View file

@ -84,11 +84,11 @@ class NfcManager : NfcAdapter.ReaderCallback, ReadingActiveListener {
nfcAdapter = null
}
fun enableReaderMode() {
private fun enableReaderMode() {
nfcAdapter?.enableReaderMode(activity, this, READER_FLAGS, Bundle())
}
fun disableReaderMode() {
private fun disableReaderMode() {
nfcAdapter?.disableReaderMode(activity)
}

View file

@ -12,6 +12,8 @@ import com.tangem.common.extensions.toHexString
import kotlinx.coroutines.CoroutineScope
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)
@ -61,11 +63,16 @@ class NfcReader : CardReader {
//TODO: send user cancelled if (cancelled)
}
override fun transceiveApdu(apdu: CommandApdu, callback: (response: CompletionResult<ResponseApdu>) -> Unit) {
val data = apdu.apduData
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(data, callback)
transcieveAndLog(apdu.apduData)
} catch (exception: TagLostException) {
callback.invoke(CompletionResult.Failure(TangemSdkError.TagLost()))
nfcTag = null
@ -82,7 +89,7 @@ class NfcReader : CardReader {
rawResponse?.let { callback.invoke(CompletionResult.Success(ResponseApdu(it))) }
}
private fun transcieveAndLog(data: ByteArray, callback: (response: CompletionResult<ResponseApdu>) -> Unit): ByteArray? {
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)

View file

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