Updated on 2026-08-14
This commit is contained in:
commit
b7ca492168
319 changed files with 6 additions and 17138 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
1
tangem-core/.gitignore
vendored
1
tangem-core/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
/build
|
||||
|
|
@ -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'
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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 it’s 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 issuer’s 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 issuer’s 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 issuer’s 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 issuer’s 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 issuer’s attestation data.
|
||||
*
|
||||
* @param cardId CID, Unique Tangem card ID number.
|
||||
* @param issuerData Data provided by issuer.
|
||||
* @param issuerDataSignature Issuer’s 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 issuer’s 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 Issuer’s 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 Issuer’s 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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 user’s 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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 manufacturer’s 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 manufacturer’s 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 issuer’s confirmation of activation.
|
||||
* is "true" if the card requires activation,
|
||||
* is 'false" if the card is activated or does not require activation
|
||||
*/
|
||||
val isActivated: Boolean,
|
||||
|
||||
/**
|
||||
* 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 card’s 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
||||
/**
|
||||
* Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card).
|
||||
* Issuer’s signature of SHA256-hashed [cardId] concatenated with [issuerData]:
|
||||
* SHA256([cardId] | [issuerData]).
|
||||
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
|
||||
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
|
||||
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
|
||||
*/
|
||||
val issuerDataSignature: ByteArray,
|
||||
|
||||
/**
|
||||
* An optional counter that protect issuer data against replay attack.
|
||||
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask]
|
||||
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
|
||||
*/
|
||||
val issuerDataCounter: Int?
|
||||
) : CommandResponse
|
||||
|
||||
|
||||
/**
|
||||
* This command returns 512-byte Issuer Data field and its issuer’s 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 issuer’s 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
||||
/**
|
||||
* Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card).
|
||||
* Issuer’s signature of SHA256-hashed [cardId] concatenated with [issuerData]:
|
||||
* SHA256([cardId] | [issuerData]).
|
||||
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
|
||||
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
|
||||
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
|
||||
*/
|
||||
val issuerDataSignature: ByteArray?,
|
||||
|
||||
/**
|
||||
* An optional counter that protects issuer data against replay attack.
|
||||
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask]
|
||||
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
|
||||
*/
|
||||
val issuerDataCounter: Int?
|
||||
) : CommandResponse
|
||||
|
||||
|
||||
/**
|
||||
* This command retrieves Issuer Extra Data field and its issuer’s 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 issuer’s 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 issuer’s attestation data.
|
||||
* @property cardId CID, Unique Tangem card ID number.
|
||||
* @property issuerData Data provided by issuer.
|
||||
* @property issuerDataSignature Issuer’s 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 issuer’s 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 Issuer’s 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 Issuer’s 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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!")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package com.tangem.commands.personalization.entities
|
||||
|
||||
import com.tangem.KeyPair
|
||||
|
||||
data class Manufacturer(
|
||||
val keyPair: KeyPair,
|
||||
val name: String? = null
|
||||
)
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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])
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package com.tangem.common.extensions
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
fun BigDecimal.isZero() : Boolean {
|
||||
return this.compareTo(BigDecimal.ZERO) == 0
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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"
|
||||
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 -> {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = ?;
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
1
tangem-devkit/.gitignore
vendored
1
tangem-devkit/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
/build
|
||||
|
|
@ -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'
|
||||
}
|
||||
21
tangem-devkit/proguard-rules.pro
vendored
21
tangem-devkit/proguard-rules.pro
vendored
|
|
@ -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
|
||||
|
|
@ -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 |
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
// }
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue