From 5821fafe068d918deb23061d42c8e48eb37c21dd Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Feb 2020 13:15:45 +0300 Subject: [PATCH 1/6] Updated on 2026-08-14 --- .../src/main/java/com/tangem/CardManager.kt | 51 ++++++- .../java/com/tangem/CardManagerDelegate.kt | 6 + .../commands/ReadIssuerExtraDataCommand.kt | 85 ++++++++++++ .../commands/WriteIssuerExtraDataCommand.kt | 97 +++++++++++++ .../tangem/common/extensions/IntExtensions.kt | 16 ++- .../java/com/tangem/common/tlv/TlvEncoder.kt | 24 +++- .../java/com/tangem/common/tlv/TlvMapper.kt | 13 +- .../main/java/com/tangem/common/tlv/TlvTag.kt | 15 ++- .../tangem/tasks/ReadIssuerExtraDataTask.kt | 107 +++++++++++++++ .../src/main/java/com/tangem/tasks/Task.kt | 5 +- .../tangem/tasks/WriteIssuerExtraDataTask.kt | 127 ++++++++++++++++++ .../com/tangem/tangemtest/MainActivity.kt | 22 +++ .../src/main/res/layout/activity_main.xml | 25 +++- .../DefaultCardManagerDelegate.kt | 23 ++++ 14 files changed, 593 insertions(+), 23 deletions(-) create mode 100644 tangem-core/src/main/java/com/tangem/commands/ReadIssuerExtraDataCommand.kt create mode 100644 tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt create mode 100644 tangem-core/src/main/java/com/tangem/tasks/ReadIssuerExtraDataTask.kt create mode 100644 tangem-core/src/main/java/com/tangem/tasks/WriteIssuerExtraDataTask.kt diff --git a/tangem-core/src/main/java/com/tangem/CardManager.kt b/tangem-core/src/main/java/com/tangem/CardManager.kt index e65d1aa050..a7e6113daa 100644 --- a/tangem-core/src/main/java/com/tangem/CardManager.kt +++ b/tangem-core/src/main/java/com/tangem/CardManager.kt @@ -101,6 +101,22 @@ class CardManager( runTask(task, cardId, callback) } + /** + * This task retrieves Issuer Extra Data field and its issuer’s signature. + * Issuer Extra Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use, + * format and payload of Issuer Data. . For example, this field may contain photo or + * biometric information for ID card product. Because of the large size of Issuer_Extra_Data, + * a series of these commands have to be executed to read the entire Issuer_Extra_Data. + * @param cardId CID, Unique Tangem card ID number. + * @param callback is triggered on the completion of the [ReadIssuerExtraDataTask], + * provides card response in the form of [ReadIssuerExtraDataResponse]. + */ + fun readIssuerExtraData(cardId: String, + callback: (result: TaskEvent) -> Unit) { + val task = ReadIssuerExtraDataTask() + runTask(task, cardId, callback) + } + /** * This command writes 512-byte Issuer Data field and its issuer’s signature. * Issuer Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use, @@ -108,7 +124,7 @@ class CardManager( * wallet balance signed by the issuer or additional issuer’s attestation data. * @param cardId CID, Unique Tangem card ID number. * @param issuerData Data provided by issuer. - * @param issuerDataSignature Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card). + * @param issuerDataSignature Issuer’s signature of [issuerData] with Issuer Data Private Key. * @param issuerDataCounter An optional counter that protect issuer data against replay attack. * @param callback is triggered on the completion of the [WriteIssuerDataCommand], * provides card response in the form of [WriteIssuerDataResponse]. @@ -127,6 +143,39 @@ class CardManager( runTask(task, cardId, callback) } + /** + * This task writes Issuer Extra Data field and its issuer’s signature. + * Issuer Extra Data is never changed or parsed from within the Tangem COS. + * The issuer defines purpose of use, format and payload of Issuer Data. + * For example, this field may contain a photo or biometric information for ID card products. + * Because of the large size of Issuer_Extra_Data, a series of these commands have to be executed + * to write entire Issuer_Extra_Data. + * @param cardId CID, Unique Tangem card ID number. + * @param issuerData Data provided by issuer. + * @param startingSignature Issuer’s signature with Issuer Data Private Key of [cardId], + * [issuerDataCounter] (if flags Protect_Issuer_Data_Against_Replay and + * Restrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]) and size of [issuerData]. + * @param finalizingSignature Issuer’s signature with Issuer Data Private Key of [cardId], + * [issuerData] and [issuerDataCounter] (the latter one only if flags Protect_Issuer_Data_Against_Replay + * andRestrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]). + * @param issuerDataCounter An optional counter that protect issuer data against replay attack. + * @param callback is triggered on the completion of the [WriteIssuerDataCommand], + * provides card response in the form of [WriteIssuerDataResponse]. + */ + fun writeIssuerExtraData(cardId: String, + issuerData: ByteArray, + startingSignature: ByteArray, + finalizingSignature: ByteArray, + issuerDataCounter: Int? = null, + callback: (result: TaskEvent) -> Unit) { + val task = WriteIssuerExtraDataTask( + issuerData, + startingSignature, finalizingSignature, + issuerDataCounter + ) + runTask(task, cardId, callback) + } + /** * This command will create a new wallet on the card having ‘Empty’ state. * A key pair WalletPublicKey / WalletPrivateKey is generated and securely stored in the card. diff --git a/tangem-core/src/main/java/com/tangem/CardManagerDelegate.kt b/tangem-core/src/main/java/com/tangem/CardManagerDelegate.kt index da742378a2..5fce77186d 100644 --- a/tangem-core/src/main/java/com/tangem/CardManagerDelegate.kt +++ b/tangem-core/src/main/java/com/tangem/CardManagerDelegate.kt @@ -21,6 +21,12 @@ interface CardManagerDelegate { */ fun onSecurityDelay(ms: Int, totalDurationSeconds: Int) + /** + * It is called when long tasks are performed. + * A user is expected to hold the card until the task is complete. + */ + fun onDelay(total: Int, current: Int, step: Int) + /** * It is called when user takes the card away from the Android device during the scanning * (for example when security delay is in progress) and the TagLostException is received. diff --git a/tangem-core/src/main/java/com/tangem/commands/ReadIssuerExtraDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/ReadIssuerExtraDataCommand.kt new file mode 100644 index 0000000000..bbec2e5fb0 --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/ReadIssuerExtraDataCommand.kt @@ -0,0 +1,85 @@ +package com.tangem.commands + +import com.tangem.common.CardEnvironment +import com.tangem.common.apdu.CommandApdu +import com.tangem.common.apdu.Instruction +import com.tangem.common.apdu.ResponseApdu +import com.tangem.common.tlv.TlvBuilder +import com.tangem.common.tlv.TlvMapper +import com.tangem.common.tlv.TlvTag +import com.tangem.tasks.ReadIssuerExtraDataTask +import com.tangem.tasks.TaskError + +class ReadIssuerExtraDataResponse( + + /** + * CID, Unique Tangem card ID number. + */ + val cardId: String, + + /** + * Size of all Issuer_Extra_Data field. + */ + val size: Int?, + + /** + * Data defined by issuer. + */ + val issuerData: ByteArray, + + /** + * Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card). + * Issuer’s signature of SHA256-hashed [cardId] concatenated with [issuerData]: + * SHA256([cardId] | [issuerData]). + * When flag [SettingsMask.protectIssuerDataAgainstReplay] set in [SettingsMask] then signature of + * SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]: + * SHA256([cardId] | [issuerData] | [issuerDataCounter]). + */ + val issuerDataSignature: ByteArray?, + + /** + * An optional counter that protect issuer data against replay attack. + * When flag [SettingsMask.protectIssuerDataAgainstReplay] set in [SettingsMask] + * then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand]. + */ + val issuerDataCounter: Int? +) : CommandResponse + + +/** + * This command retrieves Issuer Extra Data field and its issuer’s signature. + * Issuer Extra Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use, + * format and payload of Issuer Data. . For example, this field may contain photo or + * biometric information for ID card product. Because of the large size of Issuer_Extra_Data, + * a series of these commands have to be executed to read the entire Issuer_Extra_Data. + */ +class ReadIssuerExtraDataCommand : CommandSerializer() { + + var offset: Int = 0 + + override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { + val tlvBuilder = TlvBuilder() + tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1) + tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId) + tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ExtraData) + tlvBuilder.append(TlvTag.Offset, offset) + return CommandApdu(Instruction.ReadIssuerData, tlvBuilder.serialize()) + } + + override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): ReadIssuerExtraDataResponse? { + val tlvData = responseApdu.getTlvData() ?: return null + + return try { + val mapper = TlvMapper(tlvData) + ReadIssuerExtraDataResponse( + cardId = mapper.map(TlvTag.CardId), + size = mapper.mapOptional(TlvTag.Size), + issuerData = mapper.mapOptional(TlvTag.IssuerData) ?: byteArrayOf(), + issuerDataSignature = mapper.mapOptional(TlvTag.IssuerDataSignature), + issuerDataCounter = mapper.mapOptional(TlvTag.IssuerDataCounter) + ) + } catch (exception: Exception) { + throw TaskError.SerializeCommandError() + } + } +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt new file mode 100644 index 0000000000..039ccff6dd --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt @@ -0,0 +1,97 @@ +package com.tangem.commands + +import com.tangem.common.CardEnvironment +import com.tangem.common.apdu.CommandApdu +import com.tangem.common.apdu.Instruction +import com.tangem.common.apdu.ResponseApdu +import com.tangem.common.tlv.TlvBuilder +import com.tangem.common.tlv.TlvMapper +import com.tangem.common.tlv.TlvTag +import com.tangem.tasks.TaskError + +enum class IssuerDataMode(val code: Byte) { + ExtraData(1), + WriteExtraData(2), + FinalizeExtraData(3); + + companion object { + private val values = CardStatus.values() + fun byCode(code: Int): CardStatus? = values.find { it.code == code } + } +} + +/** + * This command writes Issuer Extra Data field and its issuer’s signature. + * Issuer Extra Data is never changed or parsed from within the Tangem COS. + * The issuer defines purpose of use, format and payload of Issuer Data. + * For example, this field may contain a photo or biometric information for ID card products. + * Because of the large size of Issuer_Extra_Data, a series of these commands have to be executed + * to write entire Issuer_Extra_Data. + * @param issuerData Data provided by issuer. + * @param startingSignature Issuer’s signature with Issuer Data Private Key of [cardId], + * [issuerDataCounter] (if flags Protect_Issuer_Data_Against_Replay and + * Restrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]) and size of [issuerData]. + * @param finalizingSignature Issuer’s signature with Issuer Data Private Key of [cardId], + * [issuerData] and [issuerDataCounter] (the latter one only if flags Protect_Issuer_Data_Against_Replay + * andRestrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]). + * @param issuerDataCounter An optional counter that protect issuer data against replay attack. + */ +class WriteIssuerExtraDataCommand( + private val issuerData: ByteArray, + private val startingSignature: ByteArray, + private val finalizingSignature: ByteArray, + private val issuerDataCounter: Int? = null +) : CommandSerializer() { + + var mode: IssuerDataMode = IssuerDataMode.ExtraData + var offset: Int = 0 + + override fun serialize(cardEnvironment: CardEnvironment): CommandApdu { + val tlvBuilder = TlvBuilder() + + tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1) + tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId) + tlvBuilder.append(TlvTag.Mode, mode) + + when (mode) { + IssuerDataMode.ExtraData -> { + tlvBuilder.append(TlvTag.Size, issuerData.size) + tlvBuilder.append(TlvTag.IssuerDataSignature, startingSignature) + tlvBuilder.append(TlvTag.IssuerDataCounter, issuerDataCounter) + } + IssuerDataMode.WriteExtraData -> { + tlvBuilder.append(TlvTag.IssuerData, getDataToWrite()) + tlvBuilder.append(TlvTag.Offset, offset) + } + IssuerDataMode.FinalizeExtraData -> { + tlvBuilder.append(TlvTag.IssuerDataSignature, finalizingSignature) + } + } + return CommandApdu(Instruction.WriteIssuerData, tlvBuilder.serialize()) + } + + private fun getDataToWrite(): ByteArray = + issuerData.copyOfRange(offset, offset + calculatePartSize()) + + private fun calculatePartSize(): Int { + val bytesLeft = issuerData.size - offset + return if (bytesLeft < SINGLE_WRITE_SIZE) bytesLeft else SINGLE_WRITE_SIZE + } + + override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): WriteIssuerDataResponse? { + val tlvData = responseApdu.getTlvData() ?: return null + + return try { + val mapper = TlvMapper(tlvData) + WriteIssuerDataResponse( + cardId = mapper.map(TlvTag.CardId) + ) + } catch (exception: Exception) { + throw TaskError.SerializeCommandError() + } + } + + companion object { + const val SINGLE_WRITE_SIZE = 1524 + } +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/common/extensions/IntExtensions.kt b/tangem-core/src/main/java/com/tangem/common/extensions/IntExtensions.kt index 5565803830..0e8aa998da 100644 --- a/tangem-core/src/main/java/com/tangem/common/extensions/IntExtensions.kt +++ b/tangem-core/src/main/java/com/tangem/common/extensions/IntExtensions.kt @@ -2,9 +2,15 @@ package com.tangem.common.extensions import java.nio.ByteBuffer -fun Int.toByteArray(): ByteArray { - val bufferSize = Int.SIZE_BYTES - val buffer = ByteBuffer.allocate(bufferSize) - buffer.putInt(this) - return buffer.array() +fun Int.toByteArray(size: Int = Int.SIZE_BYTES): ByteArray { + if (size == Int.SIZE_BYTES) { + val buffer = ByteBuffer.allocate(size) + buffer.putInt(this) + return buffer.array() + } else if (size == Short.SIZE_BYTES){ + return byteArrayOf( + (this ushr 8).toByte(), + this.toByte()) + } + return byteArrayOf() } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/common/tlv/TlvEncoder.kt b/tangem-core/src/main/java/com/tangem/common/tlv/TlvEncoder.kt index 561603e335..217fd9a2ca 100644 --- a/tangem-core/src/main/java/com/tangem/common/tlv/TlvEncoder.kt +++ b/tangem-core/src/main/java/com/tangem/common/tlv/TlvEncoder.kt @@ -4,20 +4,28 @@ import com.tangem.Log import com.tangem.commands.* import com.tangem.common.extensions.* import com.tangem.tasks.TaskError -import java.time.Year import java.util.* +/** + * Encodes information that is to be written on the card from parsed classes into [ByteArray] + * (according to the provided [TlvTag] and corresponding [TlvValueType]) + * and then forms [Tlv] with the encoded values. + */ class TlvEncoder { + /** + + * @param value information that is to be encoded into [Tlv]. + */ internal inline fun encode(tag: TlvTag, value: T?): Tlv { if (value != null) { - return Tlv(tag, encodeValue(value, tag)) + return Tlv(tag, encodeValue(tag, value)) } else { Log.e(this::class.simpleName!!, "Encoding error. Value for tag $tag is null") throw TaskError.SerializeCommandError() } } - private inline fun encodeValue(value: T, tag: TlvTag): ByteArray { + internal inline fun encodeValue(tag: TlvTag, value: T): ByteArray { return when (tag.valueType()) { TlvValueType.HexString -> { typeCheck(tag) @@ -31,7 +39,11 @@ class TlvEncoder { typeCheck(tag) (value as String).toByteArray() } - TlvValueType.IntValue -> { + TlvValueType.Uint16 -> { + typeCheck(tag) + (value as Int).toByteArray(2) + } + TlvValueType.Uint32 -> { typeCheck(tag) (value as Int).toByteArray() } @@ -74,6 +86,10 @@ class TlvEncoder { typeCheck(tag) (value as SigningMethod).rawValue.toByteArray() } + TlvValueType.IssuerDataMode -> { + typeCheck(tag) + byteArrayOf((value as IssuerDataMode).code) + } } } diff --git a/tangem-core/src/main/java/com/tangem/common/tlv/TlvMapper.kt b/tangem-core/src/main/java/com/tangem/common/tlv/TlvMapper.kt index 90e218c5fa..c542d583dc 100644 --- a/tangem-core/src/main/java/com/tangem/common/tlv/TlvMapper.kt +++ b/tangem-core/src/main/java/com/tangem/common/tlv/TlvMapper.kt @@ -34,7 +34,7 @@ class TlvMapper(val tlvList: List) { /** * Finds [Tlv] by its [TlvTag]. - * Throws [MissingTagException] if [Tlv] is not found, + * Throws [TaskError.MissingTag] if [Tlv] is not found, * otherwise converts [Tlv] value to [T]. * * @param tag [TlvTag] of a [Tlv] which value is to be returned. @@ -61,7 +61,7 @@ class TlvMapper(val tlvList: List) { typeCheck(tag) tlvValue.toUtf8() as T } - TlvValueType.IntValue -> { + TlvValueType.Uint16, TlvValueType.Uint32 -> { typeCheck(tag) try { tlvValue.toInt() as T @@ -129,6 +129,15 @@ class TlvMapper(val tlvList: List) { throw TaskError.ConvertError() } } + TlvValueType.IssuerDataMode -> { + typeCheck(tag) + try { + IssuerDataMode.byCode(tlvValue.toInt()) as T + } catch (exception: Exception) { + logException(tag, tlvValue.toInt().toString(), exception) + throw TaskError.ConvertError() + } + } } } diff --git a/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt b/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt index 92ca2a8885..e5320b77b1 100644 --- a/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt +++ b/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt @@ -6,7 +6,8 @@ package com.tangem.common.tlv enum class TlvValueType { HexString, Utf8String, - IntValue, + Uint16, + Uint32, BoolValue, ByteArray, EllipticCurve, @@ -14,7 +15,8 @@ enum class TlvValueType { ProductMask, SettingsMask, CardStatus, - SigningMethod + SigningMethod, + IssuerDataMode } /** @@ -60,6 +62,10 @@ enum class TlvTag(val code: Int) { IssuerDataSignature(0x33), IssuerTransactionSignature(0x34), IssuerDataCounter(0x35), + Size(0x25), + Mode(0x23), + Offset(0x24), + IsActivated(0x3A), ActivationSeed(0x3B), @@ -113,13 +119,16 @@ enum class TlvTag(val code: Int) { TlvValueType.Utf8String CurveId -> TlvValueType.EllipticCurve MaxSignatures, PauseBeforePin2, RemainingSignatures, - SignedHashes, Health, TokenDecimal, UserCounter, IssuerDataCounter -> TlvValueType.IntValue + SignedHashes, Health, TokenDecimal, + Offset, Size -> TlvValueType.Uint16 + UserCounter, IssuerDataCounter -> TlvValueType.Uint32 IsActivated, TerminalIsLinked -> TlvValueType.BoolValue ManufactureDateTime -> TlvValueType.DateTime ProductMask -> TlvValueType.ProductMask SettingsMask -> TlvValueType.SettingsMask Status -> TlvValueType.CardStatus SigningMethod -> TlvValueType.SigningMethod + Mode -> TlvValueType.IssuerDataMode else -> TlvValueType.ByteArray } } diff --git a/tangem-core/src/main/java/com/tangem/tasks/ReadIssuerExtraDataTask.kt b/tangem-core/src/main/java/com/tangem/tasks/ReadIssuerExtraDataTask.kt new file mode 100644 index 0000000000..cd8b8738a0 --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/tasks/ReadIssuerExtraDataTask.kt @@ -0,0 +1,107 @@ +package com.tangem.tasks + +import com.tangem.commands.* +import com.tangem.common.CardEnvironment +import com.tangem.common.CompletionResult +import com.tangem.common.tlv.TlvEncoder +import com.tangem.common.tlv.TlvTag +import com.tangem.crypto.CryptoUtils +import java.io.ByteArrayOutputStream + +/** + * This task performs [ReadIssuerExtraDataCommand] repeatedly until + * the issuer extra data is fully retrieved. + */ +internal class ReadIssuerExtraDataTask : Task() { + + private val issuerData = ByteArrayOutputStream() + private var issuerDataSize = 0 + private lateinit var card: Card + + override fun onRun( + cardEnvironment: CardEnvironment, + currentCard: Card?, + callback: (result: TaskEvent) -> Unit + ) { + card = currentCard!! + val command = ReadIssuerExtraDataCommand() + readIssuerData(command, cardEnvironment, callback) + } + + private fun readIssuerData( + command: ReadIssuerExtraDataCommand, + cardEnvironment: CardEnvironment, + callback: (result: TaskEvent) -> Unit) { + + if (issuerDataSize != 0) { + delegate?.onDelay( + issuerDataSize, command.offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE + ) + } + + sendCommand(command, cardEnvironment) { result -> + when (result) { + + is CompletionResult.Success -> { + if (result.data.size != null) { + if (result.data.size == 0) { + completeNfcSession(TaskError.NoData()) + callback(TaskEvent.Completion(TaskError.NoData())) + return@sendCommand + } + issuerDataSize = result.data.size + } + issuerData.write(result.data.issuerData) + if (result.data.issuerDataSignature == null) { + command.offset = issuerData.size() + readIssuerData(command, cardEnvironment, callback) + } else { + completeTask(result.data, callback) + } + } + is CompletionResult.Failure -> { + if (result.error !is TaskError.UserCancelled) { + completeNfcSession(result.error) + } + callback(TaskEvent.Completion(result.error)) + } + } + } + } + + private fun completeTask(data: ReadIssuerExtraDataResponse, + callback: (result: TaskEvent) -> Unit) { + if (isIssuerDataValid(data.issuerDataCounter, data.issuerDataSignature!!)) { + completeNfcSession() + val finalResult = ReadIssuerExtraDataResponse( + data.cardId, + issuerDataSize, + issuerData.toByteArray(), + data.issuerDataSignature, + data.issuerDataCounter + ) + callback(TaskEvent.Event(finalResult)) + callback(TaskEvent.Completion()) + } else { + completeNfcSession(TaskError.VerificationFailed()) + callback(TaskEvent.Completion(TaskError.VerificationFailed())) + } + } + + private fun isIssuerDataValid(issuerDataCounter: Int?, signature: ByteArray): Boolean { + val tlvEncoder = TlvEncoder() + val dataToVerify = ByteArrayOutputStream() + dataToVerify.write(tlvEncoder.encodeValue(TlvTag.CardId, card.cardId)) + dataToVerify.write(issuerData.toByteArray()) + if (counterIncludedInSignature(issuerDataCounter)) { + dataToVerify.write(tlvEncoder.encodeValue(TlvTag.IssuerDataCounter, issuerDataCounter)) + } + return CryptoUtils.verify(card.issuerPublicKey!!, dataToVerify.toByteArray(), signature) + } + + private fun counterIncludedInSignature(issuerDataCounter: Int?): Boolean { + return issuerDataCounter != null && + card.settingsMask?.contains(SettingsMask.protectIssuerDataAgainstReplay) != false + } + +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/tasks/Task.kt b/tangem-core/src/main/java/com/tangem/tasks/Task.kt index 2f749238f5..543100d210 100644 --- a/tangem-core/src/main/java/com/tangem/tasks/Task.kt +++ b/tangem-core/src/main/java/com/tangem/tasks/Task.kt @@ -51,6 +51,10 @@ sealed class TaskError(val code: Int): Exception() { class TagLost: TaskError(5003) class UnknownError: TaskError(6000) + + //Input Data Errors + class MissingCounter: TaskError(7001) + class NoData: TaskError(7002) } /** @@ -105,7 +109,6 @@ abstract class Task { /** * Should be called on [Task] completion, whether it was successful or with failure. * - * @param withError True when there is an error * @param taskError The error to be shown by [CardManagerDelegate] */ protected fun completeNfcSession(taskError: TaskError? = null) { diff --git a/tangem-core/src/main/java/com/tangem/tasks/WriteIssuerExtraDataTask.kt b/tangem-core/src/main/java/com/tangem/tasks/WriteIssuerExtraDataTask.kt new file mode 100644 index 0000000000..e5556d8bd1 --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/tasks/WriteIssuerExtraDataTask.kt @@ -0,0 +1,127 @@ +package com.tangem.tasks + +import com.tangem.commands.* +import com.tangem.common.CardEnvironment +import com.tangem.common.CompletionResult +import com.tangem.common.tlv.TlvEncoder +import com.tangem.common.tlv.TlvTag +import com.tangem.crypto.CryptoUtils +import java.io.ByteArrayOutputStream + +/** + * This task performs [WriteIssuerExtraDataCommand] repeatedly until the issuer extra data is fully + * written on the card. + * @param issuerData Data provided by issuer. + * @param startingSignature Issuer’s signature with Issuer Data Private Key of [cardId], + * [issuerDataCounter] (if flags Protect_Issuer_Data_Against_Replay and + * Restrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]) and size of [issuerData]. + * @param finalizingSignature Issuer’s signature with Issuer Data Private Key of [cardId], + * [issuerData] and [issuerDataCounter] (the latter one only if flags Protect_Issuer_Data_Against_Replay + * andRestrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]). + * @param issuerDataCounter An optional counter that protect issuer data against replay attack. + */ +internal class WriteIssuerExtraDataTask( + private val issuerData: ByteArray, + private val startingSignature: ByteArray, + private val finalizingSignature: ByteArray, + private val issuerDataCounter: Int? = null +) : Task() { + + private lateinit var card: Card + + override fun onRun(cardEnvironment: CardEnvironment, + currentCard: Card?, + callback: (result: TaskEvent) -> Unit) { + + card = currentCard!! + if (!isCounterValid(issuerDataCounter)) { + completeNfcSession(TaskError.MissingCounter()) + callback(TaskEvent.Completion(TaskError.MissingCounter())) + } else if (!verifySignatures()) { + completeNfcSession(TaskError.VerificationFailed()) + callback(TaskEvent.Completion(TaskError.VerificationFailed())) + } + val command = WriteIssuerExtraDataCommand( + issuerData, startingSignature, finalizingSignature, issuerDataCounter + ) + writeIssuerData(command, cardEnvironment, callback) + } + + private fun writeIssuerData( + command: WriteIssuerExtraDataCommand, + cardEnvironment: CardEnvironment, + callback: (result: TaskEvent) -> Unit) { + + if (command.mode == IssuerDataMode.WriteExtraData) { + delegate?.onDelay(issuerData.size, command.offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE) + } + sendCommand(command, cardEnvironment) { result -> + when (result) { + + is CompletionResult.Success -> { + when (command.mode) { + IssuerDataMode.ExtraData -> { + command.mode = IssuerDataMode.WriteExtraData + writeIssuerData(command, cardEnvironment, callback) + return@sendCommand + } + IssuerDataMode.WriteExtraData -> { + command.offset += WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE + if (command.offset >= issuerData.size) { + command.mode = IssuerDataMode.FinalizeExtraData + } + writeIssuerData(command, cardEnvironment, callback) + return@sendCommand + } + IssuerDataMode.FinalizeExtraData -> { + completeNfcSession() + callback(TaskEvent.Event(result.data)) + callback(TaskEvent.Completion()) + } + } + } + is CompletionResult.Failure -> { + if (result.error !is TaskError.UserCancelled) { + completeNfcSession(result.error) + } + callback(TaskEvent.Completion(result.error)) + } + } + } + } + + private fun isCounterValid(issuerDataCounter: Int?): Boolean = + if (isCounterRequired()) issuerDataCounter != null else true + + private fun isCounterRequired(): Boolean = + card.settingsMask?.contains(SettingsMask.protectIssuerDataAgainstReplay) != false + + private fun verifySignatures(): Boolean = verifyFirstSignature() && verifyFinalizingSignature() + + private fun verifyFirstSignature(): Boolean { + val tlvEncoder = TlvEncoder() + val dataToVerify = ByteArrayOutputStream() + dataToVerify.write(tlvEncoder.encodeValue(TlvTag.CardId, card.cardId)) + if (isCounterRequired()) { + dataToVerify.write(tlvEncoder.encodeValue(TlvTag.IssuerDataCounter, issuerDataCounter)) + } + dataToVerify.write(tlvEncoder.encodeValue(TlvTag.Size, issuerData.size)) + + return CryptoUtils.verify( + card.issuerPublicKey!!, dataToVerify.toByteArray(), startingSignature + ) + } + + private fun verifyFinalizingSignature(): Boolean { + val tlvEncoder = TlvEncoder() + val dataToVerify = ByteArrayOutputStream() + dataToVerify.write(tlvEncoder.encodeValue(TlvTag.CardId, card.cardId)) + dataToVerify.write(tlvEncoder.encodeValue(TlvTag.IssuerData, issuerData)) + if (isCounterRequired()) { + dataToVerify.write(tlvEncoder.encodeValue(TlvTag.IssuerDataCounter, issuerDataCounter)) + } + return CryptoUtils.verify( + card.issuerPublicKey!!, dataToVerify.toByteArray(), finalizingSignature + ) + } +} \ No newline at end of file diff --git a/tangem-demo/src/main/java/com/tangem/tangemtest/MainActivity.kt b/tangem-demo/src/main/java/com/tangem/tangemtest/MainActivity.kt index 739ca2c95a..967c62c688 100644 --- a/tangem-demo/src/main/java/com/tangem/tangemtest/MainActivity.kt +++ b/tangem-demo/src/main/java/com/tangem/tangemtest/MainActivity.kt @@ -3,6 +3,11 @@ package com.tangem.tangemtest import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.tangem.CardManager +import com.tangem.common.extensions.hexToBytes +import com.tangem.common.extensions.toByteArray +import com.tangem.common.extensions.toHexString +import com.tangem.crypto.CryptoUtils +import com.tangem.crypto.sign import com.tangem.tangem_sdk_new.extensions.init import com.tangem.tasks.ScanEvent import com.tangem.tasks.TaskError @@ -15,6 +20,7 @@ class MainActivity : AppCompatActivity() { private lateinit var cardId: String private lateinit var issuerData: ByteArray private lateinit var issuerDataSignature: ByteArray + private var issuerDataCounter: Int = 1 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -41,6 +47,8 @@ class MainActivity : AppCompatActivity() { tv_card_cid?.text = cardId btn_sign.isEnabled = true btn_read_issuer_data.isEnabled = true + btn_read_issuer_extra_data.isEnabled = true + btn_write_issuer_data.isEnabled = true btn_purge_wallet.isEnabled = true btn_create_wallet.isEnabled = true } @@ -101,6 +109,20 @@ class MainActivity : AppCompatActivity() { } } } + btn_read_issuer_extra_data?.setOnClickListener { _ -> + cardManager.readIssuerExtraData(cardId) { + when (it) { + is TaskEvent.Completion -> { + if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName } + } + is TaskEvent.Event -> runOnUiThread { + issuerDataCounter = (it.data.issuerDataCounter ?: 0) + 1 + btn_write_issuer_data.isEnabled = true + tv_card_cid?.text = "Read ${it.data.issuerData.size} bytes of data." + } + } + } + } btn_purge_wallet?.setOnClickListener { _ -> cardManager.purgeWallet( cardId) { diff --git a/tangem-demo/src/main/res/layout/activity_main.xml b/tangem-demo/src/main/res/layout/activity_main.xml index 5759633c21..f11cbdbd35 100644 --- a/tangem-demo/src/main/res/layout/activity_main.xml +++ b/tangem-demo/src/main/res/layout/activity_main.xml @@ -33,7 +33,7 @@