Updated on 2026-08-14

This commit is contained in:
Tangem 2020-06-30 15:47:01 +03:00
parent d7fd830c9c
commit 834f410d4a
32 changed files with 390 additions and 167 deletions

View file

@ -11,8 +11,9 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
classpath "com.github.dcendents:android-maven-gradle-plugin:2.1"
classpath 'com.google.gms:google-services:4.3.3'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.0.0-beta04'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.0'
classpath 'com.google.firebase:perf-plugin:1.3.1'
classpath 'com.squareup.sqldelight:gradle-plugin:1.4.0'
}
}

View file

@ -4,6 +4,7 @@ apply plugin: 'com.github.dcendents.android-maven'
apply from: '../dependencies.gradle'
apply from: '../jitpack.gradle'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.squareup.sqldelight'
group = "$jitpackSdk.group"
version "$jitpackSdk.version"
@ -11,7 +12,7 @@ version "$jitpackSdk.version"
dependencies {
// kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlidn"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.7"
@ -22,6 +23,7 @@ dependencies {
// misc
implementation 'com.google.code.gson:gson:2.8.6'
implementation "com.squareup.sqldelight:sqlite-driver:1.4.0"
//network
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
@ -36,6 +38,12 @@ dependencies {
testImplementation "com.google.truth:truth:1.0.1"
}
sqldelight {
Database {
packageName = "com.tangem"
}
}
sourceCompatibility = "8"
targetCompatibility = "8"

View file

@ -1,9 +1,11 @@
package com.tangem
import com.tangem.commands.Command
import com.tangem.commands.CommandResponse
import com.tangem.commands.OpenSessionCommand
import com.tangem.commands.ReadCommand
import com.tangem.common.CompletionResult
import com.tangem.common.PinCode
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
@ -29,6 +31,10 @@ interface CardSessionRunnable<T : CommandResponse> {
fun run(session: CardSession, callback: (result: CompletionResult<T>) -> Unit)
}
interface CardSessionStoppedListener {
fun onSessionStopped(environment: SessionEnvironment)
}
enum class CardSessionState {
Inactive,
Active
@ -61,6 +67,7 @@ class CardSession(
) {
var connectedTag: TagType? = null
var sessionStoppedListener: CardSessionStoppedListener? = null
/**
* True if some operation is still in progress.
@ -74,7 +81,7 @@ class CardSession(
private val tag = this.javaClass.simpleName
/**
* This metod starts a card session, performs preflight [ReadCommand],
* This method starts a card session, performs preflight [ReadCommand],
* invokes [CardSessionRunnable.run] and closes the session.
* @param runnable [CardSessionRunnable] that will be performed in the session.
* @param callback will be triggered with a [CompletionResult] of a session.
@ -83,6 +90,24 @@ class CardSession(
runnable: T, callback: (result: CompletionResult<R>) -> Unit
) {
if (environment.pin1 == null) {
viewDelegate.onSessionStarted(cardId)
viewDelegate.onPinRequested {
environment.pin1 = PinCode(it)
startWithRunnable(runnable, callback)
}
return
}
if ((runnable as? Command<R>)?.requiresPin2 == true && environment.pin2 == null) {
viewDelegate.onSessionStarted(cardId)
viewDelegate.onPinRequested {
environment.pin2 = PinCode(it)
startWithRunnable(runnable, callback)
}
return
}
start(runnable.performPreflightRead) { session, error ->
if (error != null) {
callback(CompletionResult.Failure(error))
@ -209,6 +234,7 @@ class CardSession(
}
private fun stopSession() {
sessionStoppedListener?.onSessionStopped(environment)
reader.stopSession()
state = CardSessionState.Inactive
scope.cancel()
@ -259,7 +285,7 @@ class CardSession(
return CompletionResult.Failure(error)
}
val uid = result.uid
val protocolKey = environment.pin1.pbkdf2Hash(uid, 50)
val protocolKey = environment.pin1!!.value.pbkdf2Hash(uid, 50)
val secret = encryptionHelper.generateSecret(result.sessionKeyB)
val sessionKey = (secret + protocolKey).calculateSha256()
environment.encryptionKey = sessionKey

View file

@ -36,6 +36,14 @@ class Config(
*/
val cardFilter: CardFilter = CardFilter(),
var handleErrors: Boolean = true
var handleErrors: Boolean = true,
var defaultPin1: String = "000000",
var defaultPin2: String = "000",
var savePin1InStaticField: Boolean = true,
var savePin2InStaticField: Boolean = false
)

View file

@ -2,7 +2,9 @@ package com.tangem
import com.tangem.commands.Card
import com.tangem.commands.EllipticCurve
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.CardValuesService
import com.tangem.common.PinCode
import com.tangem.common.TerminalKeysService
import com.tangem.crypto.CryptoUtils.generatePublicKey
@ -13,38 +15,79 @@ import com.tangem.crypto.CryptoUtils.generatePublicKey
* @property card Current card, read by preflight [com.tangem.commands.ReadCommand].
* @property terminalKeys generated terminal keys used in Linked Terminal feature.
*/
data class SessionEnvironment(
var pin2: ByteArray = DEFAULT_PIN2.calculateSha256(),
var card: Card? = null,
var terminalKeys: KeyPair? = null,
var encryptionMode: EncryptionMode = EncryptionMode.NONE,
var encryptionKey: ByteArray? = null,
var cvc: ByteArray? = null,
var cardFilter: CardFilter = CardFilter(),
val handleErrors: Boolean = true
class SessionEnvironment(
cardId: String?,
private val config: Config,
private val terminalKeysService: TerminalKeysService?,
private val cardValuesService: CardValuesService?
) {
var pin1: ByteArray = SessionEnvironment.pin1
private set
get() = SessionEnvironment.pin1
var pin1: PinCode?
var pin2: PinCode?
var cvc: ByteArray? = null
fun isCurrentPin1Default(): Boolean = Companion.pin1.contentEquals(DEFAULT_PIN.calculateSha256())
fun isCurrentPin2Default(): Boolean = pin2.contentEquals(DEFAULT_PIN2.calculateSha256())
var terminalKeys: KeyPair? = null
var cardFilter: CardFilter
val handleErrors: Boolean
fun setPin1(pin1: String) {
SessionEnvironment.pin1 = pin1.calculateSha256()
var encryptionMode: EncryptionMode
var encryptionKey: ByteArray? = null
var cardVerification: VerificationState
var cardValidation: VerificationState
var codeVerification: VerificationState
var card: Card? = null
init {
terminalKeys = if (config.linkedTerminal) terminalKeysService?.getKeys() else null
cardFilter = config.cardFilter
handleErrors = config.handleErrors
encryptionMode = config.encryptionMode
val cardValues = cardId?.let { cardValuesService?.getValues(cardId) }
cardVerification = cardValues?.cardVerification ?: VerificationState.NotVerified
cardValidation = cardValues?.cardValidation ?: VerificationState.NotVerified
codeVerification = cardValues?.codeVerification ?: VerificationState.NotVerified
pin1 = TangemSdk.pin1
?: if (cardValues?.isPin1Default != false) {
PinCode(config.defaultPin1, true)
} else {
null
}
pin2 = cardId?.let { TangemSdk.pin2[it] }
?: if (cardValues?.isPin2Default != false) {
PinCode(config.defaultPin2, true)
} else {
null
}
}
fun setPin2(pin2: String) {
this.pin2 = pin2.calculateSha256()
fun restoreCardValues() {
val cardValues = this.card?.cardId?.let { cardValuesService?.getValues(it) }
cardVerification = cardValues?.cardVerification ?: VerificationState.NotVerified
cardValidation = cardValues?.cardValidation ?: VerificationState.NotVerified
codeVerification = cardValues?.codeVerification ?: VerificationState.NotVerified
if (cardValues?.isPin1Default == false && pin1?.isDefault == true) pin1 = null
if (cardValues?.isPin2Default == false && pin1?.isDefault == true) pin2 = null
}
companion object {
const val DEFAULT_PIN = "000000"
const val DEFAULT_PIN2 = "000"
fun saveCardValues() {
if (config.savePin1InStaticField) {
TangemSdk.pin1 = pin1
}
if (config.savePin2InStaticField) {
card?.cardId?.let { cardId -> TangemSdk.pin2[cardId] = pin2 }
}
var pin1: ByteArray = DEFAULT_PIN.calculateSha256()
cardValuesService?.saveValues(this)
}
}
/**
@ -60,4 +103,8 @@ class KeyPair(val publicKey: ByteArray, val privateKey: ByteArray) {
constructor(privateKey: ByteArray, curve: EllipticCurve = EllipticCurve.Secp256k1) :
this(generatePublicKey(privateKey, curve), privateKey)
}
enum class VerificationState {
Passed, Offline, Failed, NotVerified
}

View file

@ -47,7 +47,7 @@ interface SessionViewDelegate {
/**
* It is called when a user is expected to enter pin code.
*/
fun onPinRequested(callback: (pin: String?) -> Unit)
fun onPinRequested(callback: (pin: String) -> Unit)
}
/**

View file

@ -1,5 +1,6 @@
package com.tangem
import com.squareup.sqldelight.db.SqlDriver
import com.tangem.commands.*
import com.tangem.commands.personalization.DepersonalizeCommand
import com.tangem.commands.personalization.DepersonalizeResponse
@ -10,7 +11,9 @@ 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.CardValuesDbService
import com.tangem.common.CompletionResult
import com.tangem.common.PinCode
import com.tangem.common.TerminalKeysService
import com.tangem.crypto.CryptoUtils
import com.tangem.tasks.CreateWalletTask
@ -30,7 +33,8 @@ import com.tangem.tasks.ScanTask
class TangemSdk(
private val reader: CardReader,
private val viewDelegate: SessionViewDelegate,
var config: Config = Config()
var config: Config = Config(),
private val sqlDriver: SqlDriver
) {
private var terminalKeysService: TerminalKeysService? = null
@ -203,7 +207,7 @@ class TangemSdk(
initialMessage: Message? = null,
callback: (result: CompletionResult<WriteUserDataResponse>) -> Unit
) {
val command = WriteUserDataCommand(userData = userData,userCounter = userCounter)
val command = WriteUserDataCommand(userData = userData, userCounter = userCounter)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
@ -378,13 +382,22 @@ class TangemSdk(
fun <T : CommandResponse> startSessionWithRunnable(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<T>) -> Unit) {
val cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId, initialMessage)
val cardSession = CardSession(buildEnvironment(cardId), reader, viewDelegate, cardId, initialMessage)
addSessionStoppedListener(cardSession)
Thread().run { cardSession.startWithRunnable(runnable, callback) }
}
private fun addSessionStoppedListener(session: CardSession) {
session.sessionStoppedListener = object : CardSessionStoppedListener {
override fun onSessionStopped(environment: SessionEnvironment) {
environment.saveCardValues()
}
}
}
/**
* Allows running a custom bunch of commands in one [CardSession] with lightweight closure syntax.
* Tangem SDK will start a card sesion and perform preflight [ReadCommand].
* Tangem SDK will start a card session and perform preflight [ReadCommand].
* @cardId: CID, Unique Tangem card ID number. If not null, the SDK will check that you the card
* with which you tapped a phone has this [cardId] and SDK will return
@ -395,8 +408,9 @@ class TangemSdk(
* then you can use the [CardSession] to interact with a card.
*/
fun startSession(cardId: String? = null, initialMessage: Message? = null,
callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
val cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId, initialMessage)
callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
val cardSession = CardSession(buildEnvironment(cardId), reader, viewDelegate, cardId, initialMessage)
addSessionStoppedListener(cardSession)
Thread().run { cardSession.start(callback = callback) }
}
@ -408,14 +422,14 @@ class TangemSdk(
this.terminalKeysService = terminalKeysService
}
private fun buildEnvironment(): SessionEnvironment {
val terminalKeys = if (config.linkedTerminal) terminalKeysService?.getKeys() else null
private fun buildEnvironment(cardId: String?): SessionEnvironment {
return SessionEnvironment(
terminalKeys = terminalKeys,
cardFilter = config.cardFilter,
handleErrors = config.handleErrors
cardId, config, terminalKeysService, CardValuesDbService(sqlDriver)
)
}
companion object
companion object {
var pin1: PinCode? = null
val pin2: MutableMap<String, PinCode?> = mutableMapOf()
}
}

View file

@ -82,7 +82,7 @@ class CheckWalletCommand(
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Challenge, challenge)
return CommandApdu(Instruction.CheckWallet, tlvBuilder.serialize())

View file

@ -2,6 +2,7 @@ package com.tangem.commands
import com.tangem.*
import com.tangem.common.CompletionResult
import com.tangem.common.PinCode
import com.tangem.common.apdu.*
import com.tangem.common.extensions.toInt
import com.tangem.common.tlv.TlvTag
@ -50,7 +51,6 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
open fun mapError(card: Card?, error: TangemSdkError): TangemSdkError = error
fun transceive(session: CardSession, callback: (result: CompletionResult<T>) -> Unit) {
val card = session.environment.card
if (session.environment.handleErrors && card != null) {
performPreCheck(card)?.let { error ->
@ -58,12 +58,14 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
return
}
}
if (requiresPin2 && session.environment.isCurrentPin2Default()) {
if (requiresPin2 && session.environment.pin2?.isDefault == true) {
handlePin2(session, callback)
return
}
transceiveInternal(session, callback)
}
private fun transceiveInternal(session: CardSession, callback: (result: CompletionResult<T>) -> Unit) {
val apdu = serialize(session.environment)
transceiveApdu(apdu, session) { result ->
when (result) {
@ -74,6 +76,10 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
handlePin1(session, callback)
return@transceiveApdu
}
if (error is TangemSdkError.Pin2OrCvcRequired) {
handlePin2(session, callback)
return@transceiveApdu
}
callback(CompletionResult.Failure(error))
return@transceiveApdu
@ -93,9 +99,9 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
}
private fun transceiveApdu(
apdu: CommandApdu,
session: CardSession,
callback: (result: CompletionResult<ResponseApdu>) -> Unit
apdu: CommandApdu,
session: CardSession,
callback: (result: CompletionResult<ResponseApdu>) -> Unit
) {
Log.i(this::class.simpleName!!, "transieve: ${Instruction.byCode(apdu.ins)}")
@ -114,17 +120,17 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
StatusWord.NeedPause -> {
// NeedPause is returned from the card whenever security delay is triggered.
val remainingTime =
deserializeSecurityDelay(responseApdu)
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"
this::class.simpleName!!,
"Nfc command ${this::class.simpleName!!} " +
"triggered security delay of $remainingTime milliseconds"
)
transceiveApdu(apdu, session, callback)
}
@ -149,7 +155,7 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
}
else -> {
val error = responseApdu.statusWord.toTangemSdkError()
if (error != null && !tryHandleError(error)) {
if (error != null) {
callback(CompletionResult.Failure(error))
} else {
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
@ -173,61 +179,56 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
* @return Remaining security delay in milliseconds.
*/
private fun deserializeSecurityDelay(
responseApdu: ResponseApdu
responseApdu: ResponseApdu
): Int? {
val tlv = responseApdu.getTlvData()
return tlv?.find { it.tag == TlvTag.Pause }?.value?.toInt()
}
private fun tryHandleError(error: TangemSdkError): Boolean {
return false
}
private fun handlePin1(
session: CardSession,
callback: (result: CompletionResult<T>) -> Unit
session: CardSession,
callback: (result: CompletionResult<T>) -> Unit
) {
if (!session.environment.isCurrentPin1Default()) {
session.environment.setPin1(SessionEnvironment.DEFAULT_PIN)
transceive(session, callback)
return
}
session.pause()
session.viewDelegate.onPinRequested { pin1 ->
if (!pin1.isNullOrEmpty()) {
session.environment.setPin1(pin1)
session.resume()
transceive(session, callback)
} else {
session.environment.setPin1(SessionEnvironment.DEFAULT_PIN)
session.resume()
transceive(session, callback)
}
session.environment.pin1 = PinCode(pin1)
session.resume()
transceive(session, callback)
}
}
private fun handlePin2(
session: CardSession,
callback: (result: CompletionResult<T>) -> Unit
session: CardSession,
callback: (result: CompletionResult<T>) -> Unit
) {
val checkPinCommand = SetPinCommand(session.environment.pin1, session.environment.pin2)
val currentPin1 = session.environment.pin1?.value ?: run {
callback(CompletionResult.Failure(TangemSdkError.Pin1Required()))
return
}
val currentPin2 = session.environment.pin2?.value ?: run {
callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired()))
return
}
val checkPinCommand = SetPinCommand(currentPin1, currentPin2)
checkPinCommand.run(session) { result ->
when (result) {
is CompletionResult.Failure -> {
session.viewDelegate.onPinRequested { pin2 ->
if (!pin2.isNullOrEmpty()) {
session.environment.setPin2(pin2)
transceive(session, callback)
} else {
session.environment.setPin2(SessionEnvironment.DEFAULT_PIN2)
callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired()))
}
}
session.environment.pin2 = null
getPin2FromDelegate(session, callback)
}
is CompletionResult.Success -> {
transceive(session, callback)
transceiveInternal(session, callback)
}
}
}
}
private fun getPin2FromDelegate(session: CardSession,
callback: (result: CompletionResult<T>) -> Unit) {
session.viewDelegate.onPinRequested { pin2 ->
session.environment.pin2 = PinCode(pin2)
transceive(session, callback)
}
}
}

View file

@ -62,9 +62,9 @@ class CreateWalletCommand : Command<CreateWalletResponse>() {
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
tlvBuilder.append(TlvTag.Pin2, environment.pin2?.value)
tlvBuilder.append(TlvTag.Cvc, environment.cvc)
return CommandApdu(Instruction.CreateWallet, tlvBuilder.serialize())
}

View file

@ -61,9 +61,9 @@ class PurgeWalletCommand : Command<PurgeWalletResponse>() {
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
tlvBuilder.append(TlvTag.Pin2, environment.pin2?.value)
return CommandApdu(Instruction.PurgeWallet, tlvBuilder.serialize())
}

View file

@ -382,7 +382,7 @@ class ReadCommand : Command<Card>() {
* In order to obtain cards data, [ReadCommand] should use the correct pin 1 value.
* The card will not respond if wrong pin 1 has been submitted.
*/
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.TerminalPublicKey, environment.terminalKeys?.publicKey)
return CommandApdu(Instruction.Read, tlvBuilder.serialize())
}

View file

@ -96,7 +96,7 @@ class ReadIssuerDataCommand(
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadData)
return CommandApdu(Instruction.ReadIssuerData, tlvBuilder.serialize())

View file

@ -145,7 +145,7 @@ class ReadIssuerExtraDataCommand(
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadExtraData)
tlvBuilder.append(TlvTag.Offset, offset)

View file

@ -62,7 +62,7 @@ class ReadUserDataCommand : Command<ReadUserDataResponse>() {
override fun serialize(environment: SessionEnvironment): CommandApdu {
val builder = TlvBuilder()
builder.append(TlvTag.CardId, environment.card?.cardId)
builder.append(TlvTag.Pin, environment.pin1)
builder.append(TlvTag.Pin, environment.pin1?.value)
return CommandApdu(Instruction.ReadUserData, builder.serialize())
}

View file

@ -6,7 +6,6 @@ import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.apdu.StatusWord
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
@ -40,28 +39,27 @@ enum class SetPinStatus {
}
class SetPinResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
*
*/
val status: SetPinStatus
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
*
*/
val status: SetPinStatus
) : CommandResponse
class SetPinCommand(
private val newPin1: ByteArray = SessionEnvironment.DEFAULT_PIN.calculateSha256(),
private val newPin2: ByteArray = SessionEnvironment.DEFAULT_PIN2.calculateSha256(),
private val newPin3: ByteArray? = null
private var newPin1: ByteArray,
private var newPin2: ByteArray,
private var newPin3: ByteArray? = null
) : Command<SetPinResponse>() {
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
tlvBuilder.append(TlvTag.Pin2, environment.pin2?.value)
tlvBuilder.append(TlvTag.Cvc, environment.cvc)
tlvBuilder.append(TlvTag.NewPin, newPin1)
tlvBuilder.append(TlvTag.NewPin2, newPin2)
@ -72,12 +70,13 @@ class SetPinCommand(
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): SetPinResponse {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val status = SetPinStatus.fromStatusWord(apdu.statusWord) ?: throw TangemSdkError.DecodingFailed()
val status = SetPinStatus.fromStatusWord(apdu.statusWord)
?: throw TangemSdkError.DecodingFailed()
val decoder = TlvDecoder(tlvData)
return SetPinResponse(
cardId = decoder.decode(TlvTag.CardId),
status = status
cardId = decoder.decode(TlvTag.CardId),
status = status
)
}
}

View file

@ -75,8 +75,8 @@ class SignCommand(private val hashes: Array<ByteArray>) : Command<SignResponse>(
override fun serialize(environment: SessionEnvironment): CommandApdu {
val dataToSign = flattenHashes()
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.Pin2, environment.pin2?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.TransactionOutHashSize, byteArrayOf(hashSizes.toByte()))
tlvBuilder.append(TlvTag.TransactionOutHash, dataToSign)

View file

@ -83,7 +83,7 @@ class WriteIssuerDataCommand(
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.WriteData)
tlvBuilder.append(TlvTag.IssuerData, issuerData)

View file

@ -149,7 +149,7 @@ class WriteIssuerExtraDataCommand(
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, mode)

View file

@ -57,7 +57,7 @@ class WriteUserDataCommand(private val userData: ByteArray? = null, private val
override fun serialize(environment: SessionEnvironment): CommandApdu {
val builder = TlvBuilder()
builder.append(TlvTag.CardId, environment.card?.cardId)
builder.append(TlvTag.Pin, environment.pin1)
builder.append(TlvTag.Pin, environment.pin1?.value)
builder.append(TlvTag.UserData, userData)
builder.append(TlvTag.UserCounter, userCounter)
builder.append(TlvTag.UserProtectedData, userProtectedData)

View file

@ -137,7 +137,7 @@ class VerifyCardCommand(private val onlineVerification: Boolean) : Command<Verif
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Challenge, challenge)
return CommandApdu(Instruction.VerifyCard, tlvBuilder.serialize())

View file

@ -0,0 +1,46 @@
package com.tangem.common
import com.squareup.sqldelight.EnumColumnAdapter
import com.squareup.sqldelight.db.SqlDriver
import com.tangem.CardValues
import com.tangem.CardValuesEntityQueries
import com.tangem.Database
import com.tangem.SessionEnvironment
interface CardValuesService {
fun saveValues(environment: SessionEnvironment)
fun getValues(cardId: String) : CardValues?
}
class CardValuesDbService(driver: SqlDriver) : CardValuesService {
private val cardValuesQueries: CardValuesEntityQueries
init {
val database = Database(driver, cardValuesAdapter = CardValues.Adapter(
cardVerificationAdapter = EnumColumnAdapter(),
cardValidationAdapter = EnumColumnAdapter(),
codeVerificationAdapter = EnumColumnAdapter()
))
cardValuesQueries = database.cardValuesEntityQueries
}
override fun saveValues(environment: SessionEnvironment) {
environment.card?.cardId?.let {cardId ->
cardValuesQueries.insertOrReplace(
cardId,
environment.pin1?.isDefault ?: false,
environment.pin2?.isDefault ?: false,
environment.cardVerification, environment.cardVerification,
environment.codeVerification
)
}
}
override fun getValues(cardId: String): CardValues? =
cardValuesQueries.selectByCardId(cardId).executeAsOneOrNull()
}

View file

@ -0,0 +1,10 @@
package com.tangem.common
import com.tangem.common.extensions.calculateSha256
class PinCode(
val value: ByteArray,
val isDefault: Boolean
) {
constructor(value: String, isDefault: Boolean = false): this(value.calculateSha256(), isDefault)
}

View file

@ -31,24 +31,44 @@ internal class ScanTask : CardSessionRunnable<Card> {
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.curve == null || card.walletPublicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
} else {
val checkWalletCommand = CheckWalletCommand(card.curve, card.walletPublicKey)
checkWalletCommand.run(session) { result ->
if (session.environment.pin1 != null && session.environment.pin2 != null) {
val checkPinCommand = SetPinCommand(session.environment.pin1!!.value, session.environment.pin2!!.value)
checkPinCommand.run(session) { result ->
when (result) {
is CompletionResult.Success -> callback(CompletionResult.Success(card))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
is CompletionResult.Failure -> {
session.environment.pin2 = null
}
}
session.environment.restoreCardValues()
}
} else {
runCheckWalletIfNeeded(card, session, callback)
}
}
}
}
}
private fun runCheckWalletIfNeeded(
card: Card, session: CardSession,
callback: (result: CompletionResult<Card>) -> Unit
) {
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.curve == null || card.walletPublicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
} 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))
}
}
}

View file

@ -0,0 +1,34 @@
import com.tangem.VerificationState;
CREATE TABLE cardValues (
cardId TEXT NOT NULL UNIQUE PRIMARY KEY,
isPin1Default INTEGER AS Boolean DEFAULT 1,
isPin2Default INTEGER AS Boolean DEFAULT 1,
cardVerification TEXT AS VerificationState,
cardValidation TEXT AS VerificationState,
codeVerification TEXT AS VerificationState
);
insertOrReplace:
INSERT OR REPLACE INTO cardValues(
cardId,
isPin1Default,
isPin2Default,
cardVerification,
cardValidation,
codeVerification
)
VALUES (?, ?, ?, ?, ?, ?);
selectByCardId:
SELECT *
FROM cardValues
WHERE cardId = ?;
deleteAll:
DELETE FROM cardValues;
deleteByCardId:
DELETE
FROM cardValues
WHERE cardId = ?;

View file

@ -1,23 +1,21 @@
package com.tangem.common.apdu
import com.google.common.truth.Truth.assertThat
import com.tangem.Config
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
class CommandApduTest {
val sessionEnvironment = SessionEnvironment(null, Config(), null, null)
@Test
fun `simple READ command to bytes`() {
val sessionEnvironment = SessionEnvironment()
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, sessionEnvironment.pin1)
tlvBuilder.append(TlvTag.Pin, sessionEnvironment.pin1?.value)
val commandApdu = CommandApdu(
Instruction.Read,
tlvBuilder.serialize()
@ -33,13 +31,12 @@ class CommandApduTest {
@Test
fun `READ with terminal key to bytes`() {
val sessionEnvironment = SessionEnvironment()
val terminalPublicKey = byteArrayOf(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)
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, sessionEnvironment.pin1)
tlvBuilder.append(TlvTag.Pin, sessionEnvironment.pin1?.value)
tlvBuilder.append(TlvTag.TerminalPublicKey, terminalPublicKey)
val commandApdu = CommandApdu(
Instruction.Read,

View file

@ -5,7 +5,6 @@ import android.view.View
import android.widget.CompoundButton
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.tangem.SessionEnvironment
import com.tangem.TangemSdk
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
@ -103,7 +102,7 @@ class TestUserDataActivity : AppCompatActivity() {
chb_with_ud_protected.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateProtectedData(buttonView) }
chb_with_counter.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateCounter(buttonView) }
chb_with_protected_counter.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateProtectedCounter(buttonView) }
chb_with_pin2.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updatePin2(buttonView) }
// chb_with_pin2.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updatePin2(buttonView) }
}
private fun showReadWriteSection(show: Boolean) {
@ -140,8 +139,8 @@ class WriteOptions {
userProtectedCounter = if (chbx.isChecked) value else null
}
fun updatePin2(chbx: CompoundButton) {
val value = SessionEnvironment.DEFAULT_PIN2
pin2 = if (chbx.isChecked) value else null
}
// fun updatePin2(chbx: CompoundButton) {
// val value = SessionEnvironment.pin1.DEFAULT_PIN2
// pin2 = if (chbx.isChecked) value else null
// }
}

View file

@ -62,6 +62,7 @@ dependencies {
// misc
implementation 'com.skyfishjy.ripplebackground:library:1.0.1'
implementation 'at.favre.lib:armadillo:0.9.0'
implementation "com.squareup.sqldelight:android-driver:1.4.0"
// testing
testImplementation 'junit:junit:4.12'

View file

@ -19,20 +19,19 @@ class DefaultSessionViewDelegate(private val reader: NfcReader) : SessionViewDel
}
override fun onSessionStarted(cardId: String?, message: Message?) {
postUI { showReadingDialog(activity, cardId, message) }
postUI {
if (readingDialog == null) createReadingDialog(activity)
readingDialog?.show(SessionViewDelegateState.Ready(cardId, message))
}
}
private fun showReadingDialog(activity: FragmentActivity, cardId: String?, message: Message?) {
private fun createReadingDialog(activity: FragmentActivity) {
val dialogView = activity.layoutInflater.inflate(R.layout.nfc_bottom_sheet, null)
readingDialog = NfcSessionDialog(activity)
readingDialog?.setContentView(dialogView)
readingDialog?.dismissWithAnimation = true
readingDialog?.create()
readingDialog?.setOnShowListener {
readingDialog?.show(SessionViewDelegateState.Ready(cardId, message))
}
readingDialog?.setOnCancelListener { reader.stopSession(true) }
readingDialog?.show()
}
override fun onSecurityDelay(ms: Int, totalDurationSeconds: Int) {
@ -68,7 +67,7 @@ class DefaultSessionViewDelegate(private val reader: NfcReader) : SessionViewDel
postUI { readingDialog?.show(SessionViewDelegateState.Error(error)) }
}
override fun onPinRequested(callback: (pin: String?) -> Unit) {
override fun onPinRequested(callback: (pin: String) -> Unit) {
postUI { readingDialog?.show(SessionViewDelegateState.PinRequested(callback)) }
}

View file

@ -9,7 +9,7 @@ sealed class 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: (pin: String?) -> Unit) : SessionViewDelegateState()
data class PinRequested(val callback: (pin: String) -> Unit) : SessionViewDelegateState()
object TagLost : SessionViewDelegateState()
object TagConnected : SessionViewDelegateState()
object WrongCard : SessionViewDelegateState()

View file

@ -1,7 +1,9 @@
package com.tangem.tangem_sdk_new.extensions
import androidx.fragment.app.FragmentActivity
import com.squareup.sqldelight.android.AndroidSqliteDriver
import com.tangem.Config
import com.tangem.Database
import com.tangem.SessionViewDelegate
import com.tangem.TangemSdk
import com.tangem.tangem_sdk_new.DefaultSessionViewDelegate
@ -15,7 +17,8 @@ fun TangemSdk.Companion.init(activity: FragmentActivity, config: Config = Config
val viewDelegate = DefaultSessionViewDelegate(nfcManager.reader)
viewDelegate.activity = activity
val tangemSdk = TangemSdk(nfcManager.reader, viewDelegate, config)
val databaseDriver = AndroidSqliteDriver(Database.Schema, activity.applicationContext, "cards.db")
val tangemSdk = TangemSdk(nfcManager.reader, viewDelegate, config, databaseDriver)
tangemSdk.setTerminalKeysService(TerminalKeysStorage(activity.application))
return tangemSdk
@ -32,7 +35,8 @@ fun TangemSdk.Companion.customInit(
nfcManager.reader,
viewDelegate ?: DefaultSessionViewDelegate(nfcManager.reader)
.apply { this.activity = activity },
config
config,
AndroidSqliteDriver(Database.Schema, activity.applicationContext, "cards.db")
)
tangemSdk.setTerminalKeysService(TerminalKeysStorage(activity.application))

View file

@ -10,7 +10,6 @@ import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.fragment.app.FragmentActivity
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.Log
import com.tangem.tangem_sdk_new.R
import com.tangem.tangem_sdk_new.SessionViewDelegateState
import com.tangem.tangem_sdk_new.extensions.localizedDescription
@ -24,6 +23,9 @@ class NfcSessionDialog(val activity: FragmentActivity) : BottomSheetDialog(activ
private var currentState: SessionViewDelegateState? = null
fun show(state: SessionViewDelegateState) {
if (!this.isShowing) {
this.show()
}
when (state) {
is SessionViewDelegateState.Ready -> onReady(state)
is SessionViewDelegateState.Success -> onSuccess(state)
@ -135,14 +137,21 @@ class NfcSessionDialog(val activity: FragmentActivity) : BottomSheetDialog(activ
etPin?.setOnEditorActionListener { v: TextView?, actionId: Int, event: KeyEvent? ->
postUI {
if (actionId == KeyEvent.KEYCODE_ENDCALL) {
show(lTouchCard)
tvTaskTitle?.show()
tvTaskText?.show()
val imm: InputMethodManager =
context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(v?.windowToken, 0)
if (v?.text.isNullOrBlank()) {
etPin?.error = ""
} else {
show(lTouchCard)
tvTaskTitle?.show()
tvTaskText?.show()
val imm: InputMethodManager =
context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(v?.windowToken, 0)
v?.text?.toString()?.let { state.callback(it) }
v?.text.toString().let { pin ->
v?.text = ""
state.callback(pin)
}
}
}
}
true