Updated on 2026-08-14

This commit is contained in:
Tangem 2020-07-13 10:42:47 +00:00
commit b7ca492168
319 changed files with 6 additions and 17138 deletions

View file

@ -45,10 +45,11 @@ android {
}
dependencies {
implementation project(':tangem-core')
implementation project(':tangem-sdk')
implementation project(':blockchain')
implementation 'com.tangem:core:0.10.4'
implementation 'com.tangem:sdk:0.10.4'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0'

View file

@ -40,8 +40,7 @@ android {
dependencies {
// implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':tangem-core')
implementation project(':tangem-sdk')
implementation 'com.tangem:core:1.13'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation 'androidx.appcompat:appcompat:1.1.0'

View file

@ -22,6 +22,7 @@ allprojects {
google()
jcenter()
maven { url 'https://jitpack.io' }
maven { url "https://api.bitbucket.org/2.0/repositories/tangem/maven_repository/src/releases" }
}
}

View file

@ -1 +1 @@
include ':app', ':tangem-sdk-old', ':server-android', ':tangem-card-old', ':tangem-core', ':tangem-sdk', ':tangem-devkit', ':blockchain', ':blockchain-demo'
include ':app', ':tangem-sdk-old', ':server-android', ':tangem-card-old', ':blockchain', ':blockchain-demo'

View file

@ -1 +0,0 @@
/build

View file

@ -1,78 +0,0 @@
apply plugin: "kotlin"
apply plugin: 'org.jetbrains.dokka'
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"
dependencies {
// kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$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"
// crypto
implementation "com.madgag.spongycastle:core:1.58.0.0"
implementation "com.madgag.spongycastle:prov:1.58.0.0"
implementation 'net.i2p.crypto:eddsa:0.3.0'
// 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'
implementation 'com.squareup.retrofit2:converter-moshi:2.6.0'
implementation 'com.squareup.moshi:moshi:1.9.2'
implementation "com.squareup.moshi:moshi-kotlin:1.9.2"
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.9.2")
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
// tests
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.2'
testImplementation "com.google.truth:truth:1.0.1"
}
sqldelight {
Database {
packageName = "com.tangem"
}
}
sourceCompatibility = "8"
targetCompatibility = "8"
buildscript {
ext.dokka_version = '0.10.0'
repositories {
mavenCentral()
jcenter()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
}
}
repositories {
mavenCentral()
jcenter()
}
compileKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}
task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
outputFormat = 'markdown'
}

View file

@ -1,14 +0,0 @@
package com.tangem
import com.tangem.common.extensions.CardType
import java.util.*
/**
* Filter that can be used to limit cards that can be interacted with in TangemSdk.
*
* @property allowedCardTypes Type of cards that are allowed to be interacted with in TangemSdk.
*/
data class CardFilter(
var allowedCardTypes: EnumSet<CardType> = EnumSet.allOf(CardType::class.java)
)

View file

@ -1,53 +0,0 @@
package com.tangem
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BroadcastChannel
/**
* Allows interaction between the phone or any other terminal and Tangem card.
*
* Its default implementation, NfcCardReader, is in our tangem-sdk module.
*/
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.
*
* @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]
*/
fun transceiveApdu(apdu: CommandApdu, callback: (response: CompletionResult<ResponseApdu>) -> Unit)
/**
* Signals to [CardReader] to become ready to transceive data.
*/
fun startSession()
/**
* Signals to [CardReader] that no further NFC transition is expected.
*/
fun stopSession(cancelled: Boolean = false)
fun readSlixTag(callback: (result: CompletionResult<ResponseApdu>) -> Unit)
}
interface ReadingActiveListener {
var readingIsActive: Boolean
}

View file

@ -1,318 +0,0 @@
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
import com.tangem.common.extensions.getType
import com.tangem.crypto.EncryptionHelper
import com.tangem.crypto.pbkdf2Hash
import com.tangem.tasks.PinType
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
/**
* Basic interface for running tasks and [com.tangem.commands.Command] in a [CardSession]
*/
interface CardSessionRunnable<T : CommandResponse> {
val requiresPin2: Boolean
/**
* The starting point for custom business logic.
* Implement this interface and use [TangemSdk.startSessionWithRunnable] to run.
* @param session run commands in this [CardSession].
* @param callback trigger the callback to complete the task.
*/
fun run(session: CardSession, callback: (result: CompletionResult<T>) -> Unit)
}
enum class CardSessionState {
Inactive,
Active
}
enum class TagType {
Nfc,
Slix
}
/**
* Allows interaction with Tangem cards. Should be opened before sending commands.
*
* @property environment
* @property reader is an interface that is responsible for NFC connection and
* transfer of data to and from the Tangem Card.
* @property viewDelegate is an interface that allows interaction with users and shows relevant UI.
* @property cardId ID, 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
* the [TangemSdkError.WrongCardNumber] otherwise.
* @property initialMessage A custom description that will be shown at the beginning of the NFC session.
* If null, a default header and text body will be used.
*/
class CardSession(
private val environmentService: SessionEnvironmentService,
private val reader: CardReader,
val viewDelegate: SessionViewDelegate,
private var cardId: String? = null,
private val initialMessage: Message? = null
) {
var connectedTag: TagType? = null
/**
* True if some operation is still in progress.
*/
private var state = CardSessionState.Inactive
val scope = CoroutineScope(Dispatchers.IO) + CoroutineExceptionHandler { _, ex ->
throw ex
}
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.
* @param runnable [CardSessionRunnable] that will be performed in the session.
* @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
) {
if ((runnable as? Command<*>)?.performPreflightRead == false) performPreflightRead = false
pin2Required = runnable.requiresPin2
start() { session, error ->
if (error != null) {
callback(CompletionResult.Failure(error))
return@start
}
runnable.run(this) { result ->
when (result) {
is CompletionResult.Success -> stop()
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.ExtendedLengthNotSupported) {
if (session.environment.terminalKeys != null) {
session.environment.terminalKeys = null
startWithRunnable(runnable, callback)
return@run
}
}
stopWithError(result.error)
}
}
callback(result)
}
}
}
/**
* Starts a card session and performs preflight [ReadCommand].
* @param callback: callback with the card session. Can contain [TangemSdkError] if something goes wrong.
*/
fun start(
callback: (session: CardSession, error: TangemSdkError?) -> Unit
) {
if (state != CardSessionState.Inactive) {
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)
scope.launch {
reader.tag
.asFlow()
.collect { tagType ->
if (tagType == null && connectedTag != null) {
handleTagLost()
} else if (tagType != null) {
connectedTag = tagType
viewDelegate.onTagConnected()
if (tagType == TagType.Nfc && performPreflightRead) {
preflightCheck(callback)
} else {
callback(this@CardSession, null)
}
}
}
}
reader.scope = scope
reader.startSession()
}
private fun handleTagLost() {
connectedTag = null
environment.encryptionKey = null
viewDelegate.onTagLost()
}
private fun preflightCheck(callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
val readCommand = ReadCommand()
readCommand.run(this) { result ->
when (result) {
is CompletionResult.Failure -> {
stopWithError(result.error)
callback(this, result.error)
}
is CompletionResult.Success -> {
val receivedCardId = result.data.cardId
if (cardId != null && receivedCardId != cardId) {
viewDelegate.onWrongCard()
preflightCheck(callback)
return@run
}
val allowedCardTypes = environment.cardFilter.allowedCardTypes
if (!allowedCardTypes.contains(result.data.getType())) {
stopWithError(TangemSdkError.WrongCardType())
callback(this, TangemSdkError.WrongCardType())
return@run
}
environment.card = result.data
environmentService.updateEnvironment(environment, result.data.cardId)
cardId = receivedCardId
callback(this, null)
}
}
}
}
fun readSlixTag(callback: (result: CompletionResult<ResponseApdu>) -> Unit) {
reader.readSlixTag(callback)
}
/**
* Stops the current session with the text message.
* @param message If null, the default message will be shown.
*/
private fun stop(message: Message? = null) {
stopSession()
viewDelegate.onSessionStopped(message)
}
/**
* Stops the current session on error.
* @param error An error that will be shown.
*/
private fun stopWithError(error: TangemSdkError) {
stopSession()
if (error !is TangemSdkError.UserCancelled) {
Log.e(tag, "Finishing with error: ${error::class.simpleName}: ${error.code}")
viewDelegate.onError(error)
} else {
Log.i(tag, "User cancelled NFC session")
}
}
private fun stopSession() {
environmentService.saveEnvironmentValues(environment, cardId)
reader.stopSession()
state = CardSessionState.Inactive
scope.cancel()
}
fun send(apdu: CommandApdu, callback: (result: CompletionResult<ResponseApdu>) -> Unit) {
val subscription = reader.tag.openSubscription()
scope.launch {
subscription.consumeAsFlow()
.filterNotNull()
.map { establishEncryptionIfNeeded() }
.map { apdu.encrypt(environment.encryptionMode, environment.encryptionKey) }
.map { encryptedApdu -> reader.transceiveApdu(encryptedApdu) }
.map { responseApdu -> decrypt(responseApdu) }
.catch { if (it is TangemSdkError) callback(CompletionResult.Failure(it)) }
.collect { result ->
subscription.cancel()
callback(result)
}
}
}
fun pause() {
reader.stopSession()
}
fun resume() {
reader.startSession()
}
private suspend fun establishEncryptionIfNeeded(): CompletionResult<Boolean> {
if (environment.encryptionMode == EncryptionMode.NONE || environment.encryptionKey != null) {
return CompletionResult.Success(true)
}
val encryptionHelper = EncryptionHelper.create(environment.encryptionMode)
?: return CompletionResult.Success(true)
val openSesssionCommand = OpenSessionCommand(encryptionHelper.keyA)
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!!.value.pbkdf2Hash(uid, 50)
val secret = encryptionHelper.generateSecret(result.sessionKeyB)
val sessionKey = (secret + protocolKey).calculateSha256()
environment.encryptionKey = sessionKey
return CompletionResult.Success(true)
}
is CompletionResult.Failure -> return CompletionResult.Failure(response.error)
}
}
private fun decrypt(result: CompletionResult<ResponseApdu>): CompletionResult<ResponseApdu> {
return when (result) {
is CompletionResult.Success -> {
try {
CompletionResult.Success(
result.data.decrypt(environment.encryptionKey)
)
} catch (error: TangemSdkError) {
return CompletionResult.Failure(error)
}
}
is CompletionResult.Failure -> result
}
}
}

View file

@ -1,53 +0,0 @@
package com.tangem
class Config(
/**
* Enables or disables Linked Terminal feature.
App can optionally generate ECDSA key pair Terminal_PrivateKey / Terminal_PublicKey.
And then submit Terminal_PublicKey to the card in any SIGN command.
Once SIGN is successfully executed by COS (Card Operation System),
including PIN2 verification and/or completion of security delay, the submitted
Terminal_PublicKey key is stored by COS. After that, the App instance is deemed trusted
by COS and COS will allow skipping security delay for subsequent SIGN operations
thus improving convenience without sacrificing security.
In order to skip security delay, App should use Terminal_PrivateKey to compute the signature
of the data being submitted to SIGN command for signing and transmit this signature in
Terminal_Transaction_Signature parameter in the same SIGN command. COS will verify
the correctness of Terminal_Transaction_Signature using previously stored Terminal_PublicKey
and, if correct, will skip security delay for the current SIGN operation.
*/
var linkedTerminal: Boolean = true,
/**
* If not null, it will be used to validate Issuer data and issuer extra data.
* If null, issuerPublicKey from current card will be used.
*/
var issuerPublicKey: ByteArray? = null,
/**
* Level of encryption used in communication with a Tangem Card.
*/
var encryptionMode: EncryptionMode = EncryptionMode.NONE,
/**
* Filter that can be used to limit cards that can be interacted with in TangemSdk.
*/
val cardFilter: CardFilter = CardFilter(),
var handleErrors: Boolean = true,
var defaultPin1: String = DEFAULT_PIN_1,
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

@ -1,34 +0,0 @@
package com.tangem
object Log {
private var loggerInstance: LoggerInterface? = null
fun i(logTag: String, message: String) {
loggerInstance?.i(logTag, message)
}
fun e(logTag: String, message: String) {
loggerInstance?.e(logTag, message)
}
fun v(logTag: String, message: String) {
loggerInstance?.v(logTag, message)
}
fun setLogger(logger: LoggerInterface) {
loggerInstance = logger
}
}
/**
* Interface for logging events within the SDK.
*
* It allows to use Android logger or to choose another.
*/
interface LoggerInterface {
fun i(logTag: String, message: String)
fun e(logTag: String, message: String)
fun v(logTag: String, message: String)
}

View file

@ -1,63 +0,0 @@
package com.tangem
import com.tangem.commands.Card
import com.tangem.commands.EllipticCurve
import com.tangem.common.PinCode
import com.tangem.crypto.CryptoUtils.generatePublicKey
/**
* Contains data relating to a Tangem card. It is used in constructing all the commands,
* and commands can return modified [SessionEnvironment].
*
* @param cardId Card ID, if it is known before tapping the card.
* @property config sets a number of parameters for communication with Tangem cards.
* @param terminalKeysService is used to retrieve terminal keys used in Linked Terminal feature.
* @param cardValuesStorage is used to save and retrieve some values relating to a particular card.
*
* @property pin1 An access Code, required to get access to a card. A default value is set in [Config]
* @property pin2 A code, required to perform particular operations with a card. A default value is set in [Config]
* @property terminalKeys generated terminal keys used in Linked Terminal feature
* @property cardFilter a property that defines types of card that this SDK will be able to interact with
* @property handleErrors if true, the SDK parses internal card errors into concrete [TangemSdkError]
* @property encryptionMode preferred [EncryptionMode] for interaction with cards
* @property encryptionKey is used for encrypted communication with a card
*
*/
class SessionEnvironment(
var pin1: PinCode? = PinCode(Config.DEFAULT_PIN_1, true),
var pin2: PinCode? = PinCode(Config.DEFAULT_PIN_2, true),
var cvc: ByteArray? = null,
var terminalKeys: KeyPair? = null,
var cardFilter: CardFilter = CardFilter(),
val handleErrors: Boolean = true,
var encryptionMode: EncryptionMode = EncryptionMode.NONE,
var encryptionKey: ByteArray? = null,
var cardVerification: VerificationState = VerificationState.NotVerified,
var cardValidation: VerificationState = VerificationState.NotVerified,
var codeVerification: VerificationState = VerificationState.NotVerified,
var card: Card? = null
)
/**
* All possible encryption modes.
*/
enum class EncryptionMode(val code: Int) {
NONE(0x0),
FAST(0x1),
STRONG(0x2)
}
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, Cancelled
}

View file

@ -1,77 +0,0 @@
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,63 +0,0 @@
package com.tangem
import com.tangem.tasks.PinType
/**
* Allows interaction with users and shows visual elements.
*
* Its default implementation, DefaultCardManagerDelegate, is in our tangem-sdk module.
*/
interface SessionViewDelegate {
/**
* It is called when user is expected to scan a Tangem Card with an Android device.
*/
fun onSessionStarted(cardId: String?, message: Message? = null)
/**
* It is called when security delay is triggered by the card.
* A user is expected to hold the card until the security delay is over.
*/
fun onSecurityDelay(ms: Int, totalDurationSeconds: Int)
/**
* It is called when long tasks are performed.
* A user is expected to hold the card until the task is complete.
*/
fun onDelay(total: Int, current: Int, step: Int)
/**
* It is called when user takes the card away from the Android device during the scanning
* (for example when security delay is in progress) and the TagLostException is received.
*/
fun onTagLost()
fun onTagConnected()
fun onWrongCard()
/**
* It is called when NFC session was completed and a user can take the card away from the Android device.
*/
fun onSessionStopped(message: Message? = null)
/**
* It is called when some error occur during NFC session.
*/
fun onError(error: TangemSdkError)
/**
* It is called when a user is expected to enter pin code.
*/
fun onPinRequested(pinType: PinType, callback: (pin: String) -> Unit)
/**
* It is called when a user wants to change pin code.
*/
fun onPinChangeRequested(pinType: PinType, callback: (pin: String) -> Unit)
}
/**
* Wrapper for a message that can be shown to user after a start of NFC session.
*/
data class Message(val header: String? = null, val body: String? = null)

View file

@ -1,440 +0,0 @@
package com.tangem
import com.tangem.commands.*
import com.tangem.commands.personalization.DepersonalizeCommand
import com.tangem.commands.personalization.DepersonalizeResponse
import com.tangem.commands.personalization.PersonalizeCommand
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.CardValuesStorage
import com.tangem.common.CompletionResult
import com.tangem.common.PinCode
import com.tangem.common.TerminalKeysService
import com.tangem.crypto.CryptoUtils
import com.tangem.tasks.ChangePinTask
import com.tangem.tasks.CreateWalletTask
import com.tangem.tasks.PinType
import com.tangem.tasks.ScanTask
/**
* The main interface of Tangem SDK that allows your app to communicate with Tangem cards.
*
* @property reader is an interface that is responsible for NFC connection and
* transfer of data to and from the Tangem Card.
* Its default implementation, NfcCardReader, is in our tangem-sdk module.
* @property viewDelegate An interface that allows interaction with users and shows relevant UI.
* 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(),
cardValuesStorage: CardValuesStorage,
terminalKeysService: TerminalKeysService? = null
) {
private val environmentService = SessionEnvironmentService(
config, terminalKeysService, cardValuesStorage
)
init {
CryptoUtils.initCrypto()
}
/**
* This method launches a [ScanTask] on a new thread.
*
* To start using any card, you first need to read it using the scanCard() method.
* This method launches an NFC session, and once its connected with the card,
* it obtains the card data. Optionally, if the card contains a wallet (private and public key pair),
* it proves that the wallet owns a private key that corresponds to a public one.
*
* @param callback is triggered on the completion of the [ScanTask] and provides card response
* in the form of [Card] if the task was performed successfully or [TangemSdkError] in case of an error.
*/
fun scanCard(initialMessage: Message? = null, callback: (result: CompletionResult<Card>) -> Unit) {
startSessionWithRunnable(ScanTask(), null, initialMessage, callback)
}
/**
* This method launches a [SignCommand] on a new thread.
*
* It allows you to sign one or multiple hashes.
* Simultaneous signing of array of hashes in a single [SignCommand] is required to support
* Bitcoin-type multi-input blockchains (UTXO).
* The [SignCommand] will return a corresponding array of signatures.
*
* Please note that Tangem cards usually protect the signing with a security delay
* that may last up to 90 seconds, depending on a card.
* It is for [SessionViewDelegate] to notify users of security delay.
*
* @param hashes Array of transaction hashes. It can be from one or up to ten hashes of the same length.
* @param cardId CID, Unique Tangem card ID number
* @param callback is triggered on the completion of the [SignCommand] and provides card response
* in the form of [SignResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun sign(hashes: Array<ByteArray>, cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<SignResponse>) -> Unit) {
startSessionWithRunnable(SignCommand(hashes), cardId, initialMessage, callback)
}
/**
* This method launches a [ReadIssuerDataCommand] on a new thread.
* This command returns 512-byte Issuer Data field and its issuers signature.
* Issuer Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. For example, this field may contain information about
* wallet balance signed by the issuer or additional issuers attestation data.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [ReadIssuerDataCommand] and provides
* card response in the form of [ReadIssuerDataResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun readIssuerData(cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<ReadIssuerDataResponse>) -> Unit) {
startSessionWithRunnable(ReadIssuerDataCommand(config.issuerPublicKey), cardId, initialMessage, callback)
}
/**
* This method launches a [ReadIssuerExtraDataCommand] on a new thread.
*
* This command retrieves Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. . For example, this field may contain photo or
* biometric information for ID card product. Because of the large size of Issuer_Extra_Data,
* a series of these commands have to be executed to read the entire Issuer_Extra_Data.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [ReadIssuerExtraDataCommand] and provides
* card response in the form of [ReadIssuerExtraDataResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun readIssuerExtraData(cardId: String? = null,
callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit) {
startSessionWithRunnable(ReadIssuerExtraDataCommand(config.issuerPublicKey), cardId, null, callback)
}
/**
* This method launches a [WriteIssuerDataCommand] on a new thread.
*
* This command writes 512-byte Issuer Data field and its issuers signature.
* Issuer Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. For example, this field may contain information about
* wallet balance signed by the issuer or additional issuers attestation data.
*
* @param cardId CID, Unique Tangem card ID number.
* @param issuerData Data provided by issuer.
* @param issuerDataSignature Issuers signature of [issuerData] with Issuer Data Private Key.
* @param issuerDataCounter An optional counter that protect issuer data against replay attack.
* @param callback is triggered on the completion of the [WriteIssuerDataCommand] and provides
* card response in the form of [WriteIssuerDataResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun writeIssuerData(cardId: String? = null,
issuerData: ByteArray,
issuerDataSignature: ByteArray,
issuerDataCounter: Int? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit) {
val command = WriteIssuerDataCommand(
issuerData,
issuerDataSignature,
issuerDataCounter,
config.issuerPublicKey
)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
/**
* This method launches a [WriteIssuerExtraDataCommand] on a new thread.
*
* This command writes Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS.
* The issuer defines purpose of use, format and payload of Issuer Data.
* For example, this field may contain a photo or biometric information for ID card products.
* Because of the large size of IssuerExtraData, a series of these commands have to be executed
* to write entire IssuerExtraData.
*
* @param cardId CID, Unique Tangem card ID number.
* @param issuerData Data provided by issuer.
* @param startingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerDataCounter] (if flags Protect_Issuer_Data_Against_Replay and
* Restrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]) and size of [issuerData].
* @param finalizingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerData] and [issuerDataCounter] (the latter one only if flags Protect_Issuer_Data_Against_Replay
* andRestrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]).
* @param issuerDataCounter An optional counter that protect issuer data against replay attack.
* @param callback is triggered on the completion of the [WriteIssuerExtraDataCommand] and provides
* card response in the form of [WriteIssuerDataResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun writeIssuerExtraData(cardId: String? = null,
issuerData: ByteArray,
startingSignature: ByteArray,
finalizingSignature: ByteArray,
issuerDataCounter: Int? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit) {
val command = WriteIssuerExtraDataCommand(
issuerData,
startingSignature, finalizingSignature,
issuerDataCounter,
config.issuerPublicKey
)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
/**
* This method launches a [WriteUserDataCommand] on a new thread, writing UserData and UserCounter fields.
*
* User_Data is never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and its payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* The initial value of User_Counter can be set by an App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*
* Writing of UserCounter and UserData is protected only by PIN1.
*/
fun writeUserData(
cardId: String? = null,
userData: ByteArray? = null,
userCounter: Int? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<WriteUserDataResponse>) -> Unit
) {
val command = WriteUserDataCommand(userData = userData, userCounter = userCounter)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
/**
* This method launches a [WriteUserDataCommand] on a new thread,
* writing UserProtectedData and UserProtectedCounter fields.
*
* User_ProtectedData is never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and its payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* The initial value of User_ProtectedCounter can be set by an App and increased on every signing
* of a new transaction (on SIGN command that calculate new signatures). The App defines the purpose of use.
* For example, this fields may contain blockchain nonce value.
*
* UserProtectedCounter and UserProtectedData require PIN2 for confirmation.
*/
fun writeProtectedUserData(
cardId: String? = null,
userProtectedData: ByteArray? = null,
userProtectedCounter: Int? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<WriteUserDataResponse>) -> Unit
) {
val command = WriteUserDataCommand(
userProtectedData = userProtectedData, userProtectedCounter = userProtectedCounter
)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
/**
* This method launches a [ReadUserDataCommand] on a new thread.
*
* This command returns two up to 512-byte User_Data, User_Protected_Data and two counters User_Counter and
* User_Protected_Counter fields.
* User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and it's payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [ReadUserDataCommand] and provides
* card response in the form of [ReadUserDataResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun readUserData(cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<ReadUserDataResponse>) -> Unit) {
startSessionWithRunnable(ReadUserDataCommand(), cardId, initialMessage, callback)
}
/**
* This method launches a [CreateWalletTask] on a new thread.
*
* This this will create a new wallet on the card having Empty state with [CreateWalletCommand]
* and will check the success of the operation by performing [CheckWalletCommand].
* A key pair WalletPublicKey / WalletPrivateKey is generated and securely stored in the card.
* App will need to obtain Wallet_PublicKey from the [CreateWalletResponse] or from the
* response of [ReadCommand] and then transform it into an address of corresponding
* blockchain wallet according to a specific blockchain algorithm.
* WalletPrivateKey is never revealed by the card and will be used by [SignCommand] and [CheckWalletCommand].
* RemainingSignature is set to MaxSignatures.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [CreateWalletTask] and provides
* card response in the form of [CreateWalletResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun createWallet(cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
startSessionWithRunnable(CreateWalletTask(), cardId, initialMessage, callback)
}
/**
* This method launches a [PurgeWalletCommand] on a new thread.
*
* This command deletes all wallet data. If IsReusable flag is enabled during personalization,
* or [CreateWalletCommand].
* If IsReusable flag is disabled, the card switches to Purged state.
* Purged state is final, it makes the card useless.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [PurgeWalletCommand] and provides
* card response in the form of [PurgeWalletResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun purgeWallet(cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<PurgeWalletResponse>) -> Unit) {
startSessionWithRunnable(PurgeWalletCommand(), cardId, initialMessage, callback)
}
/**
* This method launches a [VerifyCardCommand] on a new thread.
*
* The command to ensures the card has not been counterfeited.
* By using standard challenge-response scheme, the card proves possession of CardPrivateKey
* that corresponds to CardPublicKey returned by [ReadCommand]. Then the data is sent
* to Tangem server to prove that this card was indeed issued by Tangem.
* The online part of the verification is unavailable for DevKit cards.
*
*
* @param cardId CID, Unique Tangem card ID number.
* @param online flag that allows disable online verification
* @param callback is triggered on the completion of the [VerifyCardCommand] and provides
* card response in the form of [VerifyCardResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
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
*
* This method launches a [DepersonalizeCommand] on a new thread.
*
* This command resets card to initial state,
* erasing all data written during personalization and usage.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [DepersonalizeCommand] and provides
* card response in the form of [DepersonalizeResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
* */
fun depersonalize(cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<DepersonalizeResponse>) -> Unit) {
startSessionWithRunnable(DepersonalizeCommand(), cardId, initialMessage, callback)
}
/**
* Command available on SDK cards only
*
* This method launches a [PersonalizeCommand] on a new thread.
*
* Personalization is an initialization procedure, required before starting using a card.
* During this procedure a card setting is set up.
* During this procedure all data exchange is encrypted.
* @param config is a configuration file with all the card settings that are written on the card
* during personalization.
* @param issuer Issuer is a third-party team or company wishing to use Tangem cards.
* @param manufacturer Tangem Card Manufacturer.
* @param acquirer Acquirer is a trusted third-party company that operates proprietary
* (non-EMV) POS terminal infrastructure and transaction processing back-end.
* @param callback is triggered on the completion of the [PersonalizeCommand] and provides
* card response in the form of [Card] if the command was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun personalize(config: CardConfig,
issuer: Issuer, manufacturer: Manufacturer, acquirer: Acquirer? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<Card>) -> Unit) {
val command = PersonalizeCommand(config, issuer, manufacturer, acquirer)
startSessionWithRunnable(command, null, initialMessage, callback)
}
fun changePin1(cardId: String? = null,
pin: ByteArray? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<SetPinResponse>) -> Unit) {
val command = ChangePinTask(PinType.Pin1, pin)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
fun changePin2(cardId: String? = null,
pin: ByteArray? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<SetPinResponse>) -> Unit) {
val command = ChangePinTask(PinType.Pin2, pin)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
fun changePin3(cardId: String? = null,
pin: ByteArray? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<SetPinResponse>) -> Unit) {
val command = ChangePinTask(PinType.Pin3, pin)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
/**
* Allows running a custom bunch of commands in one [CardSession] by creating a custom task.
* [TangemSdk] will start a card session, perform preflight [ReadCommand],
* invoke [CardSessionRunnable.run] and close the session.
* You can find the current card in the [CardSession.environment].
* @runnable: A custom task, adopting [CardSessionRunnable] protocol
* @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
* the [TangemSdkError.WrongCardNumber] otherwise.
* @initialMessage: A custom description that shows at the beginning of the NFC session.
* If null, default message will be used.
* @callback: Standard [TangemSdk] callback.
*/
fun <T : CommandResponse> startSessionWithRunnable(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<T>) -> Unit) {
val cardSession = CardSession(environmentService, reader, viewDelegate, cardId, initialMessage)
Thread().run { cardSession.startWithRunnable(runnable, callback) }
}
/**
* Allows running a custom bunch of commands in one [CardSession] with lightweight closure syntax.
* 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
* the [TangemSdkError.WrongCardNumber] otherwise.
* @initialMessage: A custom description that shows at the beginning of the NFC session.
* If null, default message will be used.
* @callback: At first, you should check that the [TangemSdkError] is not null,
* 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(environmentService, reader, viewDelegate, cardId, initialMessage)
Thread().run { cardSession.start(callback = callback) }
}
companion object {
var pin1: PinCode? = null
val pin2: MutableMap<String, PinCode?> = mutableMapOf()
}
}

View file

@ -1,163 +0,0 @@
package com.tangem
import com.tangem.commands.Card
import com.tangem.commands.ReadCommand
import com.tangem.common.apdu.StatusWord
import com.tangem.tasks.ScanTask
/**
* An error class that represent typical errors that may occur when performing Tangem SDK tasks.
* Errors are propagated back to the caller in callbacks.
*/
sealed class TangemSdkError(val code: Int) : Exception(code.toString()) {
/**
* This error is returned when Android NFC reader loses a tag
* (e.g. a user detaches card from the phone's NFC module) while the NFC session is in progress.
*/
class TagLost : TangemSdkError(10001)
/**
* This error is returned when NFC driver on an Android device does not support sending more than 261 bytes.
*/
class ExtendedLengthNotSupported : TangemSdkError(10002)
class SerializeCommandError : TangemSdkError(20001)
class DeserializeApduFailed : TangemSdkError(20002)
class EncodingFailedTypeMismatch : TangemSdkError(20003)
class EncodingFailed : TangemSdkError(20004)
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.
*/
class UnknownStatus : TangemSdkError(30001)
/**
* This error is returned when a card's reply is [StatusWord.ErrorProcessingCommand].
* The card sends this status in case of internal card error.
*/
class ErrorProcessingCommand : TangemSdkError(30002)
/**
* This error is returned when a card's reply is [StatusWord.InvalidState].
* The card sends this status when command can not be executed in the current state of a card.
*/
class InvalidState : TangemSdkError(30003)
/**
* This error is returned when a card's reply is [StatusWord.InsNotSupported].
* The card sends this status when the card cannot process the [com.tangem.common.apdu.Instruction].
*/
class InsNotSupported : TangemSdkError(30004)
/**
* This error is returned when a card's reply is [StatusWord.InvalidParams].
* The card sends this status when there are wrong or not sufficient parameters in TLV request,
* or wrong PIN1/PIN2.
* The error may be caused, for example, by wrong parameters of the [Task], [CommandSerializer],
* mapping or serialization errors.
*/
class InvalidParams : TangemSdkError(30005)
/**
* This error is returned when a card's reply is [StatusWord.NeedEncryption]
* and the encryption was not established by TangemSdk.
*/
class NeedEncryption : TangemSdkError(30006)
//Personalization Errors
class AlreadyPersonalized : TangemSdkError(40101)
//Depersonalization Errors
class CannotBeDepersonalized : TangemSdkError(40201)
//Read Errors
class Pin1Required : TangemSdkError(40401)
//CreateWallet Errors
class AlreadyCreated : TangemSdkError(40501)
//PurgeWallet Errors
class PurgeWalletProhibited : TangemSdkError(40601)
//SetPin Errors
class Pin1CannotBeChanged : TangemSdkError(40801)
class Pin2CannotBeChanged : TangemSdkError(40802)
class Pin1CannotBeDefault : TangemSdkError(40803)
//Sign Errors
class NoRemainingSignatures : TangemSdkError(40901)
/**
* This error is returned when a [com.tangem.commands.SignCommand]
* receives only empty hashes for signature.
*/
class EmptyHashes : TangemSdkError(40902)
/**
* This error is returned when a [com.tangem.commands.SignCommand]
* receives hashes of different lengths for signature.
*/
class HashSizeMustBeEqual : TangemSdkError(40903)
class CardIsEmpty : TangemSdkError(40904)
class SignHashesNotAvailable : TangemSdkError(40905)
/**
* Tangem cards can sign currently up to 10 hashes during one [com.tangem.commands.SignCommand].
* This error is returned when a [com.tangem.commands.SignCommand] receives more than 10 hashes to sign.
*/
class TooManyHashesInOneTransaction : TangemSdkError(40906)
//Write Extra Issuer Data Errors
class ExtendedDataSizeTooLarge : TangemSdkError(41101)
//General Errors
class NotPersonalized() : TangemSdkError(40001)
class NotActivated : TangemSdkError(40002)
class CardIsPurged : TangemSdkError(40003)
class Pin2OrCvcRequired : TangemSdkError(40004)
/**
* This error is returned when a [Task] checks unsuccessfully either
* a card's ability to sign with its private key, or the validity of issuer data.
*/
class VerificationFailed : TangemSdkError(40005)
class DataSizeTooLarge : TangemSdkError(40006)
/**
* This error is returned when [ReadIssuerDataTask] or [ReadIssuerExtraDataTask] expects a counter
* (when the card's requires it), but the counter is missing.
*/
class MissingCounter : TangemSdkError(40007)
class OverwritingDataIsProhibited : TangemSdkError(40008)
class DataCannotBeWritten : TangemSdkError(40009)
class MissingIssuerPubicKey : TangemSdkError(40010)
//SDK Errors
class UnknownError: TangemSdkError(50001)
/**
* This error is returned when a user manually closes NFC Reading Bottom Sheet Dialog.
*/
class UserCancelled: TangemSdkError(50002)
/**
* This error is returned when [com.tangem.TangemSdk] was called with a new [Task],
* while a previous [Task] is still in progress.
*/
class Busy : TangemSdkError(50003)
/**
* This error is returned when a task (such as [ScanTask]) requires that [ReadCommand]
* is executed before performing other commands.
*/
class MissingPreflightRead : TangemSdkError(50004)
/**
* This error is returned when a [Task] expects a user to use a particular card,
* but the user tries to use a different card.
*/
class WrongCardNumber : TangemSdkError(50005)
/**
* This error is returned when a user scans a card of a [com.tangem.common.extensions.CardType]
* that is not specified in [Config.cardFilter].
*/
class WrongCardType : TangemSdkError(50006)
/**
* This error is returned when a [ScanTask] returns a [Card] without some of the essential fields.
*/
class CardError : TangemSdkError(50007)
}

View file

@ -1,101 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.CryptoUtils
/**
* Deserialized response from the Tangem card after [CheckWalletCommand].
*
* @property cardId Unique Tangem card ID number
* @property salt Random salt generated by the card.
* @property walletSignature Challenge and salt signed with the wallet private key.
*/
class CheckWalletResponse(
val cardId: String,
val salt: ByteArray,
val walletSignature: ByteArray
) : CommandResponse {
fun verify(curve: EllipticCurve, publicKey: ByteArray, challenge: ByteArray): Boolean {
return CryptoUtils.verify(
publicKey,
challenge + salt,
walletSignature,
curve)
}
}
/**
* This command proves that the wallet private key from the card corresponds to the wallet public key.
* Standard challenge/response scheme is used.
*
* @property pin1 Hashed users pin 1 code to access the card. Default unhashed value: 000000.
* @property cardId Unique Tangem card ID number
* @property challenge Random challenge generated by application
*/
class CheckWalletCommand(
private val curve: EllipticCurve, private val publicKey: ByteArray
) : Command<CheckWalletResponse>() {
private val challenge = CryptoUtils.generateRandomBytes(16)
override fun run(session: CardSession, callback: (result: CompletionResult<CheckWalletResponse>) -> Unit) {
super.run(session) { result ->
when (result) {
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
is CompletionResult.Success -> {
val verified = result.data.verify(
curve,
publicKey,
challenge
)
if (verified) {
callback(CompletionResult.Success(result.data))
} else {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
}
}
}
}
}
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.status == CardStatus.NotPersonalized) {
return TangemSdkError.NotPersonalized()
}
if (card.isActivated) {
return TangemSdkError.NotActivated()
}
return null
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
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())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): CheckWalletResponse {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return CheckWalletResponse(
cardId = decoder.decode(TlvTag.CardId),
salt = decoder.decode(TlvTag.Salt),
walletSignature = decoder.decode(TlvTag.Signature)
)
}
}

View file

@ -1,237 +0,0 @@
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
import com.tangem.tasks.PinType
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].
*/
interface CommandResponse
/**
* Basic class for Tangem card commands
*/
abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRunnable<T> {
open val performPreflightRead: Boolean = true
override val requiresPin2: Boolean = false
override fun run(session: CardSession, callback: (result: CompletionResult<T>) -> Unit) {
Log.i("Command", "Initializing ${this::class.java.simpleName}")
transceive(session, callback)
}
open fun performPreCheck(card: Card): TangemSdkError? = null
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 ->
callback(CompletionResult.Failure(error))
return
}
}
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) {
is CompletionResult.Failure -> {
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
}
callback(CompletionResult.Failure(error))
return@transceiveApdu
}
callback(CompletionResult.Failure(result.error))
}
is CompletionResult.Success -> {
try {
val response = deserialize(session.environment, result.data)
callback(CompletionResult.Success(response))
} catch (error: TangemSdkError) {
callback(CompletionResult.Failure(error))
}
}
}
}
}
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.Pins12Changed,
StatusWord.Pin3Changed, StatusWord.Pins13Changed, StatusWord.Pins23Changed,
StatusWord.Pins123Changed -> {
callback(CompletionResult.Success(responseApdu))
}
StatusWord.NeedPause -> {
// NeedPause is returned from the card whenever security delay is triggered.
val remainingTime =
deserializeSecurityDelay(responseApdu)
if (remainingTime != null) {
session.viewDelegate.onSecurityDelay(
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
}
}
transceiveApdu(apdu, session, callback)
}
else -> {
val error = responseApdu.statusWord.toTangemSdkError()
if (error != null) {
callback(CompletionResult.Failure(error))
} else {
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
}
}
}
}
is CompletionResult.Failure ->
if (result.error is TangemSdkError.TagLost) {
session.viewDelegate.onTagLost()
} else {
callback(CompletionResult.Failure(result.error))
}
}
}
}
/**
* Helper method to parse security delay information received from a card.
*
* @return Remaining security delay in milliseconds.
*/
private fun deserializeSecurityDelay(
responseApdu: ResponseApdu
): Int? {
val tlv = responseApdu.getTlvData()
return tlv?.find { it.tag == TlvTag.Pause }?.value?.toInt()
}
private fun handlePin1(
session: CardSession,
callback: (result: CompletionResult<T>) -> Unit
) {
session.pause()
session.viewDelegate.onPinRequested(PinType.Pin1) { pin1 ->
session.environment.pin1 = PinCode(pin1)
session.resume()
transceive(session, callback)
}
}
private fun handlePin2(
session: CardSession,
callback: (result: CompletionResult<T>) -> Unit
) {
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.environment.pin2 = null
getPin2FromDelegate(session, callback)
}
is CompletionResult.Success -> {
transceiveInternal(session, callback)
}
}
}
}
private fun getPin2FromDelegate(session: CardSession,
callback: (result: CompletionResult<T>) -> Unit) {
session.viewDelegate.onPinRequested(PinType.Pin2) { pin2 ->
session.environment.pin2 = PinCode(pin2)
transceive(session, callback)
}
}
}

View file

@ -1,86 +0,0 @@
package com.tangem.commands
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class CreateWalletResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Current status of the card [1 - Empty, 2 - Loaded, 3- Purged]
*/
val status: CardStatus,
/**
*/
val walletPublicKey: ByteArray
) : CommandResponse
/**
* This command will create a new wallet on the card having Empty state.
* A key pair WalletPublicKey / WalletPrivateKey is generated and securely stored in the card.
* App will need to obtain Wallet_PublicKey from the response of [CreateWalletCommand] or [ReadCommand]
* and then transform it into an address of corresponding blockchain wallet
* according to a specific blockchain algorithm.
* WalletPrivateKey is never revealed by the card and will be used by [SignCommand] and [CheckWalletCommand].
* RemainingSignature is set to MaxSignatures.
*
* @property cardId CID, Unique Tangem card ID number.
*/
class CreateWalletCommand : Command<CreateWalletResponse>() {
override val requiresPin2 = true
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.isActivated) {
return TangemSdkError.NotActivated()
}
return when (card.status) {
CardStatus.Empty -> null
CardStatus.NotPersonalized -> TangemSdkError.NotPersonalized()
CardStatus.Loaded -> TangemSdkError.AlreadyCreated()
CardStatus.Purged -> TangemSdkError.CardIsPurged()
null -> TangemSdkError.CardError()
}
}
override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError {
if (error is TangemSdkError.InvalidParams) {
return TangemSdkError.Pin2OrCvcRequired()
}
return error
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Pin2, environment.pin2?.value)
tlvBuilder.append(TlvTag.Cvc, environment.cvc)
return CommandApdu(Instruction.CreateWallet, tlvBuilder.serialize())
}
override fun deserialize(
environment: SessionEnvironment,
apdu: ResponseApdu
): CreateWalletResponse {
val tlvData = apdu.getTlvData()
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return CreateWalletResponse(
cardId = decoder.decode(TlvTag.CardId),
status = decoder.decode(TlvTag.Status),
walletPublicKey = decoder.decode(TlvTag.WalletPublicKey)
)
}
}

View file

@ -1,44 +0,0 @@
package com.tangem.commands
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class OpenSessionResponse(
val sessionKeyB: ByteArray,
val uid: ByteArray
) : CommandResponse
/**
* In case of encrypted communication, App should setup a session before calling any further command.
* [OpenSessionCommand] generates secret session_key that is used by both host and card
* to encrypt and decrypt commands payload.
*/
class OpenSessionCommand(private val sessionKeyA: ByteArray) : ApduSerializable<OpenSessionResponse> {
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.SessionKeyA, sessionKeyA)
return CommandApdu(
Instruction.OpenSession.code, tlvBuilder.serialize(), 0, environment.encryptionMode.code
)
}
override fun deserialize(
environment: SessionEnvironment,
apdu: ResponseApdu
): OpenSessionResponse {
val tlvData = apdu.getTlvData()
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return OpenSessionResponse(
sessionKeyB = decoder.decode(TlvTag.SessionKeyB),
uid = decoder.decode(TlvTag.Uid)
)
}
}

View file

@ -1,78 +0,0 @@
package com.tangem.commands
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class PurgeWalletResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Current status of the card [1 - Empty, 2 - Loaded, 3- Purged]
*/
val status: CardStatus
) : CommandResponse
/**
* This command deletes all wallet data. If Is_Reusable flag is enabled during personalization,
* If Is_Reusable flag is disabled, the card switches to Purged state.
* Purged state is final, it makes the card useless.
* @property cardId CID, Unique Tangem card ID number.
*/
class PurgeWalletCommand : Command<PurgeWalletResponse>() {
override val requiresPin2 = true
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.status == CardStatus.NotPersonalized) {
return TangemSdkError.NotPersonalized()
}
if (card.isActivated) {
return TangemSdkError.NotActivated()
}
if (card.settingsMask?.contains(Settings.ProhibitPurgeWallet) == true) {
return TangemSdkError.PurgeWalletProhibited()
}
return when (card.status) {
CardStatus.Loaded -> null
CardStatus.NotPersonalized -> TangemSdkError.NotPersonalized()
CardStatus.Empty -> TangemSdkError.CardIsEmpty()
CardStatus.Purged -> TangemSdkError.CardIsPurged()
null -> TangemSdkError.CardError()
}
}
override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError {
if (error is TangemSdkError.InvalidParams) {
return TangemSdkError.Pin2OrCvcRequired()
}
return error
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Pin2, environment.pin2?.value)
return CommandApdu(Instruction.PurgeWallet, tlvBuilder.serialize())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): PurgeWalletResponse {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return PurgeWalletResponse(
cardId = decoder.decode(TlvTag.CardId),
status = decoder.decode(TlvTag.Status))
}
}

View file

@ -1,396 +0,0 @@
package com.tangem.commands
import com.google.gson.annotations.SerializedName
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.common.CardDeserializer
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvTag
import java.util.*
/**
* Determines which type of data is required for signing.
*/
data class SigningMethodMask(val rawValue: Int) {
fun contains(signingMethod: SigningMethod): Boolean {
return if (rawValue and 0x80 == 0) {
signingMethod.code == rawValue
} else {
rawValue and (0x01 shl signingMethod.code) != 0
}
}
}
enum class SigningMethod(val code: Int) {
SignHash(0),
SignRaw(1),
SignHashValidateByIssuer(2),
SignRawValidateByIssuer(3),
SignHashValidateByIssuerWriteIssuerData(4),
SignRawValidateByIssuerWriteIssuerData(5),
SignPos(6)
}
class SigningMethodMaskBuilder() {
private val signingMethods = mutableSetOf<SigningMethod>()
fun add(signingMethod: SigningMethod) {
signingMethods.add(signingMethod)
}
fun build(): SigningMethodMask {
val rawValue: Int = when {
signingMethods.count() == 0 -> {
0
}
signingMethods.count() == 1 -> {
signingMethods.iterator().next().code
}
else -> {
signingMethods.fold(
0x80, { acc, singingMethod -> acc + (0x01 shl singingMethod.code) }
)
}
}
return SigningMethodMask(rawValue)
}
}
/**
* Elliptic curve used for wallet key operations.
*/
enum class EllipticCurve(val curve: String) {
@SerializedName(value = "secp256k1")
Secp256k1("secp256k1"),
@SerializedName(value = "ed25519")
Ed25519("ed25519");
companion object {
private val values = values()
fun byName(curve: String): EllipticCurve? = values.find { it.curve == curve }
}
}
/**
* Status of the card and its wallet.
*/
enum class CardStatus(val code: Int) {
NotPersonalized(0),
Empty(1),
Loaded(2),
Purged(3);
companion object {
private val values = values()
fun byCode(code: Int): CardStatus? = values.find { it.code == code }
}
}
/**
* Mask of products enabled on card
* @property rawValue Products mask values,
* while flags definitions and values are in [ProductMask.Companion] as constants.
*/
data class ProductMask(val rawValue: Int) {
fun contains(product: Product): Boolean = (rawValue and product.code) != 0
}
enum class Product(val code: Int) {
Note(0x01),
Tag(0x02),
IdCard(0x04),
IdIssuer(0x08)
}
class ProductMaskBuilder() {
private var productMaskValue = 0
fun add(product: Product) {
productMaskValue = productMaskValue or product.code
}
fun build() = ProductMask(productMaskValue)
}
/**
* Stores and maps Tangem card settings.
*
* @property rawValue Card settings in a form of flags,
* while flags definitions and possible values are in [Settings].
*/
data class SettingsMask(val rawValue: Int) {
fun contains(settings: Settings): Boolean = (rawValue and settings.code) != 0
}
enum class Settings(val code: Int) {
IsReusable(0x0001),
UseActivation(0x0002),
ProhibitPurgeWallet(0x0004),
UseBlock(0x0008),
AllowSwapPIN(0x0010),
AllowSwapPIN2(0x0020),
UseCVC(0x0040),
ForbidDefaultPIN(0x0080),
UseOneCommandAtTime(0x0100),
UseNdef(0x0200),
UseDynamicNdef(0x0400),
SmartSecurityDelay(0x0800),
ProtocolAllowUnencrypted(0x1000),
ProtocolAllowStaticEncryption(0x2000),
ProtectIssuerDataAgainstReplay(0x4000),
RestrictOverwriteIssuerDataEx(0x00100000),
AllowSelectBlockchain(0x8000),
DisablePrecomputedNdef(0x00010000),
SkipSecurityDelayIfValidatedByLinkedTerminal(0x00080000),
SkipCheckPin2andCvcIfValidatedByIssuer(0x00040000),
SkipSecurityDelayIfValidatedByIssuer(0x00020000),
RequireTermTxSignature(0x01000000),
RequireTermCertSignature(0x02000000),
CheckPIN3onCard(0x04000000)
}
class SettingsMaskBuilder() {
private var settingsMaskValue = 0
fun add(settings: Settings) {
settingsMaskValue = settingsMaskValue or settings.code
}
fun build() = SettingsMask(settingsMaskValue)
}
/**
* Detailed information about card contents.
*/
class CardData(
/**
* Tangem internal manufacturing batch ID.
*/
val batchId: String?,
/**
* Timestamp of manufacturing.
*/
val manufactureDateTime: Date?,
/**
* Name of the issuer.
*/
val issuerName: String?,
/**
* Name of the blockchain.
*/
val blockchainName: String?,
/**
* Signature of CardId with manufacturers private key.
*/
val manufacturerSignature: ByteArray?,
/**
* Mask of products enabled on card.
*/
val productMask: ProductMask?,
/**
* Name of the token.
*/
val tokenSymbol: String?,
/**
* Smart contract address.
*/
val tokenContractAddress: String?,
/**
* Number of decimals in token value.
*/
val tokenDecimal: Int?
)
/**
* Response for [ReadCommand]. Contains detailed card information.
*/
class Card(
/**
* Unique Tangem card ID number.
*/
val cardId: String,
/**
* Name of Tangem card manufacturer.
*/
val manufacturerName: String,
/**
* Current status of the card.
*/
val status: CardStatus?,
/**
* Version of Tangem COS.
*/
val firmwareVersion: String?,
/**
* Public key that is used to authenticate the card against manufacturers database.
* It is generated one time during card manufacturing.
*/
val cardPublicKey: ByteArray?,
/**
* Card settings defined by personalization (bit mask: 0 Enabled, 1 Disabled).
*/
val settingsMask: SettingsMask?,
/**
* Public key that is used by the card issuer to sign IssuerData field.
*/
val issuerPublicKey: ByteArray?,
/**
* Explicit text name of the elliptic curve used for all wallet key operations.
* Supported curves: secp256k1 and ed25519.
*/
val curve: EllipticCurve?,
/**
* Total number of signatures allowed for the wallet when the card was personalized.
*/
val maxSignatures: Int?,
/**
* Defines what data should be submitted to SIGN command.
*/
val signingMethods: SigningMethodMask?,
/**
* Delay in seconds before COS executes commands protected by PIN2.
*/
val pauseBeforePin2: Int?,
/**
* Public key of the blockchain wallet.
*/
val walletPublicKey: ByteArray?,
/**
* Remaining number of [SignCommand] operations before the wallet will stop signing transactions.
*/
val walletRemainingSignatures: Int?,
/**
* Total number of signed single hashes returned by the card in
* [SignCommand] responses since card personalization.
* Sums up array elements within all [SignCommand].
*/
val walletSignedHashes: Int?,
/**
* Any non-zero value indicates that the card experiences some hardware problems.
* User should withdraw the value to other blockchain wallet as soon as possible.
* Non-zero Health tag will also appear in responses of all other commands.
*/
val health: Int?,
/**
* Whether the card requires issuers confirmation of activation.
* is "true" if the card requires activation,
* is 'false" if the card is activated or does not require activation
*/
val isActivated: Boolean,
/**
* A random challenge generated by personalisation that should be signed and returned
* to COS by the issuer to confirm the card has been activated.
* This field will not be returned if the card is activated.
*/
val activationSeed: ByteArray?,
/**
* Returned only if [SigningMethod.SignPos] enabling POS transactions is supported by card.
*/
val paymentFlowVersion: ByteArray?,
/**
* This value can be initialized by terminal and will be increased by COS on execution of every [SignCommand].
* For example, this field can store blockchain nonce for quick one-touch transaction on POS terminals.
* Returned only if [SigningMethod.SignPos] enabling POS transactions is supported by card.
*/
val userCounter: Int?,
/**
* This value can be initialized by App (with PIN2 confirmation) and will be increased by COS
* with the execution of each [SignCommand]. For example, this field can store blockchain nonce
* for a quick one-touch transaction on POS terminals. Returned only if [SigningMethod.SignPos].
*/
val userProtectedCounter: Int?,
/**
* When this value is true, it means that the application is linked to the card,
* and COS will not enforce security delay if [SignCommand] will be called
* with [TlvTag.TerminalTransactionSignature] parameter containing a correct signature of raw data
* to be signed made with [TlvTag.TerminalPublicKey].
*/
val terminalIsLinked: Boolean,
/**
* Detailed information about card contents. Format is defined by the card issuer.
* Cards complaint with Tangem Wallet application should have TLV format.
*/
val cardData: CardData?
) : CommandResponse
/**
* This command receives from the Tangem Card all the data about the card and the wallet,
* including unique card number (CID or cardId) that has to be submitted while calling all other commands.
*/
class ReadCommand : Command<Card>() {
override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError {
if (error is TangemSdkError.InvalidParams) {
return TangemSdkError.Pin1Required()
}
return error
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
/**
* [SessionEnvironment] stores the pin1 value. If no pin1 value was set, it will contain
* default value of 000000.
* 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?.value)
tlvBuilder.append(TlvTag.TerminalPublicKey, environment.terminalKeys?.publicKey)
return CommandApdu(Instruction.Read, tlvBuilder.serialize())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): Card {
return CardDeserializer.deserialize(apdu)
}
}

View file

@ -1,119 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.common.DefaultIssuerDataVerifier
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.commands.common.IssuerDataVerifier
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class ReadIssuerDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Data defined by issuer.
*/
val issuerData: ByteArray,
/**
* Issuers signature of [issuerData] with Issuer Data Private Key (which is kept on card).
* Issuers signature of SHA256-hashed [cardId] concatenated with [issuerData]:
* SHA256([cardId] | [issuerData]).
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
*/
val issuerDataSignature: ByteArray,
/**
* An optional counter that protect issuer data against replay attack.
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask]
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
*/
val issuerDataCounter: Int?
) : CommandResponse
/**
* This command returns 512-byte Issuer Data field and its issuers signature.
* Issuer Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. For example, this field may contain information about
* wallet balance signed by the issuer or additional issuers attestation data.
* @property cardId CID, Unique Tangem card ID number.
*/
class ReadIssuerDataCommand(
val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : Command<ReadIssuerDataResponse>(), IssuerDataVerifier by verifier {
override fun run(
session: CardSession,
callback: (result: CompletionResult<ReadIssuerDataResponse>) -> Unit
) {
val publicKey = issuerPublicKey ?: session.environment.card?.issuerPublicKey
super.run(session) { result ->
when (result) {
is CompletionResult.Failure -> callback(result)
is CompletionResult.Success -> {
if (result.data.issuerData.isEmpty()) {
callback(result)
return@run
}
val issuerDataToVerify = IssuerDataToVerify(
result.data.cardId, result.data.issuerData, result.data.issuerDataCounter
)
if (verify(publicKey!!, result.data.issuerDataSignature, issuerDataToVerify)) {
callback(result)
} else {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
}
}
}
}
}
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.status == CardStatus.NotPersonalized) {
return TangemSdkError.NotPersonalized()
}
issuerPublicKey ?: card.issuerPublicKey ?: return TangemSdkError.MissingIssuerPubicKey()
return null
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
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())
}
override fun deserialize(
environment: SessionEnvironment,
apdu: ResponseApdu
): ReadIssuerDataResponse {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return ReadIssuerDataResponse(
cardId = decoder.decode(TlvTag.CardId),
issuerData = decoder.decode(TlvTag.IssuerData),
issuerDataSignature = decoder.decode(TlvTag.IssuerDataSignature),
issuerDataCounter = decoder.decodeOptional(TlvTag.IssuerDataCounter)
)
}
}

View file

@ -1,178 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.common.DefaultIssuerDataVerifier
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.commands.common.IssuerDataVerifier
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import java.io.ByteArrayOutputStream
class ReadIssuerExtraDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Size of all Issuer_Extra_Data field.
*/
val size: Int?,
/**
* Data defined by issuer.
*/
val issuerData: ByteArray,
/**
* Issuers signature of [issuerData] with Issuer Data Private Key (which is kept on card).
* Issuers signature of SHA256-hashed [cardId] concatenated with [issuerData]:
* SHA256([cardId] | [issuerData]).
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
*/
val issuerDataSignature: ByteArray?,
/**
* An optional counter that protects issuer data against replay attack.
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask]
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
*/
val issuerDataCounter: Int?
) : CommandResponse
/**
* This command retrieves Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. . For example, this field may contain photo or
* biometric information for ID card product. Because of the large size of Issuer_Extra_Data,
* a series of these commands have to be executed to read the entire Issuer_Extra_Data.
*/
class ReadIssuerExtraDataCommand(
private val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : Command<ReadIssuerExtraDataResponse>(), IssuerDataVerifier by verifier {
private val issuerData = ByteArrayOutputStream()
private var offset: Int = 0
private var issuerDataSize: Int = 0
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.status == CardStatus.NotPersonalized) {
return TangemSdkError.NotPersonalized()
}
issuerPublicKey ?: card.issuerPublicKey ?: return TangemSdkError.MissingIssuerPubicKey()
return null
}
override fun run(
session: CardSession,
callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit
) {
val publicKey = issuerPublicKey ?: session.environment.card?.issuerPublicKey
readIssuerData(session, publicKey!!, callback)
}
private fun readIssuerData(
session: CardSession, publicKey: ByteArray,
callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit
) {
if (issuerDataSize != 0) {
session.viewDelegate.onDelay(
issuerDataSize, offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE
)
}
transceive(session) { result ->
when (result) {
is CompletionResult.Success -> {
if (result.data.size != null) {
if (result.data.size == 0) {
callback(CompletionResult.Success(result.data))
return@transceive
}
issuerDataSize = result.data.size
}
issuerData.write(result.data.issuerData)
if (result.data.issuerDataSignature == null) {
offset = issuerData.size()
readIssuerData(session, publicKey, callback)
} else {
completeTask(result.data, publicKey, callback)
}
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
private fun completeTask(
data: ReadIssuerExtraDataResponse, publicKey: ByteArray,
callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit
) {
val dataToVerify = IssuerDataToVerify(
data.cardId,
issuerData.toByteArray(),
data.issuerDataCounter
)
if (verify(publicKey, data.issuerDataSignature!!, dataToVerify)) {
val finalResult = ReadIssuerExtraDataResponse(
data.cardId,
issuerDataSize,
issuerData.toByteArray(),
data.issuerDataSignature,
data.issuerDataCounter
)
callback(CompletionResult.Success(finalResult))
} else {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
}
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
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)
return CommandApdu(Instruction.ReadIssuerData, tlvBuilder.serialize())
}
override fun deserialize(
environment: SessionEnvironment,
apdu: ResponseApdu
): ReadIssuerExtraDataResponse {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return ReadIssuerExtraDataResponse(
cardId = decoder.decode(TlvTag.CardId),
size = decoder.decodeOptional(TlvTag.Size),
issuerData = decoder.decodeOptional(TlvTag.IssuerData) ?: byteArrayOf(),
issuerDataSignature = decoder.decodeOptional(TlvTag.IssuerDataSignature),
issuerDataCounter = decoder.decodeOptional(TlvTag.IssuerDataCounter)
)
}
companion object {
/**
* This mode value specifies that this command retrieves Issuer EXTRA data from the card
* (with value 0 the command will get instead simple Issuer Data from the card).
*/
const val EXTRA_DATA_MODE = 1
}
}

View file

@ -1,82 +0,0 @@
package com.tangem.commands
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class ReadUserDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Data defined by user's App.
*/
val userData: ByteArray,
/**
* Data defined by user's App (confirmed by PIN2).
*/
val userProtectedData: ByteArray,
/**
* Counter initialized by user's App and increased on every signing of new transaction
*/
val userCounter: Int,
/**
* Counter initialized by user's App (confirmed by PIN2) and increased on every signing of new transaction
*/
val userProtectedCounter: Int
) : CommandResponse
/**
* This command returns two up to 512-byte User_Data, User_Protected_Data and two counters User_Counter and
* User_Protected_Counter fields.
* User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and it's payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*/
class ReadUserDataCommand : Command<ReadUserDataResponse>() {
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.status == CardStatus.NotPersonalized) {
return TangemSdkError.NotPersonalized()
}
if (card.isActivated) {
return TangemSdkError.NotActivated()
}
return null
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val builder = TlvBuilder()
builder.append(TlvTag.CardId, environment.card?.cardId)
builder.append(TlvTag.Pin, environment.pin1?.value)
return CommandApdu(Instruction.ReadUserData, builder.serialize())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): ReadUserDataResponse {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return ReadUserDataResponse(
cardId = decoder.decode(TlvTag.CardId),
userData = decoder.decode(TlvTag.UserData),
userProtectedData = decoder.decode(TlvTag.UserProtectedData),
userCounter = decoder.decode(TlvTag.UserCounter),
userProtectedCounter = decoder.decode(TlvTag.UserProtectedCounter)
)
}
}

View file

@ -1,82 +0,0 @@
package com.tangem.commands
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
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.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
enum class SetPinStatus {
PinsNotChanged,
Pin1Changed,
Pin2Changed,
Pin3Changed,
Pins12Changed,
Pins13Changed,
Pins23Changed,
Pins123Changed,
;
companion object {
fun fromStatusWord(statusWord: StatusWord): SetPinStatus? {
return when (statusWord) {
StatusWord.ProcessCompleted -> PinsNotChanged
StatusWord.Pin1Changed -> Pin1Changed
StatusWord.Pin2Changed -> Pin2Changed
StatusWord.Pins12Changed -> Pins12Changed
StatusWord.Pin3Changed -> Pin3Changed
StatusWord.Pins13Changed -> Pins13Changed
StatusWord.Pins23Changed -> Pins23Changed
StatusWord.Pins123Changed -> Pins123Changed
else -> null
}
}
}
}
class SetPinResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
*
*/
val status: SetPinStatus
) : CommandResponse
class SetPinCommand(
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?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Pin2, environment.pin2?.value)
tlvBuilder.append(TlvTag.Cvc, environment.cvc)
tlvBuilder.append(TlvTag.NewPin, newPin1)
tlvBuilder.append(TlvTag.NewPin2, newPin2)
tlvBuilder.append(TlvTag.NewPin3, newPin3)
return CommandApdu(Instruction.SetPin, tlvBuilder.serialize())
}
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 decoder = TlvDecoder(tlvData)
return SetPinResponse(
cardId = decoder.decode(TlvTag.CardId),
status = status
)
}
}

View file

@ -1,121 +0,0 @@
package com.tangem.commands
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.sign
/**
* @param cardId CID, Unique Tangem card ID number
* @param signature Signed hashes (array of resulting signatures)
* @param walletRemainingSignatures Remaining number of sign operations before the wallet will stop signing transactions.
* @param walletSignedHashes Total number of signed single hashes returned by the card in sign command responses.
* Sums up array elements within all SIGN commands
*/
class SignResponse(
val cardId: String,
val signature: ByteArray,
val walletRemainingSignatures: Int,
val walletSignedHashes: Int
) : CommandResponse
/**
* Signs transaction hashes using a wallet private key, stored on the card.
*
* @property hashes Array of transaction hashes.
* @property cardId CID, Unique Tangem card ID number
*/
class SignCommand(private val hashes: Array<ByteArray>) : Command<SignResponse>() {
override val requiresPin2 = true
//TODO: Allow signing more than 10 hashes
private val hashSizes = if (hashes.isNotEmpty()) hashes.first().size else 0
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.isActivated) {
return TangemSdkError.NotActivated()
}
if (card.walletRemainingSignatures == 0) {
return TangemSdkError.NoRemainingSignatures()
}
if (card.signingMethods?.contains(SigningMethod.SignHash) != true) {
return TangemSdkError.SignHashesNotAvailable()
}
if (hashSizes == 0) {
return TangemSdkError.EmptyHashes()
}
if (hashes.any { it.size != hashSizes }) {
return TangemSdkError.HashSizeMustBeEqual()
}
return when (card.status) {
CardStatus.Loaded -> null
CardStatus.Empty -> TangemSdkError.
CardIsEmpty()
CardStatus.NotPersonalized -> TangemSdkError.NotPersonalized()
CardStatus.Purged -> TangemSdkError.CardIsPurged()
null -> TangemSdkError.CardError()
}
}
override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError {
if (error is TangemSdkError.InvalidParams) {
return TangemSdkError.Pin2OrCvcRequired()
}
return error
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val dataToSign = flattenHashes()
val tlvBuilder = TlvBuilder()
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)
tlvBuilder.append(TlvTag.Cvc, environment.cvc)
addTerminalSignature(environment, dataToSign, tlvBuilder)
return CommandApdu(Instruction.Sign, tlvBuilder.serialize())
}
private fun flattenHashes(): ByteArray {
return hashes.reduce { arr1, arr2 -> arr1 + arr2 }
}
/**
* Application can optionally submit a public key Terminal_PublicKey in [SignCommand].
* Submitted key is stored by the Tangem card if it differs from a previous submitted Terminal_PublicKey.
* The Tangem card will not enforce security delay if [SignCommand] will be called with
* TerminalTransactionSignature parameter containing a correct signature of raw data to be signed made with TerminalPrivateKey
* (this key should be generated and securily stored by the application).
*/
private fun addTerminalSignature(
environment: SessionEnvironment, dataToSign: ByteArray, tlvBuilder: TlvBuilder
) {
environment.terminalKeys?.let { terminalKeyPair ->
val signedData = dataToSign.sign(terminalKeyPair.privateKey)
tlvBuilder.append(TlvTag.TerminalTransactionSignature, signedData)
tlvBuilder.append(TlvTag.TerminalPublicKey, terminalKeyPair.publicKey)
}
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): SignResponse {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return SignResponse(
cardId = decoder.decode(TlvTag.CardId),
signature = decoder.decode(TlvTag.Signature),
walletRemainingSignatures = decoder.decode(TlvTag.RemainingSignatures),
walletSignedHashes = decoder.decode(TlvTag.SignedHashes)
)
}
}

View file

@ -1,111 +0,0 @@
package com.tangem.commands
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.common.DefaultIssuerDataVerifier
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.commands.common.IssuerDataVerifier
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class WriteIssuerDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String
) : CommandResponse
/**
* This command writes 512-byte Issuer Data field and its issuers signature.
* Issuer Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. For example, this field may contain information about
* wallet balance signed by the issuer or additional issuers attestation data.
* @property cardId CID, Unique Tangem card ID number.
* @property issuerData Data provided by issuer.
* @property issuerDataSignature Issuers signature of [issuerData] with Issuer Data Private Key (which is kept on card).
* @property issuerDataCounter An optional counter that protect issuer data against replay attack.
*/
class WriteIssuerDataCommand(
private val issuerData: ByteArray,
private val issuerDataSignature: ByteArray,
private val issuerDataCounter: Int? = null,
private val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : Command<WriteIssuerDataResponse>(), IssuerDataVerifier by verifier {
override fun performPreCheck(card: Card): TangemSdkError? {
val publicKey = issuerPublicKey ?: card.issuerPublicKey
?: return TangemSdkError.MissingIssuerPubicKey()
if (card.status == CardStatus.NotPersonalized) {
return TangemSdkError.NotPersonalized()
}
if (card.isActivated) {
return TangemSdkError.NotActivated()
}
if (issuerData.size > MAX_SIZE) {
return TangemSdkError.DataSizeTooLarge()
}
if (!isCounterValid(issuerDataCounter, card)) {
return TangemSdkError.MissingCounter()
}
if (!verifySignature(publicKey, card.cardId)) {
return TangemSdkError.VerificationFailed()
}
return null
}
override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError {
if (error is TangemSdkError.InvalidParams && isCounterRequired(card)) {
return TangemSdkError.DataCannotBeWritten()
}
return error
}
private fun isCounterValid(issuerDataCounter: Int?, card: Card): Boolean =
if (isCounterRequired(card)) issuerDataCounter != null else true
private fun isCounterRequired(card: Card?): Boolean =
card?.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) == true
private fun verifySignature(publicKey: ByteArray, cardId: String): Boolean {
return verify(
publicKey,
issuerDataSignature,
IssuerDataToVerify(cardId, issuerData, issuerDataCounter)
)
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
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)
tlvBuilder.append(TlvTag.IssuerDataSignature, issuerDataSignature)
tlvBuilder.append(TlvTag.IssuerDataCounter, issuerDataCounter)
return CommandApdu(Instruction.WriteIssuerData, tlvBuilder.serialize())
}
override fun deserialize(
environment: SessionEnvironment,
apdu: ResponseApdu
): WriteIssuerDataResponse {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return WriteIssuerDataResponse(
cardId = decoder.decode(TlvTag.CardId)
)
}
companion object {
const val MAX_SIZE = 512
}
}

View file

@ -1,196 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.common.DefaultIssuerDataVerifier
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.commands.common.IssuerDataVerifier
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
/**
* This command writes Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS.
* The issuer defines purpose of use, format and payload of Issuer Data.
* For example, this field may contain a photo or biometric information for ID card products.
* Because of the large size of Issuer_Extra_Data, a series of these commands have to be executed
* to write entire Issuer_Extra_Data.
* @param issuerData Data provided by issuer.
* @param startingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerDataCounter] (if flags Protect_Issuer_Data_Against_Replay and
* Restrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]) and size of [issuerData].
* @param finalizingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerData] and [issuerDataCounter] (the latter one only if flags Protect_Issuer_Data_Against_Replay
* andRestrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]).
* @param issuerDataCounter An optional counter that protect issuer data against replay attack.
*/
class WriteIssuerExtraDataCommand(
private val issuerData: ByteArray,
private val startingSignature: ByteArray,
private val finalizingSignature: ByteArray,
private val issuerDataCounter: Int? = null,
private val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : Command<WriteIssuerDataResponse>(), IssuerDataVerifier by verifier {
var mode: IssuerDataMode = IssuerDataMode.InitializeWritingExtraData
var offset: Int = 0
override fun run(
session: CardSession,
callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit
) {
writeIssuerData(session, callback)
}
override fun performPreCheck(card: Card): TangemSdkError? {
val publicKey = issuerPublicKey ?: card.issuerPublicKey
?: return TangemSdkError.MissingIssuerPubicKey()
if (card.status == CardStatus.NotPersonalized) {
return TangemSdkError.NotPersonalized()
}
if (card.isActivated) {
return TangemSdkError.NotActivated()
}
if (issuerData.size > MAX_SIZE) {
return TangemSdkError.DataSizeTooLarge()
}
if (!isCounterValid(issuerDataCounter, card)) {
return TangemSdkError.MissingCounter()
}
if (!verifySignatures(publicKey, card.cardId)) {
return TangemSdkError.VerificationFailed()
}
return null
}
override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError {
if (error is TangemSdkError.InvalidParams && isCounterRequired(card)) {
return TangemSdkError.DataCannotBeWritten()
}
if (error is TangemSdkError.InvalidState &&
card?.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) == true) {
return TangemSdkError.OverwritingDataIsProhibited()
}
return error
}
private fun isCounterValid(issuerDataCounter: Int?, card: Card): Boolean =
if (isCounterRequired(card)) issuerDataCounter != null else true
private fun isCounterRequired(card: Card?): Boolean =
card?.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) == true
private fun verifySignatures(publicKey: ByteArray, cardId: String): Boolean {
val firstData = IssuerDataToVerify(cardId, null, issuerDataCounter, issuerData.size)
val secondData = IssuerDataToVerify(cardId, issuerData, issuerDataCounter)
return verify(publicKey, startingSignature, firstData) &&
verify(publicKey, finalizingSignature, secondData)
}
private fun writeIssuerData(
session: CardSession,
callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit
) {
if (mode == IssuerDataMode.WriteExtraData) {
session.viewDelegate.onDelay(
issuerData.size,
offset,
SINGLE_WRITE_SIZE
)
}
transceive(session) { result ->
when (result) {
is CompletionResult.Success -> {
when (mode) {
IssuerDataMode.InitializeWritingExtraData -> {
mode = IssuerDataMode.WriteExtraData
writeIssuerData(session, callback)
return@transceive
}
IssuerDataMode.WriteExtraData -> {
offset += WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE
if (offset >= issuerData.size) {
mode = IssuerDataMode.FinalizeExtraData
}
writeIssuerData(session, callback)
return@transceive
}
IssuerDataMode.FinalizeExtraData -> {
callback(CompletionResult.Success(result.data))
}
}
}
is CompletionResult.Failure -> {
if (session.environment.handleErrors) {
mapError(session.environment.card, result.error)?.let {
callback(CompletionResult.Failure(it))
}
}
callback(CompletionResult.Failure(result.error))
}
}
}
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, mode)
when (mode) {
IssuerDataMode.InitializeWritingExtraData -> {
tlvBuilder.append(TlvTag.Size, issuerData.size)
tlvBuilder.append(TlvTag.IssuerDataSignature, startingSignature)
tlvBuilder.append(TlvTag.IssuerDataCounter, issuerDataCounter)
}
IssuerDataMode.WriteExtraData -> {
tlvBuilder.append(TlvTag.IssuerData, getDataToWrite())
tlvBuilder.append(TlvTag.Offset, offset)
}
IssuerDataMode.FinalizeExtraData -> {
tlvBuilder.append(TlvTag.IssuerDataSignature, finalizingSignature)
}
}
return CommandApdu(Instruction.WriteIssuerData, tlvBuilder.serialize())
}
private fun getDataToWrite(): ByteArray =
issuerData.copyOfRange(offset, offset + calculatePartSize())
private fun calculatePartSize(): Int {
val bytesLeft = issuerData.size - offset
return if (bytesLeft < SINGLE_WRITE_SIZE) bytesLeft else SINGLE_WRITE_SIZE
}
override fun deserialize(
environment: SessionEnvironment,
apdu: ResponseApdu
): WriteIssuerDataResponse {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
return WriteIssuerDataResponse(
cardId = TlvDecoder(tlvData).decode(TlvTag.CardId)
)
}
companion object {
const val SINGLE_WRITE_SIZE = 1524
const val MAX_SIZE = 32 * 1024
}
}

View file

@ -1,80 +0,0 @@
package com.tangem.commands
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class WriteUserDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String
) : CommandResponse
/**
* This command writes to the card any of User_Data, User_ProtectedData, User_Counter and User_ProtectedCounter fields.
* User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and it's payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*
* Writing of User_Counter and User_Data protected only by PIN1.
* User_ProtectedCounter and User_ProtectedData additionaly need PIN2 to confirmation.
*/
class WriteUserDataCommand(private val userData: ByteArray? = null, private val userProtectedData: ByteArray? = null,
private val userCounter: Int? = null,
private val userProtectedCounter: Int? = null) : Command<WriteUserDataResponse>() {
override val requiresPin2 = true
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.status == CardStatus.NotPersonalized) {
return TangemSdkError.NotPersonalized()
}
if (card.isActivated) {
return TangemSdkError.NotActivated()
}
if (userData?.size ?: 0 > MAX_SIZE || userProtectedData?.size ?: 0 > MAX_SIZE) {
return TangemSdkError.DataSizeTooLarge()
}
return null
}
override fun mapError(card: Card?, error: TangemSdkError): TangemSdkError {
if (error is TangemSdkError.InvalidParams) {
return TangemSdkError.Pin2OrCvcRequired()
}
return error
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val builder = TlvBuilder()
builder.append(TlvTag.CardId, environment.card?.cardId)
builder.append(TlvTag.Pin, environment.pin1?.value)
builder.append(TlvTag.UserData, userData)
builder.append(TlvTag.UserCounter, userCounter)
builder.append(TlvTag.UserProtectedData, userProtectedData)
builder.append(TlvTag.UserProtectedCounter, userProtectedCounter)
if (userProtectedCounter != null || userProtectedData != null)
builder.append(TlvTag.Pin2, environment.pin2)
return CommandApdu(Instruction.WriteUserData, builder.serialize())
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): WriteUserDataResponse {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
return WriteUserDataResponse(TlvDecoder(tlvData).decode(TlvTag.CardId))
}
companion object{
const val MAX_SIZE = 512
}
}

View file

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

View file

@ -1,44 +0,0 @@
package com.tangem.commands.common
import com.tangem.commands.WriteIssuerExtraDataCommand
/**
* This enum specifies modes for [WriteIssuerExtraDataCommand].
*/
enum class IssuerDataMode(val code: Byte) {
/**
* This mode is required to read issuer data from the card.
*/
ReadData(0),
/**
* This mode is required to write issuer data to the card.
*/
WriteData(0),
/**
* This mode is required to read issuer extra data from the card.
*/
ReadExtraData(1),
/**
* This mode is required to initiate writing issuer extra data to the card.
*/
InitializeWritingExtraData(1),
/**
* With this mode, the command writes part of issuer extra data
* (block of a size [WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE]) to the card.
*/
WriteExtraData(2),
/**
* This mode is used after the issuer extra data was fully written to the card.
* Under this mode the command provides the issuer signature
* to confirm the validity of data that was written to card.
*/
FinalizeExtraData(3);
companion object {
private val values = values()
fun byCode(code: Byte): IssuerDataMode? = values.find { it.code == code }
}
}

View file

@ -1,40 +0,0 @@
package com.tangem.commands.common
import com.tangem.common.tlv.TlvEncoder
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.CryptoUtils
import java.io.ByteArrayOutputStream
interface IssuerDataVerifier {
fun verify(
issuerPublicKey: ByteArray, signature: ByteArray, issuerDataToVerify: IssuerDataToVerify
): Boolean
}
class IssuerDataToVerify(
val cardId: String,
val issuerData: ByteArray?,
val issuerDataCounter: Int? = null,
val issuerExtraDataSize: Int? = null
)
class DefaultIssuerDataVerifier : IssuerDataVerifier {
override fun verify(
issuerPublicKey: ByteArray,
signature: ByteArray,
issuerDataToVerify: IssuerDataToVerify
): Boolean {
val tlvEncoder = TlvEncoder()
val dataToVerify = ByteArrayOutputStream()
dataToVerify.write(tlvEncoder.encodeValue(TlvTag.CardId, issuerDataToVerify.cardId))
issuerDataToVerify.issuerData?.let { dataToVerify.write(it) }
issuerDataToVerify.issuerDataCounter?.let { counter ->
dataToVerify.write(tlvEncoder.encodeValue(TlvTag.IssuerDataCounter, counter))
}
issuerDataToVerify.issuerExtraDataSize?.let {
dataToVerify.write(tlvEncoder.encodeValue(TlvTag.Size, it))
}
return CryptoUtils.verify(issuerPublicKey, dataToVerify.toByteArray(), signature)
}
}

View file

@ -1,119 +0,0 @@
package com.tangem.commands.common
import com.google.gson.*
import com.tangem.commands.*
import com.tangem.common.extensions.print
import com.tangem.common.extensions.toHexString
import java.lang.reflect.Type
import java.text.DateFormat
import java.util.*
/**
[REDACTED_AUTHOR]
*/
class ResponseConverter {
val gson: Gson by lazy { init() }
private val fieldConverter = ResponseFieldConverter()
private fun init(): Gson {
val builder = GsonBuilder().apply {
registerTypeAdapter(ByteArray::class.java, ByteTypeAdapter(fieldConverter))
registerTypeAdapter(SigningMethodMask::class.java, SigningMethodTypeAdapter(fieldConverter))
registerTypeAdapter(SettingsMask::class.java, SettingsMaskTypeAdapter(fieldConverter))
registerTypeAdapter(ProductMask::class.java, ProductMaskTypeAdapter(fieldConverter))
registerTypeAdapter(Date::class.java, DateTypeAdapter())
}
builder.setPrettyPrinting()
return builder.create()
}
fun convertResponse(response: CommandResponse?): String = gson.toJson(response)
}
class ByteTypeAdapter(
private val fieldConverter: ResponseFieldConverter
) : JsonSerializer<ByteArray> {
override fun serialize(src: ByteArray, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
return JsonPrimitive(fieldConverter.byteArrayToHex(src))
}
}
class SettingsMaskTypeAdapter(
private val fieldConverter: ResponseFieldConverter
) : JsonSerializer<SettingsMask> {
override fun serialize(src: SettingsMask, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
return JsonArray().apply {
fieldConverter.settingsMaskList(src).forEach { add(it) }
}
}
}
class ProductMaskTypeAdapter(
private val fieldConverter: ResponseFieldConverter
) : JsonSerializer<ProductMask> {
override fun serialize(src: ProductMask, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
return JsonArray().apply {
fieldConverter.productMaskList(src).forEach { add(it) }
}
}
}
class SigningMethodTypeAdapter(
private val fieldConverter: ResponseFieldConverter
) : JsonSerializer<SigningMethodMask> {
override fun serialize(src: SigningMethodMask, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
return JsonArray().apply {
fieldConverter.signingMethodList(src).forEach { add(it) }
}
}
}
class DateTypeAdapter : JsonSerializer<Date> {
override fun serialize(src: Date, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
val formatter = DateFormat.getDateInstance(DateFormat.DEFAULT, Locale("en_US"))
return JsonPrimitive(formatter.format(src).toString())
}
}
class ResponseFieldConverter {
fun productMask(productMask: ProductMask?): String {
return productMaskList(productMask).print(wrap = false)
}
fun productMaskList(productMask: ProductMask?): List<String> {
val mask = productMask ?: return emptyList()
return Product.values().filter { mask.contains(it) }.map { it.name }
}
fun signingMethod(signingMask: SigningMethodMask?): String {
return signingMethodList(signingMask).print(wrap = false)
}
fun signingMethodList(signingMask: SigningMethodMask?): List<String> {
val mask = signingMask ?: return emptyList()
return SigningMethod.values().filter { mask.contains(it) }.map { it.name }
}
fun settingsMask(settingsMask: SettingsMask?): String {
return settingsMaskList(settingsMask).print(wrap = false)
}
fun settingsMaskList(settingsMask: SettingsMask?): List<String> {
val masks = settingsMask ?: return emptyList()
return Settings.values().filter { masks.contains(it) }.map { it.name }
}
fun byteArrayToHex(byteArray: ByteArray?): String? {
return byteArray?.toHexString()
}
fun byteArrayToString(byteArray: ByteArray?): String? {
return if (byteArray == null) null else String(byteArray)
}
}

View file

@ -1,39 +0,0 @@
package com.tangem.commands.common.network
import com.tangem.Log
import kotlinx.coroutines.delay
import java.io.IOException
suspend fun <T> retryIO(
times: Int = 3,
initialDelay: Long = 100,
maxDelay: Long = 1000,
factor: Double = 2.0,
block: suspend () -> T): T
{
var currentDelay = initialDelay
repeat(times - 1) {
try {
return block()
} catch (e: IOException) {
Log.i("Network", e.localizedMessage)
}
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
return block()
}
sealed class Result<out T> {
data class Success<out T>(val data: T) : Result<T>()
data class Failure(val error: Throwable?) : Result<Nothing>()
}
suspend fun <T>performRequest(block: suspend () -> T): Result<T> {
return try {
val result = retryIO { block() }
Result.Success(result)
} catch (exception: Exception) {
Result.Failure(exception)
}
}

View file

@ -1,30 +0,0 @@
package com.tangem.commands.common.network
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
class ApiTangem {
companion object {
const val TANGEM_ENDPOINT: String = "https://verify.tangem.com/"
const val VERIFY = "verify"
const val VERIFY_AND_GET_INFO = "card/verify-and-get-info"
const val ARTWORK = "card/artwork"
}
}
private val moshi: Moshi by lazy {
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
fun createRetrofitInstance(baseUrl: String): Retrofit =
Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()

View file

@ -1,17 +0,0 @@
package com.tangem.commands.common.network
import com.tangem.commands.common.network.ApiTangem.Companion.VERIFY_AND_GET_INFO
import com.tangem.commands.verifycard.CardVerifyAndGetInfo
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
interface TangemApi {
@Headers("Content-Type: application/json")
@POST(VERIFY_AND_GET_INFO)
suspend fun getCardVerifyAndGetInfo(@Body requestBody: CardVerifyAndGetInfo.Request): CardVerifyAndGetInfo.Response
}

View file

@ -1,22 +0,0 @@
package com.tangem.commands.common.network
import com.tangem.commands.verifycard.CardVerifyAndGetInfo
class TangemService {
private val tangemApi: TangemApi by lazy {
createRetrofitInstance(ApiTangem.TANGEM_ENDPOINT).create(TangemApi::class.java)
}
suspend fun verifyAndGetInfo(
cardId: String,
cardPublicKey: String
): Result<CardVerifyAndGetInfo.Response> {
val requestsBody = CardVerifyAndGetInfo.Request()
requestsBody.requests =
listOf(CardVerifyAndGetInfo.Request.Item(cardId, cardPublicKey))
return performRequest { tangemApi.getCardVerifyAndGetInfo(requestsBody) }
}
}

View file

@ -1,31 +0,0 @@
package com.tangem.commands.personalization
import com.tangem.SessionEnvironment
import com.tangem.commands.Command
import com.tangem.commands.CommandResponse
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
data class DepersonalizeResponse(val success: Boolean) : CommandResponse
/**
* Command available on SDK cards only
*
* This command resets card to initial state,
* erasing all data written during personalization and usage.
*/
class DepersonalizeCommand : Command<DepersonalizeResponse>() {
override val performPreflightRead = false
override fun serialize(environment: SessionEnvironment): CommandApdu {
return CommandApdu(
Instruction.Depersonalize, byteArrayOf()
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): DepersonalizeResponse {
return DepersonalizeResponse(true)
}
}

View file

@ -1,88 +0,0 @@
package com.tangem.commands.personalization
import com.tangem.commands.personalization.entities.NdefRecord
import java.io.ByteArrayOutputStream
import java.nio.charset.StandardCharsets
/**
* Encodes information that is to be written on the card as an Ndef Tag.
*/
class NdefEncoder(private val ndefRecords: List<NdefRecord>, private val useDinamicNdef: Boolean) {
fun encode(): ByteArray {
val bs = ByteArrayOutputStream()
// space for size
bs.write(0)
bs.write(0)
for (i in ndefRecords.indices) {
val headerValue = (if (i == 0) 0x80 else 0x00) or (if (!useDinamicNdef && i == ndefRecords.size - 1) 0x40 else 0x00)
var value: ByteArray = ndefRecords[i].value.toByteArray(StandardCharsets.UTF_8)
encodeValue(ndefRecords[i], headerValue, bs)
}
val result = bs.toByteArray()
result[0] = (result.size - 2 shr 8).toByte()
result[1] = (result.size - 2 and 0xFF).toByte()
return result
}
private fun encodeValue(ndefRecord: NdefRecord, headerValue: Int, bs: ByteArrayOutputStream) {
when (ndefRecord.type) {
NdefRecord.Type.AAR -> {
bs.write((headerValue or 0x14)) // NDEF Header
bs.write(0x0F) // Length of the record type
bs.write(ndefRecord.valueInBytes.size) // Length of the payload data
bs.write(byteArrayOf(0x61.toByte(), 0x6E.toByte(), 0x64.toByte(), 0x72.toByte(), 0x6F.toByte(), 0x69.toByte(), 0x64.toByte(), 0x2E.toByte(), 0x63.toByte(), 0x6F.toByte(), 0x6D.toByte(), 0x3A.toByte(),
0x70.toByte(), 0x6B.toByte(), 0x67.toByte())) // type name
bs.write(ndefRecord.valueInBytes)
}
NdefRecord.Type.URI -> {
bs.write((headerValue or 0x11)) // NDEF Header
bs.write(0x01) // Length of the record type
val uriIdentifierCode: Byte
val prefix: String
when {
ndefRecord.value.startsWith("http://www.") -> {
uriIdentifierCode = 0x01.toByte()
prefix = "http://www."
}
ndefRecord.value.startsWith("https://www.") -> {
uriIdentifierCode = 0x02.toByte()
prefix = "https://www."
}
ndefRecord.value.startsWith("http://") -> {
uriIdentifierCode = 0x03.toByte()
prefix = "http://"
}
ndefRecord.value.startsWith("https://") -> {
uriIdentifierCode = 0x04.toByte()
prefix = "https://"
}
else -> {
throw Exception()
}
}
val value = ndefRecord.value.substring(prefix.length).toByteArray()
bs.write(value.size + 1) // Length of the payload data
bs.write(0x55) // URI
bs.write(uriIdentifierCode.toInt()) // ?
bs.write(value)
}
NdefRecord.Type.TEXT -> {
bs.write((headerValue or 0x11)) // NDEF Header
bs.write(0x01) // Length of the record type
bs.write(ndefRecord.valueInBytes.size.toByte() + 1 + "en".length) // Length of the payload data
bs.write(0x54) // Text
bs.write(0x02) // UTF8(MSB=0)|"en".length
bs.write("en".toByteArray(StandardCharsets.US_ASCII))
bs.write(ndefRecord.valueInBytes)
}
else -> throw Exception("Invalid NDEF record in config!")
}
}
}

View file

@ -1,125 +0,0 @@
package com.tangem.commands.personalization
import com.tangem.CardSession
import com.tangem.EncryptionMode
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.Card
import com.tangem.commands.CardData
import com.tangem.commands.CardStatus
import com.tangem.commands.Command
import com.tangem.commands.common.CardDeserializer
import com.tangem.commands.personalization.entities.*
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.sign
/**
* Command available on SDK cards only
*
* Personalization is an initialization procedure, required before starting using a card.
* During this procedure a card setting is set up.
* During this procedure all data exchange is encrypted.
* @property config is a configuration file with all the card settings that are written on the card
* during personalization.
* @property issuer Issuer is a third-party team or company wishing to use Tangem cards.
* @property manufacturer Tangem Card Manufacturer.
* @property acquirer Acquirer is a trusted third-party company that operates proprietary
* (non-EMV) POS terminal infrastructure and transaction processing back-end.
*/
class PersonalizeCommand(
private val config: CardConfig,
private val issuer: Issuer, private val manufacturer: Manufacturer,
private val acquirer: Acquirer? = null
) : Command<Card>() {
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
val encryptionMode = session.environment.encryptionMode
val encryptionKey = session.environment.encryptionKey
session.environment.encryptionMode = EncryptionMode.NONE
session.environment.encryptionKey = devPersonalizationKey
super.run(session) { result ->
session.environment.encryptionMode = encryptionMode
session.environment.encryptionKey = encryptionKey
callback(result)
}
}
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.status != CardStatus.NotPersonalized) {
return TangemSdkError.AlreadyPersonalized()
}
return null
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
return CommandApdu(Instruction.Personalize, serializePersonalizationData(config))
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): Card {
return CardDeserializer.deserialize(apdu)
}
private fun serializePersonalizationData(config: CardConfig): ByteArray {
val cardId = config.createCardId() ?: throw TangemSdkError.SerializeCommandError()
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.CardId, cardId)
tlvBuilder.append(TlvTag.CurveId, config.curveID)
tlvBuilder.append(TlvTag.MaxSignatures, config.maxSignatures)
tlvBuilder.append(TlvTag.SigningMethod, config.signingMethods)
tlvBuilder.append(TlvTag.SettingsMask, config.createSettingsMask())
tlvBuilder.append(TlvTag.PauseBeforePin2, config.pauseBeforePin2 / 10)
tlvBuilder.append(TlvTag.Cvc, config.cvc.toByteArray())
if (!config.ndefRecords.isNullOrEmpty())
tlvBuilder.append(TlvTag.NdefData, serializeNdef(config))
tlvBuilder.append(TlvTag.CreateWalletAtPersonalize, config.createWallet)
tlvBuilder.append(TlvTag.NewPin, config.pin)
tlvBuilder.append(TlvTag.NewPin2, config.pin2)
tlvBuilder.append(TlvTag.NewPin3, config.pin3)
tlvBuilder.append(TlvTag.CrExKey, config.hexCrExKey)
tlvBuilder.append(TlvTag.IssuerDataPublicKey, issuer.dataKeyPair.publicKey)
tlvBuilder.append(TlvTag.IssuerTransactionPublicKey, issuer.transactionKeyPair.publicKey)
tlvBuilder.append(TlvTag.AcquirerPublicKey, acquirer?.keyPair?.publicKey)
tlvBuilder.append(TlvTag.CardData, serializeCardData(cardId, config.cardData))
return tlvBuilder.serialize()
}
private fun serializeNdef(config: CardConfig): ByteArray {
return NdefEncoder(config.ndefRecords, config.useDynamicNdef).encode()
}
private fun serializeCardData(cardId: String, cardData: CardData): ByteArray {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Batch, cardData.batchId)
tlvBuilder.append(TlvTag.ProductMask, cardData.productMask)
tlvBuilder.append(TlvTag.ManufactureDateTime, cardData.manufactureDateTime)
tlvBuilder.append(TlvTag.IssuerId, issuer.id)
tlvBuilder.append(TlvTag.BlockchainId, cardData.blockchainName)
if (cardData.tokenSymbol != null) {
tlvBuilder.append(TlvTag.TokenSymbol, cardData.tokenSymbol)
tlvBuilder.append(TlvTag.TokenContractAddress, cardData.tokenContractAddress)
tlvBuilder.append(TlvTag.TokenDecimal, cardData.tokenDecimal)
}
tlvBuilder.append(
TlvTag.CardIdManufacturerSignature,
cardId.hexToBytes().sign(manufacturer.keyPair.privateKey)
)
return tlvBuilder.serialize()
}
companion object {
val devPersonalizationKey = "1234".calculateSha256().copyOf(32)
}
}

View file

@ -1,9 +0,0 @@
package com.tangem.commands.personalization.entities
import com.tangem.KeyPair
data class Acquirer(
val keyPair: KeyPair,
val name: String? = null,
val id: String? = null
)

View file

@ -1,77 +0,0 @@
package com.tangem.commands.personalization.entities
import com.tangem.commands.CardData
import com.tangem.commands.EllipticCurve
import com.tangem.commands.SigningMethodMask
import com.tangem.common.extensions.calculateSha256
data class NdefRecord(
val type: Type,
val value: String
) {
enum class Type {
URI, AAR, TEXT
}
@delegate:Transient
val valueInBytes: ByteArray by lazy { value.toByteArray() }
}
/**
* It is a configuration file with all the card settings that are written on the card
* during [PersonalizeCommand].
*/
class CardConfig(
val issuerName: String? = null,
val acquirerName: String? = null,
val series: String? = null,
val startNumber: Long = 0,
val count: Int = 0,
pin: String,
pin2: String,
pin3: String,
val hexCrExKey: String?,
val cvc: String,
val pauseBeforePin2: Int,
val smartSecurityDelay: Boolean,
val curveID: EllipticCurve,
val signingMethods: SigningMethodMask,
val maxSignatures: Int,
val isReusable: Boolean,
val allowSwapPin: Boolean,
val allowSwapPin2: Boolean,
val useActivation: Boolean,
val useCvc: Boolean,
val useNdef: Boolean,
val useDynamicNdef: Boolean,
val useOneCommandAtTime: Boolean,
val useBlock: Boolean,
val allowSelectBlockchain: Boolean,
val forbidPurgeWallet: Boolean,
val protocolAllowUnencrypted: Boolean,
val protocolAllowStaticEncryption: Boolean,
val protectIssuerDataAgainstReplay: Boolean,
val forbidDefaultPin: Boolean,
val disablePrecomputedNdef: Boolean,
val skipSecurityDelayIfValidatedByIssuer: Boolean,
val skipCheckPIN2andCVCIfValidatedByIssuer: Boolean,
val skipSecurityDelayIfValidatedByLinkedTerminal: Boolean,
val restrictOverwriteIssuerDataEx: Boolean,
val requireTerminalTxSignature: Boolean,
val requireTerminalCertSignature: Boolean,
val checkPin3onCard: Boolean,
val createWallet: Boolean,
val cardData: CardData,
val ndefRecords: List<NdefRecord>
) {
val pin: ByteArray = pin.calculateSha256()
val pin2: ByteArray = pin2.calculateSha256()
val pin3: ByteArray = pin3.calculateSha256()
companion object
}

View file

@ -1,81 +0,0 @@
package com.tangem.commands.personalization.entities
import com.tangem.commands.Settings
import com.tangem.commands.SettingsMask
import com.tangem.commands.SettingsMaskBuilder
internal fun CardConfig.createSettingsMask(): SettingsMask {
val builder = SettingsMaskBuilder()
if (allowSwapPin) builder.add(Settings.AllowSwapPIN)
if (allowSwapPin2) builder.add(Settings.AllowSwapPIN2)
if (useCvc) builder.add(Settings.UseCVC)
if (isReusable) builder.add(Settings.IsReusable)
if (useOneCommandAtTime) builder.add(Settings.UseOneCommandAtTime)
if (useNdef) builder.add(Settings.UseNdef)
if (useDynamicNdef) builder.add(Settings.UseDynamicNdef)
if (disablePrecomputedNdef) builder.add(Settings.DisablePrecomputedNdef)
if (protocolAllowUnencrypted) builder.add(Settings.ProtocolAllowUnencrypted)
if (protocolAllowStaticEncryption) builder.add(Settings.ProtocolAllowStaticEncryption)
if (forbidDefaultPin) builder.add(Settings.ForbidDefaultPIN)
if (useActivation) builder.add(Settings.UseActivation)
if (useBlock) builder.add(Settings.UseBlock)
if (smartSecurityDelay) builder.add(Settings.SmartSecurityDelay)
if (protectIssuerDataAgainstReplay) builder.add(Settings.ProtectIssuerDataAgainstReplay)
if (forbidPurgeWallet) builder.add(Settings.ProhibitPurgeWallet)
if (allowSelectBlockchain) builder.add(Settings.AllowSelectBlockchain)
if (skipCheckPIN2andCVCIfValidatedByIssuer) builder.add(Settings.SkipCheckPin2andCvcIfValidatedByIssuer)
if (skipSecurityDelayIfValidatedByIssuer) builder.add(Settings.SkipSecurityDelayIfValidatedByIssuer)
if (skipSecurityDelayIfValidatedByLinkedTerminal) builder.add(Settings.SkipSecurityDelayIfValidatedByLinkedTerminal)
if (restrictOverwriteIssuerDataEx) builder.add(Settings.RestrictOverwriteIssuerDataEx)
if (requireTerminalTxSignature) builder.add(Settings.RequireTermTxSignature)
if (requireTerminalCertSignature) builder.add(Settings.RequireTermCertSignature)
if (checkPin3onCard) builder.add(Settings.CheckPIN3onCard)
return builder.build()
}
internal fun CardConfig.createCardId(): String? {
if (series == null) return null
if (startNumber <= 0 || (series.length != 2 && series.length != 4)) return null
val Alf = "ABCDEF0123456789"
fun checkSeries(series: String): Boolean {
val containsList = series.filter { Alf.contains(it) }
return containsList.length == series.length
}
if (!checkSeries(series)) return null
val tail = if (series.length == 2) String.format("%013d", startNumber) else String.format("%011d", startNumber)
var cardId = (series + tail).replace(" ", "")
if (cardId.length != 15 || Alf.indexOf(cardId[0]) == -1 || Alf.indexOf(cardId[1]) == -1)
return null
cardId += "0"
val length = cardId.length
var sum = 0
for (i in 0 until length) {
// get digits in reverse order
var digit: Int
val cDigit = cardId[length - i - 1]
digit = if (cDigit in '0'..'9') cDigit - '0' else cDigit - 'A'
// every 2nd number multiply with 2
if (i % 2 == 1) digit *= 2
sum += if (digit > 9) digit - 9 else digit
}
val lunh = (10 - sum % 10) % 10
return cardId.substring(0, 15) + String.format("%d", lunh)
}

View file

@ -1,10 +0,0 @@
package com.tangem.commands.personalization.entities
import com.tangem.KeyPair
data class Issuer(
val name: String,
val id: String,
val dataKeyPair: KeyPair,
val transactionKeyPair: KeyPair
)

View file

@ -1,8 +0,0 @@
package com.tangem.commands.personalization.entities
import com.tangem.KeyPair
data class Manufacturer(
val keyPair: KeyPair,
val name: String? = null
)

View file

@ -1,159 +0,0 @@
package com.tangem.commands.verifycard
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.Card
import com.tangem.commands.CardStatus
import com.tangem.commands.Command
import com.tangem.commands.CommandResponse
import com.tangem.commands.common.network.Result
import com.tangem.commands.common.network.TangemService
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.CardType
import com.tangem.common.extensions.getType
import com.tangem.common.extensions.toHexString
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.CryptoUtils
import kotlinx.coroutines.launch
class VerifyCardResponse(
val cardId: String,
val verificationState: VerifyCardState? = null,
val artworkInfo: ArtworkInfo? = null,
internal val salt: ByteArray,
internal val cardSignature: ByteArray
) : CommandResponse {
fun verify(publicKey: ByteArray, challenge: ByteArray): Boolean {
return CryptoUtils.verify(
publicKey,
challenge + salt,
cardSignature
)
}
}
enum class VerifyCardState {
VerifiedOnline,
VerifiedOffline,
}
class VerifyCardCommand(private val onlineVerification: Boolean) : Command<VerifyCardResponse>() {
private val challenge = CryptoUtils.generateRandomBytes(16)
private val tangemService = TangemService()
override fun run(
session: CardSession,
callback: (result: CompletionResult<VerifyCardResponse>) -> Unit
) {
val card = session.environment.card
val cardPublicKey = card?.cardPublicKey
if (cardPublicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
super.run(session) { result ->
when (result) {
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
is CompletionResult.Success -> {
val response = result.data
val verified = response.verify(cardPublicKey, challenge)
if (!verified) {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
return@run
}
if (!onlineVerification || card.getType() != CardType.Release) {
callback(
CompletionResult.Success(
VerifyCardResponse(
response.cardId, VerifyCardState.VerifiedOffline, null,
response.salt, response.cardSignature
)
)
)
} else {
verify(result.data, card.cardId, cardPublicKey, session, callback)
}
}
}
}
}
private fun verify(
response: VerifyCardResponse, cardId: String, cardPublicKey: ByteArray,
session: CardSession,
callback: (result: CompletionResult<VerifyCardResponse>) -> Unit
) {
session.scope.launch {
val result = tangemService.verifyAndGetInfo(cardId, cardPublicKey.toHexString())
when (result) {
is Result.Success -> {
if (result.data.results?.firstOrNull()?.passed == true) {
callback(
CompletionResult.Success(
VerifyCardResponse(
response.cardId, VerifyCardState.VerifiedOnline, response.artworkInfo,
response.salt, response.cardSignature
)
)
)
} else {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
}
}
is Result.Failure -> {
callback(
CompletionResult.Success(
VerifyCardResponse(
response.cardId, VerifyCardState.VerifiedOffline, null,
response.salt, response.cardSignature
)
)
)
}
}
}
}
override fun performPreCheck(card: Card): TangemSdkError? {
if (card.status == CardStatus.NotPersonalized) {
return TangemSdkError.NotPersonalized()
}
if (card.isActivated) {
return TangemSdkError.NotActivated()
}
return null
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
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())
}
override fun deserialize(
environment: SessionEnvironment,
apdu: ResponseApdu
): VerifyCardResponse {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return VerifyCardResponse(
cardId = decoder.decode(TlvTag.CardId),
salt = decoder.decode(TlvTag.Salt),
cardSignature = decoder.decode(TlvTag.CardSignature)
)
}
}

View file

@ -1,60 +0,0 @@
package com.tangem.commands.verifycard
import com.squareup.moshi.JsonClass
import java.text.SimpleDateFormat
import java.util.*
class CardVerifyAndGetInfo {
@JsonClass(generateAdapter = true)
data class Request(
var requests: List<Item>? = null
) {
@JsonClass(generateAdapter = true)
data class Item(
var CID: String = "",
var publicKey: String = ""
)
}
@JsonClass(generateAdapter = true)
data class Response(
var results: List<Item>? = null
) {
@JsonClass(generateAdapter = true)
data class Item(
var error: String? = null,
var CID: String = "",
var passed: Boolean = false,
var batch: String = "",
var artwork: ArtworkInfo? = null,
var substitution: SubstitutionInfo? = null
) {
@JsonClass(generateAdapter = true)
data class SubstitutionInfo(
var data: String? = null,
var signature: String? = null
)
}
}
}
@JsonClass(generateAdapter = true)
data class ArtworkInfo(
var id: String = "",
var hash: String = "",
var date: String = ""
) {
fun getUpdateDate(): Date? {
return try {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US).parse(date)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}

View file

@ -1,56 +0,0 @@
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.VerificationState
interface CardValuesStorage {
fun saveValues(cardId: String,
isPin1Default: Boolean, isPin2Default: Boolean,
cardVerification: VerificationState?,
cardValidation: VerificationState?,
codeVerification: VerificationState?)
fun getValues(cardId: String): CardValues?
}
class CardValuesDbStorage(driver: SqlDriver) : CardValuesStorage {
private val cardValuesQueries: CardValuesEntityQueries
init {
val database = Database(driver, cardValuesAdapter = CardValues.Adapter(
cardVerificationAdapter = EnumColumnAdapter(),
cardValidationAdapter = EnumColumnAdapter(),
codeVerificationAdapter = EnumColumnAdapter()
))
cardValuesQueries = database.cardValuesEntityQueries
}
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? =
cardValuesQueries.selectByCardId(cardId).executeAsOneOrNull()
companion object {
fun initJvm() = CardValuesDbStorage(
JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY).also { Database.Schema.create(it) }
)
}
}

View file

@ -1,13 +0,0 @@
package com.tangem.common
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult.Success
/**
* Response class encapsulating successful and failed results.
* @param T Type of data that is returned in [Success].
*/
sealed class CompletionResult<T> {
class Success<T>(val data: T) : CompletionResult<T>()
class Failure<T>(val error: TangemSdkError) : CompletionResult<T>()
}

View file

@ -1,10 +0,0 @@
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

@ -1,14 +0,0 @@
package com.tangem.common
import com.tangem.KeyPair
/**
* Interface for a service for managing Terminal keypair, used for Linked Terminal feature.
* Its implementation Needs to be provided to [com.tangem.TangemSdk]
* by calling [com.tangem.TangemSdk.setTerminalKeysService].
* Default implementation is provided in tangem-sdk module: [TerminalKeysStorage].
* Linked Terminal feature can be disabled manually by editing [com.tangem.Config].
*/
interface TerminalKeysService {
fun getKeys(): KeyPair
}

View file

@ -1,89 +0,0 @@
package com.tangem.common.apdu
import com.tangem.EncryptionMode
import com.tangem.common.extensions.calculateCrc16
import com.tangem.common.extensions.toByteArray
import com.tangem.crypto.encrypt
import java.io.ByteArrayOutputStream
/**
* Class that provides conversion of serialized request and Instruction code
* to a raw data that can be sent to the card.
*
* @property ins Instruction code that determines the type of request for the card.
* @property tlvs Tlvs encoded to a [ByteArray] that are to be sent to the card.
*/
class CommandApdu(
val ins: Int,
private val tlvs: ByteArray,
private val p1: Int,
private val p2: Int,
private val le: Int = 0x00,
private val cla: Int = ISO_CLA
) {
constructor(
instruction: Instruction,
tlvs: ByteArray
) : this(
instruction.code,
tlvs,
0,
0
)
/**
* Request converted to a raw data
*/
val apduData: ByteArray
init {
apduData = toBytes()
}
private fun toBytes(): ByteArray {
val data = tlvs
val byteStream = ByteArrayOutputStream()
byteStream.write(cla)
byteStream.write(ins)
byteStream.write(p1)
byteStream.write(p2)
if (data.isNotEmpty()) {
byteStream.writeLength(data.size)
byteStream.write(data)
}
return byteStream.toByteArray()
}
private fun ByteArrayOutputStream.writeLength(lc: Int) {
this.write(0)
this.write(lc shr 8)
this.write(lc and 0xFF)
}
fun encrypt(
encryptionMode: EncryptionMode,
encryptionKey: ByteArray?
): CommandApdu {
if (encryptionKey == null || p1 != EncryptionMode.NONE.code) {
return this
}
val crc: ByteArray = tlvs.calculateCrc16()
val dataToEncrypt = tlvs.size.toByteArray(2) + crc + tlvs
val encryptedData = dataToEncrypt.encrypt(encryptionKey)
return CommandApdu(ins, encryptedData, encryptionMode.code, p2, le, cla)
}
companion object {
const val ISO_CLA = 0x00
}
}

View file

@ -1,32 +0,0 @@
package com.tangem.common.apdu
/**
* Instruction code that determines the type of the command that is sent to the Tangem card.
* It is used in the construction of [com.tangem.common.apdu.CommandApdu].
*/
enum class Instruction(var code: Int) {
Unknown(0x00),
Personalize(0xF1),
Read(0xF2),
VerifyCard(0xF3),
ValidateCard(0xF4),
VerifyCode(0xF5),
WriteIssuerData(0xF6),
ReadIssuerData(0xF7),
CreateWallet(0xF8),
CheckWallet(0xF9),
SetPin(0xFA),
Sign(0xFB),
PurgeWallet(0xFC),
Activate(0xFE),
OpenSession(0xFF),
WriteUserData(0xE0),
ReadUserData(0xE1),
Depersonalize(0xE3);
companion object {
private val values = values()
fun byCode(code: Int): Instruction = values.find { it.code == code } ?: Unknown
}
}

View file

@ -1,64 +0,0 @@
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
import java.io.ByteArrayInputStream
/**
* Stores response data from the card and parses it to [Tlv] and [StatusWord].
*
* @property data Raw response from the card.
* @property sw Status word code, reflecting the status of the response.
* @property statusWord Parsed status word.
*/
class ResponseApdu(private val data: ByteArray) {
private val sw1: Int = 0x00FF and data[data.size - 2].toInt()
private val sw2: Int = 0x00FF and data[data.size - 1].toInt()
val sw: Int = sw1 shl 8 or sw2
val statusWord: StatusWord = StatusWord.byCode(sw)
/**
* Converts raw response data to the list of TLVs.
*
* @param encryptionKey key to decrypt response.
* (Encryption / decryption functionality is not implemented yet.)
*/
fun getTlvData(): List<Tlv>? {
return if (data.size <= 2) {
null
} else {
Tlv.deserialize(data.copyOf(data.size - 2))
}
}
fun decrypt(encryptionKey: ByteArray?): ResponseApdu {
if (encryptionKey == null) return this
//nothing to decrypt
if (data.size < 18) return this
val responseData = data.copyOf(data.size - 2)
val decryptedData: ByteArray = responseData.decrypt(encryptionKey)
val inputStream = ByteArrayInputStream(decryptedData)
val baLength = ByteArray(2)
inputStream.read(baLength)
val length = (baLength[0].toInt() and 0xFF) * 256 + (baLength[1].toInt() and 0xFF)
if (length > decryptedData.size - 4) throw 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 TangemSdkError.InvalidResponse()
return ResponseApdu(answerData + data[data.size - 2] + data[data.size - 1])
}
}

View file

@ -1,49 +0,0 @@
package com.tangem.common.apdu
import com.tangem.TangemSdkError
/**
* Part of a response from the card, shows the status of the operation
*/
enum class StatusWord(val code: Int) {
ProcessCompleted(0x9000),
InvalidParams(0x6A86),
ErrorProcessingCommand(0x6286),
InvalidState(0x6985),
//PinsNotChanged(0x9000) is equal to ProcessCompleted(0x9000)
Pin1Changed(0x9001),
Pin2Changed(0x9002),
Pins12Changed(0x9003),
Pin3Changed(0x9004),
Pins13Changed(0x9005),
Pins23Changed(0x9006),
Pins123Changed(0x9007),
InsNotSupported(0x6D00),
NeedEncryption(0x6982),
NeedPause(0x9789),
Unknown(0x0000);
companion object {
private val values = values()
fun byCode(code: Int): StatusWord = values.find { it.code == code } ?: Unknown
}
}
fun StatusWord.toTangemSdkError(): TangemSdkError? {
return when (this) {
StatusWord.ProcessCompleted, StatusWord.Pin1Changed,
StatusWord.Pin2Changed, StatusWord.Pins12Changed, StatusWord.Pin3Changed,
StatusWord.Pins13Changed, StatusWord.Pins23Changed, StatusWord.Pins123Changed -> null
StatusWord.NeedPause -> null
StatusWord.InvalidParams -> TangemSdkError.InvalidParams()
StatusWord.ErrorProcessingCommand -> TangemSdkError.ErrorProcessingCommand()
StatusWord.InvalidState -> TangemSdkError.InvalidState()
StatusWord.InsNotSupported -> TangemSdkError.InsNotSupported()
StatusWord.NeedEncryption -> TangemSdkError.NeedEncryption()
StatusWord.Unknown -> TangemSdkError.UnknownStatus()
}
}

View file

@ -1,7 +0,0 @@
package com.tangem.common.extensions
import java.math.BigDecimal
fun BigDecimal.isZero() : Boolean {
return this.compareTo(BigDecimal.ZERO) == 0
}

View file

@ -1,74 +0,0 @@
package com.tangem.common.extensions
import org.spongycastle.crypto.digests.RIPEMD160Digest
import org.spongycastle.jce.ECNamedCurveTable
import java.nio.ByteBuffer
import java.security.MessageDigest
import java.util.*
import kotlin.experimental.and
import kotlin.experimental.xor
/**
* Extension functions for [ByteArray].
*/
fun ByteArray.toHexString(): String = joinToString("") { "%02X".format(it) }
fun ByteArray.toUtf8(): String = String(this).removeSuffix("\u0000")
fun ByteArray.toInt(): Int {
return when (this.size) {
1 -> (this[0] and 0xFF.toByte()).toInt()
2 -> ByteBuffer.wrap(this).short.toInt()
4 -> ByteBuffer.wrap(this).int
else -> throw IllegalArgumentException("Length must be 1,2 or 4. Length = " + this.size)
}
}
fun ByteArray.toDate(): Date {
val year = copyOfRange(0, 2).toInt()
val month = if (this.size > 2) this[2] - 1 else 0
val day = if (this.size > 3) this[3].toInt() else 0
val cd = Calendar.getInstance()
cd.set(year, month, day, 0, 0, 0)
return cd.time
}
fun ByteArray.calculateSha512(): ByteArray = MessageDigest.getInstance("SHA-512").digest(this)
fun ByteArray.calculateSha256(): ByteArray = MessageDigest.getInstance("SHA-256").digest(this)
fun ByteArray.calculateRipemd160(): ByteArray {
val digest = RIPEMD160Digest()
digest.update(this, 0, this.size)
val out = ByteArray(20)
digest.doFinal(out, 0)
return out
}
fun ByteArray.toCompressedPublicKey(): ByteArray {
return if (this.size == 65) {
val spec = ECNamedCurveTable.getParameterSpec("secp256k1")
val publicKeyPoint = spec.curve.decodePoint(this)
publicKeyPoint.getEncoded(true)
} else {
this
}
}
fun ByteArray.calculateCrc16(): ByteArray {
var chBlock: Byte
// STEP 1 Initialize the CRC-16 value
var wCRC = 0x6363 // ITU-V.41
var i = 0
// STEP 2 Update data and Calucuate their CRC
do {
chBlock = this.get(i++)
chBlock = chBlock xor (wCRC and 0x00FF).toByte()
val chBlockInt = (chBlock.toInt() xor (chBlock.toInt() shl 4))
wCRC = wCRC shr 8 xor (chBlockInt and 0xFF shl 8) and 0xFFFF xor (chBlockInt and 0xFF shl 3 and 0xFFFF) xor (chBlockInt and 0xFF shr 4 and 0xFFFF)
// (wCRC>>8)^((int)chBlock<<8)^((int) chBlock<<3)^((int)chBlock>>4);
} while (i < this.size)
return byteArrayOf((wCRC and 0xFF).toByte(), (wCRC and 0xFFFF shr 8).toByte())
}

View file

@ -1,22 +0,0 @@
package com.tangem.common.extensions
import com.tangem.commands.Card
fun Card.getType(): CardType {
val firmware = this.firmwareVersion ?: return CardType.Unknown
return when {
firmware.endsWith("d SDK") -> {
CardType.Sdk
}
firmware.endsWith("r") -> {
CardType.Release
}
else -> {
CardType.Unknown
}
}
}
enum class CardType {
Sdk, Release, Unknown
}

View file

@ -1,16 +0,0 @@
package com.tangem.common.extensions
import java.nio.ByteBuffer
fun Int.toByteArray(size: Int = Int.SIZE_BYTES): ByteArray {
if (size == Int.SIZE_BYTES) {
val buffer = ByteBuffer.allocate(size)
buffer.putInt(this)
return buffer.array()
} else if (size == Short.SIZE_BYTES){
return byteArrayOf(
(this ushr 8).toByte(),
this.toByte())
}
return byteArrayOf()
}

View file

@ -1,13 +0,0 @@
package com.tangem.common.extensions
fun <T> List<T>.print(delimiter: String = ", ", wrap: Boolean = true): String {
val builder = StringBuilder()
forEach { builder.append(it).append(delimiter) }
val length = builder.length
if (length > delimiter.length) {
builder.delete(length - delimiter.length, length)
}
val result = builder.toString()
return if (wrap) "[$result]" else result
}

View file

@ -1,26 +0,0 @@
package com.tangem.common.extensions
import java.nio.charset.Charset
import java.security.MessageDigest
/**
* Extension functions for [String].
*/
fun String.calculateSha256(): ByteArray {
val sha256 = MessageDigest.getInstance("SHA-256")
val data = this.toByteArray(Charset.forName("UTF-8"))
return sha256.digest(data)
}
fun String.calculateSha512(): ByteArray {
val sha = MessageDigest.getInstance("SHA-512")
val data = this.toByteArray(Charset.forName("UTF-8"))
return sha.digest(data)
}
fun String.hexToBytes(): ByteArray {
return ByteArray(this.length / 2)
{ i ->
Integer.parseInt(this.substring(2 * i, 2 * i + 2), 16).toByte()
}
}

View file

@ -1,105 +0,0 @@
package com.tangem.common.tlv
import com.tangem.Log
import com.tangem.common.extensions.toHexString
import java.io.ByteArrayInputStream
import java.io.IOException
/**
* The data converted to the Tag Length Value protocol.
*/
class Tlv {
val tag: TlvTag
val value: ByteArray
val tagRaw: Int
constructor(tagCode: Int, value: ByteArray = byteArrayOf()) {
this.tag = TlvTag.byCode(tagCode)
this.tagRaw = tagCode
this.value = value
}
constructor(tag: TlvTag, value: ByteArray = byteArrayOf()) {
this.tag = tag
this.tagRaw = tag.code
this.value = value
}
companion object {
private fun tlvFromBytes(stream: ByteArrayInputStream): Tlv? {
val code = stream.read()
if (code == -1) return null
var len = stream.read()
if (len == -1)
throw IOException("Can't read TLV")
if (len == 0xFF) {
val lenH = stream.read()
if (lenH == -1)
throw IOException("Can't read TLV")
len = stream.read()
if (len == -1)
throw IOException("Can't read TLV")
len = len or (lenH shl 8)
}
val value = ByteArray(len)
if (len > 0) {
if (len != stream.read(value)) {
throw IOException("Can't read TLV")
}
}
val tag = TlvTag.byCode(code)
return if (tag == TlvTag.Unknown) Tlv(code, value) else Tlv(tag, value)
}
fun deserialize(data: ByteArray, nfcV: Boolean = false): List<Tlv>? {
val tlvList = mutableListOf<Tlv>()
val stream = ByteArrayInputStream(data)
var tlv: Tlv?
do {
try {
tlv = tlvFromBytes(stream)
if (tlv != null) tlvList.add(tlv)
} catch (e: IOException) {
Log.e(this::class.java.simpleName,"TLVError: " + e.message)
if (nfcV) break else return null
}
} while (tlv != null)
return tlvList
}
}
override fun toString(): String {
return "${this.tag} ($tagRaw): ${value.toHexString()}"
}
}
fun List<Tlv>.serialize(): ByteArray =
this.map { it.serialize() }.reduce { arr1, arr2 -> arr1 + arr2 }
fun Tlv.serialize(): ByteArray {
val tag = byteArrayOf(this.tag.code.toByte())
val length = getLengthInBytes(this.value.size)
val value = if (this.value.isNotEmpty()) this.value else byteArrayOf(0x00)
return tag + length + value
}
private fun getLengthInBytes(tlvLength: Int): ByteArray {
return if (tlvLength > 0) {
if (tlvLength > 0xFE) {
byteArrayOf(
0xFF.toByte(),
(tlvLength shr 8 and 0xFF).toByte(),
(tlvLength and 0xFF).toByte()
)
} else {
byteArrayOf((tlvLength and 0xFF).toByte())
}
} else {
byteArrayOf()
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.common.tlv
import com.tangem.Log
class TlvBuilder {
private val tlvs = mutableListOf<Tlv>()
private val encoder = TlvEncoder()
internal inline fun <reified T> append(tag: TlvTag, value: T?) {
if (value == null) return
tlvs.add(encoder.encode(tag, value))
}
fun serialize(): ByteArray {
Log.v("TLV",
"Data encoded to TLVs:\n${tlvs.joinToString("\n")}")
return tlvs.serialize()
}
}

View file

@ -1,162 +0,0 @@
package com.tangem.common.tlv
import com.tangem.Log
import com.tangem.TangemSdkError
import com.tangem.commands.*
import com.tangem.commands.common.IssuerDataMode
import com.tangem.common.extensions.toDate
import com.tangem.common.extensions.toHexString
import com.tangem.common.extensions.toInt
import com.tangem.common.extensions.toUtf8
import java.util.*
/**
* Maps value fields in [Tlv] from raw [ByteArray] to concrete classes
* according to their [TlvTag] and corresponding [TlvValueType].
*
* @property tlvList List of TLVs, which values are to be converted to particular classes.
*/
class TlvDecoder(val tlvList: List<Tlv>) {
init {
Log.v("TLV",
"Decoding data from TLV:\n${tlvList.joinToString("\n")}")
}
/**
* Finds [Tlv] by its [TlvTag].
* Returns null if [Tlv] is not found, otherwise converts its value to [T].
*
* @param tag [TlvTag] of a [Tlv] which value is to be returned.
*
* @return Value converted to a nullable type [T].
*/
inline fun <reified T> decodeOptional(tag: TlvTag): T? =
try {
decode<T>(tag, false)
} catch (exception: TangemSdkError.DecodingFailedMissingTag) {
null
}
/**
* Finds [Tlv] by its [TlvTag].
* Throws [TaskError.MissingTag] if [Tlv] is not found,
* otherwise converts [Tlv] value to [T].
*
* @param tag [TlvTag] of a [Tlv] which value is to be returned.
*
* @return [Tlv] value converted to a nullable type [T].
*
* @throws [TangemSdkError.DecodingFailedMissingTag] exception if no [Tlv] is found by the Tag.
*/
inline fun <reified T> decode(tag: TlvTag, logError: Boolean = true): T {
val tlvValue: ByteArray = tlvList.find { it.tag == tag }?.value
?: if (tag.valueType() == TlvValueType.BoolValue && T::class == Boolean::class) {
return false as T
} else {
if (logError) {
Log.e(this::class.simpleName!!, "TLV $tag not found")
} else {
Log.v(this::class.simpleName!!, "TLV $tag not found, but it is not required")
}
throw TangemSdkError.DecodingFailedMissingTag()
}
return when (tag.valueType()) {
TlvValueType.HexString, TlvValueType.HexStringToHash -> {
typeCheck<T, String>(tag)
tlvValue.toHexString() as T
}
TlvValueType.Utf8String -> {
typeCheck<T, String>(tag)
tlvValue.toUtf8() as T
}
TlvValueType.Uint16, TlvValueType.Uint32 -> {
typeCheck<T, Int>(tag)
try {
tlvValue.toInt() as T
} catch (exception: IllegalArgumentException) {
Log.e(this::class.simpleName!!, exception.message ?: "")
throw TangemSdkError.DecodingFailed()
}
}
TlvValueType.BoolValue -> {
typeCheck<T, Boolean>(tag)
true as T
}
TlvValueType.ByteArray -> {
typeCheck<T, ByteArray>(tag)
tlvValue as T
}
TlvValueType.EllipticCurve -> {
typeCheck<T, EllipticCurve>(tag)
try {
EllipticCurve.byName(tlvValue.toUtf8()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.toUtf8(), exception)
throw TangemSdkError.DecodingFailed()
}
}
TlvValueType.DateTime -> {
typeCheck<T, Date>(tag)
try {
tlvValue.toDate() as T
} catch (exception: Exception) {
logException(tag, tlvValue.toHexString(), exception)
throw TangemSdkError.DecodingFailed()
}
}
TlvValueType.ProductMask -> {
typeCheck<T, ProductMask>(tag)
ProductMask(tlvValue.toInt()) as T
}
TlvValueType.SettingsMask -> {
typeCheck<T, SettingsMask>(tag)
SettingsMask(tlvValue.toInt()) as T
}
TlvValueType.CardStatus -> {
typeCheck<T, CardStatus>(tag)
try {
CardStatus.byCode(tlvValue.toInt()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.toInt().toString(), exception)
throw TangemSdkError.DecodingFailed()
}
}
TlvValueType.SigningMethod -> {
typeCheck<T, SigningMethodMask>(tag)
try {
SigningMethodMask(tlvValue.toInt()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.toInt().toString(), exception)
throw TangemSdkError.DecodingFailed()
}
}
TlvValueType.IssuerDataMode -> {
typeCheck<T, IssuerDataMode>(tag)
try {
IssuerDataMode.byCode(tlvValue.toInt().toByte()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.toInt().toString(), exception)
throw TangemSdkError.DecodingFailed()
}
}
}
}
fun logException(tag: TlvTag, value: String, exception: Exception) {
Log.e(this::class.simpleName!!,
"Unknown ${tag.name} with value of: value, \n${exception.message}")
}
inline fun <reified T, reified ExpectedT> typeCheck(tag: TlvTag) {
if (T::class != ExpectedT::class) {
Log.e(this::class.simpleName!!,
"Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
throw TangemSdkError.DecodingFailedTypeMismatch()
}
}
}

View file

@ -1,112 +0,0 @@
package com.tangem.common.tlv
import com.tangem.Log
import com.tangem.TangemSdkError
import com.tangem.commands.*
import com.tangem.commands.common.IssuerDataMode
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toByteArray
import java.util.*
/**
* Encodes information that is to be written on the card from parsed classes into [ByteArray]
* (according to the provided [TlvTag] and corresponding [TlvValueType])
* and then forms [Tlv] with the encoded values.
*/
class TlvEncoder {
/**
* @param value information that is to be encoded into [Tlv].
*/
internal inline fun <reified T> encode(tag: TlvTag, value: T?): Tlv {
if (value != null) {
return Tlv(tag, encodeValue(tag, value))
} else {
Log.e(this::class.simpleName!!, "Encoding error. Value for tag $tag is null")
throw TangemSdkError.EncodingFailed()
}
}
internal inline fun <reified T> encodeValue(tag: TlvTag, value: T): ByteArray {
return when (tag.valueType()) {
TlvValueType.HexString -> {
typeCheck<T, String>(tag)
(value as String).hexToBytes()
}
TlvValueType.HexStringToHash -> {
typeCheck<T, String>(tag)
(value as String).calculateSha256()
}
TlvValueType.Utf8String -> {
typeCheck<T, String>(tag)
(value as String).toByteArray()
}
TlvValueType.Uint16 -> {
typeCheck<T, Int>(tag)
(value as Int).toByteArray(2)
}
TlvValueType.Uint32 -> {
typeCheck<T, Int>(tag)
(value as Int).toByteArray()
}
TlvValueType.BoolValue -> {
typeCheck<T, Boolean>(tag)
val booleanValue = value as Boolean
if (booleanValue) byteArrayOf(1) else byteArrayOf(0)
}
TlvValueType.ByteArray -> {
typeCheck<T, ByteArray>(tag)
value as ByteArray
}
TlvValueType.EllipticCurve -> {
typeCheck<T, EllipticCurve>(tag)
(value as EllipticCurve).curve.toByteArray()
}
TlvValueType.DateTime -> {
typeCheck<T, Date>(tag)
val calendar = Calendar.getInstance().apply { time = (value as Date) }
val year = calendar.get(Calendar.YEAR)
val month = calendar.get(Calendar.MONTH) + 1
val day = calendar.get(Calendar.DAY_OF_MONTH)
return year.toByteArray(2) + month.toByte() + day.toByte()
}
TlvValueType.ProductMask -> {
typeCheck<T, ProductMask>(tag)
byteArrayOf(
(value as ProductMask).rawValue.toByte()
)
}
TlvValueType.SettingsMask -> {
typeCheck<T, SettingsMask>(tag)
val rawValue = (value as SettingsMask).rawValue
rawValue.toByteArray(determineByteArraySize(rawValue))
}
TlvValueType.CardStatus -> {
typeCheck<T, CardStatus>(tag)
(value as CardStatus).code.toByteArray()
}
TlvValueType.SigningMethod -> {
typeCheck<T, SigningMethodMask>(tag)
byteArrayOf((value as SigningMethodMask).rawValue.toByte())
}
TlvValueType.IssuerDataMode -> {
typeCheck<T, IssuerDataMode>(tag)
byteArrayOf((value as IssuerDataMode).code)
}
}
}
private fun determineByteArraySize(value: Int): Int {
val mask = 0xFFFF0000.toInt()
return if ((value and mask) != 0) 4 else 2
}
private inline fun <reified T, reified ExpectedT> typeCheck(tag: TlvTag) {
if (T::class != ExpectedT::class) {
Log.e(this::class.simpleName!!,
"Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
throw TangemSdkError.EncodingFailedTypeMismatch()
}
}
}

View file

@ -1,150 +0,0 @@
package com.tangem.common.tlv
/**
* Contains all possible value types that value for [TlvTag] can contain.
*/
enum class TlvValueType {
HexString,
HexStringToHash,
Utf8String,
Uint16,
Uint32,
BoolValue,
ByteArray,
EllipticCurve,
DateTime,
ProductMask,
SettingsMask,
CardStatus,
SigningMethod,
IssuerDataMode
}
/**
* Contains all TLV tags, with their code and descriptive name.
*/
enum class TlvTag(val code: Int) {
Unknown(0x00),
CardId(0x01),
Status(0x02),
CardPublicKey(0x03),
CardSignature(0x04),
CurveId(0x05),
HashAlgID(0x06),
SigningMethod(0x07),
MaxSignatures(0x08),
PauseBeforePin2(0x09),
SettingsMask(0x0A),
CardData(0x0C),
NdefData(0x0D),
CreateWalletAtPersonalize(0x0E),
Health(0x0F),
Pin(0x10),
Pin2(0x11),
NewPin(0x12),
NewPin2(0x13),
NewPinHash(0x14),
NewPin2Hash(0x15),
Challenge(0x16),
Salt(0x17),
ValidationCounter(0x18),
Cvc(0x19),
SessionKeyA(0x1A),
SessionKeyB(0x1B),
Pause(0x1C),
NewPin3(0x1E),
CrExKey(0x1F),
Uid(0x0B),
ManufactureId(0x20),
ManufacturerSignature(0x86),
IssuerDataPublicKey(0x30),
IssuerTransactionPublicKey(0x31),
IssuerData(0x32),
IssuerDataSignature(0x33),
IssuerTransactionSignature(0x34),
IssuerDataCounter(0x35),
AcquirerPublicKey(0x37),
Size(0x25),
Mode(0x23),
Offset(0x24),
IsActivated(0x3A),
ActivationSeed(0x3B),
ResetPin(0x36),
CodePageAddress(0x40),
CodePageCount(0x41),
CodeHash(0x42),
TransactionOutHash(0x50),
TransactionOutHashSize(0x51),
TransactionOutRaw(0x52),
WalletPublicKey(0x60),
Signature(0x61),
RemainingSignatures(0x62),
SignedHashes(0x63),
Firmware(0x80),
Batch(0x81),
ManufactureDateTime(0x82),
IssuerId(0x83),
BlockchainId(0x84),
ManufacturerPublicKey(0x85),
CardIdManufacturerSignature(0x86),
ProductMask(0x8A),
PaymentFlowVersion(0x54),
TokenSymbol(0xA0),
TokenContractAddress(0xA1),
TokenDecimal(0xA2),
Denomination(0xC0),
ValidatedBalance(0xC1),
LastSignDate(0xC2),
DenominationText(0xC3),
TerminalIsLinked(0x58),
TerminalPublicKey(0x5C),
TerminalTransactionSignature(0x57),
UserData(0x2A),
UserProtectedData(0x2B),
UserCounter(0x2C),
UserProtectedCounter(0x2D);
/**
* @return [TlvValueType] associated with a [TlvTag]
*/
fun valueType(): TlvValueType {
return when (this) {
CardId, Batch, CrExKey -> TlvValueType.HexString
ManufactureId, Firmware, IssuerId, BlockchainId, TokenSymbol, TokenContractAddress ->
TlvValueType.Utf8String
CurveId -> TlvValueType.EllipticCurve
PauseBeforePin2, RemainingSignatures, SignedHashes, Health, TokenDecimal,
Offset, Size -> TlvValueType.Uint16
MaxSignatures, UserCounter, UserProtectedCounter, IssuerDataCounter -> TlvValueType.Uint32
IsActivated, TerminalIsLinked, CreateWalletAtPersonalize -> TlvValueType.BoolValue
ManufactureDateTime -> TlvValueType.DateTime
ProductMask -> TlvValueType.ProductMask
SettingsMask -> TlvValueType.SettingsMask
Status -> TlvValueType.CardStatus
SigningMethod -> TlvValueType.SigningMethod
Mode -> TlvValueType.IssuerDataMode
else -> TlvValueType.ByteArray
}
}
companion object {
private val values = values()
fun byCode(code: Int): TlvTag = values.find { it.code == code } ?: Unknown
}
}

View file

@ -1,121 +0,0 @@
package com.tangem.crypto
import com.tangem.commands.EllipticCurve
import net.i2p.crypto.eddsa.EdDSASecurityProvider
import org.spongycastle.jce.provider.BouncyCastleProvider
import java.security.PublicKey
import java.security.SecureRandom
import java.security.Security
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object CryptoUtils {
fun initCrypto() {
Security.insertProviderAt(BouncyCastleProvider(), 1)
Security.addProvider(EdDSASecurityProvider())
}
/**
* Generates ByteArray of random bytes.
* It is used, among other things, to generate helper private keys
* (not the one for the blockchains, that one is generated on the card and does not leave the card).
*
* @param length length of the ByteArray that is to be generated.
*/
fun generateRandomBytes(length: Int): ByteArray {
val bytes = ByteArray(length)
SecureRandom().nextBytes(bytes)
return bytes
}
/**
* Helper function to verify that the data was signed with a private key that corresponds
* to the provided public key.
*
* @param publicKey Corresponding to the private key that was used to sing a message
* @param message The data that was signed
* @param signature Signed data
* @param curve Elliptic curve used
*
* @return Result of a verification
*/
fun verify(publicKey: ByteArray, message: ByteArray, signature: ByteArray,
curve: EllipticCurve = EllipticCurve.Secp256k1): Boolean {
return when (curve) {
EllipticCurve.Secp256k1 -> Secp256k1.verify(publicKey, message, signature)
EllipticCurve.Ed25519 -> Ed25519.verify(publicKey, message, signature)
}
}
/**
* Helper function that generates public key from a private key.
*
* @param privateKeyArray A private key from which a public key is generated
* @param curve Elliptic curve used
*
* @return Public key [ByteArray]
*/
fun generatePublicKey(
privateKeyArray: ByteArray,
curve: EllipticCurve = EllipticCurve.Secp256k1
): ByteArray {
return when (curve) {
EllipticCurve.Secp256k1 -> Secp256k1.generatePublicKey(privateKeyArray)
EllipticCurve.Ed25519 -> Ed25519.generatePublicKey(privateKeyArray)
}
}
fun loadPublicKey(
publicKey: ByteArray,
curve: EllipticCurve = EllipticCurve.Secp256k1
): PublicKey {
return when (curve) {
EllipticCurve.Secp256k1 -> Secp256k1.loadPublicKey(publicKey)
EllipticCurve.Ed25519 -> Ed25519.loadPublicKey(publicKey)
}
}
}
/**
* Extension function to sign a ByteArray with an elliptic curve cryptography.
*
* @param privateKeyArray Key to sign data
* @param curve Elliptic curve that is used to sign data
*
* @return Signed data
*/
fun ByteArray.sign(privateKeyArray: ByteArray, curve: EllipticCurve = EllipticCurve.Secp256k1): ByteArray {
return when (curve) {
EllipticCurve.Secp256k1 -> Secp256k1.sign(this, privateKeyArray)
EllipticCurve.Ed25519 -> Ed25519.sign(this, privateKeyArray)
}
}
fun ByteArray.encrypt(key: ByteArray, usePkcs7: Boolean = true): ByteArray {
val spec = if (usePkcs7) ENCRYPTION_SPEC_PKCS7 else ENCRYPTION_SPEC_NO_PADDING
val secretKeySpec = SecretKeySpec(key, spec)
val cipher = Cipher.getInstance(spec, "SC")
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, IvParameterSpec(ByteArray(16)))
return cipher.doFinal(this)
}
fun ByteArray.decrypt(key: ByteArray, usePkcs7: Boolean = true): ByteArray {
val spec = if (usePkcs7) ENCRYPTION_SPEC_PKCS7 else ENCRYPTION_SPEC_NO_PADDING
val secretKeySpec = SecretKeySpec(key, spec)
val cipher = Cipher.getInstance(spec)
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, IvParameterSpec(ByteArray(16)))
return cipher.doFinal(this.copyOfRange(0, this.size))
}
fun ByteArray.pbkdf2Hash(salt: ByteArray, iterations: Int): ByteArray {
return Pbkdf2().deriveKey(this, salt, iterations)
}
private const val ENCRYPTION_SPEC_PKCS7 = "AES/CBC/PKCS7PADDING"
private const val ENCRYPTION_SPEC_NO_PADDING = "AES/CBC/NOPADDING"

View file

@ -1,55 +0,0 @@
package com.tangem.crypto
import com.tangem.common.extensions.calculateSha512
import net.i2p.crypto.eddsa.EdDSAEngine
import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.EdDSAPublicKey
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
import java.security.MessageDigest
import java.security.PublicKey
object Ed25519 {
internal fun verify(publicKey: ByteArray, message: ByteArray, signature: ByteArray): Boolean {
val messageSha512 = message.calculateSha512()
val loadedPublicKey = loadPublicKey(publicKey)
val spec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519)
val signatureInstance = EdDSAEngine(MessageDigest.getInstance(spec.hashAlgorithm))
signatureInstance.initVerify(loadedPublicKey)
signatureInstance.update(messageSha512)
return signatureInstance.verify(signature)
}
internal fun loadPublicKey(publicKeyArray: ByteArray): PublicKey {
val spec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519)
val pubKey = EdDSAPublicKeySpec(publicKeyArray, spec)
return EdDSAPublicKey(pubKey)
}
internal fun sign(data: ByteArray, privateKeyArray: ByteArray): ByteArray {
val dataSha512 = data.calculateSha512()
val spec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519)
val signatureInstance = EdDSAEngine(MessageDigest.getInstance(spec.hashAlgorithm))
val privateKeySpec = EdDSAPrivateKeySpec(privateKeyArray, spec)
val privateKey = EdDSAPrivateKey(privateKeySpec)
signatureInstance.initSign(privateKey)
signatureInstance.update(dataSha512)
return signatureInstance.sign()
}
internal fun generatePublicKey(privateKeyArray: ByteArray): ByteArray {
val spec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519)
val privateKeySpec = EdDSAPrivateKeySpec(privateKeyArray, spec)
val publicKeySpec = EdDSAPublicKeySpec(privateKeySpec.a, spec)
val publicKey = EdDSAPublicKey(publicKeySpec)
return publicKey.abyte
}
}

View file

@ -1,61 +0,0 @@
package com.tangem.crypto
import com.tangem.EncryptionMode
import org.spongycastle.jce.interfaces.ECPublicKey
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.SecureRandom
import java.security.spec.ECGenParameterSpec
import javax.crypto.KeyAgreement
interface EncryptionHelper {
val keyA: ByteArray
fun generateSecret(keyB: ByteArray): ByteArray
companion object {
fun create(encryptionMode: EncryptionMode): EncryptionHelper? {
return when (encryptionMode) {
EncryptionMode.NONE -> null
EncryptionMode.FAST -> FastEncryptionHelper()
EncryptionMode.STRONG -> StrongEncryptionHelper()
}
}
}
}
class StrongEncryptionHelper : EncryptionHelper {
private val keyPair = generateKeyPair()
private val keyAgreement = generateKeyAgreement(keyPair)
override val keyA = provideKeyA(keyPair)
override fun generateSecret(keyB: ByteArray): ByteArray {
keyAgreement.doPhase(CryptoUtils.loadPublicKey(keyB), true)
return keyAgreement.generateSecret()
}
private fun generateKeyPair(): KeyPair {
val kpgen = KeyPairGenerator.getInstance("ECDH", "SC")
kpgen.initialize(ECGenParameterSpec("secp256k1"), SecureRandom())
return kpgen.generateKeyPair()
}
private fun generateKeyAgreement(keyPair: KeyPair): KeyAgreement {
val keyAgreement = KeyAgreement.getInstance("ECDH", "SC")
keyAgreement.init(keyPair.private)
return keyAgreement
}
private fun provideKeyA(keyPair: KeyPair): ByteArray {
val eckey = keyPair.public as ECPublicKey
return eckey.q.getEncoded(false)
}
}
class FastEncryptionHelper : EncryptionHelper {
override val keyA = CryptoUtils.generateRandomBytes(16)
override fun generateSecret(keyB: ByteArray): ByteArray {
return keyA + keyB
}
}

View file

@ -1,88 +0,0 @@
package com.tangem.crypto
import org.spongycastle.crypto.CipherParameters
import org.spongycastle.crypto.digests.SHA256Digest
import org.spongycastle.crypto.macs.HMac
import org.spongycastle.crypto.params.KeyParameter
import java.security.InvalidKeyException
import java.util.*
import kotlin.experimental.xor
import kotlin.math.min
import kotlin.math.pow
class Pbkdf2 {
private val F: HMac = HMac(SHA256Digest())
fun deriveKey(password: ByteArray, salt: ByteArray, iterations: Int): ByteArray {
val macSize = F.macSize
// Check key length
if (macSize > (2.0.pow(32.0) - 1) * macSize) throw InvalidKeyException("Derived key to long")
val derivedKey = ByteArray(macSize)
val J = 0
val K: Int = macSize
val U: Int = macSize shl 1
val B = K + U
val workingArray = ByteArray(K + U + 4)
// Initialize F
val macParams: CipherParameters = KeyParameter(password)
F.init(macParams)
// Perform iterations
var kpos = 0
var blk = 1
while (kpos < macSize) {
storeInt32BE(blk, workingArray, B)
F.update(salt, 0, salt.size)
F.reset()
F.update(salt, 0, salt.size)
F.update(workingArray, B, 4)
F.doFinal(workingArray, U)
System.arraycopy(workingArray, U, workingArray, J, K)
var i = 1
var j = J
var k = K
while (i < iterations) {
F.init(macParams)
F.update(workingArray, j, K)
F.doFinal(workingArray, k)
var u = U
var v = k
while (u < B) {
workingArray[u] = workingArray[u] xor workingArray[v]
u++
v++
}
val swp = k
k = j
j = swp
i++
}
val tocpy = min(macSize - kpos, K)
System.arraycopy(workingArray, U, derivedKey, kpos, tocpy)
kpos += K
blk++
}
Arrays.fill(workingArray, 0.toByte())
return derivedKey
}
/**
* Convert a 32-bit integer value into a big-endian byte array
*
* @param value The integer value to convert
* @param bytes The byte array to store the converted value
* @param offSet The offset in the output byte array
*/
private fun storeInt32BE(value: Int, bytes: ByteArray, offSet: Int) {
bytes[offSet + 3] = value.toByte()
bytes[offSet + 2] = (value ushr 8).toByte()
bytes[offSet + 1] = (value ushr 16).toByte()
bytes[offSet] = (value ushr 24).toByte()
}
}

View file

@ -1,118 +0,0 @@
package com.tangem.crypto
import com.tangem.common.extensions.toHexString
import org.spongycastle.asn1.ASN1EncodableVector
import org.spongycastle.asn1.ASN1Integer
import org.spongycastle.asn1.DERSequence
import org.spongycastle.jce.ECNamedCurveTable
import org.spongycastle.jce.spec.ECPrivateKeySpec
import org.spongycastle.jce.spec.ECPublicKeySpec
import java.math.BigInteger
import java.security.KeyFactory
import java.security.PublicKey
import java.security.Signature
object Secp256k1 {
internal fun verify(publicKey: ByteArray, message: ByteArray, signature: ByteArray): Boolean {
val signatureInstance = Signature.getInstance("SHA256withECDSA")
val loadedPublicKey = loadPublicKey(publicKey)
signatureInstance.initVerify(loadedPublicKey)
signatureInstance.update(message)
val v = ASN1EncodableVector()
val size = signature.size / 2
v.add(calculateR(signature, size))
v.add(calculateS(signature, size))
val sigDer = DERSequence(v).encoded
return signatureInstance.verify(sigDer)
}
internal fun loadPublicKey(publicKeyArray: ByteArray): PublicKey {
val spec = ECNamedCurveTable.getParameterSpec("secp256k1")
val factory = KeyFactory.getInstance("EC", "SC")
val p1 = spec.curve.decodePoint(publicKeyArray)
val keySpec = ECPublicKeySpec(p1, spec)
return factory.generatePublic(keySpec)
}
private fun calculateR(signature: ByteArray, size: Int): ASN1Integer =
ASN1Integer(BigInteger(1, signature.copyOfRange(0, size)))
private fun calculateS(signature: ByteArray, size: Int): ASN1Integer =
ASN1Integer(BigInteger(1, signature.copyOfRange(size, size * 2)))
internal fun sign(data: ByteArray, privateKeyArray: ByteArray): ByteArray {
val spec = ECNamedCurveTable.getParameterSpec("secp256k1")
val factory = KeyFactory.getInstance("EC", "SC")
val keySpecP = ECPrivateKeySpec(BigInteger(1, privateKeyArray), spec)
val signature = Signature.getInstance("SHA256withECDSA")
val privateKey = factory.generatePrivate(keySpecP)
signature.initSign(privateKey)
signature.update(data)
val enc = signature.sign()
checkSignatureForErrors(enc)
val res = toByte64(enc)
if (!verify(generatePublicKey(privateKeyArray), data, res)) {
throw Exception("Signature self verify failed - ,enc:" + enc.toHexString() + ",res:" + res.toHexString())
}
return res
}
private fun checkSignatureForErrors(enc: ByteArray) {
if (enc[0].toInt() != 0x30) throw Exception("bad encoding 1")
if (enc[1].toInt() and 0x80 != 0) throw Exception("unsupported length encoding 1")
if (enc[2].toInt() != 0x02) throw Exception("bad encoding 2")
if (enc[3].toInt() and 0x80 != 0) throw Exception("unsupported length encoding 2")
var rLength = enc[3].toInt()
if (enc[4 + rLength].toInt() != 0x02) throw Exception("bad encoding 3")
if (enc[5 + rLength].toInt() and 0x80 != 0)
throw Exception("unsupported length encoding 3")
}
private fun toByte64(enc: ByteArray): ByteArray {
var rLength = enc[3].toInt()
var sLength = enc[5 + rLength].toInt()
val sPos = 6 + rLength
val res = ByteArray(64)
if (rLength <= 32) {
System.arraycopy(enc, 4, res, 32 - rLength, rLength)
rLength = 32
} else if (rLength == 33 && enc[4].toInt() == 0) {
rLength--
System.arraycopy(enc, 5, res, 0, rLength)
} else {
throw Exception("unsupported r-length - r-length:" + rLength.toString() + ",s-length:" + sLength.toString() + ",enc:" + enc.toHexString())
}
if (sLength <= 32) {
System.arraycopy(enc, sPos, res, rLength + 32 - sLength, sLength)
sLength = 32
} else if (sLength == 33 && enc[sPos].toInt() == 0) {
System.arraycopy(enc, sPos + 1, res, rLength, sLength - 1)
} else {
throw Exception("unsupported s-length - r-length:" + rLength.toString() + ",s-length:" + sLength.toString() + ",enc:" + enc.toHexString())
}
return res
}
internal fun generatePublicKey(privateKeyArray: ByteArray): ByteArray {
val spec = ECNamedCurveTable.getParameterSpec("secp256k1")
return spec.g.multiply(BigInteger(1, privateKeyArray)).getEncoded(false)
}
}

View file

@ -1,101 +0,0 @@
package com.tangem.tasks
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.SessionEnvironment
import com.tangem.SessionViewDelegate
import com.tangem.commands.SetPinCommand
import com.tangem.commands.SetPinResponse
import com.tangem.common.CompletionResult
import com.tangem.common.PinCode
import com.tangem.common.extensions.calculateSha256
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
enum class PinType {
Pin1,
Pin2,
Pin3,
;
}
class ChangePinTask(
private val pinType: PinType,
private val pin: ByteArray? = null
) : CardSessionRunnable<SetPinResponse> {
override val requiresPin2 = false
override fun run(session: CardSession, callback: (result: CompletionResult<SetPinResponse>) -> Unit) {
session.scope.launch {
val pin = this@ChangePinTask.pin
?: requestNewPin(pinType, session.viewDelegate).calculateSha256()
runSetPin(pin, session, callback)
}
}
private suspend fun runSetPin(pin: ByteArray, session: CardSession, callback: (result: CompletionResult<SetPinResponse>) -> Unit) {
val pin1: ByteArray
val pin2: ByteArray
val pin3: ByteArray?
if (session.environment.pin1 == null) {
session.environment.pin1 = PinCode(requestPin(PinType.Pin1, session.viewDelegate))
}
if (session.environment.pin2 == null) {
session.environment.pin2 = PinCode(requestPin(PinType.Pin2, session.viewDelegate))
}
when (pinType) {
PinType.Pin1 -> {
pin1 = pin
pin2 = session.environment.pin2!!.value
pin3 = null
}
PinType.Pin2 -> {
pin1 = session.environment.pin1!!.value
pin2 = pin
pin3 = null
}
PinType.Pin3 -> {
pin1 = session.environment.pin1!!.value
pin2 = session.environment.pin2!!.value
pin3 = pin
}
}
val command = SetPinCommand(pin1, pin2, pin3)
command.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
savePin(pin, session.environment)
callback(result)
}
is CompletionResult.Failure -> callback(result)
}
}
}
private suspend fun requestPin(pinType: PinType, viewDelegate: SessionViewDelegate): String =
suspendCancellableCoroutine { continuation ->
viewDelegate.onPinRequested(pinType) { result ->
if (continuation.isActive) continuation.resume(result)
}
}
private suspend fun requestNewPin(pinType: PinType, viewDelegate: SessionViewDelegate): String =
suspendCancellableCoroutine { continuation ->
viewDelegate.onPinChangeRequested(pinType) { result ->
if (continuation.isActive) continuation.resume(result)
}
}
private fun savePin(pin: ByteArray, environment: SessionEnvironment) {
when (pinType) {
PinType.Pin1 -> environment.pin1 = PinCode(pin, false)
PinType.Pin2 -> environment.pin2 = PinCode(pin, false)
PinType.Pin3 -> {}
}
}
}

View file

@ -1,45 +0,0 @@
package com.tangem.tasks
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.TangemSdkError
import com.tangem.commands.CardStatus
import com.tangem.commands.CheckWalletCommand
import com.tangem.commands.CreateWalletCommand
import com.tangem.commands.CreateWalletResponse
import com.tangem.common.CompletionResult
class CreateWalletTask : CardSessionRunnable<CreateWalletResponse> {
override val requiresPin2 = false
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
val curve = session.environment.card?.curve
if (curve == null) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
val command = CreateWalletCommand()
command.run(session) { createWalletResult ->
when (createWalletResult) {
is CompletionResult.Failure -> callback(createWalletResult)
is CompletionResult.Success -> {
if (createWalletResult.data.status != CardStatus.Loaded) {
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
} else {
val checkWalletCommand = CheckWalletCommand(
curve, createWalletResult.data.walletPublicKey
)
checkWalletCommand.run(session) { result ->
when (result) {
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
is CompletionResult.Success -> callback(createWalletResult)
}
}
}
}
}
}
}
}

View file

@ -1,87 +0,0 @@
package com.tangem.tasks
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.TagType
import com.tangem.TangemSdkError
import com.tangem.commands.*
import com.tangem.commands.common.CardDeserializer
import com.tangem.common.CompletionResult
/**
* Task that allows to read Tangem card and verify its private key.
*
* It performs two commands, [ReadCommand] and [CheckWalletCommand], subsequently.
*/
internal class ScanTask : CardSessionRunnable<Card> {
override val requiresPin2 = false
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
if (session.connectedTag == TagType.Slix) {
readSlixTag(session, callback)
return
}
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(
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))
}
}
}
}
private fun readSlixTag(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
session.readSlixTag { result ->
when (result) {
is CompletionResult.Success -> {
try {
val card = CardDeserializer.deserialize(result.data)
callback(CompletionResult.Success(card))
} catch (error: TangemSdkError) {
callback(CompletionResult.Failure(error))
}
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
}

View file

@ -1,34 +0,0 @@
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,55 +0,0 @@
package com.tangem.common.apdu
import com.google.common.truth.Truth.assertThat
import com.tangem.SessionEnvironment
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvTag
import org.junit.Test
class CommandApduTest {
private val sessionEnvironment = SessionEnvironment()
@Test
fun `simple READ command to bytes`() {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, sessionEnvironment.pin1?.value)
val commandApdu = CommandApdu(
Instruction.Read,
tlvBuilder.serialize()
)
val expected = byteArrayOf(0, -14, 0, 0, 0, 0, 34, 16, 32, -111, -76, -47, 66, -126, 63, 125,
32, -59, -16, -115, -10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, -10,
-45, 19, -124, -123, -55, -94, 3)
assertThat(commandApdu.apduData)
.isEqualTo(expected)
assertThat(listOf(1, 2)).containsExactlyElementsIn(listOf(1, 2))
}
@Test
fun `READ with terminal key to bytes`() {
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?.value)
tlvBuilder.append(TlvTag.TerminalPublicKey, terminalPublicKey)
val commandApdu = CommandApdu(
Instruction.Read,
tlvBuilder.serialize()
)
val expected = byteArrayOf(0, -14, 0, 0, 0, 0, 101, 16, 32, -111, -76, -47, 66, -126, 63,
125, 32, -59, -16, -115, -10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25,
-10, -45, 19, -124, -123, -55, -94, 3, 92, 65, 4, 80, -122, 58, -42, 74, -121, -82, -118,
47, -24, 60, 26, -15, -88, 64, 60, -75, 63, 83, -28, -122, -40, 81, 29, -83, -118, 4, -120,
126, 91, 35, 82, 44, -44, 112, 36, 52, 83, -94, -103, -6, -98, 119, 35, 119, 22, 16, 58,
-68, 17, -95, -33, 56, -123, 94, -42, -14, -18, 24, 126, -100, 88, 43, -90)
assertThat(commandApdu.apduData)
.isEqualTo(expected)
}
}

View file

@ -1,44 +0,0 @@
package com.tangem.common.apdu
import com.google.common.truth.Truth.assertThat
import com.tangem.common.tlv.TlvTag
import org.junit.Test
class ResponseApduTest {
@Test
fun `get StatusWord returns Unknown`() {
val corruptData = byteArrayOf(0, 0, 0, 0)
val responseApdu = ResponseApdu(corruptData)
assertThat(responseApdu.statusWord)
.isEqualTo(StatusWord.Unknown)
}
@Test
fun `get StatusWord returns ProcessCompleted`() {
val data = byteArrayOf(0, 0, 0, 0, -112, 0)
val responseApdu = ResponseApdu(data)
assertThat(responseApdu.statusWord)
.isEqualTo(StatusWord.ProcessCompleted)
}
@Test
fun `corrupt response, getTlvData returns null`() {
val corruptData = byteArrayOf(0, 0, 0)
val responseApdu = ResponseApdu(corruptData)
assertThat(responseApdu.getTlvData())
.isNull()
}
@Test
fun `response, getTlvData returns cardId`() {
val data = byteArrayOf(1, 8, -53, 34, 0, 0, 0, 2, 115, 116, 32, 11, 83, 77, 65, 82, 84, 32, 67, 65, 83, 72, 0, 2, 1, 2, -128, 6, 50, 46, 49, 49, 114, 0, 3, 65, 4, -49, 11, -50, -66, -121, -25, -2, 65, 65, -13, 14, 49, 27, -82, -33, -85, -113, 65, 20, 8, -39, -75, 57, 45, 65, -31, 35, 44, 38, 40, 63, -44, 113, -45, -75, -95, -118, 118, 29, 65, 117, -24, -53, 82, -72, 91, -20, -96, -77, -103, -14, -63, 52, -127, -123, -27, -16, -128, -67, -3, -104, -26, -22, 65, 10, 4, 0, 0, 126, 33, 12, 90, -127, 2, 0, 41, -126, 4, 7, -29, 5, 2, -125, 7, 84, 65, 78, 71, 69, 77, 0, -124, 3, 69, 84, 72, -122, 64, 111, -103, 48, -114, -40, 18, -103, 26, -102, -12, -38, -78, -90, -9, -98, 88, -47, -100, -24, 24, -105, -70, -72, 6, 94, -96, -77, 11, -123, -28, -118, 37, 63, 107, -55, -11, 23, -12, 13, -23, -121, -63, 36, -59, 70, 116, 91, -125, -34, -69, 23, -112, 6, 17, 4, -49, 68, -56, 29, -45, 81, 10, 97, 83, 48, 65, 4, -127, -106, -86, 75, 65, 10, -60, 74, 59, -100, -50, 24, -25, -66, 34, 106, -22, 7, 10, -52, -125, -87, -49, 103, 84, 15, -84, 73, -81, 37, 18, -97, 106, 83, -118, 40, -83, 99, 65, 53, -114, 60, 79, -103, 99, 6, 79, 126, 54, 83, 114, -90, 81, -45, 116, -27, -62, 60, -35, 55, -3, 9, -101, -14, 5, 10, 115, 101, 99, 112, 50, 53, 54, 107, 49, 0, 8, 4, 0, 15, 66, 64, 7, 1, 0, 9, 2, 11, -72, 96, 65, 4, -42, -5, -41, -84, -23, 88, 2, 86, -63, -118, -123, -10, -66, -82, -107, -68, -93, 111, 47, 93, -20, -86, 74, 28, 21, 81, 93, -21, -124, -57, -102, 55, 17, 84, -66, -68, -22, -128, 126, -99, -65, -54, -42, 59, -25, -21, -124, 5, 59, -16, -72, 73, 48, 16, -27, 103, -112, -73, 2, 96, -51, 41, -42, 116, 98, 4, 0, 15, 66, 52, 99, 4, 0, 0, 0, 13, 15, 1, 0, -112, 0)
val responseApdu = ResponseApdu(data)
assertThat(responseApdu.getTlvData())
.isNotNull()
assertThat(responseApdu.getTlvData())
.isNotEmpty()
assertThat(responseApdu.getTlvData()?.filter { it.tag == TlvTag.Unknown })
.isEmpty()
}
}

View file

@ -1,105 +0,0 @@
package com.tangem.common.extensions
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import java.util.*
class ByteArrayExtensionsTest {
@Test
fun `card Id to Hex String`() {
val hex = "CB22000000027374"
val bytes = byteArrayOf(-53, 34, 0, 0, 0, 2, 115, 116)
assertThat(bytes.toHexString())
.matches(hex)
}
@Test
fun `batch Id to Hex String`() {
val hex = "0029"
val bytes = byteArrayOf(0, 41)
assertThat(bytes.toHexString())
.matches(hex)
}
@Test
fun `curve name to Utf8`() {
val bytes = byteArrayOf(115, 101, 99, 112, 50, 53, 54, 107, 49, 0)
val expected = "secp256k1"
val converted = bytes.toUtf8()
assertThat(converted)
.matches(expected)
}
@Test
fun `empty byteArray to Utf8 returns empty String`() {
val bytes = byteArrayOf()
val expected = ""
assertThat(bytes.toUtf8())
.matches(expected)
}
@Test
fun `blockchain name to Utf8`() {
val bytes = byteArrayOf(69, 84, 72)
val expected = "ETH"
val converted = bytes.toUtf8()
assertThat(converted)
.matches(expected)
}
@Test
fun `bytes to int`() {
val bytes = byteArrayOf(0, 2, 106, 3)
val expected = 158211
assertThat(bytes.toInt())
.isEqualTo(expected)
val bytes1 = byteArrayOf(0, 0, 0, 13)
val expected1 = 13
assertThat(bytes1.toInt())
.isEqualTo(expected1)
}
@Test
fun `zero to int`() {
val bytes = byteArrayOf(0)
val expected = 0
assertThat(bytes.toInt())
.isEqualTo(expected)
}
@Test
fun toDate() {
val bytes1 = byteArrayOf(7, -30, 7, 27)
val expected1 = Calendar.getInstance().apply { this.set(2018, 6, 27, 0, 0, 0) }.time
val converted1 = bytes1.toDate()
assertThat(converted1.toString())
.isEqualTo(expected1.toString())
val bytes2 = byteArrayOf(7, -30, 7, 27, 30)
val expected2 = Calendar.getInstance().apply { this.set(2018, 6, 27, 0, 0, 0) }.time
val converted2 = bytes2.toDate()
assertThat(converted2.toString())
.isEqualTo(expected2.toString())
val bytes3 = byteArrayOf(7, -30, 7)
val expected3 = Calendar.getInstance().apply { this.set(2018, 6, 0, 0, 0, 0) }.time
val converted3 = bytes3.toDate()
assertThat(converted3.toString())
.isEqualTo(expected3.toString())
}
@Test
fun `calculate sha512`() {
val bytes = ByteArray(64) { 5 }
val expected = byteArrayOf(
-123, 96, 121, 57, -117, -23, -108, 57, 25, -119, -22, 97, 11, -91,
74, -19, -88, 21, -108, -116, -100, 111, 6, -78, 114, -115, 70, -121, 29, 102, 104, 65,
-21, -68, -111, 121, -51, 109, -94, -24, -40, 108, -25, 70, -26, 61, 38, 12, -127, -34,
-77, -81, 81, -32, -89, -112, -31, -33, 91, 114, 89, 127, -123, -58)
assertThat(bytes.calculateSha512())
.isEqualTo(expected)
}
}

View file

@ -1,24 +0,0 @@
package com.tangem.common.extensions
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Test
class IntExtensionsTest {
@Test
fun `small int toByteArray`() {
val int = 13
val expected = byteArrayOf(0, 0, 0, 13)
assertThat(int.toByteArray())
.isEqualTo(expected)
}
@Test
fun `int toByteArray`() {
val int = 999
val expected = byteArrayOf(0, 0, 3, -25)
assertThat(int.toByteArray())
.isEqualTo(expected)
}
}

View file

@ -1,53 +0,0 @@
package com.tangem.common.extensions
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class StringExtensionsTest {
@Test
fun `calculate SHA 256 for default PIN 1`() {
val pin = "000000"
val expected = byteArrayOf(-111, -76, -47, 66, -126, 63, 125, 32, -59, -16, -115, -10, -111,
34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, -10, -45, 19, -124, -123, -55, -94, 3)
assertThat(pin.calculateSha256())
.isEqualTo(expected)
}
@Test
fun `calculate SHA 256 for default PIN 2`() {
val pin = "000"
val expected = byteArrayOf(42, -55, -90, 116, 106, -54, 84, 58, -8, -33, -13, -104, -108, -49,
-24, 23, 58, -5, -94, 30, -80, 28, 111, -82, 51, -43, 41, 71, 34, 40, 85, -17)
assertThat(pin.calculateSha256())
.isEqualTo(expected)
}
@Test
fun `calculate SHA 256 for a sample PIN 1`() {
val pin = "999999"
val expected = byteArrayOf(-109, 115, 119, -16, 86, 22, 15, -60, -79, 94, 11, 119, 12, 103,
19, 106, 95, 3, -63, 82, 5, -76, -45, -65, -111, -126, 104, -2, -6, 44, 109, 10)
assertThat(pin.calculateSha256())
.isEqualTo(expected)
}
@Test
fun `calculate SHA 256 for a sample PIN 2`() {
val pin = "999"
val expected = byteArrayOf(-125, -49, -117, 96, -99, -26, 0, 54, -88, 39, 123, -48, -23, 97,
53, 117, 27, -68, 7, -21, 35, 66, 86, -44, -74, 91, -119, 51, 96, 101, 27, -14)
assertThat(pin.calculateSha256())
.isEqualTo(expected)
}
@Test
fun `card ID hex to bytes`() {
val cardId = "cb22000000027374"
val expected = byteArrayOf(-53, 34, 0, 0, 0, 2, 115, 116)
assertThat(cardId.hexToBytes())
.isEqualTo(expected)
}
}

View file

@ -1,192 +0,0 @@
package com.tangem.common.tlv
import com.google.common.truth.Truth.assertThat
import com.tangem.TangemSdkError
import com.tangem.commands.*
import com.tangem.common.extensions.hexToBytes
import org.junit.Test
import org.junit.jupiter.api.assertThrows
import java.util.*
class TlvDecoderTest {
private val rawData = byteArrayOf(1, 8, -53, 34, 0, 0, 0, 2, 115, 116, 32, 11, 83, 77, 65, 82, 84, 32, 67, 65, 83, 72, 0, 2, 1, 2, -128, 6, 50, 46, 49, 49, 114, 0, 3, 65, 4, -49, 11, -50, -66, -121, -25, -2, 65, 65, -13, 14, 49, 27, -82, -33, -85, -113, 65, 20, 8, -39, -75, 57, 45, 65, -31, 35, 44, 38, 40, 63, -44, 113, -45, -75, -95, -118, 118, 29, 65, 117, -24, -53, 82, -72, 91, -20, -96, -77, -103, -14, -63, 52, -127, -123, -27, -16, -128, -67, -3, -104, -26, -22, 65, 10, 4, 0, 0, 126, 33, 12, 90, -127, 2, 0, 41, -126, 4, 7, -29, 5, 2, -125, 7, 84, 65, 78, 71, 69, 77, 0, -124, 3, 69, 84, 72, -122, 64, 111, -103, 48, -114, -40, 18, -103, 26, -102, -12, -38, -78, -90, -9, -98, 88, -47, -100, -24, 24, -105, -70, -72, 6, 94, -96, -77, 11, -123, -28, -118, 37, 63, 107, -55, -11, 23, -12, 13, -23, -121, -63, 36, -59, 70, 116, 91, -125, -34, -69, 23, -112, 6, 17, 4, -49, 68, -56, 29, -45, 81, 10, 97, 83, 48, 65, 4, -127, -106, -86, 75, 65, 10, -60, 74, 59, -100, -50, 24, -25, -66, 34, 106, -22, 7, 10, -52, -125, -87, -49, 103, 84, 15, -84, 73, -81, 37, 18, -97, 106, 83, -118, 40, -83, 99, 65, 53, -114, 60, 79, -103, 99, 6, 79, 126, 54, 83, 114, -90, 81, -45, 116, -27, -62, 60, -35, 55, -3, 9, -101, -14, 5, 10, 115, 101, 99, 112, 50, 53, 54, 107, 49, 0, 8, 4, 0, 15, 66, 64, 7, 1, 0, 9, 2, 11, -72, 96, 65, 4, -42, -5, -41, -84, -23, 88, 2, 86, -63, -118, -123, -10, -66, -82, -107, -68, -93, 111, 47, 93, -20, -86, 74, 28, 21, 81, 93, -21, -124, -57, -102, 55, 17, 84, -66, -68, -22, -128, 126, -99, -65, -54, -42, 59, -25, -21, -124, 5, 59, -16, -72, 73, 48, 16, -27, 103, -112, -73, 2, 96, -51, 41, -42, 116, 98, 4, 0, 15, 66, 52, 99, 4, 0, 0, 0, 13, 15, 1, 0)
private val tlvData = Tlv.deserialize(rawData)
private val tlvMapper = TlvDecoder(tlvData!!)
private val cardDataRaw: ByteArray = tlvMapper.decode(TlvTag.CardData)
private val cardDataMapper = TlvDecoder(Tlv.deserialize(cardDataRaw)!!)
@Test
fun `map optional when value is present`() {
val settingsMask: SettingsMask? = tlvMapper.decodeOptional(TlvTag.SettingsMask)
assertThat(settingsMask)
.isNotNull()
}
@Test
fun `map optional when no tag returns null`() {
val tokenSymbol: String? = tlvMapper.decodeOptional(TlvTag.TokenSymbol)
assertThat(tokenSymbol)
.isNull()
}
@Test
fun `map when value is null throws MissingTagException`() {
assertThrows<TangemSdkError.DecodingFailedMissingTag> {
tlvMapper.decode<String>(TlvTag.TokenSymbol)
}
}
@Test
fun `map optional to wrong type throws WrongTypeException`() {
assertThrows<TangemSdkError.DecodingFailedTypeMismatch> {
tlvMapper.decodeOptional<String?>(TlvTag.CardData)
}
}
@Test
fun `map to wrong type throws WrongTypeException`() {
assertThrows<TangemSdkError.DecodingFailedTypeMismatch> {
tlvMapper.decode<String>(TlvTag.CardData)
}
}
@Test
fun `map boolean missing flag returns false`() {
val terminalIsLinked: Boolean = tlvMapper.decode(TlvTag.TerminalIsLinked)
assertThat(terminalIsLinked)
.isFalse()
}
@Test
fun `map SettingsMask returns correct value`() {
val settingsMask: SettingsMask = tlvMapper.decode(TlvTag.SettingsMask)
assertThat(settingsMask)
.isNotNull()
assertThat(settingsMask.rawValue)
.isEqualTo(32289)
assertThat(settingsMask.contains(Settings.SkipSecurityDelayIfValidatedByLinkedTerminal))
.isFalse()
assertThat(settingsMask.contains(Settings.IsReusable))
.isTrue()
assertThat(settingsMask.contains(Settings.AllowSwapPIN2))
.isTrue()
assertThat(settingsMask.contains(Settings.UseDynamicNdef))
.isTrue()
assertThat(settingsMask.contains(Settings.ProhibitPurgeWallet))
.isFalse()
}
@Test
fun `map SigningMethods single value returns correct value`() {
val signingMethods: SigningMethodMask = tlvMapper.decode(TlvTag.SigningMethod)
assertThat(signingMethods.contains(SigningMethod.SignHash))
.isTrue()
}
@Test
fun `map SigningMethods set of methods returns correct value`() {
val localMapper = TlvDecoder(Tlv.deserialize("070195".hexToBytes())!!)
val signingMethods: SigningMethodMask = localMapper.decode(TlvTag.SigningMethod)
assertThat(signingMethods.contains(SigningMethod.SignHash))
.isTrue()
assertThat(signingMethods.contains(SigningMethod.SignHashValidateByIssuer))
.isTrue()
assertThat(signingMethods.contains(SigningMethod.SignHashValidateByIssuerWriteIssuerData))
.isTrue()
assertThat(signingMethods.contains(SigningMethod.SignRaw))
.isFalse()
assertThat(signingMethods.contains(SigningMethod.SignRawValidateByIssuer))
.isFalse()
assertThat(signingMethods.contains(SigningMethod.SignRawValidateByIssuerWriteIssuerData))
.isFalse()
assertThat(signingMethods.contains(SigningMethod.SignPos))
.isFalse()
}
@Test
fun `map CardStatus returns correct value`() {
val cardStatus: CardStatus = tlvMapper.decode(TlvTag.Status)
assertThat(cardStatus)
.isEqualTo(CardStatus.Loaded)
}
@Test
fun `map ProductMask with raw value 5 returns correct value`() {
val localMapper = TlvDecoder(listOf(Tlv(TlvTag.ProductMask, byteArrayOf(5))))
val productMask: ProductMask = localMapper.decode(TlvTag.ProductMask)
assertThat(productMask.contains(Product.Note) && productMask.contains(Product.IdCard))
.isTrue()
}
@Test
fun `map ProductMask with raw value 1 returns correct value`() {
val localMapper = TlvDecoder(listOf(Tlv(TlvTag.ProductMask, byteArrayOf(1))))
val productMask: ProductMask = localMapper.decode(TlvTag.ProductMask)
assertThat(productMask.contains(Product.Note))
.isTrue()
}
@Test
fun `map Enum with unknown code throws ConversionException error`() {
val localMapper = TlvDecoder(listOf(Tlv(TlvTag.CurveId, "test".toByteArray())))
assertThrows<TangemSdkError.DecodingFailed> {
localMapper.decode<EllipticCurve>(TlvTag.CurveId)
}
}
@Test
fun `map DateTime returns correct value`() {
val date: Date = cardDataMapper.decode(TlvTag.ManufactureDateTime)
val expected = Calendar.getInstance().apply { this.set(2019, 4, 2, 0, 0, 0) }.time
assertThat(date.toString())
.isEqualTo(expected.toString())
}
@Test
fun `map EllipticCurve returns correct value`() {
val ellipticCurve: EllipticCurve = tlvMapper.decode(TlvTag.CurveId)
assertThat(ellipticCurve)
.isEqualTo(EllipticCurve.Secp256k1)
}
@Test
fun `map ByteArray returns correctly`() {
val cardPublicKey: ByteArray = tlvMapper.decode(TlvTag.CardPublicKey)
assertThat(cardPublicKey)
.isInstanceOf(ByteArray::class.java)
}
@Test
fun `map Int returns correct value`() {
val signedHashes: Int = tlvMapper.decode(TlvTag.SignedHashes)
assertThat(signedHashes)
.isEqualTo(13)
}
@Test
fun `map Int with wrong value throws ConversionException`() {
val localMapper = TlvDecoder(listOf(Tlv(TlvTag.SignedHashes, byteArrayOf(1, 2, 3, 4, 5))))
assertThrows<TangemSdkError.DecodingFailed> {
localMapper.decode<Int>(TlvTag.SignedHashes)
}
}
@Test
fun `map UTF8 returns correct value`() {
val blockchainId: String = cardDataMapper.decode(TlvTag.BlockchainId)
assertThat(blockchainId)
.isEqualTo("ETH")
}
@Test
fun `map Hex returns correct value`() {
val cardId: String = tlvMapper.decode(TlvTag.CardId)
assertThat(cardId)
.isEqualTo("CB22000000027374")
}
}

View file

@ -1,111 +0,0 @@
package com.tangem.common.tlv
import com.google.common.truth.Truth.assertThat
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import org.junit.Test
class TlvTest {
@Test
fun `TLVs to bytes, only PIN`() {
val tlvs = listOf(
Tlv(TlvTag.Pin, "000000".calculateSha256())
)
val expected = byteArrayOf(16, 32, -111, -76, -47, 66, -126, 63, 125, 32, -59, -16, -115,
-10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, -10, -45, 19, -124,
-123, -55, -94, 3)
assertThat(tlvs.serialize())
.isEqualTo(expected)
}
@Test
fun `TLVs to bytes, check wallet`() {
val tlvs = listOf(
Tlv(TlvTag.Pin, "000000".calculateSha256()),
Tlv(TlvTag.CardId, "cb22000000027374".hexToBytes()),
Tlv(TlvTag.Challenge, byteArrayOf(-82, -78, -31, 34, 66, -19, -86, -1, 26, 8, 100, -126, -74, 20, -28, 83))
)
val expected = byteArrayOf(16, 32, -111, -76, -47, 66, -126, 63, 125, 32, -59, -16, -115, -10,
-111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, -10, -45, 19, -124, -123,
-55, -94, 3, 1, 8, -53, 34, 0, 0, 0, 2, 115, 116, 22, 16, -82, -78, -31, 34, 66, -19,
-86, -1, 26, 8, 100, -126, -74, 20, -28, 83)
assertThat(tlvs.serialize())
.isEqualTo(expected)
}
@Test
fun `Bytes to Tlvs, only PIN`() {
val bytes = byteArrayOf(16, 32, -111, -76, -47, 66, -126, 63, 125, 32, -59, -16, -115,
-10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, -10, -45, 19, -124,
-123, -55, -94, 3)
val tlvs = Tlv.deserialize(bytes)
assertThat(tlvs)
.isNotNull()
assertThat(tlvs)
.isNotEmpty()
val pin = tlvs!!.find { it.tag == TlvTag.Pin }?.value
val pinExpected = "000000".calculateSha256()
assertThat(pin)
.isEqualTo(pinExpected)
}
@Test
fun `Bytes to TLVs, check wallet TLVs`() {
val bytes = byteArrayOf(16, 32, -111, -76, -47, 66, -126, 63, 125, 32, -59, -16, -115, -10,
-111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, -10, -45, 19, -124, -123,
-55, -94, 3, 1, 8, -53, 34, 0, 0, 0, 2, 115, 116, 22, 16, -82, -78, -31, 34, 66, -19,
-86, -1, 26, 8, 100, -126, -74, 20, -28, 83)
val tlvs = Tlv.deserialize(bytes)
assertThat(tlvs)
.isNotNull()
assertThat(tlvs)
.isNotEmpty()
val pin = tlvs!!.find { it.tag == TlvTag.Pin }?.value
val pinExpected = "000000".calculateSha256()
assertThat(pin)
.isEqualTo(pinExpected)
val cardId = tlvs.find { it.tag == TlvTag.CardId }?.value
val cardIdExpected = "cb22000000027374".hexToBytes()
assertThat(cardId)
.isEqualTo(cardIdExpected)
val challenge = tlvs.find { it.tag == TlvTag.Challenge }?.value
val challengeExpected = byteArrayOf(-82, -78, -31, 34, 66, -19, -86, -1, 26, 8, 100, -126, -74, 20, -28, 83)
assertThat(challenge)
.isEqualTo(challengeExpected)
}
@Test
fun `Bytes to TLVs, wrong values`() {
val bytes = byteArrayOf(0)
val tlvs = Tlv.deserialize(bytes)
assertThat(tlvs)
.isNull()
val bytes1 = byteArrayOf(0, 0, 0, 0, 0, 0, 0)
val tlvs1 = Tlv.deserialize(bytes1)
assertThat(tlvs1)
.isNull()
}
@Test
fun `parse Slix tag response`() {
val response = "03ff010f91010b550474616e67656d2e636f6d140f11616e64726f69642e636f6d3a706b67636f6d2e74616e67656d2e77616c6c65745411c974616e67656d2e636f6d3a77616c6c657490000c618102ffff8a0102820407e40109830b54414e47454d2053444b008403584c4d86400e71c1f060387029688254320b90abeae471bcafbbe8ea3880903bdb8d1cc389d032b982e1ffd7ef49e66f1780123b763dd2f3a9a9494eb0fad4ae8cf306672c60207c967a51077c14fc49d867f23b8d0eaf60cad479a56587e894571b7fb33690176140345fbe53f5be0ec871e91c317cde2bd0396d47e4b945c138c153b0271f636a73cf531df1bc54ac4fcdbce42f81b40d58e0265d34e28121a4c50fdfe329a97f6000fe000000000000000000000000000000000000000000000000000000000000000000000000000000"
val tlvs = Tlv.deserialize(response.hexToBytes(), true)
assertThat(tlvs)
.isNotEmpty()
}
}

View file

@ -1,50 +0,0 @@
package com.tangem.crypto
import com.google.common.truth.Truth.assertThat
import com.tangem.commands.EllipticCurve
import com.tangem.crypto.CryptoUtils.generatePublicKey
import com.tangem.crypto.CryptoUtils.generateRandomBytes
import com.tangem.crypto.CryptoUtils.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class CryptoUtilsTest {
@BeforeEach
internal fun setUp() {
CryptoUtils.initCrypto()
}
@Test
fun generateRandomBytesTest() {
val privateKey: ByteArray = generateRandomBytes(32)
assertThat(privateKey)
.hasLength(32)
assertThat(privateKey.sum())
.isNotEqualTo(0)
}
@Test
internal fun verifyEd25519Test() {
val verified = verifySignature_withSampleData(EllipticCurve.Ed25519)
assertThat(verified)
.isTrue()
}
@Test
internal fun verifySecp256k1Test() {
val verified = verifySignature_withSampleData(EllipticCurve.Secp256k1)
assertThat(verified)
.isTrue()
}
private fun verifySignature_withSampleData(curve: EllipticCurve): Boolean {
val privateKey = ByteArray(32) { 1 }
val publicKey = generatePublicKey(privateKey, curve)
val message = ByteArray(64) { 5 }
val signature = message.sign(privateKey, curve)
return verify(publicKey, message, signature, curve)
}
}

View file

@ -1 +0,0 @@
/build

View file

@ -1,56 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "com.tangem.devkit"
minSdkVersion 21
targetSdkVersion 29
versionCode 4
versionName "1.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation project(':tangem-core')
implementation project(':tangem-sdk')
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0'
implementation "androidx.constraintlayout:constraintlayout:2.0.0-beta4"
implementation "androidx.navigation:navigation-fragment-ktx:2.2.1"
implementation "androidx.navigation:navigation-ui-ktx:2.2.1"
implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
implementation "com.google.android.material:material:1.2.0-alpha05"
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.github.gbIxaHue:eu4d:0.3.8'
}

View file

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -1,80 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tangem.devkit">
<uses-feature
android:name="android.hardware.nfc"
android:required="true" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name="com.tangem.devkit.AppTangemDemo"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name="com.tangem.devkit._main.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="www.tangem.com"
android:scheme="http" />
<data
android:host="www.tangem.com"
android:scheme="https" />
<data
android:host="tangem.com"
android:scheme="http" />
<data
android:host="tangem.com"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
</activity>
<activity android:name="com.tangem.devkit.TestUserDataActivity">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="www.tangem.com"
android:scheme="http" />
<data
android:host="www.tangem.com"
android:scheme="https" />
<data
android:host="tangem.com"
android:scheme="http" />
<data
android:host="tangem.com"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
</activity>
</application>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,35 +0,0 @@
package com.tangem.devkit
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import com.tangem.devkit._arch.structure.ILog
import com.tangem.devkit._arch.structure.ItemLogger
import com.tangem.devkit.commons.TangemLogger
import ru.dev.gbixahue.eu4d.lib.android.global.log.Log
/**
[REDACTED_AUTHOR]
*/
class AppTangemDemo : Application() {
override fun onCreate() {
super.onCreate()
AppTangemDemo.appInstance = this
setupLoggers()
}
private fun setupLoggers() {
Log.setLogger(TangemLogger())
ILog.setLogger(ItemLogger())
}
fun sharedPreferences(name: String = "DevKitApp", mode: Int = Context.MODE_PRIVATE): SharedPreferences {
return getSharedPreferences(name, mode)
}
companion object {
lateinit var appInstance: AppTangemDemo
}
}

View file

@ -1,143 +0,0 @@
package com.tangem.devkit
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.tangem.TangemSdk
import com.tangem.common.CompletionResult
import com.tangem.tangem_sdk_new.extensions.init
import kotlinx.android.synthetic.main.old_activity_main.*
class Old_MainActivity : AppCompatActivity() {
private lateinit var tangemSdk: TangemSdk
private lateinit var cardId: String
private lateinit var issuerData: ByteArray
private lateinit var issuerDataSignature: ByteArray
private var issuerDataCounter: Int = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.old_activity_main)
tangemSdk = TangemSdk.init(this)
btn_scan?.setOnClickListener { _ ->
tangemSdk.scanCard { taskEvent ->
when (taskEvent) {
is CompletionResult.Success -> {
// Handle returned card data
val card = taskEvent.data
cardId = card.cardId
runOnUiThread {
tv_card_cid?.text = cardId
btn_create_wallet.isEnabled = true
tv_card_cid?.text = cardId
btn_sign.isEnabled = true
btn_read_issuer_data.isEnabled = true
btn_read_issuer_extra_data.isEnabled = true
btn_write_issuer_data.isEnabled = true
btn_purge_wallet.isEnabled = true
btn_create_wallet.isEnabled = true
}
}
}
}
}
btn_sign?.setOnClickListener { _ ->
tangemSdk.sign(
createSampleHashes(),
cardId) {
when (it) {
is CompletionResult.Failure -> {
runOnUiThread { tv_card_cid?.text = it.error::class.simpleName }
}
is CompletionResult.Success -> runOnUiThread { tv_card_cid?.text = cardId + "was used to sign sample hashes." }
}
}
}
btn_read_issuer_data?.setOnClickListener { _ ->
tangemSdk.readIssuerData(cardId) {
when (it) {
is CompletionResult.Failure -> {
runOnUiThread { tv_card_cid?.text = it.error::class.simpleName }
}
is CompletionResult.Success -> runOnUiThread {
btn_write_issuer_data.isEnabled = true
tv_card_cid?.text = it.data.issuerData.contentToString()
issuerData = it.data.issuerData
issuerDataSignature = it.data.issuerDataSignature
}
}
}
}
btn_write_issuer_data?.setOnClickListener { _ ->
tangemSdk.writeIssuerData(
cardId,
issuerData,
issuerDataSignature) {
when (it) {
is CompletionResult.Failure -> {
runOnUiThread { tv_card_cid?.text = it.error::class.simpleName }
}
is CompletionResult.Success -> runOnUiThread {
tv_card_cid?.text = it.data.cardId
}
}
}
}
btn_read_issuer_extra_data?.setOnClickListener { _ ->
tangemSdk.readIssuerExtraData(cardId) {
when (it) {
is CompletionResult.Failure -> {
runOnUiThread { tv_card_cid?.text = it.error::class.simpleName }
}
is CompletionResult.Success -> runOnUiThread {
issuerDataCounter = (it.data.issuerDataCounter ?: 0) + 1
btn_write_issuer_data.isEnabled = true
tv_card_cid?.text = "Read ${it.data.issuerData.size} bytes of data."
}
}
}
}
btn_purge_wallet?.setOnClickListener { _ ->
tangemSdk.purgeWallet(
cardId) {
when (it) {
is CompletionResult.Failure -> {
runOnUiThread { tv_card_cid?.text = it.error::class.simpleName }
}
is CompletionResult.Success -> runOnUiThread {
tv_card_cid?.text = it.data.status.name
}
}
}
}
btn_create_wallet?.setOnClickListener { _ ->
tangemSdk.createWallet(
cardId) {
when (it) {
is CompletionResult.Failure -> {
runOnUiThread { tv_card_cid?.text = it.error::class.simpleName }
}
is CompletionResult.Success -> runOnUiThread {
tv_card_cid?.text = it.data.status.name
btn_sign.isEnabled = true
btn_read_issuer_data.isEnabled = true
btn_purge_wallet.isEnabled = true
btn_create_wallet.isEnabled = false
}
}
}
}
btn_read_write_user_data?.setOnClickListener { startActivity(Intent(this, TestUserDataActivity::class.java)) }
}
private fun createSampleHashes(): Array<ByteArray> {
val hash1 = ByteArray(32) { 1 }
val hash2 = ByteArray(32) { 2 }
return arrayOf(hash1, hash2)
}
}

View file

@ -1,146 +0,0 @@
package com.tangem.devkit
import android.os.Bundle
import android.view.View
import android.widget.CompoundButton
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.tangem.TangemSdk
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.tangem_sdk_new.extensions.init
import kotlinx.android.synthetic.main.activity_test_user_data.*
import java.nio.charset.StandardCharsets
/**
[REDACTED_AUTHOR]
*/
class TestUserDataActivity : AppCompatActivity() {
private lateinit var tangemSdk: TangemSdk
private lateinit var writeOptions: WriteOptions
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test_user_data)
init()
initWriteOptions()
}
private fun init() {
tangemSdk = TangemSdk.init(this)
btn_scan?.setOnClickListener { _ ->
tangemSdk.scanCard { taskEvent ->
when (taskEvent) {
is CompletionResult.Success -> {
// Handle returned card data
writeOptions.cardId = taskEvent.data.cardId
runOnUiThread { showReadWriteSection(true) }
}
}
}
}
btn_write.setOnClickListener {
if (writeOptions.cardId == null) return@setOnClickListener
tangemSdk.writeUserData(
writeOptions.cardId!!,
writeOptions.userData,
writeOptions.userCounter
) {
when (it) {
is CompletionResult.Failure -> handleError(tv_write_result, it.error)
is CompletionResult.Success -> {
runOnUiThread { tv_write_result?.text = "Success" }
}
}
}
}
btn_read.setOnClickListener {
if (writeOptions.cardId == null) return@setOnClickListener
tangemSdk.readUserData(writeOptions.cardId!!) {
when (it) {
is CompletionResult.Failure -> handleError(tv_write_result, it.error)
is CompletionResult.Success -> {
runOnUiThread {
tv_read_result?.text = "Success"
writeOptions.userData = it.data.userData
writeOptions.userProtectedData = it.data.userProtectedData
writeOptions.userCounter = it.data.userCounter
writeOptions.userProtectedCounter = it.data.userProtectedCounter
tv_card_cid.text = it.data.cardId
tv_data.text = String(it.data.userData, StandardCharsets.US_ASCII)
tv_protected_data.text = String(it.data.userProtectedData, StandardCharsets.US_ASCII)
tv_counter.text = it.data.userCounter.toString()
tv_protected_counter.text = it.data.userProtectedCounter.toString()
}
}
}
}
}
}
private fun handleError(tv: TextView, error: TangemSdkError) {
if (error is TangemSdkError.UserCancelled) return
runOnUiThread { tv.text = error::class.simpleName }
}
private fun initWriteOptions() {
writeOptions = WriteOptions()
chb_with_ud.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateData(buttonView) }
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) }
}
private fun showReadWriteSection(show: Boolean) {
val state = if (show) View.VISIBLE else View.GONE
cl_read_write.visibility = state
}
}
class WriteOptions {
var cardId: String? = null
var userData: ByteArray? = null
var userProtectedData: ByteArray? = null
var userCounter: Int? = null
var userProtectedCounter: Int? = null
var pin2: String? = null
fun updateData(chbx: CompoundButton) {
val value = "simple user data".toByteArray()
userData = if (chbx.isChecked) value else null
}
fun updateProtectedData(chbx: CompoundButton) {
val value = "protected user data".toByteArray()
userProtectedData = if (chbx.isChecked) value else null
}
fun updateCounter(chbx: CompoundButton) {
val value = if (userCounter == null) 0 else userCounter!! + 1
userCounter = if (chbx.isChecked) value else null
}
fun updateProtectedCounter(chbx: CompoundButton) {
val value = if (userProtectedCounter == null) 0 else userProtectedCounter!! + 1
userProtectedCounter = 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

@ -1,44 +0,0 @@
package com.tangem.devkit._arch
import android.util.Log
import androidx.annotation.MainThread
import androidx.annotation.Nullable
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean
/**
[REDACTED_AUTHOR]
*/
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val mPending: AtomicBoolean = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer {
if (mPending.compareAndSet(true, false)) {
observer.onChanged(it)
}
})
}
@MainThread
override fun setValue(@Nullable t: T?) {
mPending.set(true)
super.setValue(t)
}
@MainThread
fun call() {
setValue(null)
}
companion object {
private const val TAG = "SingleLiveEvent"
}
}

View file

@ -1,16 +0,0 @@
package com.tangem.devkit._arch.structure
/**
[REDACTED_AUTHOR]
*/
typealias Payload = MutableMap<String, Any?>
interface PayloadHolder {
val payload: Payload
fun get(key: String): Any? = payload[key]
fun remove(key: String): Any? = payload.remove(key)
fun set(key: String, value: Any?) {
payload[key] = value
}
}

View file

@ -1,25 +0,0 @@
package com.tangem.devkit._arch.structure
/**
[REDACTED_AUTHOR]
*/
interface Id {
companion object {
fun getTag(id: Id): String {
val className = id.javaClass.simpleName
return if (id is Enum<*>) "$className.${id.name}" else className
}
}
}
class StringId(val value: String) : Id
class StringResId(val value: Int) : Id
enum class Additional : Id {
UNDEFINED,
JSON_INCOMING,
JSON_OUTGOING,
JSON_TAILS,
}

View file

@ -1,36 +0,0 @@
package com.tangem.devkit._arch.structure
import ru.dev.gbixahue.eu4d.lib.android.global.log.Logger
import ru.dev.gbixahue.eu4d.lib.android.global.log.TagLogger
import ru.dev.gbixahue.eu4d.lib.android.global.log.profiling.SimpleLogProfiler
import ru.dev.gbixahue.eu4d.lib.kotlin.stringOf
/**
[REDACTED_AUTHOR]
*/
object ILog {
private var logger: Logger? = null
fun setLogger(logger: Logger) {
ILog.logger = logger
}
fun d(from: Any, msg: Any?, value: Any? = null) {
logger?.d(from, stringOf(msg), value)
}
fun w(from: Any, msg: Any?, value: Any? = null) {
logger?.w(from, stringOf(msg), value)
}
fun e(from: Any, msg: Any?, value: Any? = null) {
logger?.e(from, stringOf(msg), value)
}
}
class ItemLogger : TagLogger("ITEM") {
init {
msProfiler = SimpleLogProfiler()
}
}

View file

@ -1,29 +0,0 @@
package com.tangem.devkit._arch.structure.abstraction
import com.tangem.devkit._arch.structure.Id
/**
[REDACTED_AUTHOR]
*/
fun List<Item>.findItem(id: Id): Item? {
var foundItem: Item? = null
iterate {
if (it.id == id) {
foundItem = it
return@iterate
}
}
return foundItem
}
fun List<Item>.iterate(func: (Item) -> Unit) {
forEach {
when (it) {
is BaseItem -> func(it)
is ItemGroup -> {
func(it)
it.itemList.iterate(func)
}
}
}
}

View file

@ -1,47 +0,0 @@
package com.tangem.devkit._arch.structure.abstraction
import com.tangem.devkit._arch.structure.Id
/**
[REDACTED_AUTHOR]
*/
interface UpdateBy<B>{
fun update(value: B)
}
interface Item: UpdateBy<Item> {
val id: Id
var parent: Item?
var viewModel: ItemViewModel
fun added(parent: Item) {
this.parent = parent
}
fun removed(parent: Item) {
this.parent = null
}
fun <D> getData(): D? = viewModel.data as? D
fun setData(value: Any?) {
viewModel.data = value
}
fun restoreDefaultData() {
setData(viewModel.defaultData)
}
}
open class BaseItem(
override val id: Id,
override var viewModel: ItemViewModel
) : Item {
override var parent: Item? = null
override fun update(value: Item) {
viewModel.update(value.viewModel)
}
}

View file

@ -1,56 +0,0 @@
package com.tangem.devkit._arch.structure.abstraction
import com.tangem.devkit._arch.structure.ILog
import com.tangem.devkit._arch.structure.Id
/**
[REDACTED_AUTHOR]
*/
interface ItemGroup : Item {
val itemList: MutableList<Item>
fun setItems(list: MutableList<Item>)
fun getItems(): MutableList<Item>
fun addItem(item: Item)
fun removeItem(item: Item)
fun clear()
}
open class SimpleItemGroup(
override val id: Id,
override var viewModel: ItemViewModel = BaseItemViewModel()
) : ItemGroup {
override var parent: Item? = null
override val itemList: MutableList<Item> = mutableListOf()
override fun setItems(list: MutableList<Item>) {
ILog.d(this, "setItems into: $id, count: ${list.size}")
itemList.forEach { it.removed(this) }
itemList.clear()
list.forEach { addItem(it) }
}
override fun getItems(): MutableList<Item> = itemList
override fun addItem(item: Item) {
ILog.d(this, "addItem into: $id, who: ${item.id}")
itemList.add(item)
item.added(this)
}
override fun removeItem(item: Item) {
ILog.d(this, "removeItem from: $id, which: ${item.id}")
itemList.remove(item)
item.removed(this)
}
override fun clear() {
ILog.d(this, "clear $id")
itemList.clear()
}
override fun update(value: Item) {
// nothing to do
}
}

View file

@ -1,115 +0,0 @@
package com.tangem.devkit._arch.structure.abstraction
import com.tangem.devkit._arch.structure.ILog
import com.tangem.devkit._arch.structure.Payload
import com.tangem.devkit._arch.structure.PayloadHolder
/**
[REDACTED_AUTHOR]
*/
typealias ValueChanged<V> = (V?) -> Unit
typealias SafeValueChanged<V> = (V) -> Unit
class KeyValue(val key: String, val value: Any)
class ViewState(
isVisible: Boolean? = null,
bgColor: Int? = -1
) : UpdateBy<ViewState> {
class State<T>(
stateValue: T,
var onValueChanged: SafeValueChanged<T>? = null
) {
var value = stateValue
set(value) {
if (preventSameChanges && field == value) return
field = value
onValueChanged?.invoke(value)
}
internal var preventSameChanges = true
}
var isVisibleState = State(isVisible)
var backgroundColor = State(bgColor)
var descriptionVisibility = State(0x00000008)
internal fun preventSameChanges(isPrevented: Boolean) {
val states = listOf(isVisibleState, backgroundColor, descriptionVisibility)
states.forEach { it.preventSameChanges = isPrevented }
}
override fun update(value: ViewState) {
// isVisibleState.update(value.isVisibleState)
// backgroundColor.update(value.backgroundColor)
// descriptionVisibility.update(value.descriptionVisibility)
}
}
interface ItemViewModel : PayloadHolder, UpdateBy<ItemViewModel> {
val viewState: ViewState
var data: Any?
var defaultData: Any?
var onDataUpdated: ValueChanged<Any?>?
fun updateDataByView(data: Any?)
}
open class BaseItemViewModel(
value: Any? = null,
override val viewState: ViewState = ViewState()
) : ItemViewModel {
override val payload: Payload = mutableMapOf()
// Don't update it directly from a View. Use for it updateDataByView()
override var data: Any? = value
set(value) {
if (handleDataUpdates(value)) field = value
}
// Data for restoring initial value
override var defaultData: Any? = value
set(value) {
field = value
data = value
}
// Use it for handling data updates in View
override var onDataUpdated: ValueChanged<Any?>? = null
// When data updates directly it invokes onDataUpdated
// return true = data will update
// return false = data won't update
protected open fun handleDataUpdates(value: Any?): Boolean {
ILog.d(this, "handleDateUpdates: $value")
onDataUpdated?.invoke(value)
return true
}
// Use it to update the data from a View. It disables onDataUpdated to prevent a callback loop
override fun updateDataByView(data: Any?) {
ILog.d(this, "data changed: $data")
val callback = onDataUpdated
onDataUpdated = null
this.data = data
onDataUpdated = callback
}
override fun update(value: ItemViewModel) {
viewState.update(value.viewState)
defaultData = value.defaultData
data = value.data
payload.clear()
payload.putAll(value.payload)
}
}
class ListViewModel(
val itemList: List<KeyValue>,
var selectedItem: Any?,
override val viewState: ViewState = ViewState()
) : BaseItemViewModel(selectedItem)

Some files were not shown because too many files have changed in this diff Show more