Updated on 2026-08-14
This commit is contained in:
commit
903fc32bc9
51 changed files with 944 additions and 309 deletions
|
|
@ -47,6 +47,9 @@ dependencies {
|
|||
implementation 'org.bitcoinj:bitcoinj-core:0.15.2'
|
||||
implementation 'com.github.stellar:java-stellar-sdk:0.13.0'
|
||||
|
||||
implementation "com.madgag.spongycastle:core:1.58.0.0"
|
||||
implementation "com.madgag.spongycastle:prov:1.58.0.0"
|
||||
|
||||
ext.kethereum_version = '0.79.5'
|
||||
implementation "com.github.walleth.kethereum:functions:$kethereum_version"
|
||||
implementation "com.github.walleth.kethereum:keccak_shortcut:$kethereum_version"
|
||||
|
|
|
|||
|
|
@ -33,18 +33,18 @@ class BitcoinAddressValidator {
|
|||
if (testNet && firstLettersNonTestNet.contains(address.first())) return false
|
||||
if (address.length !in 26..35) return false
|
||||
val decoded = address.decodeBase58() ?: return false
|
||||
val hash = sha256(decoded, 0, 21, 2)
|
||||
val hash = recursiveSha256(decoded, 0, 21, 2)
|
||||
return hash.sliceArray(0..3).contentEquals(decoded.sliceArray(21..24))
|
||||
} else {
|
||||
return validateSegwitAddress(address, testNet)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sha256(data: ByteArray, start: Int, len: Int, recursion: Int): ByteArray {
|
||||
private fun recursiveSha256(data: ByteArray, start: Int, len: Int, recursion: Int): ByteArray {
|
||||
if (recursion == 0) return data
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
md.update(data.sliceArray(start until start + len))
|
||||
return sha256(md.digest(), 0, 32, recursion - 1)
|
||||
return recursiveSha256(md.digest(), 0, 32, recursion - 1)
|
||||
}
|
||||
|
||||
private fun String.decodeBase58(): ByteArray? {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,18 @@ class BitcoinTransactionBuilder(private val testNet: Boolean) {
|
|||
val signature = TransactionSignature(r, canonicalS)
|
||||
return ScriptBuilder.createInputScript(signature, ECKey.fromPublicOnly(publicKey))
|
||||
}
|
||||
|
||||
fun getEstimateSize(transactionData: TransactionData, walletPublicKey: ByteArray): Result<Int> {
|
||||
val buildTransactionResult = buildToSign(transactionData)
|
||||
when (buildTransactionResult) {
|
||||
is Result.Failure -> return buildTransactionResult
|
||||
is Result.Success -> {
|
||||
val hashes = buildTransactionResult.data
|
||||
val finalTransaction = buildToSend(ByteArray(64 * hashes.size) { 1 }, walletPublicKey)
|
||||
return Result.Success(finalTransaction.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TransactionData.toBitcoinJTransaction(networkParameters: NetworkParameters?,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ class BitcoinWalletManager(
|
|||
walletConfig: WalletConfig,
|
||||
isTestNet: Boolean = false
|
||||
) : WalletManager,
|
||||
TransactionEstimator,
|
||||
TransactionSender,
|
||||
FeeProvider {
|
||||
|
||||
|
|
@ -48,9 +47,9 @@ class BitcoinWalletManager(
|
|||
null,
|
||||
"unknown",
|
||||
currencyWallet.address))
|
||||
} else {
|
||||
currencyWallet.pendingTransactions.clear()
|
||||
}
|
||||
} else {
|
||||
currencyWallet.pendingTransactions.clear()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -58,18 +57,6 @@ class BitcoinWalletManager(
|
|||
Log.e(this::class.java.simpleName, error?.message ?: "")
|
||||
}
|
||||
|
||||
override suspend fun getEstimateSize(transactionData: TransactionData): Result<Int> {
|
||||
val buildTransactionResult = transactionBuilder.buildToSign(transactionData)
|
||||
when (buildTransactionResult) {
|
||||
is Result.Failure -> return buildTransactionResult
|
||||
is Result.Success -> {
|
||||
val hashes = buildTransactionResult.data
|
||||
val finalTransaction = transactionBuilder.buildToSend(ByteArray(64 * hashes.size) {1}, walletPublicKey)
|
||||
return Result.Success(finalTransaction.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
|
||||
val buildTransactionResult = transactionBuilder.buildToSign(transactionData)
|
||||
when (buildTransactionResult) {
|
||||
|
|
@ -90,10 +77,11 @@ class BitcoinWalletManager(
|
|||
when (val result = networkManager.getFee()) {
|
||||
is Result.Failure -> return result
|
||||
is Result.Success -> {
|
||||
val sizeResult = getEstimateSize(
|
||||
val sizeResult = transactionBuilder.getEstimateSize(
|
||||
TransactionData(amount,
|
||||
Amount(1.toBigDecimal().divide(SATOSHI_IN_BTC), blockchain),
|
||||
address, destination)
|
||||
address, destination),
|
||||
walletPublicKey
|
||||
)
|
||||
when (sizeResult) {
|
||||
is Result.Failure -> return sizeResult
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import co.nstant.`in`.cbor.CborDecoder
|
|||
import co.nstant.`in`.cbor.CborEncoder
|
||||
import co.nstant.`in`.cbor.model.Array
|
||||
import co.nstant.`in`.cbor.model.ByteString
|
||||
import co.nstant.`in`.cbor.model.DataItem
|
||||
import co.nstant.`in`.cbor.model.UnsignedInteger
|
||||
import com.tangem.blockchain.cardano.crypto.Blake2b
|
||||
import com.tangem.blockchain.common.extensions.decodeBase58
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.blockchain.common.TransactionData
|
|||
import com.tangem.blockchain.common.extensions.decodeBase58
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.math.BigDecimal
|
||||
|
||||
class CardanoTransactionBuilder() {
|
||||
var unspentOutputs: List<UnspentOutput> = listOf()
|
||||
|
|
@ -143,6 +144,21 @@ class CardanoTransactionBuilder() {
|
|||
return fullAmount - (amount + fee)
|
||||
}
|
||||
|
||||
fun getEstimateSize(transactionData: TransactionData, walletPublicKey: ByteArray): Int {
|
||||
val dummyFeeValue = BigDecimal.valueOf(0.1)
|
||||
|
||||
val dummyFee = transactionData.amount.copy(value = dummyFeeValue)
|
||||
val dummyAmount =
|
||||
transactionData.amount.copy(value = transactionData.amount.value!! - dummyFeeValue)
|
||||
|
||||
val dummyTransactionData = transactionData.copy(
|
||||
amount = dummyAmount,
|
||||
fee = dummyFee
|
||||
)
|
||||
buildToSign(dummyTransactionData)
|
||||
return buildToSend(ByteArray(64), walletPublicKey).size
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PROTOCOL_MAGIC: Long = 764824073
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ class CardanoWalletManager(
|
|||
private val walletPublicKey: ByteArray,
|
||||
walletConfig: WalletConfig
|
||||
) : WalletManager,
|
||||
TransactionEstimator,
|
||||
TransactionSender,
|
||||
FeeProvider {
|
||||
|
||||
|
|
@ -47,21 +46,6 @@ class CardanoWalletManager(
|
|||
Log.e(this::class.java.simpleName, error?.message ?: "")
|
||||
}
|
||||
|
||||
override suspend fun getEstimateSize(transactionData: TransactionData): Int {
|
||||
val dummyFeeValue = BigDecimal.valueOf(0.1)
|
||||
|
||||
val dummyFee = transactionData.amount.copy(value = dummyFeeValue)
|
||||
val dummyAmount =
|
||||
transactionData.amount.copy(value = transactionData.amount.value!! - dummyFeeValue)
|
||||
|
||||
val dummyTransactionData = transactionData.copy(
|
||||
amount = dummyAmount,
|
||||
fee = dummyFee
|
||||
)
|
||||
transactionBuilder.buildToSign(dummyTransactionData)
|
||||
return transactionBuilder.buildToSend(ByteArray(64), walletPublicKey).size
|
||||
}
|
||||
|
||||
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
|
||||
val transactionHash = transactionBuilder.buildToSign(transactionData)
|
||||
|
||||
|
|
@ -77,8 +61,9 @@ class CardanoWalletManager(
|
|||
override suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>> {
|
||||
val a = 0.155381
|
||||
val b = 0.000043946
|
||||
val size = getEstimateSize(TransactionData(amount, null, source, destination))
|
||||
|
||||
val size = transactionBuilder.getEstimateSize(
|
||||
TransactionData(amount, null, source, destination), walletPublicKey
|
||||
)
|
||||
val fee = (a + b * size).toBigDecimal()
|
||||
return Result.Success(listOf(Amount(blockchain.currency, fee, source, blockchain.decimals)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,6 @@ interface WalletManager {
|
|||
suspend fun update()
|
||||
}
|
||||
|
||||
interface TransactionEstimator {
|
||||
suspend fun getEstimateSize(transactionData: TransactionData): Result<Int>
|
||||
}
|
||||
|
||||
interface TransactionSender {
|
||||
suspend fun send(transactionData: TransactionData, signer: TransactionSigner) : SimpleResult
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,35 +15,40 @@ object WalletManagerFactory {
|
|||
val blockchainName: String = card.cardData?.blockchainName ?: return null
|
||||
|
||||
when {
|
||||
blockchainName.contains("btc") || blockchainName.contains("bitcoin") -> {
|
||||
blockchainName == Blockchain.Bitcoin.id -> {
|
||||
return BitcoinWalletManager(
|
||||
cardId = card.cardId,
|
||||
walletPublicKey = walletPublicKey,
|
||||
walletConfig = WalletConfig(true, true)
|
||||
)
|
||||
}
|
||||
blockchainName == Blockchain.BitcoinTestnet.id -> {
|
||||
return BitcoinWalletManager(
|
||||
cardId = card.cardId,
|
||||
walletPublicKey = walletPublicKey,
|
||||
walletConfig = WalletConfig(true, true),
|
||||
isTestNet = isTestNet(blockchainName))
|
||||
isTestNet = true
|
||||
)
|
||||
}
|
||||
blockchainName.contains("eth") -> {
|
||||
val chain = if (isTestNet(blockchainName)) {
|
||||
Chain.EthereumClassicTestnet
|
||||
} else {
|
||||
Chain.Mainnet
|
||||
}
|
||||
blockchainName == Blockchain.Ethereum.id -> {
|
||||
val chain = Chain.Mainnet
|
||||
return EthereumWalletManager(
|
||||
cardId = card.cardId,
|
||||
walletPublicKey = walletPublicKey,
|
||||
walletConfig = WalletConfig(true, true),
|
||||
chain = chain)
|
||||
chain = chain
|
||||
)
|
||||
}
|
||||
blockchainName.contains("xlm") -> {
|
||||
blockchainName == Blockchain.Stellar.id -> {
|
||||
val token = getToken(card)
|
||||
return StellarWalletManager(
|
||||
cardId = card.cardId,
|
||||
walletPublicKey = walletPublicKey,
|
||||
walletConfig = WalletConfig(true, token == null),
|
||||
token = token,
|
||||
isTestNet = isTestNet(blockchainName))
|
||||
token = token
|
||||
)
|
||||
}
|
||||
blockchainName.contains("cardano") -> {
|
||||
blockchainName == Blockchain.Cardano.id -> {
|
||||
return CardanoWalletManager(
|
||||
cardId = card.cardId,
|
||||
walletPublicKey = walletPublicKey,
|
||||
|
|
@ -61,8 +66,6 @@ object WalletManagerFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private fun isTestNet(blockchainName: String) = blockchainName.contains("test")
|
||||
|
||||
private fun getToken(card: Card): Token? {
|
||||
val symbol = card.cardData?.tokenSymbol ?: return null
|
||||
val contractAddress = card.cardData?.tokenContractAddress ?: return null
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.blockchain.common
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.CardEnvironment
|
||||
import com.tangem.blockchain.bitcoin.BitcoinWalletManager
|
||||
import com.tangem.blockchain.cardano.CardanoWalletManager
|
||||
import com.tangem.blockchain.ethereum.EthereumWalletManager
|
||||
import com.tangem.blockchain.stellar.StellarWalletManager
|
||||
import com.tangem.commands.ReadCommand
|
||||
import com.tangem.common.apdu.ResponseApdu
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import org.junit.Test
|
||||
|
||||
internal class WalletManagerFactoryTest {
|
||||
|
||||
@Test
|
||||
fun createBitcoinWalletManager() {
|
||||
val data = "0108bb00000000000304200754414e47454d00020102800a322e3432642053444b000341040876bdec26b89bd2159a668b9af3d9fe86370f318717c92b8d6c1186fb3648c32a5f9321998cc2d042901c91d40601e79a641e1cbcebe7a2358be6054e1b6e5d0a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b0084034254438640e17ceec48c5be36240c98019f95ad8b6e56acfebe60d11979c6279f715d607d76a860a137da8d109e805753f3f56b0130709f4bbf4cb9974b4c57b8469bf4b873041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd26050a736563703235366b310008040000006407010009020bb8604104752a727e14bba5bd73b6714d72500f61ffd11026ad1196d2e1c54577cbeeac3d11fc68a64700f8d533f4e311964ea8fb3aa26c588295f2133868d69c3e62869362040000005c6304000000090f01009000"
|
||||
val responseApdu = ResponseApdu(data.hexToBytes())
|
||||
val card = ReadCommand().deserialize(CardEnvironment(), responseApdu)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
|
||||
|
||||
Truth.assertThat(walletManager)
|
||||
.isInstanceOf(BitcoinWalletManager::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createEthereumWalletManager() {
|
||||
val data = "0108bb00000000000536200754414e47454d00020102800a322e3432642053444b000341046c8aea0d5a850b0a608acf9a0c453c39ea86131e88bfa78800de3cfb5bf1007aeaa7b9ffc184212255758605c2461be343c0a661d73cabafa4c9c175b3f0e59a0a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b0084034554488640431b6244acfeac479becdff201a7f720a7d70a97edc4e019fb678596baf52dfe9d0e8faf08ceb4443b82d4e66815541f2dc8ec6dd3ff83eb42f06e5eab07f25f3041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd26050a736563703235366b3100080400000064070100090205dc60410464dddc3f356744aaecfa07427f9eb996ff537d65f20fb5be3abccf0354352a6b5f8a1942e0f8ddeea3a170eda78d060be8162ad60e94e4e91fbbdf0a7054785562040000005b6304000000090f01009000"
|
||||
val responseApdu = ResponseApdu(data.hexToBytes())
|
||||
val card = ReadCommand().deserialize(CardEnvironment(), responseApdu)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
|
||||
|
||||
Truth.assertThat(walletManager)
|
||||
.isInstanceOf(EthereumWalletManager::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createStellarWalletManager() {
|
||||
val data = "0108bb00000000000379200754414e47454d00020102800a322e3432642053444b0003410487d7bb51b189213e3cedc3fcfa3fc047b3b71b7805b5b215e14639b3a8ebb1952c9dd5ea4354441b6ada4e8b8327674bb102ddae69df55be69643a2c916edf650a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b008403584c4d86409a4bc2baf0e5836887da21167cf33458d5249d1a610bced0e31dc053f23729ed24d715912bf89e6804669430dfe396ed83274e0031f6803e2bdb8c041fa993413041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd2605086564323535313900080400000064070100090205dc6020e078212d58b2b9d0edc9c936830d10081cd38b90c31778c56dfb1171027e294e62040000003863040000002c0f01009000"
|
||||
val responseApdu = ResponseApdu(data.hexToBytes())
|
||||
val card = ReadCommand().deserialize(CardEnvironment(), responseApdu)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
|
||||
|
||||
Truth.assertThat(walletManager)
|
||||
.isInstanceOf(StellarWalletManager::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createCardanoWalletManager() {
|
||||
val data = "0108bb00000000000502200754414e47454d00020102800a322e3432642053444b0003410402c1e39257d60583489da2d67d35d1cc2a1c005cc05c1021f44838edcaf25d5615cad7c9d11c2e23f5efa93e50904d33c88808d0e169060508df840992e31f4d0a04041e76310c658102ffff8a0101820407e30b0d830b54414e47454d2053444b00840743415244414e4f8640f24ef5c8c6eba0ff97560d5b013edb4a452594270db9647bd0a3543df8104dec75731d4db3ebe0fc493f2afee00195e560b51e3c41189b7c61ba7895d6434b9d3041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd2605086564323535313900080400000064070100090205dc60208a71161cfdf1e0a85d8e7ff372aa4a01136046292aceb5f9ad7ebdb98d3f60a86204000000646304000000000f01009000"
|
||||
val responseApdu = ResponseApdu(data.hexToBytes())
|
||||
val card = ReadCommand().deserialize(CardEnvironment(), responseApdu)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
|
||||
|
||||
Truth.assertThat(walletManager)
|
||||
.isInstanceOf(CardanoWalletManager::class.java)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem
|
||||
|
||||
import com.tangem.commands.*
|
||||
import com.tangem.common.CardEnvironment
|
||||
import com.tangem.common.TerminalKeysService
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import com.tangem.tasks.*
|
||||
import java.util.concurrent.Executors
|
||||
|
|
@ -16,10 +18,12 @@ import java.util.concurrent.Executors
|
|||
*/
|
||||
class CardManager(
|
||||
private val reader: CardReader,
|
||||
private val cardManagerDelegate: CardManagerDelegate? = null) {
|
||||
private val cardManagerDelegate: CardManagerDelegate? = null,
|
||||
private val config: Config = Config()
|
||||
) {
|
||||
|
||||
private var terminalKeysService: TerminalKeysService? = null
|
||||
private var isBusy = false
|
||||
private val cardEnvironmentRepository = mutableMapOf<String, CardEnvironment>()
|
||||
private val cardManagerExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
init {
|
||||
|
|
@ -67,7 +71,7 @@ class CardManager(
|
|||
callback: (result: TaskEvent<SignResponse>) -> Unit) {
|
||||
val signCommand: SignCommand
|
||||
try {
|
||||
signCommand = SignCommand(hashes, cardId)
|
||||
signCommand = SignCommand(hashes)
|
||||
} catch (error: Exception) {
|
||||
if (error is TaskError) {
|
||||
callback(TaskEvent.Completion(error))
|
||||
|
|
@ -91,7 +95,7 @@ class CardManager(
|
|||
*/
|
||||
fun readIssuerData(cardId: String,
|
||||
callback: (result: TaskEvent<ReadIssuerDataResponse>) -> Unit) {
|
||||
val getIssuerDataCommand = ReadIssuerDataCommand(cardId)
|
||||
val getIssuerDataCommand = ReadIssuerDataCommand()
|
||||
val task = SingleCommandTask(getIssuerDataCommand)
|
||||
runTask(task, cardId, callback)
|
||||
}
|
||||
|
|
@ -114,10 +118,10 @@ class CardManager(
|
|||
issuerDataCounter: Int? = null,
|
||||
callback: (result: TaskEvent<WriteIssuerDataResponse>) -> Unit) {
|
||||
val writeIssuerDataCommand = WriteIssuerDataCommand(
|
||||
cardId,
|
||||
issuerData,
|
||||
issuerDataSignature,
|
||||
issuerDataCounter)
|
||||
issuerData,
|
||||
issuerDataSignature,
|
||||
issuerDataCounter
|
||||
)
|
||||
val task = SingleCommandTask(writeIssuerDataCommand)
|
||||
runTask(task, cardId, callback)
|
||||
}
|
||||
|
|
@ -134,7 +138,7 @@ class CardManager(
|
|||
*/
|
||||
fun createWallet(cardId: String,
|
||||
callback: (result: TaskEvent<CreateWalletResponse>) -> Unit) {
|
||||
val createWalletCommand = CreateWalletCommand(cardId)
|
||||
val createWalletCommand = CreateWalletCommand()
|
||||
val task = SingleCommandTask(createWalletCommand)
|
||||
runTask(task, cardId, callback)
|
||||
}
|
||||
|
|
@ -148,7 +152,7 @@ class CardManager(
|
|||
*/
|
||||
fun purgeWallet(cardId: String,
|
||||
callback: (result: TaskEvent<PurgeWalletResponse>) -> Unit) {
|
||||
val purgeWalletCommand = PurgeWalletCommand(cardId)
|
||||
val purgeWalletCommand = PurgeWalletCommand()
|
||||
val task = SingleCommandTask(purgeWalletCommand)
|
||||
runTask(task, cardId, callback)
|
||||
}
|
||||
|
|
@ -163,7 +167,7 @@ class CardManager(
|
|||
return
|
||||
}
|
||||
|
||||
val environment = fetchCardEnvironment(cardId)
|
||||
val environment = prepareCardEnvironment(cardId)
|
||||
isBusy = true
|
||||
|
||||
task.reader = reader
|
||||
|
|
@ -187,7 +191,21 @@ class CardManager(
|
|||
runTask(task, cardId, callback)
|
||||
}
|
||||
|
||||
private fun fetchCardEnvironment(cardId: String?): CardEnvironment {
|
||||
return cardEnvironmentRepository[cardId] ?: CardEnvironment(cardId = cardId)
|
||||
/**
|
||||
* Allows to set a particular [TerminalKeysService] to retrieve terminal keys.
|
||||
* Default implementation is provided in tangem-sdk module: [TerminalKeysStorage].
|
||||
*/
|
||||
fun setTerminalKeysService(terminalKeysService: TerminalKeysService) {
|
||||
this.terminalKeysService = terminalKeysService
|
||||
}
|
||||
|
||||
private fun prepareCardEnvironment(cardId: String?): CardEnvironment {
|
||||
val terminalKeys = if (config.linkedTerminal) terminalKeysService?.getKeys() else null
|
||||
return CardEnvironment(
|
||||
cardId = cardId,
|
||||
terminalKeys = terminalKeys
|
||||
)
|
||||
}
|
||||
|
||||
companion object
|
||||
}
|
||||
|
|
@ -13,13 +13,13 @@ interface CardManagerDelegate {
|
|||
/**
|
||||
* It is called when user is expected to scan a Tangem Card with an Android device.
|
||||
*/
|
||||
fun onNfcSessionStarted()
|
||||
fun onNfcSessionStarted(cardId: String?)
|
||||
|
||||
/**
|
||||
* It is called when security delay is triggered by the card.
|
||||
* A user is expected to hold the card until the security delay is over.
|
||||
*/
|
||||
fun onSecurityDelay(ms: Int)
|
||||
fun onSecurityDelay(ms: Int, totalDurationSeconds: Int)
|
||||
|
||||
/**
|
||||
* It is called when user takes the card away from the Android device during the scanning
|
||||
|
|
|
|||
5
tangem-core/src/main/java/com/tangem/Config.kt
Normal file
5
tangem-core/src/main/java/com/tangem/Config.kt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem
|
||||
|
||||
data class Config(
|
||||
val linkedTerminal: Boolean = true
|
||||
)
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
package com.tangem.commands
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
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.extensions.calculateSha256
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.TlvBuilder
|
||||
import com.tangem.common.tlv.TlvMapper
|
||||
import com.tangem.common.tlv.TlvTag
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import com.tangem.tasks.TaskError
|
||||
|
||||
/**
|
||||
|
|
@ -22,7 +24,16 @@ class CheckWalletResponse(
|
|||
val cardId: String,
|
||||
val salt: ByteArray,
|
||||
val walletSignature: ByteArray
|
||||
) : CommandResponse
|
||||
) : CommandResponse {
|
||||
|
||||
fun verify(curve: EllipticCurve, publicKey: ByteArray, challenge: ByteArray): Boolean {
|
||||
return CryptoUtils.verify(
|
||||
publicKey,
|
||||
challenge + salt,
|
||||
walletSignature,
|
||||
curve)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This command proves that the wallet private key from the card corresponds to the wallet public key.
|
||||
|
|
@ -32,18 +43,16 @@ class CheckWalletResponse(
|
|||
* @property cardId Unique Tangem card ID number
|
||||
* @property challenge Random challenge generated by application
|
||||
*/
|
||||
class CheckWalletCommand(
|
||||
private val cardId: String,
|
||||
private val challenge: ByteArray
|
||||
) : CommandSerializer<CheckWalletResponse>() {
|
||||
class CheckWalletCommand : CommandSerializer<CheckWalletResponse>() {
|
||||
|
||||
val challenge = CryptoUtils.generateRandomBytes(16)
|
||||
|
||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
val tlvData = listOf(
|
||||
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
|
||||
Tlv(TlvTag.CardId, cardId.hexToBytes()),
|
||||
Tlv(TlvTag.Challenge, challenge)
|
||||
)
|
||||
return CommandApdu(Instruction.CheckWallet, tlvData)
|
||||
val tlvBuilder = TlvBuilder()
|
||||
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
|
||||
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
|
||||
tlvBuilder.append(TlvTag.Challenge, challenge)
|
||||
return CommandApdu(Instruction.CheckWallet, tlvBuilder.serialize())
|
||||
}
|
||||
|
||||
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): CheckWalletResponse? {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.commands
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
import com.tangem.common.CardEnvironment
|
||||
import com.tangem.common.apdu.CommandApdu
|
||||
import com.tangem.common.apdu.ResponseApdu
|
||||
import com.tangem.common.extensions.toInt
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
package com.tangem.commands
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
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.extensions.calculateSha256
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.TlvBuilder
|
||||
import com.tangem.common.tlv.TlvMapper
|
||||
import com.tangem.common.tlv.TlvTag
|
||||
import com.tangem.tasks.TaskError
|
||||
|
|
@ -37,21 +38,15 @@ class CreateWalletResponse(
|
|||
*
|
||||
* @property cardId CID, Unique Tangem card ID number.
|
||||
*/
|
||||
class CreateWalletCommand(
|
||||
private val cardId: String
|
||||
) : CommandSerializer<CreateWalletResponse>() {
|
||||
class CreateWalletCommand : CommandSerializer<CreateWalletResponse>() {
|
||||
|
||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
val tlvData = mutableListOf(
|
||||
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
|
||||
Tlv(TlvTag.CardId, cardId.hexToBytes()),
|
||||
Tlv(TlvTag.Pin2, cardEnvironment.pin2.calculateSha256())
|
||||
)
|
||||
if (cardEnvironment.cvc != null) {
|
||||
tlvData.add(Tlv(TlvTag.Cvc, cardEnvironment.cvc))
|
||||
}
|
||||
|
||||
return CommandApdu(Instruction.CreateWallet, tlvData)
|
||||
val tlvBuilder = TlvBuilder()
|
||||
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
|
||||
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
|
||||
tlvBuilder.append(TlvTag.Pin2, cardEnvironment.pin2)
|
||||
tlvBuilder.append(TlvTag.Cvc, cardEnvironment.cvc)
|
||||
return CommandApdu(Instruction.CreateWallet, tlvBuilder.serialize())
|
||||
}
|
||||
|
||||
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): CreateWalletResponse? {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
package com.tangem.commands
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
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.extensions.calculateSha256
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.TlvBuilder
|
||||
import com.tangem.common.tlv.TlvMapper
|
||||
import com.tangem.common.tlv.TlvTag
|
||||
import com.tangem.tasks.TaskError
|
||||
|
|
@ -23,23 +24,20 @@ class PurgeWalletResponse(
|
|||
) : CommandResponse
|
||||
|
||||
/**
|
||||
* This command deletes all wallet data. If Is_Reusable flag is enabled during personalization,
|
||||
* This command deletes all wallet data. If Is_Reusable flag is enabled during personalization,
|
||||
|
||||
* If Is_Reusable flag is disabled, the card switches to ‘Purged’ state.
|
||||
* ‘Purged’ state is final, it makes the card useless.
|
||||
* @property cardId CID, Unique Tangem card ID number.
|
||||
*/
|
||||
class PurgeWalletCommand(
|
||||
private val cardId: String
|
||||
) : CommandSerializer<PurgeWalletResponse>() {
|
||||
class PurgeWalletCommand : CommandSerializer<PurgeWalletResponse>() {
|
||||
|
||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
val tlvData = mutableListOf(
|
||||
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
|
||||
Tlv(TlvTag.CardId, cardId.hexToBytes()),
|
||||
Tlv(TlvTag.Pin2, cardEnvironment.pin2.calculateSha256())
|
||||
)
|
||||
return CommandApdu(Instruction.PurgeWallet, tlvData)
|
||||
val tlvBuilder = TlvBuilder()
|
||||
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
|
||||
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
|
||||
tlvBuilder.append(TlvTag.Pin2, cardEnvironment.pin2)
|
||||
return CommandApdu(Instruction.PurgeWallet, tlvBuilder.serialize())
|
||||
}
|
||||
|
||||
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): PurgeWalletResponse? {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package com.tangem.commands
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
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.extensions.calculateSha256
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.TlvBuilder
|
||||
import com.tangem.common.tlv.TlvMapper
|
||||
import com.tangem.common.tlv.TlvTag
|
||||
import com.tangem.tasks.TaskError
|
||||
|
|
@ -182,7 +183,7 @@ class Card(
|
|||
/**
|
||||
* Current status of the card.
|
||||
*/
|
||||
val status: CardStatus,
|
||||
val status: CardStatus?,
|
||||
|
||||
/**
|
||||
* Version of Tangem COS.
|
||||
|
|
@ -296,19 +297,16 @@ class Card(
|
|||
class ReadCommand : CommandSerializer<Card>() {
|
||||
|
||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
val tlvBuilder = TlvBuilder()
|
||||
/**
|
||||
* [CardEnvironment] stores the pin1 value. If no pin1 value was set, it will contain
|
||||
* default value of ‘000000’.
|
||||
* In order to obtain card’s data, [ReadCommand] should use the correct pin 1 value.
|
||||
* The card will not respond if wrong pin 1 has been submitted.
|
||||
*/
|
||||
val tlvData = mutableListOf(Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()))
|
||||
|
||||
cardEnvironment.terminalKeys?.let { terminalKeys ->
|
||||
Tlv(TlvTag.TerminalPublicKey, terminalKeys.publicKey)
|
||||
}
|
||||
|
||||
return CommandApdu(Instruction.Read, tlvData)
|
||||
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
|
||||
tlvBuilder.append(TlvTag.TerminalPublicKey, cardEnvironment.terminalKeys?.publicKey)
|
||||
return CommandApdu(Instruction.Read, tlvBuilder.serialize())
|
||||
}
|
||||
|
||||
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): Card? {
|
||||
|
|
@ -318,9 +316,9 @@ class ReadCommand : CommandSerializer<Card>() {
|
|||
val tlvMapper = TlvMapper(tlvData)
|
||||
|
||||
Card(
|
||||
cardId = tlvMapper.map(TlvTag.CardId),
|
||||
manufacturerName = tlvMapper.map(TlvTag.ManufactureId),
|
||||
status = tlvMapper.map(TlvTag.Status),
|
||||
cardId = tlvMapper.mapOptional(TlvTag.CardId) ?: "",
|
||||
manufacturerName = tlvMapper.mapOptional(TlvTag.ManufactureId) ?: "",
|
||||
status = tlvMapper.mapOptional(TlvTag.Status),
|
||||
|
||||
firmwareVersion = tlvMapper.mapOptional(TlvTag.Firmware),
|
||||
cardPublicKey = tlvMapper.mapOptional(TlvTag.CardPublicKey),
|
||||
|
|
@ -349,7 +347,7 @@ class ReadCommand : CommandSerializer<Card>() {
|
|||
|
||||
private fun deserializeCardData(tlvData: List<Tlv>): CardData? {
|
||||
val cardDataTlvs = tlvData.find { it.tag == TlvTag.CardData }?.let {
|
||||
Tlv.tlvListFromBytes(it.value)
|
||||
Tlv.deserialize(it.value)
|
||||
}
|
||||
if (cardDataTlvs.isNullOrEmpty()) return null
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
package com.tangem.commands
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
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.extensions.calculateSha256
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.TlvBuilder
|
||||
import com.tangem.common.tlv.TlvMapper
|
||||
import com.tangem.common.tlv.TlvTag
|
||||
import com.tangem.tasks.TaskError
|
||||
|
|
@ -49,16 +50,13 @@ class ReadIssuerDataResponse(
|
|||
* wallet balance signed by the issuer or additional issuer’s attestation data.
|
||||
* @property cardId CID, Unique Tangem card ID number.
|
||||
*/
|
||||
class ReadIssuerDataCommand(
|
||||
private val cardId: String
|
||||
) : CommandSerializer<ReadIssuerDataResponse>() {
|
||||
class ReadIssuerDataCommand : CommandSerializer<ReadIssuerDataResponse>() {
|
||||
|
||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
val tlvData = listOf(
|
||||
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
|
||||
Tlv(TlvTag.CardId, cardId.hexToBytes())
|
||||
)
|
||||
return CommandApdu(Instruction.ReadIssuerData, tlvData)
|
||||
val tlvBuilder = TlvBuilder()
|
||||
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
|
||||
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
|
||||
return CommandApdu(Instruction.ReadIssuerData, tlvBuilder.serialize())
|
||||
}
|
||||
|
||||
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): ReadIssuerDataResponse? {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
package com.tangem.commands
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
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.extensions.calculateSha256
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.TlvBuilder
|
||||
import com.tangem.common.tlv.TlvMapper
|
||||
import com.tangem.common.tlv.TlvTag
|
||||
import com.tangem.crypto.sign
|
||||
|
|
@ -32,7 +33,7 @@ class SignResponse(
|
|||
* @property hashes Array of transaction hashes.
|
||||
* @property cardId CID, Unique Tangem card ID number
|
||||
*/
|
||||
class SignCommand(private val hashes: Array<ByteArray>, private val cardId: String)
|
||||
class SignCommand(private val hashes: Array<ByteArray>)
|
||||
: CommandSerializer<SignResponse>() {
|
||||
|
||||
private val hashSizes = if (hashes.isNotEmpty()) hashes.first().size else 0
|
||||
|
|
@ -50,17 +51,16 @@ class SignCommand(private val hashes: Array<ByteArray>, private val cardId: Stri
|
|||
}
|
||||
|
||||
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)
|
||||
)
|
||||
val tlvBuilder = TlvBuilder()
|
||||
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
|
||||
tlvBuilder.append(TlvTag.Pin2, cardEnvironment.pin2)
|
||||
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
|
||||
tlvBuilder.append(TlvTag.TransactionOutHashSize, byteArrayOf(hashSizes.toByte()))
|
||||
tlvBuilder.append(TlvTag.TransactionOutHash, dataToSign)
|
||||
tlvBuilder.append(TlvTag.Cvc, cardEnvironment.cvc)
|
||||
|
||||
addTerminalSignature(cardEnvironment, tlvData)
|
||||
|
||||
return CommandApdu(Instruction.Sign, tlvData)
|
||||
addTerminalSignature(cardEnvironment, tlvBuilder)
|
||||
return CommandApdu(Instruction.Sign, tlvBuilder.serialize())
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -70,11 +70,11 @@ class SignCommand(private val hashes: Array<ByteArray>, private val cardId: Stri
|
|||
* TerminalTransactionSignature parameter containing a correct signature of raw data to be signed made with TerminalPrivateKey
|
||||
* (this key should be generated and securily stored by the application).
|
||||
*/
|
||||
private fun addTerminalSignature(cardEnvironment: CardEnvironment, tlvData: MutableList<Tlv>) {
|
||||
private fun addTerminalSignature(cardEnvironment: CardEnvironment, tlvBuilder: TlvBuilder) {
|
||||
cardEnvironment.terminalKeys?.let { terminalKeyPair ->
|
||||
val signedData = dataToSign.sign(terminalKeyPair.privateKey)
|
||||
tlvData.add(Tlv(TlvTag.TerminalTransactionSignature, signedData))
|
||||
tlvData.add(Tlv(TlvTag.TerminalPublicKey, terminalKeyPair.publicKey))
|
||||
tlvBuilder.append(TlvTag.TerminalTransactionSignature, signedData)
|
||||
tlvBuilder.append(TlvTag.TerminalPublicKey, terminalKeyPair.publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.commands
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
import com.tangem.common.CardEnvironment
|
||||
import com.tangem.common.apdu.CommandApdu
|
||||
import com.tangem.common.apdu.Instruction
|
||||
import com.tangem.common.apdu.ResponseApdu
|
||||
|
|
@ -8,6 +8,7 @@ import com.tangem.common.extensions.calculateSha256
|
|||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toByteArray
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.TlvBuilder
|
||||
import com.tangem.common.tlv.TlvMapper
|
||||
import com.tangem.common.tlv.TlvTag
|
||||
import com.tangem.tasks.TaskError
|
||||
|
|
@ -30,24 +31,20 @@ class WriteIssuerDataResponse(
|
|||
* @property issuerDataCounter An optional counter that protect issuer data against replay attack.
|
||||
*/
|
||||
class WriteIssuerDataCommand(
|
||||
private val cardId: String,
|
||||
private val issuerData: ByteArray,
|
||||
private val issuerDataSignature: ByteArray,
|
||||
private val issuerDataCounter: Int? = null
|
||||
) : CommandSerializer<WriteIssuerDataResponse>() {
|
||||
|
||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
val tlvData = mutableListOf(
|
||||
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
|
||||
Tlv(TlvTag.CardId, cardId.hexToBytes()),
|
||||
Tlv(TlvTag.IssuerData, issuerData),
|
||||
Tlv(TlvTag.IssuerDataSignature, issuerDataSignature)
|
||||
)
|
||||
if (issuerDataCounter != null) {
|
||||
tlvData.add(Tlv(TlvTag.IssuerDataCounter, issuerDataCounter.toByteArray()))
|
||||
}
|
||||
val tlvBuilder = TlvBuilder()
|
||||
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
|
||||
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
|
||||
tlvBuilder.append(TlvTag.IssuerData, issuerData)
|
||||
tlvBuilder.append(TlvTag.IssuerDataSignature, issuerDataSignature)
|
||||
tlvBuilder.append(TlvTag.IssuerDataCounter, issuerDataCounter)
|
||||
|
||||
return CommandApdu(Instruction.WriteIssuerData, tlvData)
|
||||
return CommandApdu(Instruction.WriteIssuerData, tlvBuilder.serialize())
|
||||
}
|
||||
|
||||
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): WriteIssuerDataResponse? {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem
|
||||
package com.tangem.common
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.common
|
||||
|
||||
/**
|
||||
* Interface for a service for managing Terminal keypair, used for Linked Terminal feature.
|
||||
* Its implementation Needs to be provided to [com.tangem.CardManager]
|
||||
* by calling [com.tangem.CardManager.setTerminalKeysService].
|
||||
* Default implementation is provided in tangem-sdk module: [TerminalKeysStorage].
|
||||
* Linked Terminal feature can be disabled manually by editing [com.tangem.Config].
|
||||
*/
|
||||
interface TerminalKeysService {
|
||||
fun getKeys(): KeyPair
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
package com.tangem.common.apdu
|
||||
|
||||
import com.tangem.EncryptionMode
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.toBytes
|
||||
import com.tangem.common.EncryptionMode
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
/**
|
||||
|
|
@ -15,7 +13,7 @@ import java.io.ByteArrayOutputStream
|
|||
class CommandApdu(
|
||||
|
||||
private val ins: Int,
|
||||
private val tlvList: List<Tlv>,
|
||||
private val tlvs: ByteArray,
|
||||
|
||||
private val cla: Byte = ISO_CLA,
|
||||
private val p1: Byte = 0x00,
|
||||
|
|
@ -28,12 +26,12 @@ class CommandApdu(
|
|||
|
||||
constructor(
|
||||
instruction: Instruction,
|
||||
tlvList: List<Tlv>,
|
||||
tlvs: ByteArray,
|
||||
encryptionMode: EncryptionMode = EncryptionMode.NONE,
|
||||
encryptionKey: ByteArray? = null
|
||||
) : this(
|
||||
instruction.code,
|
||||
tlvList,
|
||||
tlvs,
|
||||
encryptionMode = encryptionMode,
|
||||
encryptionKey = encryptionKey
|
||||
)
|
||||
|
|
@ -51,13 +49,7 @@ class CommandApdu(
|
|||
|
||||
private fun toBytes(): ByteArray {
|
||||
|
||||
val data = if (tlvList.isNotEmpty()) {
|
||||
tlvList.toBytes()
|
||||
} else {
|
||||
byteArrayOf()
|
||||
}
|
||||
|
||||
val lc = data.size
|
||||
val lc = tlvs.size
|
||||
|
||||
val byteStream = ByteArrayOutputStream()
|
||||
byteStream.write(cla.toInt())
|
||||
|
|
@ -66,7 +58,7 @@ class CommandApdu(
|
|||
byteStream.write(p2.toInt())
|
||||
if (lc != 0) {
|
||||
writeLength(byteStream, lc)
|
||||
byteStream.write(data)
|
||||
byteStream.write(tlvs)
|
||||
}
|
||||
return byteStream.toByteArray()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class ResponseApdu(private val data: ByteArray) {
|
|||
fun getTlvData(encryptionKey: ByteArray? = null): List<Tlv>? {
|
||||
return when {
|
||||
data.size <= 2 -> null
|
||||
else -> Tlv.tlvListFromBytes(data.copyOf(data.size - 2))
|
||||
else -> Tlv.deserialize(data.copyOf(data.size - 2))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,17 +53,17 @@ class Tlv {
|
|||
}
|
||||
|
||||
|
||||
fun tlvListFromBytes(mData: ByteArray): List<Tlv>? {
|
||||
fun deserialize(data: ByteArray, nfcV: Boolean = false): List<Tlv>? {
|
||||
val tlvList = mutableListOf<Tlv>()
|
||||
val stream = ByteArrayInputStream(mData)
|
||||
var tlv: Tlv? = null
|
||||
val stream = ByteArrayInputStream(data)
|
||||
var tlv: Tlv?
|
||||
do {
|
||||
try {
|
||||
tlv = Tlv.tlvFromBytes(stream)
|
||||
tlv = tlvFromBytes(stream)
|
||||
if (tlv != null) tlvList.add(tlv)
|
||||
} catch (e: IOException) {
|
||||
Log.e(this::class.java.simpleName,"TLVError: " + e.message)
|
||||
return null
|
||||
if (nfcV) break else return null
|
||||
}
|
||||
|
||||
} while (tlv != null)
|
||||
|
|
@ -73,10 +73,10 @@ class Tlv {
|
|||
|
||||
}
|
||||
|
||||
fun List<Tlv>.toBytes(): ByteArray =
|
||||
this.map { it.toBytes() }.reduce { arr1, arr2 -> arr1 + arr2 }
|
||||
fun List<Tlv>.serialize(): ByteArray =
|
||||
this.map { it.serialize() }.reduce { arr1, arr2 -> arr1 + arr2 }
|
||||
|
||||
fun Tlv.toBytes(): ByteArray {
|
||||
fun Tlv.serialize(): 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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.common.tlv
|
||||
|
||||
class TlvBuilder {
|
||||
private val tlvs = mutableListOf<Tlv>()
|
||||
private val encoder = TlvEncoder()
|
||||
|
||||
internal inline fun <reified T> append(tag: TlvTag, value: T?) {
|
||||
if (value == null) return
|
||||
|
||||
tlvs.add(encoder.encode(tag, value))
|
||||
}
|
||||
|
||||
fun serialize(): ByteArray = tlvs.serialize()
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.tangem.common.tlv
|
||||
|
||||
import com.tangem.commands.*
|
||||
import com.tangem.common.extensions.*
|
||||
import com.tangem.tasks.TaskError
|
||||
import java.time.Year
|
||||
import java.util.*
|
||||
|
||||
class TlvEncoder {
|
||||
internal inline fun <reified T> encode(tag: TlvTag, value: T?): Tlv {
|
||||
if (value != null) {
|
||||
return Tlv(tag, encodeValue(value, tag))
|
||||
} else {
|
||||
throw TaskError.SerializeCommandError("Encoding error. Value for tag $tag is null")
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T> encodeValue(value: T, tag: TlvTag): ByteArray {
|
||||
return when (tag.valueType()) {
|
||||
TlvValueType.HexString -> {
|
||||
typeCheck<T, String>(tag)
|
||||
return if (tag == TlvTag.Pin || tag == TlvTag.Pin2) {
|
||||
(value as String).calculateSha256()
|
||||
} else {
|
||||
(value as String).hexToBytes()
|
||||
}
|
||||
}
|
||||
TlvValueType.Utf8String -> {
|
||||
typeCheck<T, String>(tag)
|
||||
(value as String).toByteArray()
|
||||
}
|
||||
TlvValueType.IntValue -> {
|
||||
typeCheck<T, Int>(tag)
|
||||
(value as Int).toByteArray()
|
||||
}
|
||||
TlvValueType.BoolValue -> {
|
||||
typeCheck<T, Boolean>(tag)
|
||||
throw ConversionException("Usopported operation: Boolean to ByteArray for tag $tag")
|
||||
}
|
||||
TlvValueType.ByteArray -> {
|
||||
typeCheck<T, ByteArray>(tag)
|
||||
value as ByteArray
|
||||
}
|
||||
TlvValueType.EllipticCurve -> {
|
||||
typeCheck<T, EllipticCurve>(tag)
|
||||
(value as EllipticCurve).curve.plus("\\0").toByteArray()
|
||||
}
|
||||
TlvValueType.DateTime -> {
|
||||
typeCheck<T, Date>(tag)
|
||||
val calendar = Calendar.getInstance().apply { time = (value as Date) }
|
||||
val year = calendar.get(Calendar.YEAR)
|
||||
val month = calendar.get(Calendar.MONTH) + 1
|
||||
val day = calendar.get(Calendar.DAY_OF_MONTH)
|
||||
return year.toByteArray() + month.toByteArray() + day.toByteArray()
|
||||
}
|
||||
TlvValueType.ProductMask -> {
|
||||
typeCheck<T, ProductMask>(tag)
|
||||
byteArrayOf(
|
||||
(value as ProductMask).code
|
||||
)
|
||||
}
|
||||
TlvValueType.SettingsMask -> {
|
||||
typeCheck<T, SettingsMask>(tag)
|
||||
(value as SettingsMask).rawValue.toByteArray()
|
||||
}
|
||||
TlvValueType.CardStatus -> {
|
||||
typeCheck<T, CardStatus>(tag)
|
||||
(value as CardStatus).code.toByteArray()
|
||||
}
|
||||
TlvValueType.SigningMethod -> {
|
||||
typeCheck<T, SigningMethod>(tag)
|
||||
(value as SigningMethod).rawValue.toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T, reified ExpectedT> typeCheck(tag: TlvTag) {
|
||||
if (T::class != ExpectedT::class)
|
||||
throw WrongTypeException("Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
|
||||
}
|
||||
}
|
||||
|
|
@ -108,7 +108,7 @@ enum class TlvTag(val code: Int) {
|
|||
*/
|
||||
fun valueType(): TlvValueType {
|
||||
return when (this) {
|
||||
CardId, Pin, Batch -> TlvValueType.HexString
|
||||
CardId, Pin, Pin2, Batch -> TlvValueType.HexString
|
||||
ManufactureId, Firmware, IssuerId, BlockchainId, TokenSymbol, TokenContractAddress ->
|
||||
TlvValueType.Utf8String
|
||||
CurveId -> TlvValueType.EllipticCurve
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
package com.tangem.tasks
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
import com.tangem.commands.Card
|
||||
import com.tangem.commands.CheckWalletCommand
|
||||
import com.tangem.commands.ReadCommand
|
||||
import com.tangem.commands.*
|
||||
import com.tangem.common.CardEnvironment
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
|
||||
|
|
@ -31,64 +29,51 @@ sealed class ScanEvent {
|
|||
internal class ScanTask : Task<ScanEvent>() {
|
||||
|
||||
override fun onRun(cardEnvironment: CardEnvironment,
|
||||
currentCard: Card?,
|
||||
callback: (result: TaskEvent<ScanEvent>) -> Unit) {
|
||||
|
||||
val readCommand = ReadCommand()
|
||||
sendCommand(readCommand, cardEnvironment) { readResult ->
|
||||
if (currentCard != null) callback(TaskEvent.Event(ScanEvent.OnReadEvent(currentCard)))
|
||||
|
||||
when (readResult) {
|
||||
if (currentCard == null) {
|
||||
completeNfcSession(true, TaskError.MissingPreflightRead())
|
||||
callback(TaskEvent.Completion(TaskError.MissingPreflightRead()))
|
||||
|
||||
is CompletionResult.Failure -> {
|
||||
if (readResult.error !is TaskError.UserCancelledError) {
|
||||
completeNfcSession(true, readResult.error)
|
||||
}
|
||||
callback(TaskEvent.Completion(readResult.error))
|
||||
}
|
||||
} else if (currentCard.cardData?.productMask == ProductMask.Tag) {
|
||||
completeNfcSession()
|
||||
callback(TaskEvent.Completion())
|
||||
|
||||
is CompletionResult.Success -> {
|
||||
val card = readResult.data
|
||||
} else if (currentCard.status != CardStatus.Loaded) {
|
||||
completeNfcSession()
|
||||
callback(TaskEvent.Completion())
|
||||
|
||||
callback(TaskEvent.Event(ScanEvent.OnReadEvent(card)))
|
||||
} else if (currentCard.curve == null || currentCard.walletPublicKey == null) {
|
||||
completeNfcSession(true, TaskError.CardError())
|
||||
callback(TaskEvent.Completion(TaskError.CardError()))
|
||||
|
||||
if (card.curve == null || card.walletPublicKey == null) {
|
||||
completeNfcSession(true)
|
||||
callback(TaskEvent.Completion(TaskError.CardError()))
|
||||
return@sendCommand
|
||||
}
|
||||
} else {
|
||||
|
||||
val challenge = CryptoUtils.generateRandomBytes(16)
|
||||
val checkWalletCommand = CheckWalletCommand(
|
||||
card.cardId,
|
||||
challenge)
|
||||
val checkWalletCommand = CheckWalletCommand()
|
||||
|
||||
sendCommand(checkWalletCommand, cardEnvironment) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Failure -> {
|
||||
if (result.error !is TaskError.UserCancelledError) {
|
||||
completeNfcSession(true, result.error)
|
||||
}
|
||||
callback(TaskEvent.Completion(result.error))
|
||||
}
|
||||
|
||||
is CompletionResult.Success -> {
|
||||
completeNfcSession()
|
||||
val checkWalletResponse = result.data
|
||||
val verified = CryptoUtils.verify(
|
||||
card.walletPublicKey,
|
||||
challenge + checkWalletResponse.salt,
|
||||
checkWalletResponse.walletSignature,
|
||||
card.curve)
|
||||
if (verified) {
|
||||
callback(TaskEvent.Event(ScanEvent.OnVerifyEvent(true)))
|
||||
callback(TaskEvent.Completion())
|
||||
} else {
|
||||
callback(TaskEvent.Completion(TaskError.VefificationFailed()))
|
||||
}
|
||||
}
|
||||
sendCommand(checkWalletCommand, cardEnvironment) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Failure -> {
|
||||
if (result.error !is TaskError.UserCancelledError) {
|
||||
completeNfcSession(true, result.error)
|
||||
}
|
||||
|
||||
callback(TaskEvent.Completion(result.error))
|
||||
}
|
||||
is CompletionResult.Success -> {
|
||||
completeNfcSession()
|
||||
val verified = result.data.verify(
|
||||
currentCard.curve,
|
||||
currentCard.walletPublicKey,
|
||||
checkWalletCommand.challenge
|
||||
)
|
||||
callback(TaskEvent.Event(ScanEvent.OnVerifyEvent(verified)))
|
||||
callback(TaskEvent.Completion())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tasks
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
import com.tangem.commands.Card
|
||||
import com.tangem.common.CardEnvironment
|
||||
import com.tangem.commands.CommandResponse
|
||||
import com.tangem.commands.CommandSerializer
|
||||
import com.tangem.common.CompletionResult
|
||||
|
|
@ -14,8 +15,11 @@ class SingleCommandTask<Event : CommandResponse>(
|
|||
private val command: CommandSerializer<Event>
|
||||
) : Task<Event>() {
|
||||
|
||||
override fun onRun(cardEnvironment: CardEnvironment,
|
||||
callback: (result: TaskEvent<Event>) -> Unit) {
|
||||
override fun onRun(
|
||||
cardEnvironment: CardEnvironment,
|
||||
currentCard: Card?,
|
||||
callback: (result: TaskEvent<Event>) -> Unit
|
||||
) {
|
||||
sendCommand(command, cardEnvironment) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package com.tangem.tasks
|
||||
|
||||
import com.tangem.CardEnvironment
|
||||
import com.tangem.common.CardEnvironment
|
||||
import com.tangem.CardManagerDelegate
|
||||
import com.tangem.CardReader
|
||||
import com.tangem.Log
|
||||
import com.tangem.commands.Card
|
||||
import com.tangem.commands.CommandResponse
|
||||
import com.tangem.commands.CommandSerializer
|
||||
import com.tangem.commands.ReadCommand
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.apdu.CommandApdu
|
||||
import com.tangem.common.apdu.StatusWord
|
||||
|
|
@ -22,7 +24,7 @@ sealed class TaskError(description: String? = null) : Exception(description) {
|
|||
class Busy() : TaskError()
|
||||
class TagLost() : TaskError()
|
||||
|
||||
class ErrorProcessingCommand : TaskError()
|
||||
class ErrorProcessingCommand(description: String? = null) : TaskError(description)
|
||||
class InvalidState : TaskError()
|
||||
class InsNotSupported : TaskError()
|
||||
class InvalidParams : TaskError()
|
||||
|
|
@ -32,12 +34,15 @@ sealed class TaskError(description: String? = null) : Exception(description) {
|
|||
class VefificationFailed : TaskError()
|
||||
class CardError : TaskError()
|
||||
class ReaderError() : TaskError()
|
||||
class SerializeCommandError() : TaskError()
|
||||
class SerializeCommandError(description: String? = null) : TaskError(description)
|
||||
|
||||
class CardIsMissing() : TaskError()
|
||||
class EmptyHashes() : TaskError()
|
||||
class TooMuchHashes() : TaskError()
|
||||
class HashSizeMustBeEqual() : TaskError()
|
||||
|
||||
class WrongCard() : TaskError()
|
||||
class MissingPreflightRead() : TaskError()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -67,6 +72,8 @@ abstract class Task<T> {
|
|||
|
||||
var delegate: CardManagerDelegate? = null
|
||||
var reader: CardReader? = null
|
||||
var performPreflightRead: Boolean = true
|
||||
var securityDelayDuration: Int = 0
|
||||
|
||||
/**
|
||||
* This method should be called to run the [Task] and perform all its operations.
|
||||
|
|
@ -76,10 +83,15 @@ abstract class Task<T> {
|
|||
*/
|
||||
fun run(cardEnvironment: CardEnvironment,
|
||||
callback: (result: TaskEvent<T>) -> Unit) {
|
||||
delegate?.onNfcSessionStarted()
|
||||
delegate?.onNfcSessionStarted(cardEnvironment.cardId)
|
||||
reader?.openSession()
|
||||
Log.i(this::class.simpleName!!, "Nfc task is started")
|
||||
onRun(cardEnvironment, callback)
|
||||
|
||||
if (performPreflightRead) {
|
||||
runWithPreflightRead(cardEnvironment, callback)
|
||||
} else {
|
||||
onRun(cardEnvironment, null, callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -101,6 +113,7 @@ abstract class Task<T> {
|
|||
* In this method the individual Tasks' logic should be implemented.
|
||||
*/
|
||||
protected abstract fun onRun(cardEnvironment: CardEnvironment,
|
||||
currentCard: Card?,
|
||||
callback: (result: TaskEvent<T>) -> Unit)
|
||||
|
||||
/**
|
||||
|
|
@ -149,7 +162,7 @@ abstract class Task<T> {
|
|||
StatusWord.NeedPause -> {
|
||||
// When NeedPause is returned from the card whenever security delay is triggered.
|
||||
val remainingTime = command.deserializeSecurityDelay(responseApdu, cardEnvironment)
|
||||
if (remainingTime != null) delegate?.onSecurityDelay(remainingTime)
|
||||
if (remainingTime != null) delegate?.onSecurityDelay(remainingTime, securityDelayDuration)
|
||||
Log.i(this::class.simpleName!!, "Nfc command ${command::class.simpleName!!} triggered security delay of $remainingTime milliseconds")
|
||||
sendRequest(command, commandApdu, cardEnvironment, callback)
|
||||
}
|
||||
|
|
@ -165,6 +178,35 @@ abstract class Task<T> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runWithPreflightRead(
|
||||
environment: CardEnvironment, callback: (result: TaskEvent<T>) -> Unit) {
|
||||
sendCommand(ReadCommand(), environment) { readResult ->
|
||||
when (readResult) {
|
||||
is CompletionResult.Failure -> {
|
||||
completeNfcSession(true, readResult.error)
|
||||
callback(TaskEvent.Completion(readResult.error))
|
||||
}
|
||||
is CompletionResult.Success -> {
|
||||
|
||||
val receivedCardId = readResult.data.cardId
|
||||
securityDelayDuration = readResult.data.pauseBeforePin2 ?: 0
|
||||
|
||||
if (environment.cardId != null && environment.cardId != receivedCardId) {
|
||||
completeNfcSession(true, TaskError.WrongCard())
|
||||
callback(TaskEvent.Completion(TaskError.WrongCard()))
|
||||
return@sendCommand
|
||||
}
|
||||
|
||||
val newEnvironment = environment.copy(cardId = receivedCardId)
|
||||
onRun(newEnvironment, readResult.data, callback)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package com.tangem.common.apdu
|
|||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.TlvBuilder
|
||||
import com.tangem.common.tlv.TlvTag
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -11,12 +13,13 @@ class CommandApduTest {
|
|||
|
||||
@Test
|
||||
fun `simple READ command to bytes`() {
|
||||
val pinInBytes = byteArrayOf(-111, -76, -47, 66, -126, 63, 125, 32, -59, -16, -115, -10, -111,
|
||||
34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, -10, -45, 19, -124, -123, -55, -94, 3)
|
||||
val pin = "000000"
|
||||
val tlvBuilder = TlvBuilder()
|
||||
tlvBuilder.append(TlvTag.Pin, pin)
|
||||
val commandApdu = CommandApdu(
|
||||
Instruction.Read,
|
||||
mutableListOf(Tlv(TlvTag.Pin, pinInBytes)))
|
||||
|
||||
tlvBuilder.serialize()
|
||||
)
|
||||
val expected = byteArrayOf(0, -14, 0, 0, 0, 0, 34, 16, 32, -111, -76, -47, 66, -126, 63, 125,
|
||||
32, -59, -16, -115, -10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, -10,
|
||||
-45, 19, -124, -123, -55, -94, 3)
|
||||
|
|
@ -27,17 +30,18 @@ class CommandApduTest {
|
|||
|
||||
@Test
|
||||
fun `READ with terminal key to bytes`() {
|
||||
val pinInBytes = byteArrayOf(-111, -76, -47, 66, -126, 63, 125, 32, -59, -16, -115, -10, -111,
|
||||
34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, -10, -45, 19, -124, -123, -55, -94, 3)
|
||||
val pin = "000000"
|
||||
val terminalPublicKey = byteArrayOf(4, 80, -122, 58, -42, 74, -121, -82, -118, 47, -24, 60,
|
||||
26, -15, -88, 64, 60, -75, 63, 83, -28, -122, -40, 81, 29, -83, -118, 4, -120, 126,
|
||||
91, 35, 82, 44, -44, 112, 36, 52, 83, -94, -103, -6, -98, 119, 35, 119, 22, 16, 58,
|
||||
-68, 17, -95, -33, 56, -123, 94, -42, -14, -18, 24, 126, -100, 88, 43, -90)
|
||||
val tlvBuilder = TlvBuilder()
|
||||
tlvBuilder.append(TlvTag.Pin, pin)
|
||||
tlvBuilder.append(TlvTag.TerminalPublicKey, terminalPublicKey)
|
||||
val commandApdu = CommandApdu(
|
||||
Instruction.Read,
|
||||
mutableListOf(
|
||||
Tlv(TlvTag.Pin, pinInBytes),
|
||||
Tlv(TlvTag.TerminalPublicKey, terminalPublicKey)))
|
||||
tlvBuilder.serialize()
|
||||
)
|
||||
|
||||
val expected = byteArrayOf(0, -14, 0, 0, 0, 0, 101, 16, 32, -111, -76, -47, 66, -126, 63,
|
||||
125, 32, -59, -16, -115, -10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25,
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ class TlvMapperTest {
|
|||
|
||||
private val rawData = byteArrayOf(1, 8, -53, 34, 0, 0, 0, 2, 115, 116, 32, 11, 83, 77, 65, 82, 84, 32, 67, 65, 83, 72, 0, 2, 1, 2, -128, 6, 50, 46, 49, 49, 114, 0, 3, 65, 4, -49, 11, -50, -66, -121, -25, -2, 65, 65, -13, 14, 49, 27, -82, -33, -85, -113, 65, 20, 8, -39, -75, 57, 45, 65, -31, 35, 44, 38, 40, 63, -44, 113, -45, -75, -95, -118, 118, 29, 65, 117, -24, -53, 82, -72, 91, -20, -96, -77, -103, -14, -63, 52, -127, -123, -27, -16, -128, -67, -3, -104, -26, -22, 65, 10, 4, 0, 0, 126, 33, 12, 90, -127, 2, 0, 41, -126, 4, 7, -29, 5, 2, -125, 7, 84, 65, 78, 71, 69, 77, 0, -124, 3, 69, 84, 72, -122, 64, 111, -103, 48, -114, -40, 18, -103, 26, -102, -12, -38, -78, -90, -9, -98, 88, -47, -100, -24, 24, -105, -70, -72, 6, 94, -96, -77, 11, -123, -28, -118, 37, 63, 107, -55, -11, 23, -12, 13, -23, -121, -63, 36, -59, 70, 116, 91, -125, -34, -69, 23, -112, 6, 17, 4, -49, 68, -56, 29, -45, 81, 10, 97, 83, 48, 65, 4, -127, -106, -86, 75, 65, 10, -60, 74, 59, -100, -50, 24, -25, -66, 34, 106, -22, 7, 10, -52, -125, -87, -49, 103, 84, 15, -84, 73, -81, 37, 18, -97, 106, 83, -118, 40, -83, 99, 65, 53, -114, 60, 79, -103, 99, 6, 79, 126, 54, 83, 114, -90, 81, -45, 116, -27, -62, 60, -35, 55, -3, 9, -101, -14, 5, 10, 115, 101, 99, 112, 50, 53, 54, 107, 49, 0, 8, 4, 0, 15, 66, 64, 7, 1, 0, 9, 2, 11, -72, 96, 65, 4, -42, -5, -41, -84, -23, 88, 2, 86, -63, -118, -123, -10, -66, -82, -107, -68, -93, 111, 47, 93, -20, -86, 74, 28, 21, 81, 93, -21, -124, -57, -102, 55, 17, 84, -66, -68, -22, -128, 126, -99, -65, -54, -42, 59, -25, -21, -124, 5, 59, -16, -72, 73, 48, 16, -27, 103, -112, -73, 2, 96, -51, 41, -42, 116, 98, 4, 0, 15, 66, 52, 99, 4, 0, 0, 0, 13, 15, 1, 0)
|
||||
|
||||
private val tlvData = Tlv.tlvListFromBytes(rawData)
|
||||
private val tlvData = Tlv.deserialize(rawData)
|
||||
|
||||
private val tlvMapper = TlvMapper(tlvData!!)
|
||||
|
||||
private val cardDataRaw: ByteArray = tlvMapper.map(TlvTag.CardData)
|
||||
private val cardDataMapper = TlvMapper(Tlv.tlvListFromBytes(cardDataRaw)!!)
|
||||
private val cardDataMapper = TlvMapper(Tlv.deserialize(cardDataRaw)!!)
|
||||
|
||||
@Test
|
||||
fun `map optional when value is present`() {
|
||||
|
|
@ -88,7 +88,7 @@ class TlvMapperTest {
|
|||
|
||||
@Test
|
||||
fun `map SigningMethods set of methods returns correct value`() {
|
||||
val localMapper = TlvMapper(Tlv.tlvListFromBytes("070195".hexToBytes())!!)
|
||||
val localMapper = TlvMapper(Tlv.deserialize("070195".hexToBytes())!!)
|
||||
|
||||
val signingMethod: SigningMethod = localMapper.map(TlvTag.SigningMethod)
|
||||
assertThat(signingMethod.contains(SigningMethod.signHash))
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class TlvTest {
|
|||
-10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, -10, -45, 19, -124,
|
||||
-123, -55, -94, 3)
|
||||
|
||||
assertThat(tlvs.toBytes())
|
||||
assertThat(tlvs.serialize())
|
||||
.isEqualTo(expected)
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ class TlvTest {
|
|||
-55, -94, 3, 1, 8, -53, 34, 0, 0, 0, 2, 115, 116, 22, 16, -82, -78, -31, 34, 66, -19,
|
||||
-86, -1, 26, 8, 100, -126, -74, 20, -28, 83)
|
||||
|
||||
assertThat(tlvs.toBytes())
|
||||
assertThat(tlvs.serialize())
|
||||
.isEqualTo(expected)
|
||||
}
|
||||
|
||||
|
|
@ -44,14 +44,14 @@ class TlvTest {
|
|||
-10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, -10, -45, 19, -124,
|
||||
-123, -55, -94, 3)
|
||||
|
||||
val tlvs = Tlv.tlvListFromBytes(bytes)
|
||||
val tlvs = Tlv.deserialize(bytes)
|
||||
|
||||
assertThat(tlvs)
|
||||
.isNotNull()
|
||||
assertThat(tlvs)
|
||||
.isNotEmpty()
|
||||
|
||||
val pin = tlvs!!.find { it.tag == TlvTag.Pin }?.value
|
||||
val pin = tlvs!!.find { it.tag == TlvTag.Pin }?.value
|
||||
val pinExpected = "000000".calculateSha256()
|
||||
|
||||
assertThat(pin)
|
||||
|
|
@ -65,14 +65,14 @@ class TlvTest {
|
|||
-55, -94, 3, 1, 8, -53, 34, 0, 0, 0, 2, 115, 116, 22, 16, -82, -78, -31, 34, 66, -19,
|
||||
-86, -1, 26, 8, 100, -126, -74, 20, -28, 83)
|
||||
|
||||
val tlvs = Tlv.tlvListFromBytes(bytes)
|
||||
val tlvs = Tlv.deserialize(bytes)
|
||||
|
||||
assertThat(tlvs)
|
||||
.isNotNull()
|
||||
assertThat(tlvs)
|
||||
.isNotEmpty()
|
||||
|
||||
val pin = tlvs!!.find { it.tag == TlvTag.Pin }?.value
|
||||
val pin = tlvs!!.find { it.tag == TlvTag.Pin }?.value
|
||||
val pinExpected = "000000".calculateSha256()
|
||||
assertThat(pin)
|
||||
.isEqualTo(pinExpected)
|
||||
|
|
@ -91,13 +91,21 @@ class TlvTest {
|
|||
@Test
|
||||
fun `Bytes to TLVs, wrong values`() {
|
||||
val bytes = byteArrayOf(0)
|
||||
val tlvs = Tlv.tlvListFromBytes(bytes)
|
||||
assertThat(tlvs)
|
||||
.isNull()
|
||||
val tlvs = Tlv.deserialize(bytes)
|
||||
assertThat(tlvs)
|
||||
.isNull()
|
||||
|
||||
val bytes1 = byteArrayOf(0, 0, 0, 0, 0, 0, 0)
|
||||
val tlvs1 = Tlv.tlvListFromBytes(bytes1)
|
||||
val tlvs1 = Tlv.deserialize(bytes1)
|
||||
assertThat(tlvs1)
|
||||
.isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse Slix tag response`() {
|
||||
val response = "03ff010f91010b550474616e67656d2e636f6d140f11616e64726f69642e636f6d3a706b67636f6d2e74616e67656d2e77616c6c65745411c974616e67656d2e636f6d3a77616c6c657490000c618102ffff8a0102820407e40109830b54414e47454d2053444b008403584c4d86400e71c1f060387029688254320b90abeae471bcafbbe8ea3880903bdb8d1cc389d032b982e1ffd7ef49e66f1780123b763dd2f3a9a9494eb0fad4ae8cf306672c60207c967a51077c14fc49d867f23b8d0eaf60cad479a56587e894571b7fb33690176140345fbe53f5be0ec871e91c317cde2bd0396d47e4b945c138c153b0271f636a73cf531df1bc54ac4fcdbce42f81b40d58e0265d34e28121a4c50fdfe329a97f6000fe000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
val tlvs = Tlv.deserialize(response.hexToBytes(), true)
|
||||
assertThat(tlvs)
|
||||
.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,7 @@ package com.tangem.tangemtest
|
|||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.tangem.CardManager
|
||||
import com.tangem.tangem_sdk_new.DefaultCardManagerDelegate
|
||||
import com.tangem.tangem_sdk_new.NfcLifecycleObserver
|
||||
import com.tangem.tangem_sdk_new.nfc.NfcManager
|
||||
import com.tangem.tangem_sdk_new.extensions.init
|
||||
import com.tangem.tasks.ScanEvent
|
||||
import com.tangem.tasks.TaskError
|
||||
import com.tangem.tasks.TaskEvent
|
||||
|
|
@ -13,10 +11,7 @@ import kotlinx.android.synthetic.main.activity_main.*
|
|||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private val nfcManager = NfcManager()
|
||||
private val cardManagerDelegate: DefaultCardManagerDelegate = DefaultCardManagerDelegate(nfcManager.reader)
|
||||
private val cardManager = CardManager(nfcManager.reader, cardManagerDelegate)
|
||||
|
||||
private lateinit var cardManager: CardManager
|
||||
private lateinit var cardId: String
|
||||
private lateinit var issuerData: ByteArray
|
||||
private lateinit var issuerDataSignature: ByteArray
|
||||
|
|
@ -25,10 +20,7 @@ class MainActivity : AppCompatActivity() {
|
|||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
nfcManager.setCurrentActivity(this)
|
||||
cardManagerDelegate.activity = this
|
||||
|
||||
lifecycle.addObserver(NfcLifecycleObserver(nfcManager))
|
||||
cardManager = CardManager.init(this)
|
||||
|
||||
btn_scan?.setOnClickListener { _ ->
|
||||
cardManager.scanCard { taskEvent ->
|
||||
|
|
@ -38,6 +30,10 @@ class MainActivity : AppCompatActivity() {
|
|||
is ScanEvent.OnReadEvent -> {
|
||||
// Handle returned card data
|
||||
cardId = (taskEvent.data as ScanEvent.OnReadEvent).card.cardId
|
||||
runOnUiThread {
|
||||
tv_card_cid?.text = cardId
|
||||
btn_create_wallet.isEnabled = true
|
||||
}
|
||||
}
|
||||
is ScanEvent.OnVerifyEvent -> {
|
||||
//Handle card verification
|
||||
|
|
@ -127,6 +123,11 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
is TaskEvent.Event -> runOnUiThread {
|
||||
tv_card_cid?.text = it.data.status.name
|
||||
btn_sign.isEnabled = true
|
||||
btn_read_issuer_data.isEnabled = true
|
||||
btn_purge_wallet.isEnabled = true
|
||||
btn_create_wallet.isEnabled = false
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#008577</color>
|
||||
<color name="colorPrimaryDark">#00574B</color>
|
||||
<color name="colorAccent">#D81B60</color>
|
||||
<color name="colorPrimary">#027aff</color>
|
||||
<color name="colorPrimaryDark">#027AFF</color>
|
||||
<color name="colorAccent">#027AFF</color>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
<tech-list>
|
||||
<tech>android.nfc.tech.IsoDep</tech>
|
||||
<tech>android.nfc.tech.Ndef</tech>
|
||||
<tech>android.nfc.tech.NfcV</tech>
|
||||
</tech-list>
|
||||
</resources>
|
||||
|
|
@ -40,16 +40,19 @@ dependencies {
|
|||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.1.0-beta01'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'com.skyfishjy.ripplebackground:library:1.0.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
|
||||
implementation "androidx.lifecycle:lifecycle-runtime:2.1.0"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.1.0"
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:1.3.61"
|
||||
|
||||
implementation 'at.favre.lib:armadillo:0.9.0'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:1.3.50"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
package com.tangem.tangem_sdk_new
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.tangem.CardManagerDelegate
|
||||
import com.tangem.Log
|
||||
import com.tangem.LoggerInterface
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.tangem_sdk_new.extensions.hide
|
||||
import com.tangem.tangem_sdk_new.extensions.show
|
||||
import com.tangem.tangem_sdk_new.nfc.NfcReader
|
||||
import com.tangem.tangem_sdk_new.ui.NfcEnableDialog
|
||||
import com.tangem.tangem_sdk_new.ui.TouchCardAnimation
|
||||
|
|
@ -28,13 +32,13 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
|
|||
setLogger()
|
||||
}
|
||||
|
||||
override fun onNfcSessionStarted() {
|
||||
override fun onNfcSessionStarted(cardId: String?) {
|
||||
reader.readingCancelled = false
|
||||
postUI { showReadingDialog(activity) }
|
||||
postUI { showReadingDialog(activity, cardId) }
|
||||
if (!reader.nfcEnabled) showNFCEnableDialog()
|
||||
}
|
||||
|
||||
private fun showReadingDialog(activity: FragmentActivity) {
|
||||
private fun showReadingDialog(activity: FragmentActivity, cardId: String?) {
|
||||
val dialogView = activity.getLayoutInflater().inflate(R.layout.nfc_bottom_sheet, null)
|
||||
readingDialog = BottomSheetDialog(activity)
|
||||
readingDialog?.setContentView(dialogView)
|
||||
|
|
@ -46,6 +50,11 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
|
|||
activity, readingDialog!!.ivHandCardHorizontal,
|
||||
readingDialog!!.ivHandCardVertical, readingDialog!!.llHand, readingDialog!!.llNfc)
|
||||
nfcDeviceAntenna.init()
|
||||
if (cardId != null) {
|
||||
readingDialog?.tvCard?.visibility = View.VISIBLE
|
||||
readingDialog?.tvCardId?.visibility = View.VISIBLE
|
||||
readingDialog?.tvCardId?.text = cardId
|
||||
}
|
||||
}
|
||||
readingDialog?.setOnCancelListener {
|
||||
reader.readingCancelled = true
|
||||
|
|
@ -60,21 +69,36 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
|
|||
activity.supportFragmentManager.let { nfcEnableDialog?.show(it, NfcEnableDialog.TAG) }
|
||||
}
|
||||
|
||||
override fun onSecurityDelay(ms: Int) {
|
||||
override fun onSecurityDelay(ms: Int, totalDurationSeconds: Int) {
|
||||
postUI {
|
||||
readingDialog?.lTouchCard?.visibility = View.GONE
|
||||
readingDialog?.lTouchCard?.hide()
|
||||
readingDialog?.tvRemainingTime?.text = ms.div(100).toString()
|
||||
readingDialog?.flSecurityDelay?.visibility = View.VISIBLE
|
||||
readingDialog?.flSecurityDelay?.show()
|
||||
readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_security_delay)
|
||||
readingDialog?.tvTaskText?.text =
|
||||
activity.getText(R.string.dialog_security_delay_description)
|
||||
// readingDialog?.pbSecurityDelay?.isIndeterminate = false
|
||||
|
||||
if (readingDialog?.pbSecurityDelay?.max != totalDurationSeconds) {
|
||||
readingDialog?.pbSecurityDelay?.max = totalDurationSeconds
|
||||
}
|
||||
readingDialog?.pbSecurityDelay?.progress = totalDurationSeconds - ms + 100
|
||||
|
||||
val animation = ObjectAnimator.ofInt(
|
||||
readingDialog?.pbSecurityDelay,
|
||||
"progress",
|
||||
totalDurationSeconds - ms,
|
||||
totalDurationSeconds - ms + 100)
|
||||
animation.duration = 500
|
||||
animation.interpolator = DecelerateInterpolator()
|
||||
animation.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTagLost() {
|
||||
postUI {
|
||||
readingDialog?.lTouchCard?.visibility = View.VISIBLE
|
||||
readingDialog?.flSecurityDelay?.visibility = View.GONE
|
||||
readingDialog?.lTouchCard?.show()
|
||||
readingDialog?.flSecurityDelay?.hide()
|
||||
readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_ready_to_scan)
|
||||
readingDialog?.tvTaskText?.text = activity.getText(R.string.dialog_scan_text)
|
||||
}
|
||||
|
|
@ -82,9 +106,9 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
|
|||
|
||||
override fun onNfcSessionCompleted() {
|
||||
postUI {
|
||||
readingDialog?.lTouchCard?.visibility = View.GONE
|
||||
readingDialog?.flSecurityDelay?.visibility = View.GONE
|
||||
readingDialog?.flCompletion?.visibility = View.VISIBLE
|
||||
readingDialog?.lTouchCard?.hide()
|
||||
readingDialog?.flSecurityDelay?.hide()
|
||||
readingDialog?.flCompletion?.show()
|
||||
readingDialog?.ivCompletion?.setImageDrawable(activity.getDrawable(R.drawable.ic_done_135dp))
|
||||
}
|
||||
postUI(300) { readingDialog?.dismiss() }
|
||||
|
|
@ -92,10 +116,10 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
|
|||
|
||||
override fun onError(error: TaskError?) {
|
||||
postUI {
|
||||
readingDialog?.lTouchCard?.visibility = View.GONE
|
||||
readingDialog?.flSecurityDelay?.visibility = View.GONE
|
||||
readingDialog?.flCompletion?.visibility = View.VISIBLE
|
||||
readingDialog?.ivCompletion?.setImageDrawable(activity.getDrawable(R.drawable.ic_error_outline_135dp))
|
||||
readingDialog?.lTouchCard?.hide()
|
||||
readingDialog?.flSecurityDelay?.hide()
|
||||
readingDialog?.flCompletion?.hide()
|
||||
readingDialog?.flError?.show()
|
||||
readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_error)
|
||||
readingDialog?.tvTaskText?.text = if (error != null) error::class.simpleName else ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.tangem_sdk_new
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import at.favre.lib.armadillo.Armadillo
|
||||
import com.tangem.common.KeyPair
|
||||
import com.tangem.common.TerminalKeysService
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
|
||||
|
||||
/**
|
||||
* Service for managing Terminal keypair, used for Linked Terminal feature.
|
||||
* Needs to be provided to [com.tangem.CardManager] by calling [com.tangem.CardManager.setTerminalKeysService]
|
||||
* Linked Terminal feature can be disabled manually by editing [com.tangem.Config].
|
||||
* @param applicationContext is required to retrieve an instance of [SharedPreferences]
|
||||
*/
|
||||
class TerminalKeysStorage(applicationContext: Application): TerminalKeysService {
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Armadillo.create(applicationContext, PREFERENCES_KEY)
|
||||
.encryptionFingerprint(applicationContext)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves generated keys from encrypted shared preferences if the keys exist.
|
||||
* Generates new and stores them in encrypted shared preferences otherwise
|
||||
*/
|
||||
override fun getKeys(): KeyPair {
|
||||
val privateKey = preferences.getString(TERMINAL_PRIVATE_KEY, null)?.hexToBytes()
|
||||
val publicKey = preferences.getString(TERMINAL_PUBLIC_KEY, null)?.hexToBytes()
|
||||
return if (privateKey == null || publicKey == null) {
|
||||
generateAndSaveKeys()
|
||||
} else {
|
||||
KeyPair(publicKey, privateKey)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateAndSaveKeys(): KeyPair {
|
||||
val privateKey = CryptoUtils.generateRandomBytes(32)
|
||||
val publicKey = CryptoUtils.generatePublicKey(privateKey)
|
||||
preferences.edit().putString(TERMINAL_PRIVATE_KEY, privateKey.toHexString()).apply()
|
||||
preferences.edit().putString(TERMINAL_PUBLIC_KEY, publicKey.toHexString()).apply()
|
||||
return KeyPair(publicKey, privateKey)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
const val PREFERENCES_KEY = "myPrefs"
|
||||
const val TERMINAL_PRIVATE_KEY = "terminalPrivateKey"
|
||||
const val TERMINAL_PUBLIC_KEY = "terminalPublicKey"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.tangem_sdk_new.extensions
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.tangem.CardManager
|
||||
import com.tangem.tangem_sdk_new.DefaultCardManagerDelegate
|
||||
import com.tangem.tangem_sdk_new.NfcLifecycleObserver
|
||||
import com.tangem.tangem_sdk_new.TerminalKeysStorage
|
||||
import com.tangem.tangem_sdk_new.nfc.NfcManager
|
||||
|
||||
fun CardManager.Companion.init(activity: FragmentActivity): CardManager {
|
||||
val nfcManager = NfcManager().apply {
|
||||
this.setCurrentActivity(activity)
|
||||
activity.lifecycle.addObserver(NfcLifecycleObserver(this))
|
||||
}
|
||||
val cardManagerDelegate = DefaultCardManagerDelegate(nfcManager.reader).apply {
|
||||
this.activity = activity
|
||||
}
|
||||
return CardManager(nfcManager.reader, cardManagerDelegate).apply {
|
||||
this.setTerminalKeysService(TerminalKeysStorage(activity.application))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.tangem_sdk_new.extensions
|
||||
|
||||
import android.view.View
|
||||
|
||||
internal fun View.show() {
|
||||
this.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
internal fun View.hide() {
|
||||
this.visibility = View.GONE
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ import android.os.Bundle
|
|||
import com.tangem.Log
|
||||
|
||||
/**
|
||||
* Helps use of NFC, leveraging Android NFC functionality.
|
||||
* Helps use NFC, leveraging Android NFC functionality.
|
||||
* Launches [NfcAdapter], manages it with [Activity] lifecycle,
|
||||
* enables and disables Nfc Reading Mode, receives NFC [Tag].
|
||||
*/
|
||||
|
|
@ -87,6 +87,7 @@ class NfcManager : NfcAdapter.ReaderCallback {
|
|||
companion object {
|
||||
// reader mode flags: listen for type A (not B), skipping ndef check
|
||||
private const val READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A or
|
||||
NfcAdapter.FLAG_READER_NFC_V or
|
||||
NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK or
|
||||
NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.tangem_sdk_new.nfc
|
|||
import android.nfc.Tag
|
||||
import android.nfc.TagLostException
|
||||
import android.nfc.tech.IsoDep
|
||||
import android.nfc.tech.NfcV
|
||||
import com.tangem.CardReader
|
||||
import com.tangem.Log
|
||||
import com.tangem.common.CompletionResult
|
||||
|
|
@ -41,8 +42,8 @@ class NfcReader : CardReader {
|
|||
}
|
||||
}
|
||||
|
||||
var data: ByteArray? = null
|
||||
var callback: ((response: CompletionResult<ResponseApdu>) -> Unit)? = null
|
||||
private var data: ByteArray? = null
|
||||
private var callback: ((response: CompletionResult<ResponseApdu>) -> Unit)? = null
|
||||
|
||||
override fun openSession() {
|
||||
readingActive = true
|
||||
|
|
@ -90,6 +91,7 @@ class NfcReader : CardReader {
|
|||
}
|
||||
|
||||
fun onTagDiscovered(tag: Tag?) {
|
||||
NfcV.get(tag)?.let { onNfcVDiscovered(it) }
|
||||
isoDep = IsoDep.get(tag)
|
||||
transceiveData()
|
||||
}
|
||||
|
|
@ -103,4 +105,17 @@ class NfcReader : CardReader {
|
|||
Log.i(this::class.simpleName!!, "Nfc session is started")
|
||||
}
|
||||
|
||||
private fun onNfcVDiscovered(nfcV: NfcV) {
|
||||
val response = SlixTagReader().transceive(nfcV)
|
||||
when (response) {
|
||||
is SlixReadResult.Failure -> {
|
||||
callback?.invoke(CompletionResult.Failure(
|
||||
TaskError.ErrorProcessingCommand(response.exception.message))
|
||||
)
|
||||
}
|
||||
is SlixReadResult.Success -> {
|
||||
callback?.invoke(CompletionResult.Success(ResponseApdu(response.data)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
package com.tangem.tangem_sdk_new.nfc
|
||||
|
||||
import android.nfc.NdefMessage
|
||||
import android.nfc.NdefRecord
|
||||
import android.nfc.tech.NfcV
|
||||
import com.tangem.common.apdu.StatusWord
|
||||
import com.tangem.common.extensions.toByteArray
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.tlv.Tlv
|
||||
import com.tangem.common.tlv.TlvMapper
|
||||
import com.tangem.common.tlv.TlvTag
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
class SlixTagReader() {
|
||||
|
||||
private lateinit var nfcV: NfcV
|
||||
|
||||
fun transceive(nfcV: NfcV): SlixReadResult {
|
||||
this.nfcV = nfcV
|
||||
if (!nfcV.isConnected) {
|
||||
try {
|
||||
nfcV.connect()
|
||||
} catch (e: Exception) {
|
||||
return SlixReadResult.Failure(e)
|
||||
}
|
||||
}
|
||||
return try {
|
||||
runRead()
|
||||
} catch (e: Exception) {
|
||||
nfcV.close()
|
||||
SlixReadResult.Failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun runRead(): SlixReadResult {
|
||||
val ndefMessage = runReadNDEF()
|
||||
val records: Array<NdefRecord> = ndefMessage.records
|
||||
for (record in records) {
|
||||
if (record.toUri() != null && record.toUri().toString() == "vnd.android.nfc://ext/tangem.com:wallet") {
|
||||
val payload = record.payload
|
||||
val status = StatusWord.ProcessCompleted.code.toByteArray()
|
||||
val data = payload.copyOfRange(2, payload.size) + status
|
||||
nfcV.close()
|
||||
return SlixReadResult.Success(data)
|
||||
}
|
||||
}
|
||||
return SlixReadResult.Failure(Exception("No relevant data was found."))
|
||||
}
|
||||
|
||||
private fun runReadNDEF(): NdefMessage {
|
||||
val answerCC = readSingleBlock(0x00)
|
||||
|
||||
if (answerCC.size != 4 || answerCC[0].toInt() != 0xE1 || ((answerCC[1].toInt() and 0xF0) != 0x40)) {
|
||||
|
||||
} else {
|
||||
throw Exception("Failed! Invalid CC read " + answerCC.toHexString())
|
||||
}
|
||||
if ((answerCC[3].toInt() and 0x01) != 0x01) {
|
||||
throw Exception("Multiple block read unsupported!")
|
||||
}
|
||||
val areaSize = 8 * answerCC[2]
|
||||
val blocksCount = areaSize / 4
|
||||
|
||||
val areaBuf = readMultipleBlocks(1, blocksCount)
|
||||
|
||||
val tlvNdef = TlvMapper(Tlv.deserialize(areaBuf, true) ?: listOf())
|
||||
|
||||
return NdefMessage(tlvNdef.map<ByteArray>(TlvTag.CardPublicKey))
|
||||
}
|
||||
|
||||
private fun readSingleBlock(blockNo: Int): ByteArray {
|
||||
return performRead(0x20, blockNo)!!
|
||||
}
|
||||
|
||||
private fun readMultipleBlocks(startBlock: Int, blocksCount: Int): ByteArray {
|
||||
val resultBuf = ByteArrayOutputStream()
|
||||
val maxBlocksAtOnce = 32
|
||||
var blocksRemaining = blocksCount
|
||||
var firstBlockToRead = startBlock
|
||||
while (blocksRemaining > 0) {
|
||||
val blocksToRead = if (blocksRemaining > maxBlocksAtOnce) {
|
||||
maxBlocksAtOnce
|
||||
} else {
|
||||
blocksRemaining
|
||||
}
|
||||
val blocks = performRead(0x23.toByte(), firstBlockToRead, blocksToRead - 1)
|
||||
blocksRemaining -= blocksToRead
|
||||
firstBlockToRead += blocksToRead
|
||||
resultBuf.write(blocks!!)
|
||||
}
|
||||
return resultBuf.toByteArray()
|
||||
}
|
||||
|
||||
private fun performRead(cmd: Byte, p1: Int?, p2: Int? = null, params: ByteArray? = null): ByteArray? {
|
||||
val command: ByteArray
|
||||
val res: ByteArray?
|
||||
val os = ByteArrayOutputStream()
|
||||
os.write(REQ_FLAG.toInt())
|
||||
os.write(cmd.toInt())
|
||||
p1?.let { os.write(p1) }
|
||||
p2?.let { os.write(it) }
|
||||
params?.let { os.write(params, 0, params.size) }
|
||||
command = os.toByteArray()
|
||||
if (!nfcV.isConnected) throw IOException("Connection lost")
|
||||
res = nfcV.transceive(command)
|
||||
val errorCode = res[0].toInt()
|
||||
if (errorCode != 0) {
|
||||
throw IOException("Error! Code: " + String.format("0x%02x", errorCode))
|
||||
}
|
||||
return res.copyOfRange(1, res.size)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// iso15693 flags
|
||||
private const val REQ_FLAG: Byte = 0x02
|
||||
}
|
||||
}
|
||||
|
||||
sealed class SlixReadResult {
|
||||
data class Success(val data: ByteArray) : SlixReadResult()
|
||||
data class Failure(val exception: Exception) : SlixReadResult()
|
||||
}
|
||||
6
tangem-sdk/src/main/res/drawable/ic_error.xml
Normal file
6
tangem-sdk/src/main/res/drawable/ic_error.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<vector android:height="100dp" android:tint="#FF9D30"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="100dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M12,19m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="M10,3h4v12h-4z"/>
|
||||
</vector>
|
||||
10
tangem-sdk/src/main/res/drawable/ic_nfc.xml
Normal file
10
tangem-sdk/src/main/res/drawable/ic_nfc.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M10,0C4.48,0 0,4.48 0,10s4.48,10 10,10 10,-4.48 10,-10S15.52,0 10,0zM6.46,12.45l-1.36,-0.62c0.28,-0.61 0.41,-1.24 0.4,-1.86a4.42,4.42 0,0 0,-0.4 -1.8l1.36,-0.63c0.35,0.75 0.53,1.56 0.54,2.4 0.01,0.86 -0.17,1.7 -0.54,2.51zM9.53,14.01l-1.3,-0.74c0.52,-0.92 0.78,-1.98 0.78,-3.15 0,-1.19 -0.27,-2.33 -0.8,-3.4l1.34,-0.67c0.64,1.28 0.96,2.65 0.96,4.07 0,1.43 -0.33,2.74 -0.98,3.89zM12.67,15.33l-1.35,-0.66c0.78,-1.6 1.18,-3.18 1.18,-4.69 0,-1.51 -0.4,-3.07 -1.18,-4.64l1.34,-0.67C13.56,6.45 14,8.23 14,9.98c0,1.74 -0.44,3.54 -1.33,5.35z"
|
||||
android:fillColor="#027AFF"
|
||||
android:fillType="nonZero"/>
|
||||
</vector>
|
||||
45
tangem-sdk/src/main/res/drawable/pb_circle.xml
Normal file
45
tangem-sdk/src/main/res/drawable/pb_circle.xml
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/secondaryProgress">
|
||||
<shape
|
||||
android:shape="ring"
|
||||
android:thickness="5dp"
|
||||
android:useLevel="true">
|
||||
|
||||
<gradient
|
||||
android:centerColor="#eae9e9"
|
||||
android:endColor="#eae9e9"
|
||||
android:startColor="#eae9e9"
|
||||
android:type="sweep" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item android:id="@android:id/progress">
|
||||
<rotate
|
||||
android:fromDegrees="270"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%"
|
||||
android:toDegrees="270">
|
||||
|
||||
<shape
|
||||
android:shape="ring"
|
||||
android:thickness="5dp"
|
||||
android:useLevel="true">
|
||||
|
||||
<rotate
|
||||
android:fromDegrees="0"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%"
|
||||
android:toDegrees="360" />
|
||||
|
||||
<gradient
|
||||
android:centerColor="@color/colorAccent"
|
||||
android:endColor="@color/colorAccent"
|
||||
android:startColor="@color/colorAccent"
|
||||
android:type="sweep" />
|
||||
|
||||
</shape>
|
||||
</rotate>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
|
@ -1,11 +1,60 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="@string/bottom_sheet_behavior">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:background="#e2e5e6"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="14dp"
|
||||
android:src="@drawable/ic_nfc" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCard"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:backgroundTint="#f3f5f6"
|
||||
android:fontFamily="sans-serif"
|
||||
android:gravity="center"
|
||||
android:letterSpacing="0.02"
|
||||
android:text="@string/header_card"
|
||||
android:textColor="#000000"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="normal"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardId"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:gravity="center"
|
||||
android:letterSpacing="0.02"
|
||||
android:paddingStart="5dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/colorAccent"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="normal"
|
||||
tools:text="cb22000000027374"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTaskTitle"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
@ -34,13 +83,20 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textSize="40dp" />
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:textColor="@color/colorAccent"
|
||||
android:textSize="60dp"
|
||||
android:textStyle="normal" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/pbSecurityDelay"
|
||||
android:layout_width="135dp"
|
||||
android:layout_height="135dp"
|
||||
android:layout_gravity="center" />
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="false"
|
||||
android:progressDrawable="@drawable/pb_circle"
|
||||
android:secondaryProgress="100" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
|
@ -55,8 +111,47 @@
|
|||
android:id="@+id/ivCompletion"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_done_135dp"
|
||||
android:layout_gravity="center"/>
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_done_135dp" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/pbCompletion"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="false"
|
||||
android:progress="100"
|
||||
android:progressDrawable="@drawable/pb_circle"
|
||||
android:secondaryProgress="100" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/flError"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="200dp"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivError"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_error" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/pbError"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="false"
|
||||
android:progress="100"
|
||||
android:progressDrawable="@drawable/pb_circle"
|
||||
android:progressTint="#FF9D30"
|
||||
android:secondaryProgress="100" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,5 +8,6 @@
|
|||
<string name="dialog_error">Error</string>
|
||||
<string name="dialog_ready_to_scan">Ready to Scan</string>
|
||||
<string name="dialog_scan_text">Scan card with your phone as shown above</string>
|
||||
<string name="header_card">Card</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue