Updated on 2026-08-14

This commit is contained in:
Tangem 2020-02-11 16:48:00 +03:00
parent 8de6a51871
commit d382162bef
13 changed files with 136 additions and 99 deletions

View file

@ -12,6 +12,7 @@ dependencies {
implementation 'net.i2p.crypto:eddsa:0.3.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
testImplementation "com.google.truth:truth:1.0"
implementation "org.jetbrains.kotlin:kotlin-reflect:1.3.61"
}
sourceCompatibility = "8"

View file

@ -76,7 +76,8 @@ class CardManager(
if (error is TaskError) {
callback(TaskEvent.Completion(error))
} else {
callback(TaskEvent.Completion(TaskError.GenericError(error.message)))
Log.e(this::class.simpleName!!, error.message ?: "")
callback(TaskEvent.Completion(TaskError.UnknownError()))
}
return
}

View file

@ -35,7 +35,7 @@ interface CardManagerDelegate {
/**
* It is called when some error occur during NFC session.
*/
fun onError(error: TaskError? = null)
fun onError(error: TaskError)
/**
* It is called when a user is expected to enter pin code.

View file

@ -46,7 +46,7 @@ class SignCommand(private val hashes: Array<ByteArray>)
private fun checkForErrors() {
if (hashes.isEmpty()) throw TaskError.EmptyHashes()
if (hashes.size > 10) throw TaskError.TooMuchHashes()
if (hashes.size > 10) throw TaskError.TooMuchHashesInOneTransaction()
if (hashes.any { it.size != hashSizes }) throw TaskError.HashSizeMustBeEqual()
}

View file

@ -1,5 +1,6 @@
package com.tangem.common.tlv
import com.tangem.Log
import com.tangem.commands.*
import com.tangem.common.extensions.*
import com.tangem.tasks.TaskError
@ -11,7 +12,8 @@ class TlvEncoder {
if (value != null) {
return Tlv(tag, encodeValue(value, tag))
} else {
throw TaskError.SerializeCommandError("Encoding error. Value for tag $tag is null")
Log.e(this::class.simpleName!!, "Encoding error. Value for tag $tag is null")
throw TaskError.SerializeCommandError()
}
}
@ -35,7 +37,8 @@ class TlvEncoder {
}
TlvValueType.BoolValue -> {
typeCheck<T, Boolean>(tag)
throw ConversionException("Usopported operation: Boolean to ByteArray for tag $tag")
Log.e(this::class.simpleName!!, "Unsupported operation: Boolean to ByteArray for tag $tag")
throw TaskError.ConvertError()
}
TlvValueType.ByteArray -> {
typeCheck<T, ByteArray>(tag)
@ -75,7 +78,10 @@ class TlvEncoder {
}
private inline fun <reified T, reified ExpectedT> typeCheck(tag: TlvTag) {
if (T::class != ExpectedT::class)
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
if (T::class != ExpectedT::class){
Log.e(this::class.simpleName!!,
"Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
throw TaskError.WrongType()
}
}
}

View file

@ -1,19 +1,14 @@
package com.tangem.common.tlv
import com.tangem.Log
import com.tangem.commands.*
import com.tangem.common.extensions.toDate
import com.tangem.common.extensions.toHexString
import com.tangem.common.extensions.toInt
import com.tangem.common.extensions.toUtf8
import com.tangem.tasks.TaskError
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 ConversionException(message: String? = null) : TlvMapperException(message)
/**
* Maps value fields in [Tlv] from raw [ByteArray] to concrete classes
* according to their [TlvTag] and corresponding [TlvValueType].
@ -33,7 +28,7 @@ class TlvMapper(val tlvList: List<Tlv>) {
inline fun <reified T> mapOptional(tag: TlvTag): T? =
try {
map<T>(tag)
} catch (exception: MissingTagException) {
} catch (exception: TaskError.MissingTag) {
null
}
@ -46,85 +41,108 @@ class TlvMapper(val tlvList: List<Tlv>) {
*
* @return [Tlv] value converted to a nullable type [T].
*
* @throws [MissingTagException] if no [Tlv] is found by the Tag.
* @throws [TaskError.MissingTag] exception 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) {
return false as T
} else {
throw MissingTagException("Tag $tag not found")
Log.e(this::class.simpleName!!, "Tag $tag not found")
throw TaskError.MissingTag()
}
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}")
typeCheck<T, String>(tag)
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}")
typeCheck<T, String>(tag)
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}")
typeCheck<T, Int>(tag)
try {
tlvValue.toInt() as T
} catch (exception: IllegalArgumentException) {
throw ConversionException(exception.message)
Log.e(this::class.simpleName!!, exception.message ?: "")
throw TaskError.ConvertError()
}
}
TlvValueType.BoolValue -> {
if (T::class != Boolean::class)
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
typeCheck<T, Boolean>(tag)
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}")
typeCheck<T, ByteArray>(tag)
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
?: throw ConversionException("Unknown Elliptic Curve value: ${tlvValue.toUtf8()}")
typeCheck<T, EllipticCurve>(tag)
try {
EllipticCurve.byName(tlvValue.toUtf8()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.toUtf8(), exception)
throw TaskError.ConvertError()
}
}
TlvValueType.DateTime -> {
if (T::class != Date::class)
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
typeCheck<T, Date>(tag)
try {
tlvValue.toDate() as T
} catch (exception: Exception) {
throw ConversionException("Converting to date with the following exception: " + exception.message)
logException(tag, tlvValue.toHexString(), exception)
throw TaskError.ConvertError()
}
}
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
?: throw ConversionException("Unknown Product Mask Code: ${tlvValue.first()}.")
typeCheck<T, ProductMask>(tag)
try {
ProductMask.byCode(tlvValue.first()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.first().toString(), exception)
throw TaskError.ConvertError()
}
}
TlvValueType.SettingsMask -> {
if (T::class != SettingsMask::class)
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
typeCheck<T, SettingsMask>(tag)
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()}")
typeCheck<T, CardStatus>(tag)
try {
CardStatus.byCode(tlvValue.toInt()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.toInt().toString(), exception)
throw TaskError.ConvertError()
}
}
TlvValueType.SigningMethod -> {
if (T::class != SigningMethod::class)
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
SigningMethod(tlvValue.toInt()) as T
?: throw ConversionException("Unknown Signing Method with code of: ${tlvValue.toInt()}")
typeCheck<T, SigningMethod>(tag)
try {
SigningMethod(tlvValue.toInt()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.toInt().toString(), exception)
throw TaskError.ConvertError()
}
}
}
}
fun logException(tag: TlvTag, value: String, exception: Exception) {
Log.e(this::class.simpleName!!,
"Unknown ${tag.name} with value of: value, \n${exception.message}")
}
inline fun <reified T, reified ExpectedT> typeCheck(tag: TlvTag) {
if (T::class != ExpectedT::class) {
Log.e(this::class.simpleName!!,
"Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
throw TaskError.WrongType()
}
}
}

View file

@ -35,7 +35,7 @@ internal class ScanTask : Task<ScanEvent>() {
if (currentCard != null) callback(TaskEvent.Event(ScanEvent.OnReadEvent(currentCard)))
if (currentCard == null) {
completeNfcSession(true, TaskError.MissingPreflightRead())
completeNfcSession(TaskError.MissingPreflightRead())
callback(TaskEvent.Completion(TaskError.MissingPreflightRead()))
} else if (currentCard.cardData?.productMask == ProductMask.Tag) {
@ -47,7 +47,7 @@ internal class ScanTask : Task<ScanEvent>() {
callback(TaskEvent.Completion())
} else if (currentCard.curve == null || currentCard.walletPublicKey == null) {
completeNfcSession(true, TaskError.CardError())
completeNfcSession(TaskError.CardError())
callback(TaskEvent.Completion(TaskError.CardError()))
} else {
@ -57,8 +57,8 @@ internal class ScanTask : Task<ScanEvent>() {
sendCommand(checkWalletCommand, cardEnvironment) { result ->
when (result) {
is CompletionResult.Failure -> {
if (result.error !is TaskError.UserCancelledError) {
completeNfcSession(true, result.error)
if (result.error !is TaskError.UserCancelled) {
completeNfcSession(result.error)
}
callback(TaskEvent.Completion(result.error))
}

View file

@ -28,8 +28,8 @@ class SingleCommandTask<Event : CommandResponse>(
callback(TaskEvent.Completion())
}
is CompletionResult.Failure -> {
if (result.error !is TaskError.UserCancelledError) {
completeNfcSession(true, result.error)
if (result.error !is TaskError.UserCancelled) {
completeNfcSession(result.error)
}
callback(TaskEvent.Completion(result.error))
}

View file

@ -16,33 +16,41 @@ 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()
class GenericError(description: String? = null) : TaskError(description)
class UserCancelledError() : TaskError()
class Busy() : TaskError()
class TagLost() : TaskError()
sealed class TaskError(val code: Int): Exception() {
class ErrorProcessingCommand(description: String? = null) : TaskError(description)
class InvalidState : TaskError()
class InsNotSupported : TaskError()
class InvalidParams : TaskError()
class NeedEncryption : TaskError()
class NeedPause : TaskError()
//Errors in serializing APDU
class SerializeCommandError: TaskError(1001)
class EncodingError: TaskError(1002)
class MissingTag: TaskError(1003)
class WrongType: TaskError(1004)
class ConvertError: TaskError(1005)
class VefificationFailed : TaskError()
class CardError : TaskError()
class ReaderError() : TaskError()
class SerializeCommandError(description: String? = null) : TaskError(description)
//Card errors
class UnknownStatus: TaskError(2001)
class ErrorProcessingCommand: TaskError(2002)
class MissingPreflightRead: TaskError(2003)
class InvalidState: TaskError(2004)
class InsNotSupported: TaskError(2005)
class InvalidParams: TaskError(2006)
class NeedEncryption: TaskError(2007)
class CardIsMissing() : TaskError()
class EmptyHashes() : TaskError()
class TooMuchHashes() : TaskError()
class HashSizeMustBeEqual() : TaskError()
//Scan errors
class VerificationFailed: TaskError(3000)
class CardError: TaskError(3001)
class WrongCard: TaskError(3002)
class TooMuchHashesInOneTransaction: TaskError(3003)
class EmptyHashes: TaskError(3004)
class HashSizeMustBeEqual: TaskError(3005)
class WrongCard() : TaskError()
class MissingPreflightRead() : TaskError()
class Busy: TaskError(4000)
class UserCancelled: TaskError(4001)
class UnsupportedDevice: TaskError(4002)
//NFC error
class NfcReaderError: TaskError(5002)
class TagLost: TaskError(5003)
class UnknownError: TaskError(6000)
}
/**
@ -100,9 +108,9 @@ abstract class Task<T> {
* @param withError True when there is an error
* @param taskError The error to be shown by [CardManagerDelegate]
*/
protected fun completeNfcSession(withError: Boolean = false, taskError: TaskError? = null) {
protected fun completeNfcSession(taskError: TaskError? = null) {
reader?.closeSession()
if (withError) {
if (taskError != null) {
delegate?.onError(taskError)
} else {
delegate?.onNfcSessionCompleted()
@ -153,7 +161,10 @@ abstract class Task<T> {
}
}
StatusWord.InvalidParams -> callback(CompletionResult.Failure(TaskError.InvalidParams()))
StatusWord.Unknown -> callback(CompletionResult.Failure(TaskError.UnknownStatus(result.data.sw)))
StatusWord.Unknown -> {
Log.e(this::class.simpleName!!, "Unknown status error: ${result.data.sw}")
callback(CompletionResult.Failure(TaskError.UnknownStatus()))
}
StatusWord.ErrorProcessingCommand -> callback(CompletionResult.Failure(TaskError.ErrorProcessingCommand()))
StatusWord.InvalidState -> callback(CompletionResult.Failure(TaskError.InvalidState()))
@ -169,10 +180,10 @@ abstract class Task<T> {
}
}
is CompletionResult.Failure ->
if (result.error is TaskError.TagLost) {
if (result.error == TaskError.TagLost()) {
delegate?.onTagLost()
} else if (result.error is TaskError.UserCancelledError) {
callback(CompletionResult.Failure(TaskError.UserCancelledError()))
} else if (result.error is TaskError.UserCancelled) {
callback(CompletionResult.Failure(TaskError.UserCancelled()))
reader?.closeSession()
}
}
@ -184,7 +195,7 @@ abstract class Task<T> {
sendCommand(ReadCommand(), environment) { readResult ->
when (readResult) {
is CompletionResult.Failure -> {
completeNfcSession(true, readResult.error)
completeNfcSession(readResult.error)
callback(TaskEvent.Completion(readResult.error))
}
is CompletionResult.Success -> {
@ -193,7 +204,7 @@ abstract class Task<T> {
securityDelayDuration = readResult.data.pauseBeforePin2 ?: 0
if (environment.cardId != null && environment.cardId != receivedCardId) {
completeNfcSession(true, TaskError.WrongCard())
completeNfcSession(TaskError.WrongCard())
callback(TaskEvent.Completion(TaskError.WrongCard()))
return@sendCommand
}

View file

@ -3,6 +3,7 @@ package com.tangem.common.tlv
import com.google.common.truth.Truth.assertThat
import com.tangem.commands.*
import com.tangem.common.extensions.hexToBytes
import com.tangem.tasks.TaskError
import org.junit.Test
import org.junit.jupiter.api.assertThrows
import java.util.*
@ -34,21 +35,21 @@ class TlvMapperTest {
@Test
fun `map when value is null throws MissingTagException`() {
assertThrows<MissingTagException> {
assertThrows<TaskError.MissingTag> {
tlvMapper.map<String>(TlvTag.TokenSymbol)
}
}
@Test
fun `map optional to wrong type throws WrongTypeException`() {
assertThrows<WrongTypeException> {
assertThrows<TaskError.WrongType> {
tlvMapper.mapOptional<String?>(TlvTag.CardData)
}
}
@Test
fun `map to wrong type throws WrongTypeException`() {
assertThrows<WrongTypeException> {
assertThrows<TaskError.WrongType> {
tlvMapper.map<String>(TlvTag.CardData)
}
}
@ -125,7 +126,7 @@ class TlvMapperTest {
@Test
fun `map Enum with unknown code throws ConversionException error`() {
val localMapper = TlvMapper(listOf(Tlv(TlvTag.ProductMask, byteArrayOf(5))))
assertThrows<ConversionException> {
assertThrows<TaskError.ConvertError> {
localMapper.map<ProductMask>(TlvTag.ProductMask)
}
}
@ -162,7 +163,7 @@ class TlvMapperTest {
@Test
fun `map Int with wrong value throws ConversionException`() {
val localMapper = TlvMapper(listOf(Tlv(TlvTag.SignedHashes, byteArrayOf(1, 2, 3, 4, 5))))
assertThrows<ConversionException> {
assertThrows<TaskError.ConvertError> {
localMapper.map<Int>(TlvTag.SignedHashes)
}
}

View file

@ -49,7 +49,7 @@ class MainActivity : AppCompatActivity() {
}
is TaskEvent.Completion -> {
if (taskEvent.error != null) {
if (taskEvent.error is TaskError.UserCancelledError) {
if (taskEvent.error is TaskError.UserCancelled) {
// Handle case when user cancelled manually
}
// Handle other errors

View file

@ -114,14 +114,14 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
postUI(300) { readingDialog?.dismiss() }
}
override fun onError(error: TaskError?) {
override fun onError(error: TaskError) {
postUI {
readingDialog?.lTouchCard?.hide()
readingDialog?.flSecurityDelay?.hide()
readingDialog?.flCompletion?.hide()
readingDialog?.flError?.show()
readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_error)
readingDialog?.tvTaskText?.text = if (error != null) error::class.simpleName else ""
readingDialog?.tvTaskText?.text = "${error::class.simpleName}: ${error.code}"
}
}

View file

@ -38,7 +38,7 @@ class NfcReader : CardReader {
// Stops reading and sends failure callback to a task
// if reading is cancelled (when user closes nfc bottom sheet dialog).
closeSession()
callback?.invoke(CompletionResult.Failure(TaskError.UserCancelledError()))
callback?.invoke(CompletionResult.Failure(TaskError.UserCancelled()))
}
}
@ -67,7 +67,7 @@ class NfcReader : CardReader {
private fun transceiveData() {
if (readingCancelled) {
callback?.invoke(CompletionResult.Failure(TaskError.UserCancelledError()))
callback?.invoke(CompletionResult.Failure(TaskError.UserCancelled()))
return
}
if (data == null) return
@ -109,9 +109,8 @@ class NfcReader : CardReader {
val response = SlixTagReader().transceive(nfcV)
when (response) {
is SlixReadResult.Failure -> {
callback?.invoke(CompletionResult.Failure(
TaskError.ErrorProcessingCommand(response.exception.message))
)
Log.e(this::class.simpleName!!, "${response.exception.message}")
callback?.invoke(CompletionResult.Failure(TaskError.ErrorProcessingCommand()))
}
is SlixReadResult.Success -> {
callback?.invoke(CompletionResult.Success(ResponseApdu(response.data)))