Updated on 2026-08-14

This commit is contained in:
Tangem 2019-11-14 12:00:59 +03:00
parent 587f7d2913
commit d6d7c292e7
27 changed files with 344 additions and 23 deletions

View file

@ -1,6 +1,10 @@
package com.tangem
/**
* Contains data relating to a Tangem card. It is used in constructing all the commands,
* and commands can return modified [CardEnvironment].
*/
data class CardEnvironment(
val pin1: String = DEFAULT_PIN,
val pin2: String = DEFAULT_PIN2,

View file

@ -8,6 +8,15 @@ import com.tangem.crypto.CryptoUtils
import com.tangem.tasks.*
import java.util.concurrent.Executors
/**
* 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 cardManagerDelegate is an interface that allows interaction with users and shows relevant UI.
* Its default implementation, DefaultCardManagerDelegate, is in our tangem-sdk module.
*/
class CardManager(
private val reader: CardReader,
private val cardManagerDelegate: CardManagerDelegate? = null) {
@ -20,11 +29,35 @@ class CardManager(
CryptoUtils.initCrypto()
}
/**
* A method that allows to read a card and verify that its private key.
* It launches on the new thread a [ScanTask] that will send the following events in a callback:
* [ScanEvent.OnReadEvent] after completing [com.tangem.commands.ReadCommand]
* [ScanEvent.OnVerifyEvent] after completing [com.tangem.commands.CheckWalletCommand]
* [TaskEvent.Completion] with an error field null after successful completion of a task or
* [TaskEvent.Completion] with a [TaskError] if some error occurs.
*/
fun scanCard(callback: (result: TaskEvent<ScanEvent>) -> Unit) {
val task = ScanTask()
runTask(task, callback = callback)
}
/**
* A method that allows to sign hashes (usually a blockchain transaction) with a private key
* from a Tangem card. (Please note that the private key itself never leaves the Tangem card).
*
* This method launches on the new thread [SignCommand] that will send the following events in a callback:
* [SignResponse] after completing [SignCommand]
* [TaskEvent.Completion] with an error field null after successful completion of a task or
* [TaskEvent.Completion] with a [TaskError] if some error occurs.
* 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 [CardManagerDelegate] to notify users of security delay.
* @param hashes Array of transaction hashes. It can be a single hash or several hashes of the same length.
* @param cardId CID, Unique Tangem card ID number
* @param callback
*
*/
fun sign(hashes: Array<ByteArray>, cardId: String,
callback: (result: TaskEvent<SignResponse>) -> Unit) {
val signCommand: SignCommand
@ -42,6 +75,9 @@ class CardManager(
runTask(task, cardId, callback)
}
/**
*/
fun <T> runTask(task: Task<T>, cardId: String? = null,
callback: (result: TaskEvent<T>) -> Unit) {
if (isBusy) {
@ -63,6 +99,9 @@ class CardManager(
}
}
/**
*/
fun <T : CommandResponse> runCommand(command: CommandSerializer<T>,
cardId: String? = null,
callback: (result: TaskEvent<T>) -> Unit) {

View file

@ -3,6 +3,11 @@ package com.tangem
import com.tangem.common.CompletionResult
import com.tangem.tasks.TaskError
/**
* Allows interaction with users and shows visual elements.
*
* Its default implementation, DefaultCardManagerDelegate, is in our tangem-sdk module.
*/
interface CardManagerDelegate {
fun onNfcSessionStarted()

View file

@ -4,9 +4,35 @@ import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
/**
* Allows interaction between the phone or any other terminal and Tangem card.
*
* Its default implementation, NfcCardReader, is in our tangem-sdk module.
*/
interface CardReader {
/**
* [com.tangem.tasks.Task] sets it to true before the first command,
* and it should be set to false on completion of the task.
*/
var readingActive: Boolean
/**
* 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 startNfcSession()
/**
* Signals to [CardReader] that no further NFC transition is expected.
*/
fun closeSession()
}

View file

@ -22,6 +22,11 @@ object Log {
}
}
/**
* 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)

View file

@ -11,13 +11,29 @@ import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.tasks.TaskError
/**
* 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 Signature with wallet private key of challenge and salt.
* It uses SHA256 for secp256k1 curve and SHA512 for ed25519 curve.
*/
class CheckWalletResponse(
val cardId: String,
val salt: ByteArray,
val walletSignature: ByteArray
) : CommandResponse
/**
* This command proves that the wallet private key from the card corresponds to the wallet public key.
* Standard challenge/response scheme is used.
*
* @property pin1 hashed users pin 1 code to access the card. Default unhashed value: 000000.
* Pin code should be taken from card environment.
* @property cardId Unique Tangem card ID number
* @property challenge Random challenge generated by application
*/
class CheckWalletCommand(
private val pin1: String,
private val cardId: String,

View file

@ -2,22 +2,41 @@ package com.tangem.commands
import com.tangem.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extentions.toInt
import com.tangem.common.tlv.TlvTag
/**
* Simple interface for responses received after sending commands to Tangem cards.
*/
interface CommandResponse
/**
* Abstract class for all Tangem card commands.
*/
abstract class CommandSerializer<T : CommandResponse> {
abstract val instruction: Instruction
abstract val instructionCode: Int
/**
* Serializes data into a [List] of [com.tangem.common.tlv.Tlv],
* then creates [CommandApdu] with this data.
*
* @return command data that can be converted to raw bytes with a method [CommandApdu.toBytes].
*/
abstract fun serialize(cardEnvironment: CardEnvironment): CommandApdu
/**
* Deserializes data, received from a card and stored in [ResponseApdu],
* into a [List] of [com.tangem.common.tlv.Tlv]. Then this method maps it into a [CommandResponse].
*
* @return card response, converted to a [CommandResponse] of a type [T].
*/
abstract fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): T?
/**
* Helper method to parse security delay information received from a card.
*
* @return Remaining security delay in milliseconds.
*/
fun deserializeSecurityDelay(responseApdu: ResponseApdu, cardEnvironment: CardEnvironment): Int? {
val tlv = responseApdu.getTlvData(cardEnvironment.encryptionKey)
return tlv?.find { it.tag == TlvTag.Pause }?.value?.toInt()

View file

@ -152,7 +152,6 @@ class Card(
/**
* 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 : CommandSerializer<Card>() {

View file

@ -12,6 +12,13 @@ import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.sign
import com.tangem.tasks.TaskError
/**
* @param cardId CID, Unique Tangem card ID number
* @param signature signed hashes (array of resulting signatures)
* @param remainingSignatures Remaining number of sign operations before the wallet will stop signing transactions.
* @param signedHashes 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,
@ -19,7 +26,12 @@ class SignResponse(
val signedHashes: 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>, private val cardId: String)
: CommandSerializer<SignResponse>() {
@ -51,6 +63,12 @@ class SignCommand(private val hashes: Array<ByteArray>, private val cardId: Stri
return CommandApdu(Instruction.Sign, tlvData)
}
/**
* Adds to the command data the terminal public key (generated by the application) and
* transaction hashes signed by the terminal private key.
* This allows to link the card to the Android device and skip security delay.
* (as described in Linked Terminal section of the Tangem Card Manual).
*/
private fun addTerminalSignature(cardEnvironment: CardEnvironment, tlvData: MutableList<Tlv>) {
cardEnvironment.terminalKeys?.let { terminalKeyPair ->
val signedData = dataToSign.sign(terminalKeyPair.privateKey)

View file

@ -1,7 +1,12 @@
package com.tangem.common
import com.tangem.common.CompletionResult.Success
import com.tangem.tasks.TaskError
/**
* Response class encapsulating successful and failed results.
* [T] is a 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: TaskError) : CompletionResult<T>()

View file

@ -5,6 +5,13 @@ import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.toBytes
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 tlvList list of TLVs that are to be sent to the card
*/
class CommandApdu(
private val ins: Int,
@ -31,6 +38,10 @@ class CommandApdu(
encryptionKey = encryptionKey
)
/**
* Request converted to a raw data
*/
val apduData: ByteArray
init {

View file

@ -1,5 +1,9 @@
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),
Read(0xF2),

View file

@ -2,6 +2,13 @@ package com.tangem.common.apdu
import com.tangem.common.tlv.Tlv
/**
* 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(val data: ByteArray) {
private val sw1: Int = 0x00FF and data[data.size - 2].toInt()
@ -11,6 +18,12 @@ class ResponseApdu(val data: ByteArray) {
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(encryptionKey: ByteArray? = null): List<Tlv>? {
return when {
data.size < 2 -> null

View file

@ -1,6 +1,9 @@
package com.tangem.common.apdu
enum class StatusWord (val code: Int, val description: String){
/**
* Part of a response from the card, shows the status of the operation
*/
enum class StatusWord(val code: Int, val description: String) {
ProcessCompleted(0x9000, "SW_PROCESS_COMPLETED"),
InvalidParams(0x6A86, "SW_INVALID_PARAMS"),

View file

@ -5,6 +5,9 @@ import java.security.MessageDigest
import java.util.*
import kotlin.experimental.and
/**
* Extension functions for [ByteArray].
*/
fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }

View file

@ -3,7 +3,9 @@ package com.tangem.common.extentions
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"))

View file

@ -3,6 +3,9 @@ package com.tangem.common.tlv
import java.io.ByteArrayInputStream
import java.io.IOException
/**
* The data converted to the Tag Length Value protocol.
*/
class Tlv {
val tag: TlvTag

View file

@ -14,8 +14,22 @@ class MissingTagException(message: String? = null) : TlvMapperException(message)
class WrongTypeException(message: String? = null) : TlvMapperException(message)
class ConversionException(message: String? = null) : TlvMapperException(message)
/**
* 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 TlvMapper(val tlvList: List<Tlv>) {
/**
* 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> mapOptional(tag: TlvTag): T? =
try {
map<T>(tag)
@ -23,6 +37,17 @@ class TlvMapper(val tlvList: List<Tlv>) {
null
}
/**
* Finds [Tlv] by its [TlvTag].
* Throws [MissingTagException] 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 [MissingTagException] if no [Tlv] is found by the Tag.
*/
inline fun <reified T> map(tag: TlvTag): T {
val tlvValue: ByteArray = tlvList.find { it.tag == tag }?.value
?: if (tag.valueType() == TlvValueType.BoolValue && T::class == Boolean::class) {

View file

@ -1,5 +1,8 @@
package com.tangem.common.tlv
/**
* Contains all possible value types that value for [TlvTag] can contain.
*/
enum class TlvValueType {
HexString,
Utf8String,
@ -14,6 +17,9 @@ enum class TlvValueType {
SigningMethod
}
/**
* Contains all TLV tags, with their code and descriptive name.
*/
enum class TlvTag(val code: Int) {
Unknown(0x00),
CardId(0x01),
@ -97,13 +103,9 @@ enum class TlvTag(val code: Int) {
TerminalPublicKey(0x5C),
TerminalTransactionSignature(0x57);
fun hasNestedTlv(): Boolean {
return when (this) {
TlvTag.CardData -> true
else -> false
}
}
/**
* @return [TlvValueType] associated with a [TlvTag]
*/
fun valueType(): TlvValueType {
return when (this) {
CardId, Pin, Batch -> TlvValueType.HexString

View file

@ -13,21 +13,65 @@ object CryptoUtils {
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 public key 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 -> verifySecp256k1(publicKey, message, signature)
EllipticCurve.Ed25519 -> verifyEd25519(publicKey, message, signature)
EllipticCurve.Secp256k1 -> Sepc256k1.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 -> Sepc256k1.generatePublicKey(privateKeyArray)
EllipticCurve.Ed25519 -> Ed25519.generatePublicKey(privateKeyArray)
}
}
}
/**
* Extension function to sign a ByteArray with an elliptic curve cryptography.
*
* @param privateKeyArray key to sign data
* @param curve 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 -> signSecp256k1(this, privateKeyArray)

View file

@ -7,12 +7,27 @@ import com.tangem.commands.ReadCommand
import com.tangem.common.CompletionResult
import com.tangem.crypto.CryptoUtils
/**
* Events that [ScanTask] returns on completion of its commands.
*/
sealed class ScanEvent {
/**
* Contains data from a Tangem card after successful completion of [ReadCommand].
*/
data class OnReadEvent(val card: Card) : ScanEvent()
/**
* Shows whether the Tangem card was verified on completion of [CheckWalletCommand].
*/
data class OnVerifyEvent(val isGenuine: Boolean) : ScanEvent()
}
/**
* Task that allows to read Tangem card and verify its private key.
*
* It performs two commands, [ReadCommand] and [CheckWalletCommand], subsequently.
*/
internal class ScanTask : Task<ScanEvent>() {
override fun onRun(cardEnvironment: CardEnvironment,

View file

@ -5,6 +5,11 @@ import com.tangem.commands.CommandResponse
import com.tangem.commands.CommandSerializer
import com.tangem.common.CompletionResult
/**
* Allows to perform a single command.
*
* @property command is a command that will be performed.
*/
class SingleCommandTask<Event : CommandResponse>(
private val command: CommandSerializer<Event>
) : Task<Event>() {

View file

@ -10,6 +10,10 @@ import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.StatusWord
/**
* 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 TaskError(description: String? = null) : Exception(description) {
class UnknownStatus(sw: Int) : TaskError("Unknown StatusWord: $sw")
class MappingError : TaskError()
@ -36,16 +40,41 @@ sealed class TaskError(description: String? = null) : Exception(description) {
class HashSizeMustBeEqual() : TaskError()
}
/**
* Events that are are sent in callbacks from [Task] during [Task] and Commands completions.
*/
sealed class TaskEvent<T> {
/**
* A callback that is triggered when a command returns response from a card
* (on a completion of a [CommandSerializer]).
*/
class Event<T>(val data: T) : TaskEvent<T>()
/**
* A callback that is triggered when a [Task] is completed.
*
* @param error is null if it's a successful completion of a [Task]
*/
class Completion<T>(val error: TaskError? = null) : TaskEvent<T>()
}
/**
* Allows to perform a group of commands interacting between the card and the application.
* A task opens an NFC session, sends commands to the card and receives its responses,
* repeats the commands if needed, and closes session after receiving the last answer.
*/
abstract class Task<T> {
var delegate: CardManagerDelegate? = null
var reader: CardReader? = null
/**
* This method should be called to run the [Task] and perform all its operations.
*
* @param cardEnvironment is a relevant current version of a card environment
* @param callback is a callback that will be triggered during the performance of the [Task]
*/
fun run(cardEnvironment: CardEnvironment,
callback: (result: TaskEvent<T>) -> Unit) {
delegate?.onNfcSessionStarted()
@ -54,6 +83,12 @@ abstract class Task<T> {
onRun(cardEnvironment, callback)
}
/**
* Should be called on [Task] completion, whether it was successful or with failure.
*
* @param withError is true when there is an error
* @param taskError the error to be shown by [CardManagerDelegate]
*/
protected fun completeNfcSession(withError: Boolean = false, taskError: TaskError? = null) {
reader?.closeSession()
if (withError) {
@ -63,9 +98,16 @@ abstract class Task<T> {
}
}
/**
* In this method the individual Tasks' logic should be implemented.
*/
protected abstract fun onRun(cardEnvironment: CardEnvironment,
callback: (result: TaskEvent<T>) -> Unit)
/**
* This method should be called by Tasks in their [onRun] method wherever
* they need to communicate with the Tangem Card by launching commands.
*/
protected fun <T : CommandResponse> sendCommand(
command: CommandSerializer<T>,
cardEnvironment: CardEnvironment,
@ -116,7 +158,7 @@ abstract class Task<T> {
}
is CompletionResult.Failure ->
if (result.error is TaskError.TagLost) {
delegate?.hideSecurityDelay()
delegate?.onTagLost()
} else if (result.error is TaskError.UserCancelledError) {
callback(CompletionResult.Failure(TaskError.UserCancelledError()))
reader?.readingActive = false

View file

@ -14,7 +14,10 @@ import com.tangem.tasks.TaskError
import kotlinx.android.synthetic.main.layout_touch_card.*
import kotlinx.android.synthetic.main.nfc_bottom_sheet.*
/**
* Default implementation of [CardManagerDelegate].
* If no customisation is required, this is the preferred way to use Tangem SDK.
*/
class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDelegate {
lateinit var activity: FragmentActivity

View file

@ -6,7 +6,9 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent
import com.tangem.tangem_sdk_new.nfc.NfcManager
/**
* [LifecycleObserver] for [NfcManager], helps to coordinate NFC modes with Activity lifecycle.
*/
class NfcLifecycleObserver(private var nfcManager: NfcManager) : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)

View file

@ -12,6 +12,11 @@ import android.os.Build
import android.os.Bundle
import com.tangem.Log
/**
* Helps use of NFC, leveraging Android NFC functionality.
* Launches [NfcAdapter], manages it with [Activity] lifecycle,
* enables and disables Nfc Reading Mode, receives NFC [Tag].
*/
class NfcManager : NfcAdapter.ReaderCallback {
val reader = NfcReader()

View file

@ -10,6 +10,9 @@ import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import com.tangem.tasks.TaskError
/**
* Provides NFC communication between an Android application and Tangem card.
*/
class NfcReader : CardReader {
override var readingActive = false