Updated on 2026-08-14
This commit is contained in:
commit
587f7d2913
26 changed files with 239 additions and 292 deletions
|
|
@ -4,7 +4,7 @@ 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.crypto.CryptoUtils
|
||||
import com.tangem.tasks.*
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ class CardManager(
|
|||
private val cardManagerExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
init {
|
||||
initCrypto()
|
||||
CryptoUtils.initCrypto()
|
||||
}
|
||||
|
||||
fun scanCard(callback: (result: TaskEvent<ScanEvent>) -> Unit) {
|
||||
|
|
@ -56,26 +56,21 @@ class CardManager(
|
|||
task.delegate = cardManagerDelegate
|
||||
|
||||
cardManagerExecutor.execute {
|
||||
task.run(environment) {
|
||||
when (it) {
|
||||
is TaskEvent.Event -> callback(it)
|
||||
is TaskEvent.Completion -> {
|
||||
isBusy = false
|
||||
callback(it)
|
||||
}
|
||||
}
|
||||
task.run(environment) { taskEvent ->
|
||||
if (taskEvent is TaskEvent.Completion) isBusy = false
|
||||
callback(taskEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : CommandResponse> runCommand(command: CommandSerializer<T>,
|
||||
cardId: String? = null,
|
||||
callback: (result: TaskEvent<T>) -> Unit) {
|
||||
val task = SingleCommandTask(command)
|
||||
runTask(task, cardId, callback)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,10 +5,11 @@ import com.tangem.tasks.TaskError
|
|||
|
||||
interface CardManagerDelegate {
|
||||
|
||||
fun onTaskStarted()
|
||||
fun showSecurityDelay(ms: Int)
|
||||
fun onTaskCompleted()
|
||||
fun onTaskError(error: TaskError? = null)
|
||||
fun onNfcSessionStarted()
|
||||
fun onSecurityDelay(ms: Int)
|
||||
fun onTagLost()
|
||||
fun onNfcSessionCompleted()
|
||||
fun onError(error: TaskError? = null)
|
||||
|
||||
fun requestPin(callback: (result: CompletionResult<String>) -> Unit)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,6 @@ import com.tangem.common.apdu.ResponseApdu
|
|||
interface CardReader {
|
||||
var readingActive: Boolean
|
||||
fun transceiveApdu(apdu: CommandApdu, callback: (response: CompletionResult<ResponseApdu>) -> Unit)
|
||||
fun setStartSession()
|
||||
fun startNfcSession()
|
||||
fun closeSession()
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem
|
||||
|
||||
interface DataStorage {
|
||||
|
||||
fun getTerminalPublicKey(): ByteArray?
|
||||
fun getTerminalPrivateKey(): ByteArray?
|
||||
fun getPin1(): String?
|
||||
fun getPin2(): String?
|
||||
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
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
|
||||
//)
|
||||
//
|
||||
|
||||
|
||||
|
|
@ -19,20 +19,19 @@ class CheckWalletResponse(
|
|||
|
||||
|
||||
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
|
||||
private val pin1: String,
|
||||
private val cardId: String,
|
||||
private val challenge: ByteArray
|
||||
) : CommandSerializer<CheckWalletResponse>() {
|
||||
|
||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
val tlvData = listOf(
|
||||
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
|
||||
Tlv(TlvTag.CardId, cid.hexToBytes()),
|
||||
Tlv(TlvTag.CardId, cardId.hexToBytes()),
|
||||
Tlv(TlvTag.Challenge, challenge)
|
||||
)
|
||||
|
||||
return CommandApdu(instructionCode, tlvData)
|
||||
return CommandApdu(Instruction.CheckWallet, tlvData)
|
||||
}
|
||||
|
||||
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): CheckWalletResponse? {
|
||||
|
|
|
|||
|
|
@ -23,9 +23,6 @@ class SignResponse(
|
|||
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()
|
||||
|
||||
|
|
@ -36,7 +33,7 @@ class SignCommand(private val hashes: Array<ByteArray>, private val cardId: Stri
|
|||
|
||||
private fun checkForErrors() {
|
||||
if (hashes.isEmpty()) throw TaskError.EmptyHashes()
|
||||
if (hashes.size > 10) throw TaskError.TooMuchHashes()
|
||||
if (hashes.size > 10) throw TaskError.TooMuchHashes()
|
||||
if (hashes.any { it.size != hashSizes }) throw TaskError.HashSizeMustBeEqual()
|
||||
}
|
||||
|
||||
|
|
@ -45,20 +42,20 @@ class SignCommand(private val hashes: Array<ByteArray>, private val cardId: Stri
|
|||
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.TransactionOutHashSize, byteArrayOf(hashSizes.toByte())),
|
||||
Tlv(TlvTag.TransactionOutHash, dataToSign)
|
||||
)
|
||||
|
||||
addTerminalSignature(cardEnvironment, tlvData)
|
||||
|
||||
return CommandApdu(instructionCode, tlvData)
|
||||
return CommandApdu(Instruction.Sign, tlvData)
|
||||
}
|
||||
|
||||
private fun addTerminalSignature(cardEnvironment: CardEnvironment, tlvData: MutableList<Tlv>) {
|
||||
cardEnvironment.terminalKeys?.let {
|
||||
val signedData = dataToSign.sign(it.privateKey)
|
||||
cardEnvironment.terminalKeys?.let { terminalKeyPair ->
|
||||
val signedData = dataToSign.sign(terminalKeyPair.privateKey)
|
||||
tlvData.add(Tlv(TlvTag.TerminalTransactionSignature, signedData))
|
||||
tlvData.add(Tlv(TlvTag.TerminalPublicKey, it.publicKey))
|
||||
tlvData.add(Tlv(TlvTag.TerminalPublicKey, terminalKeyPair.publicKey))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +64,7 @@ class SignCommand(private val hashes: Array<ByteArray>, private val cardId: Stri
|
|||
|
||||
val tlvMapper = TlvMapper(tlvData)
|
||||
return SignResponse(
|
||||
cardId= tlvMapper.map(TlvTag.CardId),
|
||||
cardId = tlvMapper.map(TlvTag.CardId),
|
||||
signature = tlvMapper.map(TlvTag.Signature),
|
||||
remainingSignatures = tlvMapper.map(TlvTag.RemainingSignatures),
|
||||
signedHashes = tlvMapper.map(TlvTag.SignedHashes)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import java.io.ByteArrayOutputStream
|
|||
|
||||
class CommandApdu(
|
||||
|
||||
private val instruction: Int,
|
||||
private val ins: Int,
|
||||
private val tlvList: List<Tlv>,
|
||||
|
||||
private val cla: Byte = ISO_CLA,
|
||||
|
|
@ -19,6 +19,18 @@ class CommandApdu(
|
|||
private val encryptionMode: EncryptionMode = EncryptionMode.NONE,
|
||||
private val encryptionKey: ByteArray? = null) {
|
||||
|
||||
constructor(
|
||||
instruction: Instruction,
|
||||
tlvList: List<Tlv>,
|
||||
encryptionMode: EncryptionMode = EncryptionMode.NONE,
|
||||
encryptionKey: ByteArray? = null
|
||||
) : this(
|
||||
instruction.code,
|
||||
tlvList,
|
||||
encryptionMode = encryptionMode,
|
||||
encryptionKey = encryptionKey
|
||||
)
|
||||
|
||||
val apduData: ByteArray
|
||||
|
||||
init {
|
||||
|
|
@ -39,7 +51,7 @@ class CommandApdu(
|
|||
|
||||
val byteStream = ByteArrayOutputStream()
|
||||
byteStream.write(cla.toInt())
|
||||
byteStream.write(instruction)
|
||||
byteStream.write(ins)
|
||||
byteStream.write(p1.toInt())
|
||||
byteStream.write(p2.toInt())
|
||||
if (lc != 0) {
|
||||
|
|
@ -58,7 +70,7 @@ class CommandApdu(
|
|||
|
||||
|
||||
private fun encrypt() {
|
||||
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ enum class Instruction(var code: Int) {
|
|||
|
||||
|
||||
companion object {
|
||||
fun byCode(code: Int): Instruction = values().find { it.code == code } ?: Unknown
|
||||
private val values = values()
|
||||
fun byCode(code: Int): Instruction = values.find { it.code == code } ?: Unknown
|
||||
}
|
||||
}
|
||||
|
|
@ -6,16 +6,17 @@ enum class StatusWord (val code: Int, val description: String){
|
|||
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");
|
||||
NeedPause(0x9789, "SW_NEED_PAUSE"),
|
||||
Unknown(0x0000, "SW_UNKNOWN");
|
||||
|
||||
companion object {
|
||||
fun byCode(code: Int): StatusWord = values().find { it.code == code } ?: InvalidParams
|
||||
private val values = values()
|
||||
fun byCode(code: Int): StatusWord = values.find { it.code == code } ?: Unknown
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,17 +7,17 @@ class Tlv {
|
|||
|
||||
val tag: TlvTag
|
||||
val value: ByteArray
|
||||
val tagCode: Int
|
||||
val tagRaw: Int
|
||||
|
||||
constructor(tagCode: Int, value: ByteArray = byteArrayOf()) {
|
||||
this.tag = TlvTag.byCode(tagCode)
|
||||
this.tagCode = tagCode
|
||||
this.tagRaw = tagCode
|
||||
this.value = value
|
||||
}
|
||||
|
||||
constructor(tag: TlvTag, value: ByteArray = byteArrayOf()) {
|
||||
this.tag = tag
|
||||
this.tagCode = tag.code
|
||||
this.tagRaw = tag.code
|
||||
this.value = value
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
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.commands.*
|
||||
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.*
|
||||
|
||||
|
||||
|
|
@ -16,7 +12,7 @@ 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 ConversionException(message: String? = null) : TlvMapperException(message)
|
||||
|
||||
class TlvMapper(val tlvList: List<Tlv>) {
|
||||
|
||||
|
|
@ -49,7 +45,11 @@ class TlvMapper(val tlvList: List<Tlv>) {
|
|||
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
|
||||
try {
|
||||
tlvValue.toInt() as T
|
||||
} catch (exception: IllegalArgumentException) {
|
||||
throw ConversionException(exception.message)
|
||||
}
|
||||
}
|
||||
TlvValueType.BoolValue -> {
|
||||
if (T::class != Boolean::class)
|
||||
|
|
@ -64,34 +64,40 @@ class TlvMapper(val tlvList: List<Tlv>) {
|
|||
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
|
||||
EllipticCurve.byName(tlvValue.toUtf8()) as? T
|
||||
?: throw ConversionException("Unknown Elliptic Curve value: ${tlvValue.toUtf8()}")
|
||||
}
|
||||
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
|
||||
try {
|
||||
tlvValue.toDate() as T
|
||||
} catch (exception: Exception) {
|
||||
throw ConversionException("Converting to date with the following exception: " + exception.message)
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
ProductMask.byCode(tlvValue.first()) as? T
|
||||
?: throw ConversionException("Unknown Product Mask Code: ${tlvValue.first()}.")
|
||||
}
|
||||
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
|
||||
?: throw ConversionException("Unknown Card Status with code of: ${tlvValue.toInt()}")
|
||||
}
|
||||
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
|
||||
?: throw ConversionException("Unknown Signing Method with code of: ${tlvValue.toInt()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,19 @@
|
|||
package com.tangem.common.tlv
|
||||
|
||||
enum class TlvValueType {
|
||||
HexString,
|
||||
Utf8String,
|
||||
IntValue,
|
||||
BoolValue,
|
||||
ByteArray,
|
||||
EllipticCurve,
|
||||
DateTime,
|
||||
ProductMask,
|
||||
SettingsMask,
|
||||
CardStatus,
|
||||
SigningMethod
|
||||
}
|
||||
|
||||
enum class TlvTag(val code: Int) {
|
||||
Unknown(0x00),
|
||||
CardId(0x01),
|
||||
|
|
@ -108,9 +122,9 @@ enum class TlvTag(val code: Int) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
fun byCode(code: Int): TlvTag = values().find { it.code == code } ?: Unknown
|
||||
private val values = values()
|
||||
fun byCode(code: Int): TlvTag = values.find { it.code == code } ?: Unknown
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
package com.tangem.common.tlv
|
||||
|
||||
enum class TlvValueType {
|
||||
HexString,
|
||||
Utf8String,
|
||||
IntValue,
|
||||
BoolValue,
|
||||
ByteArray,
|
||||
EllipticCurve,
|
||||
DateTime,
|
||||
ProductMask,
|
||||
SettingsMask,
|
||||
CardStatus,
|
||||
SigningMethod
|
||||
}
|
||||
|
|
@ -5,35 +5,34 @@ 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)
|
||||
object CryptoUtils {
|
||||
|
||||
fun initCrypto() {
|
||||
Security.insertProviderAt(org.spongycastle.jce.provider.BouncyCastleProvider(), 1)
|
||||
Security.addProvider(EdDSASecurityProvider())
|
||||
}
|
||||
|
||||
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.Secp256k1 -> signSecp256k1(this, privateKeyArray)
|
||||
EllipticCurve.Ed25519 -> signEd25519(this, privateKeyArray)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,9 @@ 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.commands.ReadCommand
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.crypto.generateRandomBytes
|
||||
import com.tangem.crypto.verify
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
|
||||
sealed class ScanEvent {
|
||||
data class OnReadEvent(val card: Card) : ScanEvent()
|
||||
|
|
@ -17,76 +15,67 @@ sealed class 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 ->
|
||||
val readCommand = ReadCommand()
|
||||
sendCommand(readCommand, cardEnvironment) { readResult ->
|
||||
|
||||
when (readResult) {
|
||||
|
||||
is CompletionResult.Failure -> {
|
||||
if (readResult.error !is TaskError.UserCancelledError) {
|
||||
completeNfcSession(true, readResult.error)
|
||||
}
|
||||
callback(TaskEvent.Completion(readResult.error))
|
||||
}
|
||||
|
||||
when (readEvent) {
|
||||
is CompletionResult.Success -> {
|
||||
cardData = readEvent.data
|
||||
val card = readResult.data
|
||||
|
||||
callback(TaskEvent.Event(ScanEvent.OnReadEvent(cardData)))
|
||||
callback(TaskEvent.Event(ScanEvent.OnReadEvent(card)))
|
||||
|
||||
if (cardData.curve != null && cardData.walletPublicKey != null) {
|
||||
curve = cardData.curve!!
|
||||
walletPublickKey = cardData.walletPublicKey!!
|
||||
} else {
|
||||
onTaskCompleted(true)
|
||||
if (card.curve == null || card.walletPublicKey == null) {
|
||||
completeNfcSession(true)
|
||||
callback(TaskEvent.Completion(TaskError.CardError()))
|
||||
return@sendCommand
|
||||
}
|
||||
|
||||
val checkWalletCommand = prepareCheckWalletCommand(cardEnvironment)
|
||||
val challenge = CryptoUtils.generateRandomBytes(16)
|
||||
val checkWalletCommand = CheckWalletCommand(
|
||||
cardEnvironment.pin1,
|
||||
card.cardId,
|
||||
challenge)
|
||||
|
||||
sendCommand(checkWalletCommand, cardEnvironment) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Failure -> {
|
||||
if (result.error !is TaskError.UserCancelledError) {
|
||||
completeNfcSession(true, result.error)
|
||||
}
|
||||
callback(TaskEvent.Completion(result.error))
|
||||
}
|
||||
|
||||
sendCommand(checkWalletCommand, cardEnvironment) { checkWalletEvent ->
|
||||
when (checkWalletEvent) {
|
||||
is CompletionResult.Success -> {
|
||||
val checkWalletResponse = checkWalletEvent.data
|
||||
val verified = verify(walletPublickKey,
|
||||
completeNfcSession()
|
||||
val checkWalletResponse = result.data
|
||||
val verified = CryptoUtils.verify(
|
||||
card.walletPublicKey,
|
||||
challenge + checkWalletResponse.salt,
|
||||
checkWalletResponse.walletSignature,
|
||||
curve)
|
||||
card.curve)
|
||||
if (verified) {
|
||||
onTaskCompleted()
|
||||
callback(TaskEvent.Completion())
|
||||
callback(TaskEvent.Event(ScanEvent.OnVerifyEvent(true)))
|
||||
callback(TaskEvent.Completion())
|
||||
} else {
|
||||
onTaskCompleted(true)
|
||||
callback(TaskEvent.Completion(TaskError.VefificationFailed()))
|
||||
}
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
if (checkWalletEvent.error !is TaskError.UserCancelledError) {
|
||||
onTaskCompleted(true, checkWalletEvent.error)
|
||||
}
|
||||
callback(TaskEvent.Completion(checkWalletEvent.error))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
if (readEvent.error !is TaskError.UserCancelledError) {
|
||||
onTaskCompleted(true, readEvent.error)
|
||||
}
|
||||
callback(TaskEvent.Completion(readEvent.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareCheckWalletCommand(cardEnvironment: CardEnvironment): CheckWalletCommand {
|
||||
challenge = generateRandomBytes(16)
|
||||
return CheckWalletCommand(
|
||||
cardEnvironment.pin1,
|
||||
cardData.cardId,
|
||||
challenge,
|
||||
byteArrayOf())
|
||||
}
|
||||
}
|
||||
|
|
@ -6,22 +6,23 @@ import com.tangem.commands.CommandSerializer
|
|||
import com.tangem.common.CompletionResult
|
||||
|
||||
class SingleCommandTask<Event : CommandResponse>(
|
||||
private val commandSerializer: CommandSerializer<Event>) : Task<Event>() {
|
||||
private val command: CommandSerializer<Event>
|
||||
) : Task<Event>() {
|
||||
|
||||
override fun onRun(cardEnvironment: CardEnvironment,
|
||||
callback: (result: TaskEvent<Event>) -> Unit) {
|
||||
sendCommand(commandSerializer, cardEnvironment) { completionResult ->
|
||||
when (completionResult) {
|
||||
sendCommand(command, cardEnvironment) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
onTaskCompleted()
|
||||
callback(TaskEvent.Event(completionResult.data))
|
||||
completeNfcSession()
|
||||
callback(TaskEvent.Event(result.data))
|
||||
callback(TaskEvent.Completion())
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
if (completionResult.error !is TaskError.UserCancelledError) {
|
||||
onTaskCompleted(true, completionResult.error)
|
||||
if (result.error !is TaskError.UserCancelledError) {
|
||||
completeNfcSession(true, result.error)
|
||||
}
|
||||
callback(TaskEvent.Completion(completionResult.error))
|
||||
callback(TaskEvent.Completion(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,40 @@ import com.tangem.Log
|
|||
import com.tangem.commands.CommandResponse
|
||||
import com.tangem.commands.CommandSerializer
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.apdu.CommandApdu
|
||||
import com.tangem.common.apdu.StatusWord
|
||||
|
||||
sealed class TaskError(description: String? = null) : Exception(description) {
|
||||
class UnknownStatus(sw: Int) : TaskError("Unknown StatusWord: $sw")
|
||||
class MappingError : TaskError()
|
||||
class GenericError(description: String? = null) : TaskError(description)
|
||||
class UserCancelledError() : TaskError()
|
||||
class Busy() : TaskError()
|
||||
class TagLost() : 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>()
|
||||
}
|
||||
|
||||
abstract class Task<T> {
|
||||
|
||||
var delegate: CardManagerDelegate? = null
|
||||
|
|
@ -16,18 +48,18 @@ abstract class Task<T> {
|
|||
|
||||
fun run(cardEnvironment: CardEnvironment,
|
||||
callback: (result: TaskEvent<T>) -> Unit) {
|
||||
delegate?.onTaskStarted()
|
||||
reader?.setStartSession()
|
||||
delegate?.onNfcSessionStarted()
|
||||
reader?.startNfcSession()
|
||||
Log.i(this::class.simpleName!!, "Nfc task is started")
|
||||
onRun(cardEnvironment, callback)
|
||||
}
|
||||
|
||||
protected fun onTaskCompleted(withError: Boolean = false, taskError: TaskError? = null) {
|
||||
protected fun completeNfcSession(withError: Boolean = false, taskError: TaskError? = null) {
|
||||
reader?.closeSession()
|
||||
if (withError) {
|
||||
delegate?.onTaskError(taskError)
|
||||
delegate?.onError(taskError)
|
||||
} else {
|
||||
delegate?.onTaskCompleted()
|
||||
delegate?.onNfcSessionCompleted()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -35,15 +67,22 @@ abstract class Task<T> {
|
|||
callback: (result: TaskEvent<T>) -> Unit)
|
||||
|
||||
protected fun <T : CommandResponse> sendCommand(
|
||||
commandSerializer: CommandSerializer<T>,
|
||||
command: CommandSerializer<T>,
|
||||
cardEnvironment: CardEnvironment,
|
||||
callback: (result: CompletionResult<T>) -> Unit) {
|
||||
|
||||
Log.i(this::class.simpleName!!, "Nfc command ${commandSerializer::class.simpleName!!} is initiated")
|
||||
Log.i(this::class.simpleName!!, "Nfc command ${command::class.simpleName!!} is initiated")
|
||||
|
||||
val commandApdu = command.serialize(cardEnvironment)
|
||||
sendRequest(command, commandApdu, cardEnvironment, callback)
|
||||
}
|
||||
|
||||
reader?.transceiveApdu(
|
||||
commandSerializer.serialize(cardEnvironment)) { result ->
|
||||
private fun <T : CommandResponse> sendRequest(command: CommandSerializer<T>,
|
||||
commandApdu: CommandApdu,
|
||||
cardEnvironment: CardEnvironment,
|
||||
callback: (result: CompletionResult<T>) -> Unit) {
|
||||
|
||||
reader?.transceiveApdu(commandApdu) { result ->
|
||||
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
|
|
@ -52,24 +91,26 @@ abstract class Task<T> {
|
|||
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")
|
||||
val responseData = command.deserialize(cardEnvironment, responseApdu)
|
||||
Log.i(this::class.simpleName!!, "Nfc command ${command::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.Unknown -> callback(CompletionResult.Failure(TaskError.UnknownStatus(result.data.sw)))
|
||||
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)
|
||||
// When NeedPause is returned from the card whenever security delay is triggered.
|
||||
val remainingTime = command.deserializeSecurityDelay(responseApdu, cardEnvironment)
|
||||
if (remainingTime != null) delegate?.onSecurityDelay(remainingTime)
|
||||
Log.i(this::class.simpleName!!, "Nfc command ${command::class.simpleName!!} triggered security delay of $remainingTime milliseconds")
|
||||
sendRequest(command, commandApdu, cardEnvironment, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -80,18 +121,9 @@ abstract class Task<T> {
|
|||
callback(CompletionResult.Failure(TaskError.UserCancelledError()))
|
||||
reader?.readingActive = false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns in callback from tasks
|
||||
*/
|
||||
sealed class TaskEvent<T> {
|
||||
class Event<T>(val data: T) : TaskEvent<T>()
|
||||
class Completion<T>(val error: TaskError? = null) : TaskEvent<T>()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
package com.tangem.tasks
|
||||
|
||||
/**
|
||||
* An error class that covers typical errors that may occur when performing Tangem SDK tasks.
|
||||
* Errors are got propagated back within callbacks.
|
||||
*/
|
||||
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()
|
||||
}
|
||||
|
|
@ -25,8 +25,7 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
|
|||
setLogger()
|
||||
}
|
||||
|
||||
override fun onTaskStarted() {
|
||||
// reader.isoDep = null
|
||||
override fun onNfcSessionStarted() {
|
||||
reader.readingCancelled = false
|
||||
postUI { showReadingDialog(activity) }
|
||||
if (!reader.nfcEnabled) showNFCEnableDialog()
|
||||
|
|
@ -58,7 +57,7 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
|
|||
activity.supportFragmentManager.let { nfcEnableDialog?.show(it, NfcEnableDialog.TAG) }
|
||||
}
|
||||
|
||||
override fun showSecurityDelay(ms: Int) {
|
||||
override fun onSecurityDelay(ms: Int) {
|
||||
postUI {
|
||||
readingDialog?.lTouchCard?.visibility = View.GONE
|
||||
readingDialog?.tvRemainingTime?.text = ms.div(100).toString()
|
||||
|
|
@ -69,7 +68,7 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
|
|||
}
|
||||
}
|
||||
|
||||
override fun hideSecurityDelay() {
|
||||
override fun onTagLost() {
|
||||
postUI {
|
||||
readingDialog?.lTouchCard?.visibility = View.VISIBLE
|
||||
readingDialog?.flSecurityDelay?.visibility = View.GONE
|
||||
|
|
@ -78,7 +77,7 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
|
|||
}
|
||||
}
|
||||
|
||||
override fun onTaskCompleted() {
|
||||
override fun onNfcSessionCompleted() {
|
||||
postUI {
|
||||
readingDialog?.lTouchCard?.visibility = View.GONE
|
||||
readingDialog?.flSecurityDelay?.visibility = View.GONE
|
||||
|
|
@ -88,7 +87,7 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
|
|||
postUI(300) { readingDialog?.dismiss() }
|
||||
}
|
||||
|
||||
override fun onTaskError(error: TaskError?) {
|
||||
override fun onError(error: TaskError?) {
|
||||
postUI {
|
||||
readingDialog?.lTouchCard?.visibility = View.GONE
|
||||
readingDialog?.flSecurityDelay?.visibility = View.GONE
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@ import android.os.Looper
|
|||
|
||||
val uiHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
internal fun postUI(msTime: Long = 0, func: () -> Unit) {
|
||||
internal fun postUI(msTime: Long = 0, func: () -> Unit) {
|
||||
if (msTime > 0) uiHandler.postDelayed({ func() }, msTime) else uiHandler.post(func)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tangem_sdk_new.nfc
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
|
@ -9,16 +10,15 @@ import android.nfc.Tag
|
|||
import android.nfc.tech.IsoDep
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.tangem.Log
|
||||
|
||||
class NfcManager : NfcAdapter.ReaderCallback {
|
||||
|
||||
val reader = NfcReader()
|
||||
private var activity: FragmentActivity? = null
|
||||
private var activity: Activity? = null
|
||||
private var nfcAdapter: NfcAdapter? = null
|
||||
|
||||
fun setCurrentActivity(activity: FragmentActivity) {
|
||||
fun setCurrentActivity(activity: Activity) {
|
||||
this.activity = activity
|
||||
nfcAdapter = NfcAdapter.getDefaultAdapter(activity)
|
||||
}
|
||||
|
|
@ -49,11 +49,13 @@ class NfcManager : NfcAdapter.ReaderCallback {
|
|||
reader.nfcEnabled = true
|
||||
enableReaderMode()
|
||||
}
|
||||
reader.manager = this
|
||||
}
|
||||
|
||||
fun onPause() {
|
||||
activity?.unregisterReceiver(mBroadcastReceiver)
|
||||
disableReaderMode()
|
||||
reader.manager = null
|
||||
}
|
||||
|
||||
fun onDestroy() {
|
||||
|
|
@ -61,12 +63,11 @@ class NfcManager : NfcAdapter.ReaderCallback {
|
|||
nfcAdapter = null
|
||||
}
|
||||
|
||||
private fun enableReaderMode() {
|
||||
val options = Bundle()
|
||||
nfcAdapter?.enableReaderMode(activity, this, READER_FLAGS, options)
|
||||
fun enableReaderMode() {
|
||||
nfcAdapter?.enableReaderMode(activity, this, READER_FLAGS, Bundle())
|
||||
}
|
||||
|
||||
private fun disableReaderMode() {
|
||||
fun disableReaderMode() {
|
||||
nfcAdapter?.disableReaderMode(activity)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tangem_sdk_new.nfc
|
||||
|
||||
import android.nfc.Tag
|
||||
import android.nfc.TagLostException
|
||||
import android.nfc.tech.IsoDep
|
||||
import com.tangem.CardReader
|
||||
import com.tangem.Log
|
||||
|
|
@ -13,14 +14,15 @@ class NfcReader : CardReader {
|
|||
|
||||
override var readingActive = false
|
||||
var nfcEnabled = false
|
||||
var isoDep: IsoDep? = null
|
||||
var manager: NfcManager? = null
|
||||
private var isoDep: IsoDep? = null
|
||||
set(value) {
|
||||
// if tag is received, call connect first before transceiving data
|
||||
// don't reassign when there's an active tag already
|
||||
if (field == null) {
|
||||
field = value
|
||||
// if tag is received, call connect first before transceiving data
|
||||
connect()
|
||||
}
|
||||
// don't reassign when there's an active tag already
|
||||
if (value == null) field = value
|
||||
}
|
||||
|
||||
|
|
@ -38,9 +40,11 @@ class NfcReader : CardReader {
|
|||
var data: ByteArray? = null
|
||||
var callback: ((response: CompletionResult<ResponseApdu>) -> Unit)? = null
|
||||
|
||||
override fun setStartSession() {
|
||||
override fun startNfcSession() {
|
||||
readingActive = true
|
||||
readingCancelled = false
|
||||
manager?.disableReaderMode()
|
||||
manager?.enableReaderMode()
|
||||
}
|
||||
|
||||
override fun closeSession() {
|
||||
|
|
@ -93,7 +97,6 @@ class NfcReader : CardReader {
|
|||
isoDep?.connect()
|
||||
isoDep?.timeout = 240000
|
||||
Log.i(this::class.simpleName!!, "Nfc session is started")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ class NfcEnableDialog : DialogFragment() {
|
|||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val builder = AlertDialog.Builder(requireContext())
|
||||
val builder = AlertDialog.Builder(requireContext())
|
||||
builder.setCancelable(false)
|
||||
.setIcon(R.drawable.ic_action_nfc_gray)
|
||||
.setTitle(R.string.dialog_nfc_enable_title)
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ import android.widget.RelativeLayout
|
|||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
|
||||
class TouchCardAnimation(private var context: Context,
|
||||
private var ivHandCardHorizontal: ImageView,
|
||||
private var ivHandCardVertical: ImageView,
|
||||
private var llHand: LinearLayoutCompat,
|
||||
private var llNfc: LinearLayoutCompat) {
|
||||
private var ivHandCardHorizontal: ImageView,
|
||||
private var ivHandCardVertical: ImageView,
|
||||
private var llHand: LinearLayoutCompat,
|
||||
private var llNfc: LinearLayoutCompat) {
|
||||
|
||||
companion object {
|
||||
const val CARD_ON_BACK = 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue