Updated on 2026-08-14

This commit is contained in:
Tangem 2019-11-01 15:49:37 +03:00
parent 4ee67b0555
commit bfa7d5dbcd
35 changed files with 6 additions and 6 deletions

View file

@ -0,0 +1,24 @@
package com.tangem
data class CardEnvironment(
val pin1: String = DEFAULT_PIN,
val pin2: String = DEFAULT_PIN2,
val cardId: String? = null,
val terminalKeys: KeyPair? = null,
val encryptionKey: ByteArray? = null
) {
companion object {
const val DEFAULT_PIN = "000000"
const val DEFAULT_PIN2 = "000"
}
}
enum class EncryptionMode(val code: Byte) {
NONE(0x0),
FAST(0x1),
STRONG(0x2)
}
class KeyPair(val publicKey: ByteArray, val privateKey: ByteArray)

View file

@ -0,0 +1,81 @@
package com.tangem
import com.tangem.commands.CommandResponse
import com.tangem.commands.CommandSerializer
import com.tangem.commands.SignCommand
import com.tangem.commands.SignResponse
import com.tangem.crypto.initCrypto
import com.tangem.tasks.*
import java.util.concurrent.Executors
class CardManager(
private val reader: CardReader,
private val cardManagerDelegate: CardManagerDelegate? = null) {
private var isBusy = false
private val cardEnvironmentRepository = mutableMapOf<String, CardEnvironment>()
private val cardManagerExecutor = Executors.newSingleThreadExecutor()
init {
initCrypto()
}
fun scanCard(callback: (result: TaskEvent<ScanEvent>) -> Unit) {
val task = ScanTask()
runTask(task, callback = callback)
}
fun sign(hashes: Array<ByteArray>, cardId: String,
callback: (result: TaskEvent<SignResponse>) -> Unit) {
val signCommand: SignCommand
try {
signCommand = SignCommand(hashes, cardId)
} catch (error: Exception) {
if (error is TaskError) {
callback(TaskEvent.Completion(error))
} else {
callback(TaskEvent.Completion(TaskError.GenericError(error.message)))
}
return
}
val task = SingleCommandTask(signCommand)
runTask(task, cardId, callback)
}
fun <T> runTask(task: Task<T>, cardId: String? = null,
callback: (result: TaskEvent<T>) -> Unit) {
if (isBusy) {
callback(TaskEvent.Completion(TaskError.Busy()))
return
}
val environment = fetchCardEnvironment(cardId)
isBusy = true
task.reader = reader
task.delegate = cardManagerDelegate
cardManagerExecutor.execute {
task.run(environment) {
when (it) {
is TaskEvent.Event -> callback(it)
is TaskEvent.Completion -> {
isBusy = false
callback(it)
}
}
}
}
}
private fun fetchCardEnvironment(cardId: String?): CardEnvironment {
return cardEnvironmentRepository[cardId] ?: CardEnvironment()
}
fun <T : CommandResponse> runCommand(commandSerializer: CommandSerializer<T>,
cardId: String? = null,
callback: (result: TaskEvent<T>) -> Unit) {
val task = SingleCommandTask(commandSerializer)
runTask(task, cardId, callback)
}
}

View file

@ -0,0 +1,15 @@
package com.tangem
import com.tangem.common.CompletionResult
import com.tangem.tasks.TaskError
interface CardManagerDelegate {
fun onTaskStarted()
fun showSecurityDelay(ms: Int)
fun onTaskCompleted()
fun onTaskError(error: TaskError? = null)
fun requestPin(callback: (result: CompletionResult<String>) -> Unit)
}

View file

@ -0,0 +1,12 @@
package com.tangem
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
interface CardReader {
var readingActive: Boolean
fun transceiveApdu(apdu: CommandApdu, callback: (response: CompletionResult<ResponseApdu>) -> Unit)
fun setStartSession()
fun closeSession()
}

View file

@ -0,0 +1,10 @@
package com.tangem
interface DataStorage {
fun getTerminalPublicKey(): ByteArray?
fun getTerminalPrivateKey(): ByteArray?
fun getPin1(): String?
fun getPin2(): String?
}

View file

@ -0,0 +1,29 @@
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 LoggerInterface {
fun i(logTag: String, message: String)
fun e(logTag: String, message: String)
fun v(logTag: String, message: String)
}

View file

@ -0,0 +1,16 @@
package com.tangem
//
//data class SignResponse(
// val cid: String,
// val signature: ByteArray,
// val remainingSignatures: Int,
// val signedHashes: Int
//)
//
//
//data class CardError(
// val code: Int = 0
//)
//

View file

@ -0,0 +1,54 @@
package com.tangem.commands
import com.tangem.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extentions.calculateSha256
import com.tangem.common.extentions.hexToBytes
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.enums.Instruction
import com.tangem.tasks.TaskError
class CheckWalletResponse(
val cardId: String,
val salt: ByteArray,
val walletSignature: ByteArray
) : CommandResponse
class CheckWalletCommand(
val pin1: String, val cid: String,
val challenge: ByteArray, val publicKeyChallenge: ByteArray) : CommandSerializer<CheckWalletResponse>() {
override val instruction = Instruction.CheckWallet
override val instructionCode = instruction.code
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val tlvData = listOf(
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
Tlv(TlvTag.CardId, cid.hexToBytes()),
Tlv(TlvTag.Challenge, challenge)
)
return CommandApdu(instructionCode, tlvData)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): CheckWalletResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
return try {
val mapper = TlvMapper(tlvData)
CheckWalletResponse(
cardId = mapper.map(TlvTag.CardId),
salt = mapper.map(TlvTag.Salt),
walletSignature = mapper.map(TlvTag.Signature)
)
} catch (exception: Exception) {
throw TaskError.SerializeCommandError()
}
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.commands
import com.tangem.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extentions.toInt
import com.tangem.common.tlv.TlvTag
import com.tangem.enums.Instruction
interface CommandResponse
abstract class CommandSerializer<T : CommandResponse> {
abstract val instruction: Instruction
abstract val instructionCode: Int
abstract fun serialize(cardEnvironment: CardEnvironment): CommandApdu
abstract fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): T?
fun deserializeSecurityDelay(responseApdu: ResponseApdu, cardEnvironment: CardEnvironment): Int? {
val tlv = responseApdu.getTlvData(cardEnvironment.encryptionKey)
return tlv?.find { it.tag == TlvTag.Pause }?.value?.toInt()
}
}

View file

@ -0,0 +1,158 @@
package com.tangem.commands
import com.tangem.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extentions.calculateSha256
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.data.SettingsMask
import com.tangem.enums.Instruction
import com.tangem.tasks.TaskError
import java.util.*
enum class SigningMethod(val code: Int) {
SignHash(0),
SignRaw(1),
SignHashValidatedByIssuer(2),
SignRawValidatedByIssuer(3),
SignHashValidatedByIssuerAndWriteIssuerData(4),
SignRawValidatedByIssuerAndWriteIssuerData(5),
SignPos(6);
companion object {
fun byCode(code: Int): SigningMethod? = values().find { it.code == code }
}
}
enum class EllipticCurve(val curve: String) {
Secp256k1("secp256k1"),
Ed25519("ed25519");
companion object {
fun byName(curve: String): EllipticCurve? = values().find { it.curve == curve }
}
}
enum class CardStatus(val code: Int) {
NotPersonalized(0),
Empty(1),
Loaded(2),
Purged(3);
companion object {
fun byCode(code: Int): CardStatus? = values().find { it.code == code }
}
}
enum class ProductMask(val code: Byte) {
Note(0x01),
Tag(0x02),
Card(0x04);
companion object {
fun byCode(code: Byte): ProductMask? = values().find { it.code == code }
}
}
class Card(
val cardId: String,
val manufacturerName: String,
val status: CardStatus,
val firmwareVersion: String?,
val cardPublicKey: ByteArray?,
val settingsMask: SettingsMask?,
val issuerPublicKey: ByteArray?,
val curve: EllipticCurve?,
val maxSignatures: Int?,
val signingMethod: SigningMethod?,
val pauseBeforePin2: Int?,
val walletPublicKey: ByteArray?,
val walletRemainingSignatures: Int?,
val walletSignedHashes: Int?,
val health: Int?,
val isActivated: Boolean,
val activationSeed: ByteArray?,
val paymentFlowVersion: ByteArray?,
val userCounter: Int?,
val terminalIsLinked: Boolean,
//Card Data
val batchId: String?,
val manufactureDateTime: Date?,
val issuerName: String?,
val blockchainName: String?,
val manufacturerSignature: ByteArray?,
val productMask: ProductMask?,
val tokenSymbol: String?,
val tokenContractAddress: String?,
val tokenDecimal: Int?
) : CommandResponse
class ReadCardCommand : CommandSerializer<Card>() {
override val instruction = Instruction.Read
override val instructionCode = instruction.code
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val tlvData = mutableListOf(Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()))
cardEnvironment.terminalKeys?.let {
Tlv(TlvTag.TerminalPublicKey, it.publicKey)
}
return CommandApdu(instructionCode, tlvData)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): Card? {
val tlvData = responseApdu.getTlvData() ?: return null
return try {
val tlvMapper = TlvMapper(tlvData)
Card(
cardId = tlvMapper.map(TlvTag.CardId),
manufacturerName = tlvMapper.map(TlvTag.ManufactureId),
status = tlvMapper.map(TlvTag.Status),
firmwareVersion = tlvMapper.mapOptional(TlvTag.Firmware),
cardPublicKey = tlvMapper.mapOptional(TlvTag.CardPublicKey),
settingsMask = tlvMapper.mapOptional(TlvTag.SettingsMask),
issuerPublicKey = tlvMapper.mapOptional(TlvTag.IssuerDataPublicKey),
curve = tlvMapper.mapOptional(TlvTag.CurveId),
maxSignatures = tlvMapper.mapOptional(TlvTag.MaxSignatures),
signingMethod = tlvMapper.mapOptional(TlvTag.SigningMethod),
pauseBeforePin2 = tlvMapper.mapOptional(TlvTag.PauseBeforePin2),
walletPublicKey = tlvMapper.mapOptional(TlvTag.WalletPublicKey),
walletRemainingSignatures = tlvMapper.mapOptional(TlvTag.RemainingSignatures),
walletSignedHashes = tlvMapper.mapOptional(TlvTag.SignedHashes),
health = tlvMapper.mapOptional(TlvTag.Health),
isActivated = tlvMapper.map(TlvTag.IsActivated),
activationSeed = tlvMapper.mapOptional(TlvTag.ActivationSeed),
paymentFlowVersion = tlvMapper.mapOptional(TlvTag.PaymentFlowVersion),
userCounter = tlvMapper.mapOptional(TlvTag.UserCounter),
terminalIsLinked = tlvMapper.map(TlvTag.TerminalIsLinked),
batchId = tlvMapper.mapOptional(TlvTag.Batch),
manufactureDateTime = tlvMapper.mapOptional(TlvTag.ManufactureDateTime),
issuerName = tlvMapper.mapOptional(TlvTag.IssuerId),
blockchainName = tlvMapper.mapOptional(TlvTag.BlockchainId),
manufacturerSignature = tlvMapper.mapOptional(TlvTag.ManufacturerSignature),
productMask = tlvMapper.mapOptional(TlvTag.ProductMask),
tokenSymbol = tlvMapper.mapOptional(TlvTag.TokenSymbol),
tokenContractAddress = tlvMapper.mapOptional(TlvTag.TokenContractAddress),
tokenDecimal = tlvMapper.mapOptional(TlvTag.TokenDecimal)
)
} catch (exception: Exception) {
throw TaskError.SerializeCommandError()
}
}
}

View file

@ -0,0 +1,76 @@
package com.tangem.commands
import com.tangem.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extentions.calculateSha256
import com.tangem.common.extentions.hexToBytes
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.sign
import com.tangem.enums.Instruction
import com.tangem.tasks.TaskError
class SignResponse(
val cardId: String,
val signature: ByteArray,
val remainingSignatures: Int,
val signedHashes: Int
) : CommandResponse
class SignCommand(private val hashes: Array<ByteArray>, private val cardId: String)
: CommandSerializer<SignResponse>() {
override val instruction = Instruction.Sign
override val instructionCode = instruction.code
private val hashSizes = if (hashes.isNotEmpty()) hashes.first().size else 0
private val dataToSign = flattenHashes()
private fun flattenHashes(): ByteArray {
checkForErrors()
return hashes.reduce { arr1, arr2 -> arr1 + arr2 }
}
private fun checkForErrors() {
if (hashes.isEmpty()) throw TaskError.EmptyHashes()
if (hashes.size > 10) throw TaskError.TooMuchHashes()
if (hashes.any { it.size != hashSizes }) throw TaskError.HashSizeMustBeEqual()
}
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val tlvData = mutableListOf(
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
Tlv(TlvTag.Pin2, cardEnvironment.pin2.calculateSha256()),
Tlv(TlvTag.CardId, cardId.hexToBytes()),
Tlv(TlvTag.TransactionOutHashSize,byteArrayOf(hashSizes.toByte())),
Tlv(TlvTag.TransactionOutHash, dataToSign)
)
addTerminalSignature(cardEnvironment, tlvData)
return CommandApdu(instructionCode, tlvData)
}
private fun addTerminalSignature(cardEnvironment: CardEnvironment, tlvData: MutableList<Tlv>) {
cardEnvironment.terminalKeys?.let {
val signedData = dataToSign.sign(it.privateKey)
tlvData.add(Tlv(TlvTag.TerminalTransactionSignature, signedData))
tlvData.add(Tlv(TlvTag.TerminalPublicKey, it.publicKey))
}
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): SignResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
val tlvMapper = TlvMapper(tlvData)
return SignResponse(
cardId= tlvMapper.map(TlvTag.CardId),
signature = tlvMapper.map(TlvTag.Signature),
remainingSignatures = tlvMapper.map(TlvTag.RemainingSignatures),
signedHashes = tlvMapper.map(TlvTag.SignedHashes)
)
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.common
import com.tangem.tasks.TaskError
sealed class CompletionResult<T> {
class Success<T>(val data: T) : CompletionResult<T>()
class Failure<T>(val error: TaskError) : CompletionResult<T>()
}

View file

@ -0,0 +1,67 @@
package com.tangem.common.apdu
import com.tangem.EncryptionMode
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.toBytes
import java.io.ByteArrayOutputStream
class CommandApdu(
private val instruction: Int,
private val tlvList: List<Tlv>,
private val cla: Byte = ISO_CLA,
private val p1: Byte = 0x00,
private val p2: Byte = 0x00,
private val le: Int = 0x00,
private val encryptionMode: EncryptionMode = EncryptionMode.NONE,
private val encryptionKey: ByteArray? = null) {
val apduData: ByteArray
init {
apduData = toBytes()
}
private fun toBytes(): ByteArray {
val data = if (tlvList.isNotEmpty()) {
tlvList.toBytes()
} else {
byteArrayOf()
}
val lc = data.size
val byteStream = ByteArrayOutputStream()
byteStream.write(cla.toInt())
byteStream.write(instruction)
byteStream.write(p1.toInt())
byteStream.write(p2.toInt())
if (lc != 0) {
writeLength(byteStream, lc)
byteStream.write(data)
}
return byteStream.toByteArray()
}
private fun writeLength(stream: ByteArrayOutputStream, lc: Int) {
stream.write(0)
stream.write(lc shr 8)
stream.write(lc and 0xFF)
}
private fun encrypt() {
}
companion object {
const val ISO_CLA = 0x00.toByte()
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.common.apdu
import com.tangem.common.tlv.Tlv
import com.tangem.enums.StatusWord
class ResponseApdu(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)
fun getTlvData(encryptionKey: ByteArray? = null): List<Tlv>? {
val tlvs = when {
data.size < 2 -> null
data.size == 2 -> emptyList()
else -> Tlv.fromBytes(data.copyOf(data.size - 2))
}
return flattenNestedTlvs(tlvs)
}
private fun flattenNestedTlvs(tlvs: List<Tlv>?): List<Tlv>? =
tlvs?.flatMap {
if (it.tag.hasNestedTlv()) {
Tlv.fromBytes(it.value)
} else {
listOf(it)
}
}
private fun decrypt(encryptionKey: ByteArray) {
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.common.extentions
import java.nio.ByteBuffer
import java.security.MessageDigest
import java.util.*
import kotlin.experimental.and
fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
fun ByteArray.toUtf8(): String {
val string = String(this)
if (this.isNotEmpty() && this.last() == 0.toByte()) return string.dropLast(1)
return string
}
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 = ((this[0] and 0xFF.toByte()).toInt() shl 8) or (this[1] and 0xFF.toByte()).toInt()
val month = this[2] - 1
val day = this[3].toInt()
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)

View file

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

View file

@ -0,0 +1,95 @@
package com.tangem.common.tlv
import java.io.ByteArrayInputStream
import java.io.IOException
class Tlv {
val tag: TlvTag
val value: ByteArray
val tagCode: Int
constructor(tagCode: Int, value: ByteArray = byteArrayOf()) {
this.tag = TlvTag.byCode(tagCode)
this.tagCode = tagCode
this.value = value
}
constructor(tag: TlvTag, value: ByteArray = byteArrayOf()) {
this.tag = tag
this.tagCode = tag.code
this.value = value
}
companion object {
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 fromBytes(mData: ByteArray): List<Tlv> {
val tlvList = mutableListOf<Tlv>()
val stream = ByteArrayInputStream(mData)
var tlv: Tlv? = null
do {
try {
tlv = Tlv.tlvFromBytes(stream)
if (tlv != null) tlvList.add(tlv)
} catch (e: IOException) {
throw TlvMapperException("TLVError: " + e.message)
}
} while (tlv != null)
return tlvList
}
}
}
fun List<Tlv>.toBytes(): ByteArray =
this.map { it.toBytes() }.reduce { arr1, arr2 -> arr1 + arr2 }
fun Tlv.toBytes(): ByteArray {
val tag = byteArrayOf(this.tag.code.toByte())
val length = getLengthInBytes(this.value.size)
val value = if (this.value.isNotEmpty()) this.value else byteArrayOf(0x00)
return tag + length + value
}
private fun getLengthInBytes(tlvLength: Int): ByteArray {
return if (tlvLength > 0) {
if (tlvLength > 0xFE) {
byteArrayOf(
0xFF.toByte(),
(tlvLength shr 8 and 0xFF).toByte(),
(tlvLength and 0xFF).toByte()
)
} else {
byteArrayOf((tlvLength and 0xFF).toByte())
}
} else {
byteArrayOf()
}
}

View file

@ -0,0 +1,99 @@
package com.tangem.common.tlv
import com.tangem.commands.CardStatus
import com.tangem.commands.EllipticCurve
import com.tangem.commands.ProductMask
import com.tangem.commands.SigningMethod
import com.tangem.common.extentions.toDate
import com.tangem.common.extentions.toHexString
import com.tangem.common.extentions.toInt
import com.tangem.common.extentions.toUtf8
import com.tangem.data.SettingsMask
import java.util.*
open class TlvMapperException(message: String?) : Exception(message)
class MissingTagException(message: String? = null) : TlvMapperException(message)
class WrongTypeException(message: String? = null) : TlvMapperException(message)
class ConvertionException(message: String? = null) : TlvMapperException(message)
class TlvMapper(val tlvList: List<Tlv>) {
inline fun <reified T> mapOptional(tag: TlvTag): T? =
try {
map<T>(tag)
} catch (exception: MissingTagException) {
null
}
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) {
return false as T
} else {
throw MissingTagException("Tag $tag not found")
}
return when (tag.valueType()) {
TlvValueType.HexString -> {
if (T::class != String::class)
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
tlvValue.toHexString() as T
}
TlvValueType.Utf8String -> {
if (T::class != String::class)
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
tlvValue.toUtf8() as T
}
TlvValueType.IntValue -> {
if (T::class != Integer::class)
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
tlvValue.toInt() as T
}
TlvValueType.BoolValue -> {
if (T::class != Boolean::class)
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
true as T
}
TlvValueType.ByteArray -> {
if (T::class != ByteArray::class)
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
tlvValue as T
}
TlvValueType.EllipticCurve -> {
if (T::class != EllipticCurve::class)
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
EllipticCurve.byName(tlvValue.toUtf8()) as T
}
TlvValueType.DateTime -> {
if (T::class != Date::class)
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
tlvValue.toDate() as T
}
TlvValueType.ProductMask -> {
if (T::class != ProductMask::class)
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
ProductMask.byCode(tlvValue.first()) as T
}
TlvValueType.SettingsMask -> {
if (T::class != SettingsMask::class)
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
SettingsMask(tlvValue.toInt()) as T
}
TlvValueType.CardStatus -> {
if (T::class != CardStatus::class)
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
CardStatus.byCode(tlvValue.toInt()) as T
}
TlvValueType.SigningMethod -> {
if (T::class != SigningMethod::class)
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
SigningMethod.byCode(tlvValue.toInt()) as T
}
}
}
}

View file

@ -0,0 +1,116 @@
package com.tangem.common.tlv
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),
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),
ManufactureId(0x20),
ManufacturerSignature(0x21),
IssuerDataPublicKey(0x30),
IssuerTransactionPublicKey(0x31),
IssuerData(0x32),
IssuerDataSignature(0x33),
IssuerTransactionSignature(0x34),
IssuerDataCounter(0x35),
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),
UserCounter(0x2C),
TokenSymbol(0xA0),
TokenContractAddress(0xA1),
TokenDecimal(0xA2),
Denomination(0xC0),
ValidatedBalance(0xC1),
LastSignDate(0xC2),
DenominationText(0xC3),
TerminalIsLinked(0x58),
TerminalPublicKey(0x5C),
TerminalTransactionSignature(0x57);
fun hasNestedTlv(): Boolean {
return when (this) {
TlvTag.CardData -> true
else -> false
}
}
fun valueType(): TlvValueType {
return when (this) {
CardId, Pin, Batch -> TlvValueType.HexString
ManufactureId, Firmware, IssuerId, BlockchainId, TokenSymbol, TokenContractAddress ->
TlvValueType.Utf8String
CurveId -> TlvValueType.EllipticCurve
MaxSignatures, PauseBeforePin2, RemainingSignatures,
SignedHashes, Health, TokenDecimal, UserCounter -> TlvValueType.IntValue
IsActivated, TerminalIsLinked -> TlvValueType.BoolValue
ManufactureDateTime -> TlvValueType.DateTime
ProductMask -> TlvValueType.ProductMask
SettingsMask -> TlvValueType.SettingsMask
Status -> TlvValueType.CardStatus
SigningMethod -> TlvValueType.SigningMethod
else -> TlvValueType.ByteArray
}
}
companion object {
fun byCode(code: Int): TlvTag = values().find { it.code == code } ?: Unknown
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.common.tlv
enum class TlvValueType {
HexString,
Utf8String,
IntValue,
BoolValue,
ByteArray,
EllipticCurve,
DateTime,
ProductMask,
SettingsMask,
CardStatus,
SigningMethod
}

View file

@ -0,0 +1,39 @@
package com.tangem.crypto
import com.tangem.commands.EllipticCurve
import net.i2p.crypto.eddsa.EdDSASecurityProvider
import java.security.SecureRandom
import java.security.Security
fun generateRandomBytes(length: Int): ByteArray {
val bytes = ByteArray(length)
SecureRandom().nextBytes(bytes)
return bytes
}
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)
}
}
fun initCrypto() {
Security.insertProviderAt(org.spongycastle.jce.provider.BouncyCastleProvider(), 1)
Security.addProvider(EdDSASecurityProvider())
}
fun ByteArray.sign(privateKeyArray: ByteArray, curve: EllipticCurve = EllipticCurve.Secp256k1): ByteArray {
return when (curve) {
EllipticCurve.Secp256k1 -> signSecp256k1(this,privateKeyArray)
EllipticCurve.Ed25519 -> signEd25519(this, privateKeyArray)
}
}

View file

@ -0,0 +1,45 @@
package com.tangem.crypto
import com.tangem.common.extentions.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
internal fun verifyEd25519(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)
}
private fun loadPublicKey(publicKeyArray: ByteArray): PublicKey {
val spec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519)
val pubKey = EdDSAPublicKeySpec(publicKeyArray, spec)
return EdDSAPublicKey(pubKey)
}
internal fun signEd25519(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()
}

View file

@ -0,0 +1,115 @@
package com.tangem.crypto
import com.tangem.common.extentions.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
internal fun verifySecp256k1(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)
}
private 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 signSecp256k1(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 (!verifySecp256k1(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
}
private fun generatePublicKey(privateKeyArray: ByteArray): ByteArray {
val spec = ECNamedCurveTable.getParameterSpec("secp256k1")
return spec.g.multiply(BigInteger(1, privateKeyArray)).getEncoded(false)
}

View file

@ -0,0 +1,32 @@
package com.tangem.data
data class SettingsMask(val rawValue: Int) {
companion object{
const val IsReusable = 0x0001
const val UseActivation = 0x0002
const val ForbidPurgeWallet = 0x0004
const val UseBlock = 0x0008
const val AllowSwapPIN = 0x0010
const val AllowSwapPIN2 = 0x0020
const val UseCVC = 0x0040
const val ForbidDefaultPIN = 0x0080
const val UseOneCommandAtTime = 0x0100
const val UseNDEF = 0x0200
const val UseDynamicNDEF = 0x0400
const val SmartSecurityDelay = 0x0800
const val Protocol_AllowUnencrypted = 0x1000
const val Protocol_AllowStaticEncryption = 0x2000
const val ProtectIssuerDataAgainstReplay = 0x4000
const val AllowSelectBlockchain = 0x8000
const val DisablePrecomputedNDEF = 0x00010000
const val SkipSecurityDelayIfValidatedByLinkedTerminal = 0x00080000
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.enums
enum class Instruction(var code: Int) {
Unknown(0x00),
Read(0xF2),
VerifyCard(0xF3),
ValidateCard(0xF4),
VerifyCode(0xF5),
WriteIssuerData(0xF6),
GetIssuerData(0xF7),
CreateWallet(0xF8),
CheckWallet(0xF9),
SwapPIN(0xFA),
Sign(0xFB),
PurgeWallet(0xFC),
Activate(0xFE),
OpenSession(0xFF);
companion object {
fun byCode(code: Int): Instruction = values().find { it.code == code } ?: Unknown
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.enums
enum class StatusWord (val code: Int, val description: String){
ProcessCompleted(0x9000, "SW_PROCESS_COMPLETED"),
InvalidParams(0x6A86, "SW_INVALID_PARAMS"),
ErrorProcessingCommand(0x6286, "SW_ERROR_PROCESSING_COMMAND"),
InvalidState(0x6985, "SW_INVALID_STATE"),
// PinsNotChanged(ProcessCompleted.code, ProcessCompleted.description),
Pin1Changed(ProcessCompleted.code + 0x0001, "SW_PIN1_CHANGED"),
Pin2Changed(ProcessCompleted.code + 0x0002, "SW_PIN2_CHANGED"),
PinsChanged(ProcessCompleted.code + 0x0003, "SW_PINS_CHANGED"),
InsNotSupported(0x6D00, "SW_INS_NOT_SUPPORTED"),
NeedEncryption(0x6982, "SW_NEED_ENCRYPTION"),
NeedPause(0x9789, "SW_NEED_PAUSE");
companion object {
fun byCode(code: Int): StatusWord = values().find { it.code == code } ?: InvalidParams
}
}

View file

@ -0,0 +1,88 @@
package com.tangem.tasks
import com.tangem.CardEnvironment
import com.tangem.commands.Card
import com.tangem.commands.CheckWalletCommand
import com.tangem.commands.EllipticCurve
import com.tangem.commands.ReadCardCommand
import com.tangem.common.CompletionResult
import com.tangem.crypto.generateRandomBytes
import com.tangem.crypto.verify
sealed class ScanEvent {
data class OnReadEvent(val card: Card) : ScanEvent()
data class OnVerifyEvent(val isGenuine: Boolean) : ScanEvent()
}
internal class ScanTask : Task<ScanEvent>() {
private lateinit var cardData: Card
private lateinit var challenge: ByteArray
private lateinit var curve: EllipticCurve
private lateinit var walletPublickKey: ByteArray
override fun onRun(cardEnvironment: CardEnvironment,
callback: (result: TaskEvent<ScanEvent>) -> Unit) {
val readCommand = ReadCardCommand()
sendCommand(readCommand, cardEnvironment) { readEvent ->
when (readEvent) {
is CompletionResult.Success -> {
cardData = readEvent.data
callback(TaskEvent.Event(ScanEvent.OnReadEvent(cardData)))
if (cardData.curve != null && cardData.walletPublicKey != null) {
curve = cardData.curve!!
walletPublickKey = cardData.walletPublicKey!!
} else {
delegate?.onTaskError()
callback(TaskEvent.Completion(TaskError.CardError()))
}
val checkWalletCommand = prepareCheckWalletCommand(cardEnvironment)
sendCommand(checkWalletCommand, cardEnvironment) { checkWalletEvent ->
when (checkWalletEvent) {
is CompletionResult.Success -> {
val checkWalletResponse = checkWalletEvent.data
val verified = verify(walletPublickKey,
challenge + checkWalletResponse.salt,
checkWalletResponse.walletSignature,
curve)
if (verified) {
delegate?.onTaskCompleted()
callback(TaskEvent.Completion())
callback(TaskEvent.Event(ScanEvent.OnVerifyEvent(true)))
} else {
delegate?.onTaskError()
callback(TaskEvent.Completion(TaskError.VefificationFailed()))
}
}
is CompletionResult.Failure -> {
if (checkWalletEvent.error !is TaskError.UserCancelledError) delegate?.onTaskError()
callback(TaskEvent.Completion(checkWalletEvent.error))
}
}
}
}
is CompletionResult.Failure -> {
if (readEvent.error !is TaskError.UserCancelledError) delegate?.onTaskError()
callback(TaskEvent.Completion(readEvent.error))
}
}
}
}
private fun prepareCheckWalletCommand(cardEnvironment: CardEnvironment): CheckWalletCommand {
challenge = generateRandomBytes(16)
return CheckWalletCommand(
cardEnvironment.pin1,
cardData.cardId,
challenge,
byteArrayOf())
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.tasks
import com.tangem.CardEnvironment
import com.tangem.commands.CommandResponse
import com.tangem.commands.CommandSerializer
import com.tangem.common.CompletionResult
class SingleCommandTask<Event : CommandResponse>(
private val commandSerializer: CommandSerializer<Event>) : Task<Event>() {
override fun onRun(cardEnvironment: CardEnvironment,
callback: (result: TaskEvent<Event>) -> Unit) {
sendCommand(commandSerializer, cardEnvironment) {
when (it) {
is CompletionResult.Success -> {
delegate?.onTaskCompleted()
callback(TaskEvent.Event(it.data))
callback(TaskEvent.Completion())
}
is CompletionResult.Failure -> {
if (it.error !is TaskError.UserCancelledError) delegate?.onTaskError()
callback(TaskEvent.Completion(it.error))
}
}
}
}
}

View file

@ -0,0 +1,108 @@
package com.tangem.tasks
import com.tangem.CardEnvironment
import com.tangem.CardManagerDelegate
import com.tangem.CardReader
import com.tangem.Log
import com.tangem.commands.CommandResponse
import com.tangem.commands.CommandSerializer
import com.tangem.common.CompletionResult
import com.tangem.enums.StatusWord
abstract class Task<T> {
var delegate: CardManagerDelegate? = null
var reader: CardReader? = null
fun run(cardEnvironment: CardEnvironment,
callback: (result: TaskEvent<T>) -> Unit) {
delegate?.onTaskStarted()
reader?.setStartSession()
Log.i(this::class.simpleName!!, "Nfc task is started")
onRun(cardEnvironment, callback)
}
abstract fun onRun(cardEnvironment: CardEnvironment,
callback: (result: TaskEvent<T>) -> Unit)
protected fun <T : CommandResponse> sendCommand(
commandSerializer: CommandSerializer<T>,
cardEnvironment: CardEnvironment,
callback: (result: CompletionResult<T>) -> Unit) {
Log.i(this::class.simpleName!!, "Nfc command ${commandSerializer::class.simpleName!!} is initiated")
reader?.transceiveApdu(
commandSerializer.serialize(cardEnvironment)) { result ->
when (result) {
is CompletionResult.Success -> {
val responseApdu = result.data
when (responseApdu.statusWord) {
StatusWord.ProcessCompleted, StatusWord.Pin1Changed, StatusWord.Pin2Changed, StatusWord.PinsChanged
-> {
try {
val responseData = commandSerializer.deserialize(cardEnvironment, responseApdu)
Log.i(this::class.simpleName!!, "Nfc command ${commandSerializer::class.simpleName!!} is completed")
callback(CompletionResult.Success(responseData as T))
} catch (error: TaskError) {
callback(CompletionResult.Failure(error))
}
}
StatusWord.InvalidParams -> callback(CompletionResult.Failure(TaskError.InvalidParams()))
StatusWord.ErrorProcessingCommand -> callback(CompletionResult.Failure(TaskError.ErrorProcessingCommand()))
StatusWord.InvalidState -> callback(CompletionResult.Failure(TaskError.InvalidState()))
StatusWord.InsNotSupported -> callback(CompletionResult.Failure(TaskError.InsNotSupported()))
StatusWord.NeedEncryption -> callback(CompletionResult.Failure(TaskError.NeedEncryption()))
StatusWord.NeedPause -> {
val remainingTime = commandSerializer.deserializeSecurityDelay(responseApdu, cardEnvironment)
if (remainingTime != null) delegate?.showSecurityDelay(remainingTime)
Log.i(this::class.simpleName!!, "Nfc command ${commandSerializer::class.simpleName!!} triggered security delay of $remainingTime milliseconds")
sendCommand(commandSerializer, cardEnvironment, callback)
}
}
}
is CompletionResult.Failure ->
if (result.error is TaskError.UserCancelledError) {
callback(CompletionResult.Failure(TaskError.UserCancelledError()))
reader?.readingActive = false
}
}
}
}
}
sealed class TaskError(description: String? = null) : Exception(description) {
class UnknownStatus(sw: Int) : TaskError()
class MappingError : TaskError()
class GenericError(description: String? = null) : TaskError(description)
class UserCancelledError() : TaskError()
class Busy() : TaskError()
class ErrorProcessingCommand : TaskError()
class InvalidState : TaskError()
class InsNotSupported : TaskError()
class InvalidParams : TaskError()
class NeedEncryption : TaskError()
class NeedPause : TaskError()
class VefificationFailed : TaskError()
class CardError : TaskError()
class ReaderError() : TaskError()
class SerializeCommandError() : TaskError()
class CardIsMissing() : TaskError()
class EmptyHashes() : TaskError()
class TooMuchHashes() : TaskError()
class HashSizeMustBeEqual() : TaskError()
}
sealed class TaskEvent<T> {
class Event<T>(val data: T) : TaskEvent<T>()
class Completion<T>(val error: TaskError? = null) : TaskEvent<T>()
}