Updated on 2026-08-14

This commit is contained in:
Tangem 2019-10-18 18:32:50 +03:00
parent da003bf0d3
commit 6ed8c7d0fb
17 changed files with 543 additions and 203 deletions

View file

@ -0,0 +1,102 @@
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) {
fun toBytes(): ByteArray {
// var length = 4 // CLA, INS, P1, P2
val data = if (tlvList.isNotEmpty()) {
tlvList.toBytes()
} else {
byteArrayOf()
}
val lc = data.size
//
// if (data.isNotEmpty()) {
// length += 1 // LC
// if (lc >= 256)
// length += 2
// length += data.size // DATA
// }
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()
// val apdu = ByteArray(length)
//
// var index = 0
// apdu[index] = cla
// index++
// apdu[index] = instruction
// index++
// apdu[index] = p1
// index++
// apdu[index] = p2
// index++
// if (lc != 0) {
// if (lc < 256) {
// apdu[index] = lc.toByte()
// index++
// } else {
// apdu[index] = 0
// index++
// apdu[index] = (lc shr 8).toByte()
// index++
// apdu[index] = (lc and 0xFF).toByte()
// index++
// }
//
// System.arraycopy(data, 0, apdu, index, data.size)
// index += data.size
// }
// return apdu
}
private fun writeLength(stream: ByteArrayOutputStream, lc: Int) {
// if (lc < 256) {
// stream.write(lc)
// } else {
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.Status
class ResponseApdu(val data: ByteArray) {
val sw1: Int = 0x00FF and data[data.size - 2].toInt()
val sw2: Int = 0x00FF and data[data.size - 1].toInt()
val sw: Int = sw1 shl 8 or sw2
val status: Status = Status.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,31 @@
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.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,80 @@
package com.tangem.common.tlv
import java.io.ByteArrayInputStream
import java.io.IOException
class Tlv(val tag: TlvTag, val tagCode: Int, val value: ByteArray = byteArrayOf()) {
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 Tlv(tag, code, 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,96 @@
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.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
?: throw MissingTagException()
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}")
String(tlvValue) 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(String(tlvValue)) 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

@ -1,12 +1,12 @@
package com.tangem.enums
package com.tangem.common.tlv
enum class TlvTag(val code: Int) {
Unknown(0x00),
CardID(0x01),
CardId(0x01),
Status(0x02),
CardPublicKey(0x03),
CardSignature(0x04),
CurveID(0x05),
CurveId(0x05),
HashAlgID(0x06),
SigningMethod(0x07),
MaxSignatures(0x08),
@ -49,9 +49,9 @@ enum class TlvTag(val code: Int) {
CodePageCount(0x41),
CodeHash(0x42),
TrOutHash(0x50),
TrOutHashSize(0x51),
TrOutRaw(0x52),
TransactionOutHash(0x50),
TransactionOutHashSize(0x51),
TransactionOutRaw(0x52),
WalletPublicKey(0x60),
Signature(0x61),
@ -66,6 +66,11 @@ enum class TlvTag(val code: Int) {
ManufacturerPublicKey(0x85),
CardIdManufacturerSignature(0x86),
ProductMask(0x8A),
PaymentFlowVersion(0x54),
UserCounter(0x2C),
TokenSymbol(0xA0),
TokenContractAddress(0xA1),
TokenDecimal(0xA2),
@ -78,7 +83,34 @@ enum class TlvTag(val code: Int) {
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,20 @@
package com.tangem.crypto
import com.tangem.commands.EllipticCurve
import java.security.SecureRandom
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)
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.crypto
import com.tangem.common.extentions.calculateSha512
import net.i2p.crypto.eddsa.EdDSAEngine
import net.i2p.crypto.eddsa.EdDSAPublicKey
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
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)
}

View file

@ -0,0 +1,43 @@
package com.tangem.crypto
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.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)))

View file

@ -1,75 +0,0 @@
package com.tangem.data
import com.tangem.enums.Instruction
class CommandApdu(
val tlvList: List<Tlv>,
val instruction: Instruction,
val p1: Int = 0x00,
val p2: Int = 0x00,
val encryptionKey: ByteArray? = null,
val rawInstruction: ByteArray = byteArrayOf()) {
val cla = ISO_CLA
fun serialize(): ByteArray {
var length = 4 // CLA, INS, P1, P2
val data = if (tlvList.isNotEmpty()) {
tlvList.toBytes()
} else {
byteArrayOf()
}
val lc = data.size
if (data.isNotEmpty()) {
length += 1 // LC
if (lc >= 256)
length += 2
length += data.size // DATA
}
val apdu = ByteArray(length)
var index = 0
apdu[index] = cla
index++
apdu[index] = instruction.code.toByte()
index++
apdu[index] = p1.toByte()
index++
apdu[index] = p2.toByte()
index++
if (lc != 0) {
if (lc < 256) {
apdu[index] = lc.toByte()
index++
} else {
apdu[index] = 0
index++
apdu[index] = (lc shr 8).toByte()
index++
apdu[index] = (lc and 0xFF).toByte()
index++
}
System.arraycopy(data, 0, apdu, index, data.size)
index += data.size
}
return apdu
}
private fun encrypt() {
}
companion object {
const val ISO_CLA = 0x00.toByte()
}
}

View file

@ -1,34 +0,0 @@
package com.tangem.data
import com.tangem.enums.Status
class ResponseApdu(val data: ByteArray) {
private val tlvParser = TlvParser()
fun deserialize(encryptionKey: ByteArray? = null): ResponseApduParsed? {
if (data.size < 2) return null
if (data.size == 2) return ResponseApduParsed(parseStatus(data[0], data[1]), emptyList())
val tlvList = tlvParser.fromBytes(data.copyOf(data.size - 2))
val sw1 = data[data.size - 2]
val sw2 = data[data.size -1]
return ResponseApduParsed(parseStatus(sw1, sw2), tlvList)
}
private fun parseStatus(sw1: Byte, sw2: Byte): Status {
val code = (0x00FF and sw1.toInt()) shl 8 or (0x00FF and sw2.toInt())
return Status.byCode(code)
}
private fun decrypt(encryptionKey: ByteArray) {
}
}
data class ResponseApduParsed(val status: Status, val tlvList: List<Tlv>? = null, val parsingError: String? = null) {
fun statusCompleted(): Boolean = status == Status.ProcessCompleted
}

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

@ -1,31 +0,0 @@
package com.tangem.data
import com.tangem.enums.TlvTag
class Tlv(val tag: TlvTag, val tagCode: Int, val value: ByteArray = byteArrayOf())
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

@ -1,55 +0,0 @@
package com.tangem.data
import com.tangem.enums.TlvTag
import java.io.ByteArrayInputStream
import java.io.IOException
class TlvParser {
fun readFromStream(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 Tlv(tag, code, value)
}
fun fromBytes(data: ByteArray): List<Tlv> {
val tlvList = mutableListOf<Tlv>()
val stream = ByteArrayInputStream(data)
var tlv: Tlv? = null
do {
try {
tlv = readFromStream(stream)
if (tlv != null) tlvList.add(tlv)
} catch (e: IOException) {
throw IOException("TLVError: " + e.message)
}
} while (tlv != null)
return tlvList
}
fun parseTlv(data: ByteArray) {
}
}

View file

@ -6,7 +6,7 @@ enum class Status (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),
// 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"),