Updated on 2026-08-14
This commit is contained in:
parent
4ee67b0555
commit
bfa7d5dbcd
35 changed files with 6 additions and 6 deletions
24
tangem-core/src/main/java/com/tangem/CardEnvironment.kt
Normal file
24
tangem-core/src/main/java/com/tangem/CardEnvironment.kt
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem
|
||||
|
||||
|
||||
data class CardEnvironment(
|
||||
val pin1: String = DEFAULT_PIN,
|
||||
val pin2: String = DEFAULT_PIN2,
|
||||
val cardId: String? = null,
|
||||
val terminalKeys: KeyPair? = null,
|
||||
val encryptionKey: ByteArray? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_PIN = "000000"
|
||||
const val DEFAULT_PIN2 = "000"
|
||||
}
|
||||
}
|
||||
|
||||
enum class EncryptionMode(val code: Byte) {
|
||||
NONE(0x0),
|
||||
FAST(0x1),
|
||||
STRONG(0x2)
|
||||
}
|
||||
|
||||
class KeyPair(val publicKey: ByteArray, val privateKey: ByteArray)
|
||||
81
tangem-core/src/main/java/com/tangem/CardManager.kt
Normal file
81
tangem-core/src/main/java/com/tangem/CardManager.kt
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package com.tangem
|
||||
|
||||
import com.tangem.commands.CommandResponse
|
||||
import com.tangem.commands.CommandSerializer
|
||||
import com.tangem.commands.SignCommand
|
||||
import com.tangem.commands.SignResponse
|
||||
import com.tangem.crypto.initCrypto
|
||||
import com.tangem.tasks.*
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class CardManager(
|
||||
private val reader: CardReader,
|
||||
private val cardManagerDelegate: CardManagerDelegate? = null) {
|
||||
|
||||
private var isBusy = false
|
||||
private val cardEnvironmentRepository = mutableMapOf<String, CardEnvironment>()
|
||||
private val cardManagerExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
init {
|
||||
initCrypto()
|
||||
}
|
||||
|
||||
fun scanCard(callback: (result: TaskEvent<ScanEvent>) -> Unit) {
|
||||
val task = ScanTask()
|
||||
runTask(task, callback = callback)
|
||||
}
|
||||
|
||||
fun sign(hashes: Array<ByteArray>, cardId: String,
|
||||
callback: (result: TaskEvent<SignResponse>) -> Unit) {
|
||||
val signCommand: SignCommand
|
||||
try {
|
||||
signCommand = SignCommand(hashes, cardId)
|
||||
} catch (error: Exception) {
|
||||
if (error is TaskError) {
|
||||
callback(TaskEvent.Completion(error))
|
||||
} else {
|
||||
callback(TaskEvent.Completion(TaskError.GenericError(error.message)))
|
||||
}
|
||||
return
|
||||
}
|
||||
val task = SingleCommandTask(signCommand)
|
||||
runTask(task, cardId, callback)
|
||||
}
|
||||
|
||||
fun <T> runTask(task: Task<T>, cardId: String? = null,
|
||||
callback: (result: TaskEvent<T>) -> Unit) {
|
||||
if (isBusy) {
|
||||
callback(TaskEvent.Completion(TaskError.Busy()))
|
||||
return
|
||||
}
|
||||
|
||||
val environment = fetchCardEnvironment(cardId)
|
||||
isBusy = true
|
||||
|
||||
task.reader = reader
|
||||
task.delegate = cardManagerDelegate
|
||||
|
||||
cardManagerExecutor.execute {
|
||||
task.run(environment) {
|
||||
when (it) {
|
||||
is TaskEvent.Event -> callback(it)
|
||||
is TaskEvent.Completion -> {
|
||||
isBusy = false
|
||||
callback(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchCardEnvironment(cardId: String?): CardEnvironment {
|
||||
return cardEnvironmentRepository[cardId] ?: CardEnvironment()
|
||||
}
|
||||
|
||||
fun <T : CommandResponse> runCommand(commandSerializer: CommandSerializer<T>,
|
||||
cardId: String? = null,
|
||||
callback: (result: TaskEvent<T>) -> Unit) {
|
||||
val task = SingleCommandTask(commandSerializer)
|
||||
runTask(task, cardId, callback)
|
||||
}
|
||||
}
|
||||
15
tangem-core/src/main/java/com/tangem/CardManagerDelegate.kt
Normal file
15
tangem-core/src/main/java/com/tangem/CardManagerDelegate.kt
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.tasks.TaskError
|
||||
|
||||
interface CardManagerDelegate {
|
||||
|
||||
fun onTaskStarted()
|
||||
fun showSecurityDelay(ms: Int)
|
||||
fun onTaskCompleted()
|
||||
fun onTaskError(error: TaskError? = null)
|
||||
|
||||
fun requestPin(callback: (result: CompletionResult<String>) -> Unit)
|
||||
|
||||
}
|
||||
12
tangem-core/src/main/java/com/tangem/CardReader.kt
Normal file
12
tangem-core/src/main/java/com/tangem/CardReader.kt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.apdu.CommandApdu
|
||||
import com.tangem.common.apdu.ResponseApdu
|
||||
|
||||
interface CardReader {
|
||||
var readingActive: Boolean
|
||||
fun transceiveApdu(apdu: CommandApdu, callback: (response: CompletionResult<ResponseApdu>) -> Unit)
|
||||
fun setStartSession()
|
||||
fun closeSession()
|
||||
}
|
||||
10
tangem-core/src/main/java/com/tangem/DataStorage.kt
Normal file
10
tangem-core/src/main/java/com/tangem/DataStorage.kt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem
|
||||
|
||||
interface DataStorage {
|
||||
|
||||
fun getTerminalPublicKey(): ByteArray?
|
||||
fun getTerminalPrivateKey(): ByteArray?
|
||||
fun getPin1(): String?
|
||||
fun getPin2(): String?
|
||||
|
||||
}
|
||||
29
tangem-core/src/main/java/com/tangem/Log.kt
Normal file
29
tangem-core/src/main/java/com/tangem/Log.kt
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem
|
||||
|
||||
object Log {
|
||||
|
||||
private var loggerInstance: LoggerInterface? = null
|
||||
|
||||
fun i(logTag: String, message: String) {
|
||||
loggerInstance?.i(logTag, message)
|
||||
}
|
||||
|
||||
fun e(logTag: String, message: String) {
|
||||
loggerInstance?.e(logTag, message)
|
||||
}
|
||||
|
||||
fun v(logTag: String, message: String) {
|
||||
|
||||
loggerInstance?.v(logTag, message)
|
||||
}
|
||||
|
||||
fun setLogger(logger: LoggerInterface) {
|
||||
loggerInstance = logger
|
||||
}
|
||||
}
|
||||
|
||||
interface LoggerInterface {
|
||||
fun i(logTag: String, message: String)
|
||||
fun e(logTag: String, message: String)
|
||||
fun v(logTag: String, message: String)
|
||||
}
|
||||
16
tangem-core/src/main/java/com/tangem/Response.kt
Normal file
16
tangem-core/src/main/java/com/tangem/Response.kt
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem
|
||||
//
|
||||
//data class SignResponse(
|
||||
// val cid: String,
|
||||
// val signature: ByteArray,
|
||||
// val remainingSignatures: Int,
|
||||
// val signedHashes: Int
|
||||
//)
|
||||
//
|
||||
//
|
||||
//data class CardError(
|
||||
// val code: Int = 0
|
||||
//)
|
||||
//
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.commands
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
import com.tangem.common.apdu.CommandApdu
|
||||
import com.tangem.common.apdu.ResponseApdu
|
||||
import com.tangem.common.extentions.calculateSha256
|
||||
import com.tangem.common.extentions.hexToBytes
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.TlvMapper
|
||||
import com.tangem.common.tlv.TlvTag
|
||||
import com.tangem.enums.Instruction
|
||||
import com.tangem.tasks.TaskError
|
||||
|
||||
class CheckWalletResponse(
|
||||
val cardId: String,
|
||||
val salt: ByteArray,
|
||||
val walletSignature: ByteArray
|
||||
) : CommandResponse
|
||||
|
||||
|
||||
class CheckWalletCommand(
|
||||
val pin1: String, val cid: String,
|
||||
val challenge: ByteArray, val publicKeyChallenge: ByteArray) : CommandSerializer<CheckWalletResponse>() {
|
||||
|
||||
override val instruction = Instruction.CheckWallet
|
||||
override val instructionCode = instruction.code
|
||||
|
||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
val tlvData = listOf(
|
||||
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
|
||||
Tlv(TlvTag.CardId, cid.hexToBytes()),
|
||||
Tlv(TlvTag.Challenge, challenge)
|
||||
)
|
||||
|
||||
return CommandApdu(instructionCode, tlvData)
|
||||
}
|
||||
|
||||
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): CheckWalletResponse? {
|
||||
val tlvData = responseApdu.getTlvData() ?: return null
|
||||
|
||||
return try {
|
||||
val mapper = TlvMapper(tlvData)
|
||||
CheckWalletResponse(
|
||||
cardId = mapper.map(TlvTag.CardId),
|
||||
salt = mapper.map(TlvTag.Salt),
|
||||
walletSignature = mapper.map(TlvTag.Signature)
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
throw TaskError.SerializeCommandError()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.commands
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
import com.tangem.common.apdu.CommandApdu
|
||||
import com.tangem.common.apdu.ResponseApdu
|
||||
import com.tangem.common.extentions.toInt
|
||||
import com.tangem.common.tlv.TlvTag
|
||||
import com.tangem.enums.Instruction
|
||||
|
||||
interface CommandResponse
|
||||
|
||||
|
||||
abstract class CommandSerializer<T : CommandResponse> {
|
||||
|
||||
abstract val instruction: Instruction
|
||||
abstract val instructionCode: Int
|
||||
|
||||
abstract fun serialize(cardEnvironment: CardEnvironment): CommandApdu
|
||||
abstract fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): T?
|
||||
|
||||
fun deserializeSecurityDelay(responseApdu: ResponseApdu, cardEnvironment: CardEnvironment): Int? {
|
||||
val tlv = responseApdu.getTlvData(cardEnvironment.encryptionKey)
|
||||
return tlv?.find { it.tag == TlvTag.Pause }?.value?.toInt()
|
||||
}
|
||||
}
|
||||
158
tangem-core/src/main/java/com/tangem/commands/ReadCardCommand.kt
Normal file
158
tangem-core/src/main/java/com/tangem/commands/ReadCardCommand.kt
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
package com.tangem.commands
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
import com.tangem.common.apdu.CommandApdu
|
||||
import com.tangem.common.apdu.ResponseApdu
|
||||
import com.tangem.common.extentions.calculateSha256
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.TlvMapper
|
||||
import com.tangem.common.tlv.TlvTag
|
||||
import com.tangem.data.SettingsMask
|
||||
import com.tangem.enums.Instruction
|
||||
import com.tangem.tasks.TaskError
|
||||
import java.util.*
|
||||
|
||||
enum class SigningMethod(val code: Int) {
|
||||
SignHash(0),
|
||||
SignRaw(1),
|
||||
SignHashValidatedByIssuer(2),
|
||||
SignRawValidatedByIssuer(3),
|
||||
SignHashValidatedByIssuerAndWriteIssuerData(4),
|
||||
SignRawValidatedByIssuerAndWriteIssuerData(5),
|
||||
SignPos(6);
|
||||
|
||||
companion object {
|
||||
fun byCode(code: Int): SigningMethod? = values().find { it.code == code }
|
||||
}
|
||||
}
|
||||
|
||||
enum class EllipticCurve(val curve: String) {
|
||||
Secp256k1("secp256k1"),
|
||||
Ed25519("ed25519");
|
||||
|
||||
companion object {
|
||||
fun byName(curve: String): EllipticCurve? = values().find { it.curve == curve }
|
||||
}
|
||||
}
|
||||
|
||||
enum class CardStatus(val code: Int) {
|
||||
NotPersonalized(0),
|
||||
Empty(1),
|
||||
Loaded(2),
|
||||
Purged(3);
|
||||
|
||||
companion object {
|
||||
fun byCode(code: Int): CardStatus? = values().find { it.code == code }
|
||||
}
|
||||
}
|
||||
|
||||
enum class ProductMask(val code: Byte) {
|
||||
Note(0x01),
|
||||
Tag(0x02),
|
||||
Card(0x04);
|
||||
|
||||
companion object {
|
||||
fun byCode(code: Byte): ProductMask? = values().find { it.code == code }
|
||||
}
|
||||
}
|
||||
|
||||
class Card(
|
||||
val cardId: String,
|
||||
val manufacturerName: String,
|
||||
val status: CardStatus,
|
||||
|
||||
val firmwareVersion: String?,
|
||||
val cardPublicKey: ByteArray?,
|
||||
val settingsMask: SettingsMask?,
|
||||
val issuerPublicKey: ByteArray?,
|
||||
val curve: EllipticCurve?,
|
||||
val maxSignatures: Int?,
|
||||
val signingMethod: SigningMethod?,
|
||||
val pauseBeforePin2: Int?,
|
||||
val walletPublicKey: ByteArray?,
|
||||
val walletRemainingSignatures: Int?,
|
||||
val walletSignedHashes: Int?,
|
||||
val health: Int?,
|
||||
val isActivated: Boolean,
|
||||
val activationSeed: ByteArray?,
|
||||
val paymentFlowVersion: ByteArray?,
|
||||
val userCounter: Int?,
|
||||
val terminalIsLinked: Boolean,
|
||||
|
||||
//Card Data
|
||||
val batchId: String?,
|
||||
val manufactureDateTime: Date?,
|
||||
val issuerName: String?,
|
||||
val blockchainName: String?,
|
||||
val manufacturerSignature: ByteArray?,
|
||||
val productMask: ProductMask?,
|
||||
|
||||
val tokenSymbol: String?,
|
||||
val tokenContractAddress: String?,
|
||||
val tokenDecimal: Int?
|
||||
) : CommandResponse
|
||||
|
||||
|
||||
class ReadCardCommand : CommandSerializer<Card>() {
|
||||
|
||||
override val instruction = Instruction.Read
|
||||
override val instructionCode = instruction.code
|
||||
|
||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
|
||||
val tlvData = mutableListOf(Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()))
|
||||
|
||||
cardEnvironment.terminalKeys?.let {
|
||||
Tlv(TlvTag.TerminalPublicKey, it.publicKey)
|
||||
}
|
||||
|
||||
return CommandApdu(instructionCode, tlvData)
|
||||
}
|
||||
|
||||
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): Card? {
|
||||
val tlvData = responseApdu.getTlvData() ?: return null
|
||||
|
||||
return try {
|
||||
val tlvMapper = TlvMapper(tlvData)
|
||||
|
||||
Card(
|
||||
cardId = tlvMapper.map(TlvTag.CardId),
|
||||
manufacturerName = tlvMapper.map(TlvTag.ManufactureId),
|
||||
status = tlvMapper.map(TlvTag.Status),
|
||||
|
||||
firmwareVersion = tlvMapper.mapOptional(TlvTag.Firmware),
|
||||
cardPublicKey = tlvMapper.mapOptional(TlvTag.CardPublicKey),
|
||||
settingsMask = tlvMapper.mapOptional(TlvTag.SettingsMask),
|
||||
issuerPublicKey = tlvMapper.mapOptional(TlvTag.IssuerDataPublicKey),
|
||||
curve = tlvMapper.mapOptional(TlvTag.CurveId),
|
||||
maxSignatures = tlvMapper.mapOptional(TlvTag.MaxSignatures),
|
||||
signingMethod = tlvMapper.mapOptional(TlvTag.SigningMethod),
|
||||
pauseBeforePin2 = tlvMapper.mapOptional(TlvTag.PauseBeforePin2),
|
||||
walletPublicKey = tlvMapper.mapOptional(TlvTag.WalletPublicKey),
|
||||
walletRemainingSignatures = tlvMapper.mapOptional(TlvTag.RemainingSignatures),
|
||||
walletSignedHashes = tlvMapper.mapOptional(TlvTag.SignedHashes),
|
||||
health = tlvMapper.mapOptional(TlvTag.Health),
|
||||
isActivated = tlvMapper.map(TlvTag.IsActivated),
|
||||
activationSeed = tlvMapper.mapOptional(TlvTag.ActivationSeed),
|
||||
paymentFlowVersion = tlvMapper.mapOptional(TlvTag.PaymentFlowVersion),
|
||||
userCounter = tlvMapper.mapOptional(TlvTag.UserCounter),
|
||||
terminalIsLinked = tlvMapper.map(TlvTag.TerminalIsLinked),
|
||||
|
||||
batchId = tlvMapper.mapOptional(TlvTag.Batch),
|
||||
manufactureDateTime = tlvMapper.mapOptional(TlvTag.ManufactureDateTime),
|
||||
issuerName = tlvMapper.mapOptional(TlvTag.IssuerId),
|
||||
blockchainName = tlvMapper.mapOptional(TlvTag.BlockchainId),
|
||||
manufacturerSignature = tlvMapper.mapOptional(TlvTag.ManufacturerSignature),
|
||||
productMask = tlvMapper.mapOptional(TlvTag.ProductMask),
|
||||
|
||||
tokenSymbol = tlvMapper.mapOptional(TlvTag.TokenSymbol),
|
||||
tokenContractAddress = tlvMapper.mapOptional(TlvTag.TokenContractAddress),
|
||||
tokenDecimal = tlvMapper.mapOptional(TlvTag.TokenDecimal)
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
throw TaskError.SerializeCommandError()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
76
tangem-core/src/main/java/com/tangem/commands/SignCommand.kt
Normal file
76
tangem-core/src/main/java/com/tangem/commands/SignCommand.kt
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package com.tangem.commands
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
import com.tangem.common.apdu.CommandApdu
|
||||
import com.tangem.common.apdu.ResponseApdu
|
||||
import com.tangem.common.extentions.calculateSha256
|
||||
import com.tangem.common.extentions.hexToBytes
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.TlvMapper
|
||||
import com.tangem.common.tlv.TlvTag
|
||||
import com.tangem.crypto.sign
|
||||
import com.tangem.enums.Instruction
|
||||
import com.tangem.tasks.TaskError
|
||||
|
||||
class SignResponse(
|
||||
val cardId: String,
|
||||
val signature: ByteArray,
|
||||
val remainingSignatures: Int,
|
||||
val signedHashes: Int
|
||||
) : CommandResponse
|
||||
|
||||
|
||||
class SignCommand(private val hashes: Array<ByteArray>, private val cardId: String)
|
||||
: CommandSerializer<SignResponse>() {
|
||||
|
||||
override val instruction = Instruction.Sign
|
||||
override val instructionCode = instruction.code
|
||||
|
||||
private val hashSizes = if (hashes.isNotEmpty()) hashes.first().size else 0
|
||||
private val dataToSign = flattenHashes()
|
||||
|
||||
private fun flattenHashes(): ByteArray {
|
||||
checkForErrors()
|
||||
return hashes.reduce { arr1, arr2 -> arr1 + arr2 }
|
||||
}
|
||||
|
||||
private fun checkForErrors() {
|
||||
if (hashes.isEmpty()) throw TaskError.EmptyHashes()
|
||||
if (hashes.size > 10) throw TaskError.TooMuchHashes()
|
||||
if (hashes.any { it.size != hashSizes }) throw TaskError.HashSizeMustBeEqual()
|
||||
}
|
||||
|
||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
val tlvData = mutableListOf(
|
||||
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
|
||||
Tlv(TlvTag.Pin2, cardEnvironment.pin2.calculateSha256()),
|
||||
Tlv(TlvTag.CardId, cardId.hexToBytes()),
|
||||
Tlv(TlvTag.TransactionOutHashSize,byteArrayOf(hashSizes.toByte())),
|
||||
Tlv(TlvTag.TransactionOutHash, dataToSign)
|
||||
)
|
||||
|
||||
addTerminalSignature(cardEnvironment, tlvData)
|
||||
|
||||
return CommandApdu(instructionCode, tlvData)
|
||||
}
|
||||
|
||||
private fun addTerminalSignature(cardEnvironment: CardEnvironment, tlvData: MutableList<Tlv>) {
|
||||
cardEnvironment.terminalKeys?.let {
|
||||
val signedData = dataToSign.sign(it.privateKey)
|
||||
tlvData.add(Tlv(TlvTag.TerminalTransactionSignature, signedData))
|
||||
tlvData.add(Tlv(TlvTag.TerminalPublicKey, it.publicKey))
|
||||
}
|
||||
}
|
||||
|
||||
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): SignResponse? {
|
||||
val tlvData = responseApdu.getTlvData() ?: return null
|
||||
|
||||
val tlvMapper = TlvMapper(tlvData)
|
||||
return SignResponse(
|
||||
cardId= tlvMapper.map(TlvTag.CardId),
|
||||
signature = tlvMapper.map(TlvTag.Signature),
|
||||
remainingSignatures = tlvMapper.map(TlvTag.RemainingSignatures),
|
||||
signedHashes = tlvMapper.map(TlvTag.SignedHashes)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.common
|
||||
|
||||
import com.tangem.tasks.TaskError
|
||||
|
||||
sealed class CompletionResult<T> {
|
||||
class Success<T>(val data: T) : CompletionResult<T>()
|
||||
class Failure<T>(val error: TaskError) : CompletionResult<T>()
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.common.apdu
|
||||
|
||||
import com.tangem.EncryptionMode
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.toBytes
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
class CommandApdu(
|
||||
|
||||
private val instruction: Int,
|
||||
private val tlvList: List<Tlv>,
|
||||
|
||||
private val cla: Byte = ISO_CLA,
|
||||
private val p1: Byte = 0x00,
|
||||
private val p2: Byte = 0x00,
|
||||
|
||||
private val le: Int = 0x00,
|
||||
|
||||
private val encryptionMode: EncryptionMode = EncryptionMode.NONE,
|
||||
private val encryptionKey: ByteArray? = null) {
|
||||
|
||||
val apduData: ByteArray
|
||||
|
||||
init {
|
||||
apduData = toBytes()
|
||||
}
|
||||
|
||||
|
||||
private fun toBytes(): ByteArray {
|
||||
|
||||
val data = if (tlvList.isNotEmpty()) {
|
||||
tlvList.toBytes()
|
||||
} else {
|
||||
byteArrayOf()
|
||||
}
|
||||
|
||||
val lc = data.size
|
||||
|
||||
|
||||
val byteStream = ByteArrayOutputStream()
|
||||
byteStream.write(cla.toInt())
|
||||
byteStream.write(instruction)
|
||||
byteStream.write(p1.toInt())
|
||||
byteStream.write(p2.toInt())
|
||||
if (lc != 0) {
|
||||
writeLength(byteStream, lc)
|
||||
byteStream.write(data)
|
||||
}
|
||||
return byteStream.toByteArray()
|
||||
}
|
||||
|
||||
private fun writeLength(stream: ByteArrayOutputStream, lc: Int) {
|
||||
|
||||
stream.write(0)
|
||||
stream.write(lc shr 8)
|
||||
stream.write(lc and 0xFF)
|
||||
}
|
||||
|
||||
|
||||
private fun encrypt() {
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ISO_CLA = 0x00.toByte()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.common.apdu
|
||||
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.enums.StatusWord
|
||||
|
||||
class ResponseApdu(val data: ByteArray) {
|
||||
|
||||
private val sw1: Int = 0x00FF and data[data.size - 2].toInt()
|
||||
private val sw2: Int = 0x00FF and data[data.size - 1].toInt()
|
||||
|
||||
val sw: Int = sw1 shl 8 or sw2
|
||||
|
||||
val statusWord: StatusWord = StatusWord.byCode(sw)
|
||||
|
||||
fun getTlvData(encryptionKey: ByteArray? = null): List<Tlv>? {
|
||||
val tlvs = when {
|
||||
data.size < 2 -> null
|
||||
data.size == 2 -> emptyList()
|
||||
else -> Tlv.fromBytes(data.copyOf(data.size - 2))
|
||||
}
|
||||
return flattenNestedTlvs(tlvs)
|
||||
}
|
||||
|
||||
private fun flattenNestedTlvs(tlvs: List<Tlv>?): List<Tlv>? =
|
||||
tlvs?.flatMap {
|
||||
if (it.tag.hasNestedTlv()) {
|
||||
Tlv.fromBytes(it.value)
|
||||
} else {
|
||||
listOf(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun decrypt(encryptionKey: ByteArray) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.common.extentions
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import kotlin.experimental.and
|
||||
|
||||
|
||||
fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
|
||||
|
||||
fun ByteArray.toUtf8(): String {
|
||||
val string = String(this)
|
||||
if (this.isNotEmpty() && this.last() == 0.toByte()) return string.dropLast(1)
|
||||
return string
|
||||
}
|
||||
|
||||
|
||||
fun ByteArray.toInt(): Int {
|
||||
return when (this.size) {
|
||||
1 -> (this[0] and 0xFF.toByte()).toInt()
|
||||
2 -> ByteBuffer.wrap(this).short.toInt()
|
||||
4 -> ByteBuffer.wrap(this).int
|
||||
else -> throw IllegalArgumentException("Length must be 1,2 or 4. Length = " + this.size)
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray.toDate(): Date {
|
||||
val year = ((this[0] and 0xFF.toByte()).toInt() shl 8) or (this[1] and 0xFF.toByte()).toInt()
|
||||
val month = this[2] - 1
|
||||
val day = this[3].toInt()
|
||||
val cd = Calendar.getInstance()
|
||||
cd.set(year, month, day, 0, 0, 0)
|
||||
return cd.time
|
||||
}
|
||||
|
||||
fun ByteArray.calculateSha512(): ByteArray = MessageDigest.getInstance("SHA-512").digest(this)
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
95
tangem-core/src/main/java/com/tangem/common/tlv/Tlv.kt
Normal file
95
tangem-core/src/main/java/com/tangem/common/tlv/Tlv.kt
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package com.tangem.common.tlv
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
|
||||
class Tlv {
|
||||
|
||||
val tag: TlvTag
|
||||
val value: ByteArray
|
||||
val tagCode: Int
|
||||
|
||||
constructor(tagCode: Int, value: ByteArray = byteArrayOf()) {
|
||||
this.tag = TlvTag.byCode(tagCode)
|
||||
this.tagCode = tagCode
|
||||
this.value = value
|
||||
}
|
||||
|
||||
constructor(tag: TlvTag, value: ByteArray = byteArrayOf()) {
|
||||
this.tag = tag
|
||||
this.tagCode = tag.code
|
||||
this.value = value
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
fun tlvFromBytes(stream: ByteArrayInputStream): Tlv? {
|
||||
val code = stream.read()
|
||||
if (code == -1) return null
|
||||
var len = stream.read()
|
||||
if (len == -1)
|
||||
throw IOException("Can't read TLV")
|
||||
if (len == 0xFF) {
|
||||
val lenH = stream.read()
|
||||
if (lenH == -1)
|
||||
throw IOException("Can't read TLV")
|
||||
len = stream.read()
|
||||
if (len == -1)
|
||||
throw IOException("Can't read TLV")
|
||||
len = len or (lenH shl 8)
|
||||
}
|
||||
val value = ByteArray(len)
|
||||
if (len > 0) {
|
||||
if (len != stream.read(value)) {
|
||||
throw IOException("Can't read TLV")
|
||||
}
|
||||
}
|
||||
val tag = TlvTag.byCode(code)
|
||||
return if (tag == TlvTag.Unknown) Tlv(code, value) else Tlv(tag, value)
|
||||
}
|
||||
|
||||
|
||||
fun fromBytes(mData: ByteArray): List<Tlv> {
|
||||
val tlvList = mutableListOf<Tlv>()
|
||||
val stream = ByteArrayInputStream(mData)
|
||||
var tlv: Tlv? = null
|
||||
do {
|
||||
try {
|
||||
tlv = Tlv.tlvFromBytes(stream)
|
||||
if (tlv != null) tlvList.add(tlv)
|
||||
} catch (e: IOException) {
|
||||
throw TlvMapperException("TLVError: " + e.message)
|
||||
}
|
||||
|
||||
} while (tlv != null)
|
||||
return tlvList
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun List<Tlv>.toBytes(): ByteArray =
|
||||
this.map { it.toBytes() }.reduce { arr1, arr2 -> arr1 + arr2 }
|
||||
|
||||
fun Tlv.toBytes(): ByteArray {
|
||||
val tag = byteArrayOf(this.tag.code.toByte())
|
||||
val length = getLengthInBytes(this.value.size)
|
||||
val value = if (this.value.isNotEmpty()) this.value else byteArrayOf(0x00)
|
||||
return tag + length + value
|
||||
}
|
||||
|
||||
private fun getLengthInBytes(tlvLength: Int): ByteArray {
|
||||
return if (tlvLength > 0) {
|
||||
if (tlvLength > 0xFE) {
|
||||
byteArrayOf(
|
||||
0xFF.toByte(),
|
||||
(tlvLength shr 8 and 0xFF).toByte(),
|
||||
(tlvLength and 0xFF).toByte()
|
||||
)
|
||||
} else {
|
||||
byteArrayOf((tlvLength and 0xFF).toByte())
|
||||
}
|
||||
} else {
|
||||
byteArrayOf()
|
||||
}
|
||||
}
|
||||
99
tangem-core/src/main/java/com/tangem/common/tlv/TlvMapper.kt
Normal file
99
tangem-core/src/main/java/com/tangem/common/tlv/TlvMapper.kt
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
package com.tangem.common.tlv
|
||||
|
||||
import com.tangem.commands.CardStatus
|
||||
import com.tangem.commands.EllipticCurve
|
||||
import com.tangem.commands.ProductMask
|
||||
import com.tangem.commands.SigningMethod
|
||||
import com.tangem.common.extentions.toDate
|
||||
import com.tangem.common.extentions.toHexString
|
||||
import com.tangem.common.extentions.toInt
|
||||
import com.tangem.common.extentions.toUtf8
|
||||
import com.tangem.data.SettingsMask
|
||||
import java.util.*
|
||||
|
||||
|
||||
open class TlvMapperException(message: String?) : Exception(message)
|
||||
|
||||
class MissingTagException(message: String? = null) : TlvMapperException(message)
|
||||
class WrongTypeException(message: String? = null) : TlvMapperException(message)
|
||||
class ConvertionException(message: String? = null) : TlvMapperException(message)
|
||||
|
||||
class TlvMapper(val tlvList: List<Tlv>) {
|
||||
|
||||
inline fun <reified T> mapOptional(tag: TlvTag): T? =
|
||||
try {
|
||||
map<T>(tag)
|
||||
} catch (exception: MissingTagException) {
|
||||
null
|
||||
}
|
||||
|
||||
inline fun <reified T> map(tag: TlvTag): T {
|
||||
val tlvValue: ByteArray = tlvList.find { it.tag == tag }?.value
|
||||
?: if (tag.valueType() == TlvValueType.BoolValue && T::class == Boolean::class) {
|
||||
return false as T
|
||||
} else {
|
||||
throw MissingTagException("Tag $tag not found")
|
||||
}
|
||||
|
||||
return when (tag.valueType()) {
|
||||
TlvValueType.HexString -> {
|
||||
if (T::class != String::class)
|
||||
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
|
||||
tlvValue.toHexString() as T
|
||||
}
|
||||
TlvValueType.Utf8String -> {
|
||||
if (T::class != String::class)
|
||||
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
|
||||
tlvValue.toUtf8() as T
|
||||
}
|
||||
TlvValueType.IntValue -> {
|
||||
if (T::class != Integer::class)
|
||||
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
|
||||
tlvValue.toInt() as T
|
||||
}
|
||||
TlvValueType.BoolValue -> {
|
||||
if (T::class != Boolean::class)
|
||||
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
|
||||
true as T
|
||||
}
|
||||
TlvValueType.ByteArray -> {
|
||||
if (T::class != ByteArray::class)
|
||||
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
|
||||
tlvValue as T
|
||||
}
|
||||
TlvValueType.EllipticCurve -> {
|
||||
if (T::class != EllipticCurve::class)
|
||||
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
|
||||
EllipticCurve.byName(tlvValue.toUtf8()) as T
|
||||
}
|
||||
TlvValueType.DateTime -> {
|
||||
if (T::class != Date::class)
|
||||
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
|
||||
tlvValue.toDate() as T
|
||||
}
|
||||
TlvValueType.ProductMask -> {
|
||||
if (T::class != ProductMask::class)
|
||||
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
|
||||
ProductMask.byCode(tlvValue.first()) as T
|
||||
|
||||
}
|
||||
TlvValueType.SettingsMask -> {
|
||||
if (T::class != SettingsMask::class)
|
||||
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
|
||||
SettingsMask(tlvValue.toInt()) as T
|
||||
|
||||
}
|
||||
TlvValueType.CardStatus -> {
|
||||
if (T::class != CardStatus::class)
|
||||
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
|
||||
CardStatus.byCode(tlvValue.toInt()) as T
|
||||
}
|
||||
TlvValueType.SigningMethod -> {
|
||||
if (T::class != SigningMethod::class)
|
||||
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
|
||||
SigningMethod.byCode(tlvValue.toInt()) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
116
tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt
Normal file
116
tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
package com.tangem.common.tlv
|
||||
|
||||
enum class TlvTag(val code: Int) {
|
||||
Unknown(0x00),
|
||||
CardId(0x01),
|
||||
Status(0x02),
|
||||
CardPublicKey(0x03),
|
||||
CardSignature(0x04),
|
||||
CurveId(0x05),
|
||||
HashAlgID(0x06),
|
||||
SigningMethod(0x07),
|
||||
MaxSignatures(0x08),
|
||||
PauseBeforePin2(0x09),
|
||||
SettingsMask(0x0A),
|
||||
CardData(0x0C),
|
||||
NdefData(0x0D),
|
||||
Health(0x0F),
|
||||
|
||||
Pin(0x10),
|
||||
Pin2(0x11),
|
||||
NewPin(0x12),
|
||||
NewPin2(0x13),
|
||||
NewPinHash(0x14),
|
||||
NewPin2Hash(0x15),
|
||||
Challenge(0x16),
|
||||
Salt(0x17),
|
||||
ValidationCounter(0x18),
|
||||
Cvc(0x19),
|
||||
|
||||
SessionKeyA(0x1A),
|
||||
SessionKeyB(0x1B),
|
||||
Pause(0x1C),
|
||||
|
||||
ManufactureId(0x20),
|
||||
ManufacturerSignature(0x21),
|
||||
|
||||
IssuerDataPublicKey(0x30),
|
||||
IssuerTransactionPublicKey(0x31),
|
||||
IssuerData(0x32),
|
||||
IssuerDataSignature(0x33),
|
||||
IssuerTransactionSignature(0x34),
|
||||
IssuerDataCounter(0x35),
|
||||
|
||||
IsActivated(0x3A),
|
||||
ActivationSeed(0x3B),
|
||||
ResetPin(0x36),
|
||||
|
||||
CodePageAddress(0x40),
|
||||
CodePageCount(0x41),
|
||||
CodeHash(0x42),
|
||||
|
||||
TransactionOutHash(0x50),
|
||||
TransactionOutHashSize(0x51),
|
||||
TransactionOutRaw(0x52),
|
||||
|
||||
WalletPublicKey(0x60),
|
||||
Signature(0x61),
|
||||
RemainingSignatures(0x62),
|
||||
SignedHashes(0x63),
|
||||
|
||||
Firmware(0x80),
|
||||
Batch(0x81),
|
||||
ManufactureDateTime(0x82),
|
||||
IssuerId(0x83),
|
||||
BlockchainId(0x84),
|
||||
ManufacturerPublicKey(0x85),
|
||||
CardIdManufacturerSignature(0x86),
|
||||
|
||||
ProductMask(0x8A),
|
||||
PaymentFlowVersion(0x54),
|
||||
UserCounter(0x2C),
|
||||
|
||||
|
||||
TokenSymbol(0xA0),
|
||||
TokenContractAddress(0xA1),
|
||||
TokenDecimal(0xA2),
|
||||
Denomination(0xC0),
|
||||
ValidatedBalance(0xC1),
|
||||
LastSignDate(0xC2),
|
||||
DenominationText(0xC3),
|
||||
|
||||
TerminalIsLinked(0x58),
|
||||
TerminalPublicKey(0x5C),
|
||||
TerminalTransactionSignature(0x57);
|
||||
|
||||
fun hasNestedTlv(): Boolean {
|
||||
return when (this) {
|
||||
TlvTag.CardData -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun valueType(): TlvValueType {
|
||||
return when (this) {
|
||||
CardId, Pin, Batch -> TlvValueType.HexString
|
||||
ManufactureId, Firmware, IssuerId, BlockchainId, TokenSymbol, TokenContractAddress ->
|
||||
TlvValueType.Utf8String
|
||||
CurveId -> TlvValueType.EllipticCurve
|
||||
MaxSignatures, PauseBeforePin2, RemainingSignatures,
|
||||
SignedHashes, Health, TokenDecimal, UserCounter -> TlvValueType.IntValue
|
||||
IsActivated, TerminalIsLinked -> TlvValueType.BoolValue
|
||||
ManufactureDateTime -> TlvValueType.DateTime
|
||||
ProductMask -> TlvValueType.ProductMask
|
||||
SettingsMask -> TlvValueType.SettingsMask
|
||||
Status -> TlvValueType.CardStatus
|
||||
SigningMethod -> TlvValueType.SigningMethod
|
||||
else -> TlvValueType.ByteArray
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
fun byCode(code: Int): TlvTag = values().find { it.code == code } ?: Unknown
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.common.tlv
|
||||
|
||||
enum class TlvValueType {
|
||||
HexString,
|
||||
Utf8String,
|
||||
IntValue,
|
||||
BoolValue,
|
||||
ByteArray,
|
||||
EllipticCurve,
|
||||
DateTime,
|
||||
ProductMask,
|
||||
SettingsMask,
|
||||
CardStatus,
|
||||
SigningMethod
|
||||
}
|
||||
39
tangem-core/src/main/java/com/tangem/crypto/CryptoUtils.kt
Normal file
39
tangem-core/src/main/java/com/tangem/crypto/CryptoUtils.kt
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.crypto
|
||||
|
||||
import com.tangem.commands.EllipticCurve
|
||||
import net.i2p.crypto.eddsa.EdDSASecurityProvider
|
||||
import java.security.SecureRandom
|
||||
import java.security.Security
|
||||
|
||||
fun generateRandomBytes(length: Int): ByteArray {
|
||||
val bytes = ByteArray(length)
|
||||
SecureRandom().nextBytes(bytes)
|
||||
return bytes
|
||||
}
|
||||
|
||||
fun verify(publicKey: ByteArray, message: ByteArray, signature: ByteArray,
|
||||
curve: EllipticCurve = EllipticCurve.Secp256k1): Boolean {
|
||||
return when (curve) {
|
||||
EllipticCurve.Secp256k1 -> verifySecp256k1(publicKey, message, signature)
|
||||
EllipticCurve.Ed25519 -> verifyEd25519(publicKey, message, signature)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun initCrypto() {
|
||||
Security.insertProviderAt(org.spongycastle.jce.provider.BouncyCastleProvider(), 1)
|
||||
Security.addProvider(EdDSASecurityProvider())
|
||||
}
|
||||
|
||||
|
||||
fun ByteArray.sign(privateKeyArray: ByteArray, curve: EllipticCurve = EllipticCurve.Secp256k1): ByteArray {
|
||||
|
||||
return when (curve) {
|
||||
EllipticCurve.Secp256k1 -> signSecp256k1(this,privateKeyArray)
|
||||
EllipticCurve.Ed25519 -> signEd25519(this, privateKeyArray)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
45
tangem-core/src/main/java/com/tangem/crypto/Ed25519.kt
Normal file
45
tangem-core/src/main/java/com/tangem/crypto/Ed25519.kt
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.crypto
|
||||
|
||||
import com.tangem.common.extentions.calculateSha512
|
||||
import net.i2p.crypto.eddsa.EdDSAEngine
|
||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
|
||||
import java.security.MessageDigest
|
||||
import java.security.PublicKey
|
||||
|
||||
internal fun verifyEd25519(publicKey: ByteArray, message: ByteArray, signature: ByteArray): Boolean {
|
||||
val messageSha512 = message.calculateSha512()
|
||||
val loadedPublicKey = loadPublicKey(publicKey)
|
||||
val spec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519)
|
||||
val signatureInstance = EdDSAEngine(MessageDigest.getInstance(spec.hashAlgorithm))
|
||||
signatureInstance.initVerify(loadedPublicKey)
|
||||
|
||||
signatureInstance.update(messageSha512)
|
||||
|
||||
return signatureInstance.verify(signature)
|
||||
}
|
||||
|
||||
private fun loadPublicKey(publicKeyArray: ByteArray): PublicKey {
|
||||
val spec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519)
|
||||
val pubKey = EdDSAPublicKeySpec(publicKeyArray, spec)
|
||||
return EdDSAPublicKey(pubKey)
|
||||
}
|
||||
|
||||
internal fun signEd25519(data: ByteArray, privateKeyArray: ByteArray): ByteArray {
|
||||
|
||||
val dataSha512 = data.calculateSha512()
|
||||
val spec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519)
|
||||
val signatureInstance = EdDSAEngine(MessageDigest.getInstance(spec.hashAlgorithm))
|
||||
|
||||
val privateKeySpec = EdDSAPrivateKeySpec(privateKeyArray, spec)
|
||||
val privateKey = EdDSAPrivateKey(privateKeySpec)
|
||||
|
||||
signatureInstance.initSign(privateKey)
|
||||
signatureInstance.update(dataSha512)
|
||||
|
||||
return signatureInstance.sign()
|
||||
|
||||
}
|
||||
115
tangem-core/src/main/java/com/tangem/crypto/Sepc256k1.kt
Normal file
115
tangem-core/src/main/java/com/tangem/crypto/Sepc256k1.kt
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
package com.tangem.crypto
|
||||
|
||||
import com.tangem.common.extentions.toHexString
|
||||
import org.spongycastle.asn1.ASN1EncodableVector
|
||||
import org.spongycastle.asn1.ASN1Integer
|
||||
import org.spongycastle.asn1.DERSequence
|
||||
import org.spongycastle.jce.ECNamedCurveTable
|
||||
import org.spongycastle.jce.spec.ECPrivateKeySpec
|
||||
import org.spongycastle.jce.spec.ECPublicKeySpec
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyFactory
|
||||
import java.security.PublicKey
|
||||
import java.security.Signature
|
||||
|
||||
internal fun verifySecp256k1(publicKey: ByteArray, message: ByteArray, signature: ByteArray): Boolean {
|
||||
val signatureInstance = Signature.getInstance("SHA256withECDSA")
|
||||
val loadedPublicKey = loadPublicKey(publicKey)
|
||||
signatureInstance.initVerify(loadedPublicKey)
|
||||
signatureInstance.update(message)
|
||||
|
||||
val v = ASN1EncodableVector()
|
||||
val size = signature.size / 2
|
||||
v.add(calculateR(signature, size))
|
||||
v.add(calculateS(signature, size))
|
||||
val sigDer = DERSequence(v).encoded
|
||||
|
||||
return signatureInstance.verify(sigDer)
|
||||
}
|
||||
|
||||
private fun loadPublicKey(publicKeyArray: ByteArray): PublicKey {
|
||||
|
||||
val spec = ECNamedCurveTable.getParameterSpec("secp256k1")
|
||||
val factory = KeyFactory.getInstance("EC", "SC")
|
||||
|
||||
val p1 = spec.curve.decodePoint(publicKeyArray)
|
||||
val keySpec = ECPublicKeySpec(p1, spec)
|
||||
|
||||
return factory.generatePublic(keySpec)
|
||||
}
|
||||
|
||||
private fun calculateR(signature: ByteArray, size: Int): ASN1Integer =
|
||||
ASN1Integer(BigInteger(1, signature.copyOfRange(0, size)))
|
||||
|
||||
private fun calculateS(signature: ByteArray, size: Int): ASN1Integer =
|
||||
ASN1Integer(BigInteger(1, signature.copyOfRange(size, size * 2)))
|
||||
|
||||
|
||||
internal fun signSecp256k1(data: ByteArray, privateKeyArray: ByteArray): ByteArray {
|
||||
|
||||
val spec = ECNamedCurveTable.getParameterSpec("secp256k1")
|
||||
val factory = KeyFactory.getInstance("EC", "SC")
|
||||
|
||||
val keySpecP = ECPrivateKeySpec(BigInteger(1, privateKeyArray), spec)
|
||||
|
||||
val signature = Signature.getInstance("SHA256withECDSA")
|
||||
|
||||
val privateKey = factory.generatePrivate(keySpecP)
|
||||
signature.initSign(privateKey)
|
||||
signature.update(data)
|
||||
|
||||
val enc = signature.sign()
|
||||
checkSignatureForErrors(enc)
|
||||
|
||||
val res = toByte64(enc)
|
||||
|
||||
if (!verifySecp256k1(generatePublicKey(privateKeyArray), data, res)) {
|
||||
throw Exception("Signature self verify failed - ,enc:" + enc.toHexString() + ",res:" + res.toHexString())
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
private fun checkSignatureForErrors(enc: ByteArray) {
|
||||
if (enc[0].toInt() != 0x30) throw Exception("bad encoding 1")
|
||||
if (enc[1].toInt() and 0x80 != 0) throw Exception("unsupported length encoding 1")
|
||||
if (enc[2].toInt() != 0x02) throw Exception("bad encoding 2")
|
||||
if (enc[3].toInt() and 0x80 != 0) throw Exception("unsupported length encoding 2")
|
||||
var rLength = enc[3].toInt()
|
||||
if (enc[4 + rLength].toInt() != 0x02) throw Exception("bad encoding 3")
|
||||
if (enc[5 + rLength].toInt() and 0x80 != 0)
|
||||
throw Exception("unsupported length encoding 3")
|
||||
}
|
||||
|
||||
private fun toByte64(enc: ByteArray): ByteArray {
|
||||
|
||||
var rLength = enc[3].toInt()
|
||||
var sLength = enc[5 + rLength].toInt()
|
||||
|
||||
val sPos = 6 + rLength
|
||||
val res = ByteArray(64)
|
||||
if (rLength <= 32) {
|
||||
System.arraycopy(enc, 4, res, 32 - rLength, rLength)
|
||||
rLength = 32
|
||||
} else if (rLength == 33 && enc[4].toInt() == 0) {
|
||||
rLength--
|
||||
System.arraycopy(enc, 5, res, 0, rLength)
|
||||
} else {
|
||||
throw Exception("unsupported r-length - r-length:" + rLength.toString() + ",s-length:" + sLength.toString() + ",enc:" + enc.toHexString())
|
||||
}
|
||||
if (sLength <= 32) {
|
||||
System.arraycopy(enc, sPos, res, rLength + 32 - sLength, sLength)
|
||||
sLength = 32
|
||||
} else if (sLength == 33 && enc[sPos].toInt() == 0) {
|
||||
System.arraycopy(enc, sPos + 1, res, rLength, sLength - 1)
|
||||
} else {
|
||||
throw Exception("unsupported s-length - r-length:" + rLength.toString() + ",s-length:" + sLength.toString() + ",enc:" + enc.toHexString())
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
private fun generatePublicKey(privateKeyArray: ByteArray): ByteArray {
|
||||
val spec = ECNamedCurveTable.getParameterSpec("secp256k1")
|
||||
return spec.g.multiply(BigInteger(1, privateKeyArray)).getEncoded(false)
|
||||
}
|
||||
32
tangem-core/src/main/java/com/tangem/data/SettingsMask.kt
Normal file
32
tangem-core/src/main/java/com/tangem/data/SettingsMask.kt
Normal 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
|
||||
}
|
||||
}
|
||||
23
tangem-core/src/main/java/com/tangem/enums/Instruction.kt
Normal file
23
tangem-core/src/main/java/com/tangem/enums/Instruction.kt
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.enums
|
||||
|
||||
enum class Instruction(var code: Int) {
|
||||
Unknown(0x00),
|
||||
Read(0xF2),
|
||||
VerifyCard(0xF3),
|
||||
ValidateCard(0xF4),
|
||||
VerifyCode(0xF5),
|
||||
WriteIssuerData(0xF6),
|
||||
GetIssuerData(0xF7),
|
||||
CreateWallet(0xF8),
|
||||
CheckWallet(0xF9),
|
||||
SwapPIN(0xFA),
|
||||
Sign(0xFB),
|
||||
PurgeWallet(0xFC),
|
||||
Activate(0xFE),
|
||||
OpenSession(0xFF);
|
||||
|
||||
|
||||
companion object {
|
||||
fun byCode(code: Int): Instruction = values().find { it.code == code } ?: Unknown
|
||||
}
|
||||
}
|
||||
21
tangem-core/src/main/java/com/tangem/enums/StatusWord.kt
Normal file
21
tangem-core/src/main/java/com/tangem/enums/StatusWord.kt
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.enums
|
||||
|
||||
enum class StatusWord (val code: Int, val description: String){
|
||||
|
||||
ProcessCompleted(0x9000, "SW_PROCESS_COMPLETED"),
|
||||
InvalidParams(0x6A86, "SW_INVALID_PARAMS"),
|
||||
ErrorProcessingCommand(0x6286, "SW_ERROR_PROCESSING_COMMAND"),
|
||||
InvalidState(0x6985, "SW_INVALID_STATE"),
|
||||
// PinsNotChanged(ProcessCompleted.code, ProcessCompleted.description),
|
||||
Pin1Changed(ProcessCompleted.code + 0x0001, "SW_PIN1_CHANGED"),
|
||||
Pin2Changed(ProcessCompleted.code + 0x0002, "SW_PIN2_CHANGED"),
|
||||
PinsChanged(ProcessCompleted.code + 0x0003, "SW_PINS_CHANGED"),
|
||||
InsNotSupported(0x6D00, "SW_INS_NOT_SUPPORTED"),
|
||||
NeedEncryption(0x6982, "SW_NEED_ENCRYPTION"),
|
||||
NeedPause(0x9789, "SW_NEED_PAUSE");
|
||||
|
||||
companion object {
|
||||
fun byCode(code: Int): StatusWord = values().find { it.code == code } ?: InvalidParams
|
||||
}
|
||||
|
||||
}
|
||||
88
tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt
Normal file
88
tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package com.tangem.tasks
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
import com.tangem.commands.Card
|
||||
import com.tangem.commands.CheckWalletCommand
|
||||
import com.tangem.commands.EllipticCurve
|
||||
import com.tangem.commands.ReadCardCommand
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.crypto.generateRandomBytes
|
||||
import com.tangem.crypto.verify
|
||||
|
||||
sealed class ScanEvent {
|
||||
data class OnReadEvent(val card: Card) : ScanEvent()
|
||||
data class OnVerifyEvent(val isGenuine: Boolean) : ScanEvent()
|
||||
}
|
||||
|
||||
|
||||
internal class ScanTask : Task<ScanEvent>() {
|
||||
|
||||
private lateinit var cardData: Card
|
||||
private lateinit var challenge: ByteArray
|
||||
private lateinit var curve: EllipticCurve
|
||||
private lateinit var walletPublickKey: ByteArray
|
||||
|
||||
override fun onRun(cardEnvironment: CardEnvironment,
|
||||
callback: (result: TaskEvent<ScanEvent>) -> Unit) {
|
||||
|
||||
val readCommand = ReadCardCommand()
|
||||
sendCommand(readCommand, cardEnvironment) { readEvent ->
|
||||
|
||||
when (readEvent) {
|
||||
is CompletionResult.Success -> {
|
||||
cardData = readEvent.data
|
||||
|
||||
callback(TaskEvent.Event(ScanEvent.OnReadEvent(cardData)))
|
||||
|
||||
if (cardData.curve != null && cardData.walletPublicKey != null) {
|
||||
curve = cardData.curve!!
|
||||
walletPublickKey = cardData.walletPublicKey!!
|
||||
} else {
|
||||
delegate?.onTaskError()
|
||||
callback(TaskEvent.Completion(TaskError.CardError()))
|
||||
}
|
||||
|
||||
val checkWalletCommand = prepareCheckWalletCommand(cardEnvironment)
|
||||
|
||||
sendCommand(checkWalletCommand, cardEnvironment) { checkWalletEvent ->
|
||||
when (checkWalletEvent) {
|
||||
is CompletionResult.Success -> {
|
||||
val checkWalletResponse = checkWalletEvent.data
|
||||
val verified = verify(walletPublickKey,
|
||||
challenge + checkWalletResponse.salt,
|
||||
checkWalletResponse.walletSignature,
|
||||
curve)
|
||||
if (verified) {
|
||||
delegate?.onTaskCompleted()
|
||||
callback(TaskEvent.Completion())
|
||||
callback(TaskEvent.Event(ScanEvent.OnVerifyEvent(true)))
|
||||
} else {
|
||||
delegate?.onTaskError()
|
||||
callback(TaskEvent.Completion(TaskError.VefificationFailed()))
|
||||
}
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
if (checkWalletEvent.error !is TaskError.UserCancelledError) delegate?.onTaskError()
|
||||
callback(TaskEvent.Completion(checkWalletEvent.error))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
if (readEvent.error !is TaskError.UserCancelledError) delegate?.onTaskError()
|
||||
callback(TaskEvent.Completion(readEvent.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareCheckWalletCommand(cardEnvironment: CardEnvironment): CheckWalletCommand {
|
||||
challenge = generateRandomBytes(16)
|
||||
return CheckWalletCommand(
|
||||
cardEnvironment.pin1,
|
||||
cardData.cardId,
|
||||
challenge,
|
||||
byteArrayOf())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.tasks
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
import com.tangem.commands.CommandResponse
|
||||
import com.tangem.commands.CommandSerializer
|
||||
import com.tangem.common.CompletionResult
|
||||
|
||||
class SingleCommandTask<Event : CommandResponse>(
|
||||
private val commandSerializer: CommandSerializer<Event>) : Task<Event>() {
|
||||
|
||||
override fun onRun(cardEnvironment: CardEnvironment,
|
||||
callback: (result: TaskEvent<Event>) -> Unit) {
|
||||
sendCommand(commandSerializer, cardEnvironment) {
|
||||
when (it) {
|
||||
is CompletionResult.Success -> {
|
||||
delegate?.onTaskCompleted()
|
||||
callback(TaskEvent.Event(it.data))
|
||||
callback(TaskEvent.Completion())
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
if (it.error !is TaskError.UserCancelledError) delegate?.onTaskError()
|
||||
callback(TaskEvent.Completion(it.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
108
tangem-core/src/main/java/com/tangem/tasks/Task.kt
Normal file
108
tangem-core/src/main/java/com/tangem/tasks/Task.kt
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
package com.tangem.tasks
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
import com.tangem.CardManagerDelegate
|
||||
import com.tangem.CardReader
|
||||
import com.tangem.Log
|
||||
import com.tangem.commands.CommandResponse
|
||||
import com.tangem.commands.CommandSerializer
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.enums.StatusWord
|
||||
|
||||
abstract class Task<T> {
|
||||
|
||||
var delegate: CardManagerDelegate? = null
|
||||
var reader: CardReader? = null
|
||||
|
||||
fun run(cardEnvironment: CardEnvironment,
|
||||
callback: (result: TaskEvent<T>) -> Unit) {
|
||||
delegate?.onTaskStarted()
|
||||
reader?.setStartSession()
|
||||
Log.i(this::class.simpleName!!, "Nfc task is started")
|
||||
onRun(cardEnvironment, callback)
|
||||
}
|
||||
|
||||
abstract fun onRun(cardEnvironment: CardEnvironment,
|
||||
callback: (result: TaskEvent<T>) -> Unit)
|
||||
|
||||
protected fun <T : CommandResponse> sendCommand(
|
||||
commandSerializer: CommandSerializer<T>,
|
||||
cardEnvironment: CardEnvironment,
|
||||
callback: (result: CompletionResult<T>) -> Unit) {
|
||||
|
||||
Log.i(this::class.simpleName!!, "Nfc command ${commandSerializer::class.simpleName!!} is initiated")
|
||||
|
||||
|
||||
reader?.transceiveApdu(
|
||||
commandSerializer.serialize(cardEnvironment)) { result ->
|
||||
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
val responseApdu = result.data
|
||||
when (responseApdu.statusWord) {
|
||||
StatusWord.ProcessCompleted, StatusWord.Pin1Changed, StatusWord.Pin2Changed, StatusWord.PinsChanged
|
||||
-> {
|
||||
try {
|
||||
val responseData = commandSerializer.deserialize(cardEnvironment, responseApdu)
|
||||
Log.i(this::class.simpleName!!, "Nfc command ${commandSerializer::class.simpleName!!} is completed")
|
||||
callback(CompletionResult.Success(responseData as T))
|
||||
} catch (error: TaskError) {
|
||||
callback(CompletionResult.Failure(error))
|
||||
}
|
||||
}
|
||||
StatusWord.InvalidParams -> callback(CompletionResult.Failure(TaskError.InvalidParams()))
|
||||
StatusWord.ErrorProcessingCommand -> callback(CompletionResult.Failure(TaskError.ErrorProcessingCommand()))
|
||||
StatusWord.InvalidState -> callback(CompletionResult.Failure(TaskError.InvalidState()))
|
||||
|
||||
StatusWord.InsNotSupported -> callback(CompletionResult.Failure(TaskError.InsNotSupported()))
|
||||
StatusWord.NeedEncryption -> callback(CompletionResult.Failure(TaskError.NeedEncryption()))
|
||||
StatusWord.NeedPause -> {
|
||||
val remainingTime = commandSerializer.deserializeSecurityDelay(responseApdu, cardEnvironment)
|
||||
if (remainingTime != null) delegate?.showSecurityDelay(remainingTime)
|
||||
Log.i(this::class.simpleName!!, "Nfc command ${commandSerializer::class.simpleName!!} triggered security delay of $remainingTime milliseconds")
|
||||
sendCommand(commandSerializer, cardEnvironment, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
is CompletionResult.Failure ->
|
||||
if (result.error is TaskError.UserCancelledError) {
|
||||
callback(CompletionResult.Failure(TaskError.UserCancelledError()))
|
||||
reader?.readingActive = false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TaskError(description: String? = null) : Exception(description) {
|
||||
class UnknownStatus(sw: Int) : TaskError()
|
||||
class MappingError : TaskError()
|
||||
class GenericError(description: String? = null) : TaskError(description)
|
||||
class UserCancelledError() : TaskError()
|
||||
class Busy() : TaskError()
|
||||
|
||||
class ErrorProcessingCommand : TaskError()
|
||||
class InvalidState : TaskError()
|
||||
class InsNotSupported : TaskError()
|
||||
class InvalidParams : TaskError()
|
||||
class NeedEncryption : TaskError()
|
||||
class NeedPause : TaskError()
|
||||
|
||||
class VefificationFailed : TaskError()
|
||||
class CardError : TaskError()
|
||||
class ReaderError() : TaskError()
|
||||
class SerializeCommandError() : TaskError()
|
||||
|
||||
class CardIsMissing() : TaskError()
|
||||
class EmptyHashes() : TaskError()
|
||||
class TooMuchHashes() : TaskError()
|
||||
class HashSizeMustBeEqual() : TaskError()
|
||||
}
|
||||
|
||||
sealed class TaskEvent<T> {
|
||||
class Event<T>(val data: T) : TaskEvent<T>()
|
||||
class Completion<T>(val error: TaskError? = null) : TaskEvent<T>()
|
||||
}
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue