Updated on 2026-08-14

This commit is contained in:
Tangem 2020-07-08 13:13:13 +03:00
parent 336ec4a1ed
commit 53599d8a3e
14 changed files with 201 additions and 191 deletions

View file

@ -1,7 +1,6 @@
package com.tangem.blockchain.common
import com.google.common.truth.Truth
import com.tangem.Config
import com.tangem.SessionEnvironment
import com.tangem.blockchain.blockchains.binance.BinanceWalletManager
import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager
@ -20,7 +19,7 @@ import org.junit.Test
internal class WalletManagerFactoryTest {
private val sessionEnvironment = SessionEnvironment(null, Config(), null, null)
private val sessionEnvironment = SessionEnvironment()
@Test
fun createBitcoinWalletManager() {

View file

@ -1,5 +1,6 @@
package com.tangem
import com.tangem.commands.Command
import com.tangem.commands.CommandResponse
import com.tangem.commands.OpenSessionCommand
import com.tangem.commands.ReadCommand
@ -20,8 +21,6 @@ import kotlinx.coroutines.flow.*
*/
interface CardSessionRunnable<T : CommandResponse> {
val performPreflightRead: Boolean
val requiresPin2: Boolean
/**
@ -57,7 +56,7 @@ enum class TagType {
* If null, a default header and text body will be used.
*/
class CardSession(
val environment: SessionEnvironment,
private val environmentService: SessionEnvironmentService,
private val reader: CardReader,
val viewDelegate: SessionViewDelegate,
private var cardId: String? = null,
@ -77,6 +76,11 @@ class CardSession(
private val tag = this.javaClass.simpleName
private var performPreflightRead = true
private var pin2Required = false
val environment = environmentService.createEnvironment(cardId)
/**
* This method starts a card session, performs preflight [ReadCommand],
* invokes [CardSessionRunnable.run] and closes the session.
@ -87,25 +91,10 @@ class CardSession(
runnable: T, callback: (result: CompletionResult<R>) -> Unit
) {
if (environment.pin1 == null) {
viewDelegate.onSessionStarted(cardId)
viewDelegate.onPinRequested(PinType.Pin1) {
environment.pin1 = PinCode(it)
startWithRunnable(runnable, callback)
}
return
}
if ((runnable as? Command<*>)?.performPreflightRead == false) performPreflightRead = false
pin2Required = runnable.requiresPin2
if (runnable.requiresPin2 && environment.pin2 == null) {
viewDelegate.onSessionStarted(cardId)
viewDelegate.onPinRequested(PinType.Pin2) {
environment.pin2 = PinCode(it)
startWithRunnable(runnable, callback)
}
return
}
start(runnable.performPreflightRead) { session, error ->
start() { session, error ->
if (error != null) {
callback(CompletionResult.Failure(error))
return@start
@ -135,7 +124,6 @@ class CardSession(
* @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
) {
@ -143,6 +131,26 @@ class CardSession(
callback(this, TangemSdkError.Busy())
return
}
if (environment.pin1 == null) {
viewDelegate.onSessionStarted(cardId)
viewDelegate.onPinRequested(PinType.Pin1) {
environment.pin1 = PinCode(it)
start(callback)
}
return
}
if (pin2Required && environment.pin2 == null) {
viewDelegate.onSessionStarted(cardId)
viewDelegate.onPinRequested(PinType.Pin2) {
environment.pin2 = PinCode(it)
start(callback)
}
return
}
state = CardSessionState.Active
viewDelegate.onSessionStarted(cardId)
@ -196,6 +204,7 @@ class CardSession(
return@run
}
environment.card = result.data
environmentService.updateEnvironment(environment, result.data.cardId)
cardId = receivedCardId
callback(this, null)
}
@ -231,7 +240,7 @@ class CardSession(
}
private fun stopSession() {
environment.saveCardValues()
environmentService.saveEnvironmentValues(environment, cardId)
reader.stopSession()
state = CardSessionState.Inactive
scope.cancel()

View file

@ -38,12 +38,16 @@ class Config(
var handleErrors: Boolean = true,
var defaultPin1: String = "000000",
var defaultPin1: String = DEFAULT_PIN_1,
var defaultPin2: String = "000",
var defaultPin2: String = DEFAULT_PIN_2,
var savePin1InStaticField: Boolean = true,
var savePin2InStaticField: Boolean = false
)
) {
companion object {
const val DEFAULT_PIN_1 = "000000"
const val DEFAULT_PIN_2 = "000"
}
}

View file

@ -2,9 +2,7 @@ package com.tangem
import com.tangem.commands.Card
import com.tangem.commands.EllipticCurve
import com.tangem.common.CardValuesStorage
import com.tangem.common.PinCode
import com.tangem.common.TerminalKeysService
import com.tangem.crypto.CryptoUtils.generatePublicKey
@ -27,79 +25,23 @@ import com.tangem.crypto.CryptoUtils.generatePublicKey
*
*/
class SessionEnvironment(
cardId: String?,
private val config: Config,
terminalKeysService: TerminalKeysService?,
private val cardValuesStorage: CardValuesStorage?
) {
var pin1: PinCode? = PinCode(Config.DEFAULT_PIN_1, true),
var pin2: PinCode? = PinCode(Config.DEFAULT_PIN_2, true),
var cvc: ByteArray? = null,
var pin1: PinCode?
var pin2: PinCode?
var cvc: ByteArray? = null
var terminalKeys: KeyPair? = null,
var cardFilter: CardFilter = CardFilter(),
val handleErrors: Boolean = true,
var terminalKeys: KeyPair? = null
var cardFilter: CardFilter
val handleErrors: Boolean
var encryptionMode: EncryptionMode = EncryptionMode.NONE,
var encryptionKey: ByteArray? = null,
var encryptionMode: EncryptionMode
var encryptionKey: ByteArray? = null
var cardVerification: VerificationState = VerificationState.NotVerified,
var cardValidation: VerificationState = VerificationState.NotVerified,
var codeVerification: VerificationState = VerificationState.NotVerified,
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 { cardValuesStorage?.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 restoreCardValues() {
val cardValues = this.card?.cardId?.let { cardValuesStorage?.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
}
fun saveCardValues() {
if (config.savePin1InStaticField) {
TangemSdk.pin1 = pin1
}
if (config.savePin2InStaticField) {
card?.cardId?.let { cardId -> TangemSdk.pin2[cardId] = pin2 }
}
cardValuesStorage?.saveValues(this)
}
}
var card: Card? = null
)
/**
* All possible encryption modes.

View file

@ -0,0 +1,77 @@
package com.tangem
import com.tangem.common.CardValuesStorage
import com.tangem.common.PinCode
import com.tangem.common.TerminalKeysService
class SessionEnvironmentService(
private val config: Config,
private val terminalKeysService: TerminalKeysService?,
private val cardValuesStorage: CardValuesStorage?
) {
fun createEnvironment(cardId: String?): SessionEnvironment {
val terminalKeys = if (config.linkedTerminal) terminalKeysService?.getKeys() else null
val cardValues = cardId?.let { cardValuesStorage?.getValues(cardId) }
val cardVerification = cardValues?.cardVerification ?: VerificationState.NotVerified
val cardValidation = cardValues?.cardValidation ?: VerificationState.NotVerified
val codeVerification = cardValues?.codeVerification ?: VerificationState.NotVerified
val pin1 = TangemSdk.pin1
?: if (cardValues?.isPin1Default != false) {
PinCode(config.defaultPin1, true)
} else {
null
}
val pin2 = cardId?.let { TangemSdk.pin2[it] }
?: if (cardValues?.isPin2Default != false) {
PinCode(config.defaultPin2, true)
} else {
null
}
return SessionEnvironment(
terminalKeys = terminalKeys,
cardFilter = config.cardFilter,
handleErrors = config.handleErrors,
encryptionMode = config.encryptionMode,
cardVerification = cardVerification,
cardValidation = cardValidation,
codeVerification = codeVerification,
pin1 = pin1,
pin2 = pin2
)
}
fun updateEnvironment(environment: SessionEnvironment, cardId: String) {
val cardValues = cardId.let { cardValuesStorage?.getValues(it) }
environment.cardVerification = cardValues?.cardVerification ?: VerificationState.NotVerified
environment.cardValidation = cardValues?.cardValidation ?: VerificationState.NotVerified
environment.codeVerification = cardValues?.codeVerification ?: VerificationState.NotVerified
if (cardValues?.isPin1Default == false && environment.pin1?.isDefault == true) environment.pin1 = null
if (cardValues?.isPin2Default == false && environment.pin1?.isDefault == true) environment.pin2 = null
}
fun saveEnvironmentValues(environment: SessionEnvironment, cardId: String?) {
if (config.savePin1InStaticField) {
TangemSdk.pin1 = environment.pin1
}
if (config.savePin2InStaticField) {
cardId?.let { cardId -> TangemSdk.pin2[cardId] = environment.pin2 }
}
cardId?.let { cardId ->
cardValuesStorage?.saveValues(cardId,
environment.pin1?.isDefault ?: false,
environment.pin2?.isDefault ?: false,
environment.cardVerification, environment.cardVerification,
environment.codeVerification)
}
}
}

View file

@ -1,6 +1,5 @@
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
@ -11,7 +10,7 @@ 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.CardValuesDbStorage
import com.tangem.common.CardValuesStorage
import com.tangem.common.CompletionResult
import com.tangem.common.PinCode
import com.tangem.common.TerminalKeysService
@ -31,15 +30,19 @@ import com.tangem.tasks.ScanTask
* Its default implementation, DefaultCardSessionViewDelegate, is in our tangem-sdk module.
* @property config allows to change a number of parameters for communication with Tangem cards.
* Do not change the default values unless you know what you are doing.
* @property terminalKeysService allows to retrieve saved terminal keys.
*/
class TangemSdk(
private val reader: CardReader,
private val viewDelegate: SessionViewDelegate,
var config: Config = Config(),
private val sqlDriver: SqlDriver
cardValuesStorage: CardValuesStorage,
terminalKeysService: TerminalKeysService? = null
) {
private var terminalKeysService: TerminalKeysService? = null
private val environmentService = SessionEnvironmentService(
config, terminalKeysService, cardValuesStorage
)
init {
CryptoUtils.initCrypto()
@ -369,8 +372,8 @@ class TangemSdk(
fun changePin1(cardId: String? = null,
pin: ByteArray? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<SetPinResponse>) -> Unit) {
initialMessage: Message? = null,
callback: (result: CompletionResult<SetPinResponse>) -> Unit) {
val command = ChangePinTask(PinType.Pin1, pin)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
@ -408,7 +411,7 @@ class TangemSdk(
fun <T : CommandResponse> startSessionWithRunnable(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<T>) -> Unit) {
val cardSession = CardSession(buildEnvironment(cardId), reader, viewDelegate, cardId, initialMessage)
val cardSession = CardSession(environmentService, reader, viewDelegate, cardId, initialMessage)
Thread().run { cardSession.startWithRunnable(runnable, callback) }
}
@ -426,24 +429,10 @@ class TangemSdk(
*/
fun startSession(cardId: String? = null, initialMessage: Message? = null,
callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
val cardSession = CardSession(buildEnvironment(cardId), reader, viewDelegate, cardId, initialMessage)
val cardSession = CardSession(environmentService, reader, viewDelegate, cardId, initialMessage)
Thread().run { cardSession.start(callback = callback) }
}
/**
* Allows to set a particular [TerminalKeysService] to retrieve terminal keys.
* Default implementation is provided in tangem-sdk module: [TerminalKeysStorage].
*/
fun setTerminalKeysService(terminalKeysService: TerminalKeysService) {
this.terminalKeysService = terminalKeysService
}
private fun buildEnvironment(cardId: String?): SessionEnvironment {
return SessionEnvironment(
cardId, config, terminalKeysService, CardValuesDbStorage(sqlDriver)
)
}
companion object {
var pin1: PinCode? = null
val pin2: MutableMap<String, PinCode?> = mutableMapOf()

View file

@ -39,7 +39,7 @@ interface CommandResponse
*/
abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRunnable<T> {
override val performPreflightRead: Boolean = true
open val performPreflightRead: Boolean = true
override val requiresPin2: Boolean = false
override fun run(session: CardSession, callback: (result: CompletionResult<T>) -> Unit) {
@ -74,10 +74,12 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
if (session.environment.handleErrors) {
val error = mapError(session.environment.card, result.error)
if (error is TangemSdkError.Pin1Required) {
session.environment.pin1 = null
handlePin1(session, callback)
return@transceiveApdu
}
if (error is TangemSdkError.Pin2OrCvcRequired) {
session.environment.pin2 = null
handlePin2(session, callback)
return@transceiveApdu
}

View file

@ -19,18 +19,6 @@ class DepersonalizeCommand : Command<DepersonalizeResponse>() {
override val performPreflightRead = false
// override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<DepersonalizeResponse>) -> Unit): Boolean {
// if (session.environment.card?.status == CardStatus.NotPersonalized) {
// callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
// return true
// }
// if (session.environment.card?.firmwareVersion?.contains("SDK") == false) {
// callback(CompletionResult.Failure(TangemSdkError.CannotBeDepersonalized()))
// return true
// }
// return false
// }
override fun serialize(environment: SessionEnvironment): CommandApdu {
return CommandApdu(
Instruction.Depersonalize, byteArrayOf()

View file

@ -2,16 +2,21 @@ package com.tangem.common
import com.squareup.sqldelight.EnumColumnAdapter
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
import com.tangem.CardValues
import com.tangem.CardValuesEntityQueries
import com.tangem.Database
import com.tangem.SessionEnvironment
import com.tangem.VerificationState
interface CardValuesStorage {
fun saveValues(environment: SessionEnvironment)
fun saveValues(cardId: String,
isPin1Default: Boolean, isPin2Default: Boolean,
cardVerification: VerificationState?,
cardValidation: VerificationState?,
codeVerification: VerificationState?)
fun getValues(cardId: String) : CardValues?
fun getValues(cardId: String): CardValues?
}
@ -28,19 +33,24 @@ class CardValuesDbStorage(driver: SqlDriver) : CardValuesStorage {
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 saveValues(cardId: String,
isPin1Default: Boolean, isPin2Default: Boolean,
cardVerification: VerificationState?,
cardValidation: VerificationState?,
codeVerification: VerificationState?) {
cardValuesQueries.insertOrReplace(
cardId,
isPin1Default, isPin2Default,
cardVerification, cardValidation, codeVerification
)
}
override fun getValues(cardId: String): CardValues? =
override fun getValues(cardId: String): CardValues? =
cardValuesQueries.selectByCardId(cardId).executeAsOneOrNull()
companion object {
fun initJvm() = CardValuesDbStorage(
JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY).also { Database.Schema.create(it) }
)
}
}

View file

@ -24,7 +24,6 @@ class ChangePinTask(
private val pinType: PinType,
private val pin: ByteArray? = null
) : CardSessionRunnable<SetPinResponse> {
override val performPreflightRead = true
override val requiresPin2 = false
override fun run(session: CardSession, callback: (result: CompletionResult<SetPinResponse>) -> Unit) {
@ -67,7 +66,10 @@ class ChangePinTask(
val command = SetPinCommand(pin1, pin2, pin3)
command.run(session) { result ->
when (result) {
is CompletionResult.Success -> savePin(pin, session.environment)
is CompletionResult.Success -> {
savePin(pin, session.environment)
callback(result)
}
is CompletionResult.Failure -> callback(result)
}
}

View file

@ -11,7 +11,6 @@ import com.tangem.common.CompletionResult
class CreateWalletTask : CardSessionRunnable<CreateWalletResponse> {
override val performPreflightRead = true
override val requiresPin2 = false
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {

View file

@ -15,41 +15,32 @@ import com.tangem.common.CompletionResult
*/
internal class ScanTask : CardSessionRunnable<Card> {
override val performPreflightRead = false
override val requiresPin2 = false
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
if (session.connectedTag == TagType.Slix) {
readSlixTag(session, callback)
return
}
ReadCommand().run(session) { readResult ->
when(readResult) {
is CompletionResult.Failure -> callback(readResult)
is CompletionResult.Success -> {
val card = readResult.data
session.environment.card = card
session.environment.restoreCardValues()
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.Failure -> {
session.environment.pin2 = null
}
}
runCheckWalletIfNeeded(card, session, callback)
}
} else {
runCheckWalletIfNeeded(card, session, callback)
}
}
}
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
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.Failure -> {
session.environment.pin2 = null
}
}
runCheckWalletIfNeeded(card, session, callback)
}
} else {
runCheckWalletIfNeeded(card, session, callback)
}
}
private fun runCheckWalletIfNeeded(

View file

@ -1,7 +1,6 @@
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
@ -10,7 +9,7 @@ import org.junit.Test
class CommandApduTest {
val sessionEnvironment = SessionEnvironment(null, Config(), null, null)
private val sessionEnvironment = SessionEnvironment()
@Test
fun `simple READ command to bytes`() {
@ -26,7 +25,7 @@ class CommandApduTest {
assertThat(commandApdu.apduData)
.isEqualTo(expected)
assertThat(listOf(1, 2)).containsExactlyElementsIn(listOf(1,2))
assertThat(listOf(1, 2)).containsExactlyElementsIn(listOf(1, 2))
}
@Test

View file

@ -6,6 +6,7 @@ import com.tangem.Config
import com.tangem.Database
import com.tangem.SessionViewDelegate
import com.tangem.TangemSdk
import com.tangem.common.CardValuesDbStorage
import com.tangem.tangem_sdk_new.DefaultSessionViewDelegate
import com.tangem.tangem_sdk_new.NfcLifecycleObserver
import com.tangem.tangem_sdk_new.TerminalKeysStorage
@ -18,10 +19,10 @@ fun TangemSdk.Companion.init(activity: FragmentActivity, config: Config = Config
viewDelegate.activity = activity
val databaseDriver = AndroidSqliteDriver(Database.Schema, activity.applicationContext, "cards.db")
val tangemSdk = TangemSdk(nfcManager.reader, viewDelegate, config, databaseDriver)
tangemSdk.setTerminalKeysService(TerminalKeysStorage(activity.application))
return tangemSdk
return TangemSdk(
nfcManager.reader, viewDelegate, config,
CardValuesDbStorage(databaseDriver), TerminalKeysStorage(activity.application)
)
}
fun TangemSdk.Companion.customInit(
@ -30,17 +31,15 @@ fun TangemSdk.Companion.customInit(
config: Config = Config()
): TangemSdk {
val nfcManager = TangemSdk.initNfcManager(activity)
val databaseDriver = AndroidSqliteDriver(Database.Schema, activity.applicationContext, "cards.db")
val tangemSdk = TangemSdk(
return TangemSdk(
nfcManager.reader,
viewDelegate ?: DefaultSessionViewDelegate(nfcManager.reader)
.apply { this.activity = activity },
config,
AndroidSqliteDriver(Database.Schema, activity.applicationContext, "cards.db")
CardValuesDbStorage(databaseDriver), TerminalKeysStorage(activity.application)
)
tangemSdk.setTerminalKeysService(TerminalKeysStorage(activity.application))
return tangemSdk
}
fun TangemSdk.Companion.initNfcManager(activity: FragmentActivity): NfcManager {