Updated on 2026-08-14

This commit is contained in:
Tangem 2020-07-17 18:34:56 +03:00
commit 8078d93258
324 changed files with 361 additions and 15984 deletions

View file

@ -91,7 +91,7 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
resources.getColorStateList(R.color.btn_dark)
}
private val activeColor: ColorStateList by lazy {
val color = if ((Util.bytesToHex(ctx.card?.cid)?.startsWith("10") == true)) {
val color = if (ctx.card?.isStart2CoinCard() == true) {
R.color.start2coin_orange
} else {
R.color.colorAccent
@ -175,7 +175,7 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
if (Util.bytesToHex(ctx.card?.cid)?.startsWith("10") == true) {
if (ctx.card?.isStart2CoinCard() == true) {
btnLoad?.visibility = View.GONE
}
@ -961,4 +961,8 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
Toast.makeText(activity, R.string.loaded_wallet_toast_copied, Toast.LENGTH_LONG).show()
}
private fun TangemCard.isStart2CoinCard(): Boolean {
return (Util.bytesToHex(ctx.card?.cid)?.startsWith("1") == true)
}
}

View file

@ -209,7 +209,7 @@ public class TokenEngine extends CoinEngine {
if (coinData == null) return false;
if (coinData.getBalanceInInternalUnits() == null && coinData.getBalanceAlterInInternalUnits() == null)
return false;
return (coinData.getBalanceInInternalUnits() != null && coinData.getBalanceInInternalUnits().notZero() ) ||
return (coinData.getBalanceInInternalUnits() != null && coinData.getBalanceInInternalUnits().notZero()) ||
(coinData.getBalanceAlterInInternalUnits() != null && coinData.getBalanceAlterInInternalUnits().notZero());
}
@ -576,7 +576,10 @@ public class TokenEngine extends CoinEngine {
int gasLimitInt = 60000;
if (amountValue.getCurrency().equals("DGX") || amountValue.getCurrency().equals("CGT")) {
if (amountValue.getCurrency().equals("DGX") ||
amountValue.getCurrency().equals("CGT") ||
amountValue.getCurrency().equals("AWG")
) {
gasLimitInt = 300000;
}
@ -740,7 +743,7 @@ public class TokenEngine extends CoinEngine {
public void onFail(String method, String message) {
Log.e(TAG, "onFail: " + method + " " + message);
ctx.setError(message);
if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) {
if (serverApiInfura.isRequestsSequenceCompleted() && serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(false);
} else {
blockchainRequestsCallbacks.onProgress();
@ -775,7 +778,7 @@ public class TokenEngine extends CoinEngine {
Log.e(TAG, "FAIL BLOCKCYPHER_ADDRESS Exception");
}
if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) {
if (serverApiInfura.isRequestsSequenceCompleted() && serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
@ -923,7 +926,7 @@ public class TokenEngine extends CoinEngine {
@Override
public void onSuccess(String method, InfuraResponse infuraResponse) {
if (method.equals(ServerApiInfura.INFURA_ETH_SEND_RAW_TRANSACTION)) {
if (infuraResponse.getResult()==null || infuraResponse.getResult().isEmpty()) {
if (infuraResponse.getResult() == null || infuraResponse.getResult().isEmpty()) {
ctx.setError("Rejected by node: " + infuraResponse.getError());
blockchainRequestsCallbacks.onComplete(false);
} else {
@ -961,5 +964,7 @@ public class TokenEngine extends CoinEngine {
return getBalance().getCurrency().equals(Blockchain.Ethereum.getCurrency());
}
public int pendingTransactionTimeoutInSeconds() { return 10; }
public int pendingTransactionTimeoutInSeconds() {
return 10;
}
}

View file

@ -116,7 +116,7 @@ public class XlmAssetEngine extends CoinEngine {
@Override
public boolean hasBalanceInfo() {
if (coinData == null) return false;
return (coinData.getXlmBalance() != null && coinData.getAssetBalance() != null) || (coinData.isError404());
return coinData.getXlmBalance() != null || coinData.isError404();
}

View file

@ -45,10 +45,11 @@ android {
}
dependencies {
implementation project(':tangem-core')
implementation project(':tangem-sdk')
implementation project(':blockchain')
implementation 'com.tangem:core:0.10.4'
implementation 'com.tangem:sdk:0.10.4'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0'

View file

@ -40,8 +40,7 @@ android {
dependencies {
// implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':tangem-core')
implementation project(':tangem-sdk')
implementation 'com.tangem:core:1.13'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation 'androidx.appcompat:appcompat:1.1.0'

View file

@ -1,15 +1,18 @@
package com.tangem.blockchain.blockchains.bitcoin
import com.tangem.blockchain.blockchains.ducatus.DucatusMainNetParams
import com.tangem.blockchain.blockchains.litecoin.LitecoinMainNetParams
import com.tangem.blockchain.common.AddressService
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.calculateRipemd160
import com.tangem.common.extensions.calculateSha256
import org.bitcoinj.core.*
import org.bitcoinj.core.Base58
import org.bitcoinj.core.LegacyAddress
import org.bitcoinj.core.NetworkParameters
import org.bitcoinj.core.SegwitAddress
import org.bitcoinj.params.MainNetParams
import org.bitcoinj.params.TestNet3Params
import java.security.MessageDigest
class BitcoinAddressService(private val blockchain: Blockchain) : AddressService {
@ -17,6 +20,7 @@ class BitcoinAddressService(private val blockchain: Blockchain) : AddressService
Blockchain.Bitcoin -> MainNetParams()
Blockchain.BitcoinTestnet -> TestNet3Params()
Blockchain.Litecoin -> LitecoinMainNetParams()
Blockchain.Ducatus -> DucatusMainNetParams()
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
}
@ -52,6 +56,7 @@ class BitcoinAddressService(private val blockchain: Blockchain) : AddressService
Blockchain.Bitcoin -> LegacyAddress.fromBase58(MainNetParams(), address)
Blockchain.BitcoinTestnet -> LegacyAddress.fromBase58(TestNet3Params(), address)
Blockchain.Litecoin -> LegacyAddress.fromBase58(LitecoinMainNetParams(), address)
Blockchain.Ducatus -> LegacyAddress.fromBase58(DucatusMainNetParams(), address)
else -> return false
}
true

View file

@ -1,5 +1,6 @@
package com.tangem.blockchain.blockchains.bitcoin
import com.tangem.blockchain.blockchains.ducatus.DucatusMainNetParams
import com.tangem.blockchain.blockchains.litecoin.LitecoinMainNetParams
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
@ -23,6 +24,7 @@ open class BitcoinTransactionBuilder(
Blockchain.Bitcoin, Blockchain.BitcoinCash -> MainNetParams()
Blockchain.BitcoinTestnet -> TestNet3Params()
Blockchain.Litecoin -> LitecoinMainNetParams()
Blockchain.Ducatus -> DucatusMainNetParams()
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
}
var unspentOutputs: List<BitcoinUnspentOutput>? = null
@ -64,7 +66,7 @@ open class BitcoinTransactionBuilder(
}
fun calculateChange(transactionData: TransactionData, unspentOutputs: List<BitcoinUnspentOutput>): BigDecimal {
val fullAmount = unspentOutputs!!.map { it.amount }.reduce { acc, number -> acc + number }
val fullAmount = unspentOutputs.map { it.amount }.reduce { acc, number -> acc + number }
return fullAmount - (transactionData.amount.value!! + (transactionData.fee?.value
?: 0.toBigDecimal()))
}

View file

@ -13,7 +13,7 @@ import java.math.BigDecimal
open class BitcoinWalletManager(
cardId: String,
wallet: Wallet,
private val transactionBuilder: BitcoinTransactionBuilder,
protected val transactionBuilder: BitcoinTransactionBuilder,
private val networkManager: BitcoinProvider
) : WalletManager(cardId, wallet), TransactionSender {

View file

@ -0,0 +1,69 @@
package com.tangem.blockchain.blockchains.ducatus;
import org.bitcoinj.core.Utils;
import org.bitcoinj.params.AbstractBitcoinNetParams;
import org.bitcoinj.params.MainNetParams;
import org.spongycastle.util.encoders.Hex;
import static com.google.common.base.Preconditions.checkState;
public class DucatusMainNetParams extends AbstractBitcoinNetParams {
public static final int MAINNET_MAJORITY_WINDOW = MainNetParams.MAINNET_MAJORITY_WINDOW;
public static final int MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED = MainNetParams.MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED;
public static final int MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE = MainNetParams.MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE;
public DucatusMainNetParams() {
super();
id = "org.bitcoinj.ducatus_mainnet";
// Genesis hash is 12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2
packetMagic = 0xfbc0b6db;
maxTarget = Utils.decodeCompactBits(0x1e0fffffL);
port = 9333;
addressHeader = 49;
p2shHeader = 51; //TODO: is this right? Haven't seen Ducatus p2sh address ever
segwitAddressHrp = "duc"; //TODO: does Ducatus even have bech32 addresses? At least other blockchain's addresses won't pass
dumpedPrivateKeyHeader = 176;
spendableCoinbaseDepth = 100;
subsidyDecreaseBlockCount = 840000;
genesisBlock.setTime(1317972665L);
genesisBlock.setDifficultyTarget(0x1e0ffff0L);
genesisBlock.setNonce(2084524493);
String genesisHash = genesisBlock.getHashAsString();
checkState(genesisHash.equals("5155a7ed2219a75c0735c58b5d459c6d07d97917570e27b9d1d4546fb8431381"));
alertSigningKey = Hex.decode("040184710fa689ad5023690c80f3a49c8f13f8d45b8c857fbcbc8bc4a8e4d3eb4b10f4d4604fa08dce601aaf0f470216fe1b51850b4acf21b179c45070ac7b03a9");
majorityEnforceBlockUpgrade = MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE;
majorityRejectBlockOutdated = MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED;
majorityWindow = MAINNET_MAJORITY_WINDOW;
dnsSeeds = new String[]{
"dnsseed.litecointools.com",
"dnsseed.litecoinpool.org",
"dnsseed.ltc.xurious.com",
"dnsseed.koin-project.com",
"dnsseed.weminemnc.com"
};
bip32HeaderP2PKHpub = 0x0488B21E;
bip32HeaderP2PKHpriv = 0x0488ADE4;
}
@Override
public String getPaymentProtocolId() {
return PAYMENT_PROTOCOL_ID_MAINNET;
}
private static DucatusMainNetParams instance;
public static synchronized DucatusMainNetParams get() {
if (instance == null) {
instance = new DucatusMainNetParams();
}
return instance;
}
}

View file

@ -0,0 +1,40 @@
package com.tangem.blockchain.blockchains.ducatus
import com.tangem.blockchain.blockchains.bitcoin.BitcoinTransactionBuilder
import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager
import com.tangem.blockchain.blockchains.ducatus.network.DucatusNetworkManager
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.extensions.Result
import java.math.BigDecimal
class DucatusWalletManager(
cardId: String,
wallet: Wallet,
transactionBuilder: BitcoinTransactionBuilder,
networkManager: DucatusNetworkManager
) : BitcoinWalletManager(cardId, wallet, transactionBuilder, networkManager), TransactionSender {
override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> {
val feeValue = BigDecimal.ONE.movePointLeft(blockchain.decimals())
val sizeResult = transactionBuilder.getEstimateSize(
TransactionData(amount, Amount(amount, feeValue), wallet.address, destination)
)
return when (sizeResult) {
is Result.Failure -> sizeResult
is Result.Success -> {
val transactionSize = sizeResult.data.toBigDecimal()
val minFee = BigDecimal.valueOf(0.00000089).multiply(transactionSize)
val normalFee = BigDecimal.valueOf(0.00000144).multiply(transactionSize)
val priorityFee = BigDecimal.valueOf(0.00000350).multiply(transactionSize)
val fees = listOf(
Amount(minFee, blockchain),
Amount(normalFee, blockchain),
Amount(priorityFee, blockchain)
)
Result.Success(fees)
}
}
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.blockchain.blockchains.ducatus.network
import com.tangem.blockchain.blockchains.ducatus.network.bitcore.BitcoreApi
import com.tangem.blockchain.blockchains.ducatus.network.bitcore.BitcoreProvider
import com.tangem.blockchain.network.API_DUCATUS
import com.tangem.blockchain.network.createRetrofitInstance
class DucatusNetworkManager() : BitcoreProvider(createRetrofitInstance(API_DUCATUS).create(BitcoreApi::class.java))

View file

@ -0,0 +1,21 @@
package com.tangem.blockchain.blockchains.ducatus.network.bitcore
import com.squareup.moshi.JsonClass
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
interface BitcoreApi {
@GET("api/DUC/mainnet/address/{address}/balance")
suspend fun getBalance(@Path("address") address: String): BitcoreBalance
@GET("api/DUC/mainnet/address/{address}/?unspent=true")
suspend fun getUnspents(@Path("address") address: String): List<BitcoreUtxo>
@POST("api/DUC/mainnet/tx/send")
suspend fun sendTransaction(@Body body: BitcoreSendBody): BitcoreSendResponse
}
@JsonClass(generateAdapter = true)
data class BitcoreSendBody(val rawTx: List<String>)

View file

@ -0,0 +1,64 @@
package com.tangem.blockchain.blockchains.ducatus.network.bitcore
import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinAddressResponse
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinFee
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinProvider
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.extensions.retryIO
import com.tangem.common.extensions.hexToBytes
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
open class BitcoreProvider(private val api: BitcoreApi) : BitcoinProvider{
private val decimals = Blockchain.Ducatus.decimals()
override suspend fun getInfo(address: String): Result<BitcoinAddressResponse> {
return try {
coroutineScope {
val balanceDeferred = retryIO { async { api.getBalance(address) } }
val unspentsDeferred = retryIO { async { api.getUnspents(address) } }
val balanceData = balanceDeferred.await()
val unspents = unspentsDeferred.await()
val unspentTransactions = unspents.map {
BitcoinUnspentOutput(
amount = it.amount!!.toBigDecimal().movePointLeft(decimals),
outputIndex = it.index!!.toLong(),
transactionHash = it.transactionHash!!.hexToBytes(),
outputScript = it.script!!.hexToBytes()
)
}
Result.Success(BitcoinAddressResponse(
balance = balanceData.confirmed!!.toBigDecimal().movePointLeft(decimals),//only confirmed balance is returned right
hasUnconfirmed = balanceData.unconfirmed != null,
unspentOutputs = unspentTransactions
))
}
} catch (error: Exception) {
Result.Failure(error)
}
}
override suspend fun getFee(): Result<BitcoinFee> {
TODO("Not yet implemented")// bitcore is used only in ducatus and fee is hardcoded there
}
override suspend fun sendTransaction(transaction: String): SimpleResult {
return try {
val response = retryIO { api.sendTransaction(BitcoreSendBody(listOf(transaction))) }
if (response.txid != null) {
SimpleResult.Success
} else {
SimpleResult.Failure(Exception("Unknown send transaction error"))
}
} catch (error: Exception) {
SimpleResult.Failure(error)
}
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.blockchain.blockchains.ducatus.network.bitcore
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class BitcoreBalance(
@Json(name = "confirmed")
var confirmed: Long? = null,
@Json(name = "unconfirmed")
var unconfirmed: Long? = null
)
@JsonClass(generateAdapter = true)
data class BitcoreUtxo(
@Json(name = "mintTxid")
var transactionHash: String? = null,
@Json(name = "mintIndex")
var index: Int? = null,
@Json(name = "value")
var amount: Long? = null,
@Json(name = "script")
var script: String? = null
)
@JsonClass(generateAdapter = true)
data class BitcoreSendResponse(
@Json(name = "txid")
var txid: String? = null
)

View file

@ -1,8 +1,8 @@
package com.tangem.blockchain.blockchains.ethereum
import android.util.Log
import com.tangem.blockchain.blockchains.ethereum.network.EthereumNetworkManager
import com.tangem.blockchain.blockchains.ethereum.network.EthereumInfoResponse
import com.tangem.blockchain.blockchains.ethereum.network.EthereumNetworkManager
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
@ -24,7 +24,11 @@ class EthereumWalletManager(
override suspend fun update() {
val result = networkManager.getInfo(wallet.address, wallet.amounts[AmountType.Token]?.address)
val result = networkManager.getInfo(
wallet.address,
wallet.amounts[AmountType.Token]?.address,
wallet.amounts[AmountType.Token]?.decimals
)
when (result) {
is Result.Failure -> updateError(result.error)
is Result.Success -> updateWallet(result.data)

View file

@ -10,7 +10,6 @@ import com.tangem.blockchain.network.createRetrofitInstance
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.kethereum.ETH_IN_WEI
import java.math.BigDecimal
import java.math.BigInteger
import java.math.RoundingMode
@ -35,6 +34,7 @@ class EthereumNetworkManager(blockchain: Blockchain) {
}
private val provider: EthereumProvider by lazy { EthereumProvider(api, apiKey) }
private val decimals = Blockchain.Ethereum.decimals()
suspend fun sendTransaction(transaction: String): SimpleResult {
return try {
@ -59,19 +59,20 @@ class EthereumNetworkManager(blockchain: Blockchain) {
}
}
suspend fun getInfo(address: String, contractAddress: String? = null): Result<EthereumInfoResponse> {
suspend fun getInfo(address: String, contractAddress: String? = null, tokenDecimals: Int? = null)
: Result<EthereumInfoResponse> {
return try {
coroutineScope {
val balanceResponse = retryIO { async { provider.getBalance(address) } }
val txCountResponse = retryIO { async { provider.getTxCount(address) } }
val pendingTxCountResponse = retryIO { async { provider.getPendingTxCount(address) } }
var tokenBalanceResponse: Deferred<EthereumResponse>? = null
if (contractAddress != null) {
if (contractAddress != null && tokenDecimals != null) {
tokenBalanceResponse = retryIO { async { provider.getTokenBalance(address, contractAddress) } }
}
Result.Success(EthereumInfoResponse(
balanceResponse.await().result!!.parseAmount(),
tokenBalanceResponse?.await()?.result?.parseAmount(),
balanceResponse.await().result!!.parseAmount(decimals),
tokenBalanceResponse?.await()?.result?.parseAmount(tokenDecimals!!),
txCountResponse.await().result?.responseToNumber()?.toLong() ?: 0,
pendingTxCountResponse.await().result?.responseToNumber()?.toLong() ?: 0
))
@ -87,20 +88,19 @@ class EthereumNetworkManager(blockchain: Blockchain) {
val normalFee = minFee.multiply(BigDecimal(1.2)).setScale(0, RoundingMode.HALF_UP)
val priorityFee = minFee.multiply(BigDecimal(1.5)).setScale(0, RoundingMode.HALF_UP)
return listOf(
minFee.convertFeeToEth(),
normalFee.convertFeeToEth(),
priorityFee.convertFeeToEth()
minFee.movePointLeft(decimals),
normalFee.movePointLeft(decimals),
priorityFee.movePointLeft(decimals)
)
}
private fun String.responseToNumber(): BigInteger = this.substring(2).toBigInteger(16)
private fun String.parseAmount(): BigDecimal =
this.responseToNumber().toBigDecimal().divide(ETH_IN_WEI.toBigDecimal())
private fun String.parseAmount(decimals: Int): BigDecimal =
this.responseToNumber().toBigDecimal().movePointLeft(decimals)
private fun BigDecimal.convertFeeToEth(): BigDecimal {
return this.divide(ETH_IN_WEI.toBigDecimal())
.setScale(12, BigDecimal.ROUND_DOWN).stripTrailingZeros()
return this.movePointLeft(decimals).setScale(decimals, BigDecimal.ROUND_DOWN).stripTrailingZeros()
}
}

View file

@ -19,6 +19,7 @@ enum class Blockchain(
BitcoinTestnet("BTC/test", "BTCt", "Bitcoin Testnet"),
BitcoinCash("BCH", "BCH", "Bitcoin Cash"),
Litecoin("LTC", "LTC", "Litecoin"),
Ducatus("DUC", "DUC", "Ducatus"),
Ethereum("ETH", "ETH", "Ethereum"),
RSK("RSK", "RBTC", "RSK"),
Cardano("CARDANO", "ADA", "Cardano"),
@ -29,7 +30,7 @@ enum class Blockchain(
Tezos("TEZOS", "XTZ", "Tezos");
fun decimals(): Int = when (this) {
Bitcoin, BitcoinTestnet, BitcoinCash, Binance, BinanceTestnet, Litecoin -> 8
Bitcoin, BitcoinTestnet, BitcoinCash, Binance, BinanceTestnet, Litecoin, Ducatus -> 8
Cardano, XRP, Tezos -> 6
Ethereum, RSK -> 18
Stellar -> 7
@ -45,8 +46,7 @@ enum class Blockchain(
fun validateAddress(address: String): Boolean = getAddressService().validate(address)
private fun getAddressService(): AddressService = when (this) {
Unknown -> throw Exception("unsupported blockchain")
Bitcoin, BitcoinTestnet, Litecoin -> BitcoinAddressService(this)
Bitcoin, BitcoinTestnet, Litecoin, Ducatus -> BitcoinAddressService(this)
BitcoinCash -> BitcoinCashAddressService()
Ethereum, RSK -> EthereumAddressService()
Cardano -> CardanoAddressService()
@ -55,6 +55,7 @@ enum class Blockchain(
BinanceTestnet -> BinanceAddressService(true)
Stellar -> StellarAddressService()
Tezos -> TezosAddressService()
Unknown -> throw Exception("unsupported blockchain")
}
fun getShareUri(address: String): String = when (this) {
@ -71,9 +72,10 @@ enum class Blockchain(
BitcoinTestnet -> "https://live.blockcypher.com/btc-testnet/address/$address"
BitcoinCash -> "https://blockchair.com/bitcoin-cash/address/$address"
Litecoin -> "https://live.blockcypher.com/ltc/address/$address"
Ducatus -> "https://insight.ducatus.io/#/DUC/mainnet/address/$address"
Cardano -> "https://cardanoexplorer.com/address/$address"
Ethereum -> if (token == null) {
"https://etherscan.io/address/"
"https://etherscan.io/address/$address"
} else {
"https://etherscan.io/token/${token.contractAddress}?a=$address"
}

View file

@ -32,7 +32,7 @@ abstract class WalletManager(val cardId: String, var wallet: Wallet) {
}
private fun validateAmount(amount: Amount): Boolean {
return !amount.isAboveZero() &&
return amount.isAboveZero() &&
wallet.fundsAvailable(amount.type) >= amount.value
}
}

View file

@ -12,6 +12,8 @@ import com.tangem.blockchain.blockchains.bitcoincash.BitcoinCashWalletManager
import com.tangem.blockchain.blockchains.cardano.CardanoTransactionBuilder
import com.tangem.blockchain.blockchains.cardano.CardanoWalletManager
import com.tangem.blockchain.blockchains.cardano.network.CardanoNetworkManager
import com.tangem.blockchain.blockchains.ducatus.DucatusWalletManager
import com.tangem.blockchain.blockchains.ducatus.network.DucatusNetworkManager
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionBuilder
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.ethereum.network.EthereumNetworkManager
@ -19,7 +21,6 @@ import com.tangem.blockchain.blockchains.litecoin.LitecoinNetworkManager
import com.tangem.blockchain.blockchains.litecoin.LitecoinWalletManager
import com.tangem.blockchain.blockchains.stellar.StellarNetworkManager
import com.tangem.blockchain.blockchains.stellar.StellarTransactionBuilder
import com.tangem.blockchain.blockchains.stellar.StellarWalletManager
import com.tangem.blockchain.blockchains.tezos.TezosTransactionBuilder
import com.tangem.blockchain.blockchains.tezos.TezosWalletManager
@ -72,6 +73,13 @@ object WalletManagerFactory {
LitecoinNetworkManager()
)
}
Blockchain.Ducatus -> {
return DucatusWalletManager(
cardId, wallet,
BitcoinTransactionBuilder(walletPublicKey, blockchain),
DucatusNetworkManager()
)
}
Blockchain.Ethereum, Blockchain.RSK -> {
return EthereumWalletManager(
cardId, wallet,

View file

@ -55,4 +55,5 @@ const val API_RIPPLED = "https://s1.ripple.com:51234/"
const val API_RIPPLED_RESERVE = "https://s2.ripple.com:51234/"
const val API_BLOCKCHAIR = "https://api.blockchair.com/"
const val API_TEZOS = "https://teznode.letzbake.com"
const val API_TEZOS_RESERVE = "https://mainnet.tezrpc.me"
const val API_TEZOS_RESERVE = "https://mainnet.tezrpc.me"
const val API_DUCATUS = "https://ducapi.rocknblock.io/"

View file

@ -0,0 +1,28 @@
package com.tangem.blockchain.blockchains.ducatus
import com.google.common.truth.Truth
import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressService
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.hexToBytes
import org.junit.Test
class DucatusAddressTest {
private val addressService = BitcoinAddressService(Blockchain.Ducatus)
@Test
fun makeAddressFromCorrectPublicKey() {
val walletPublicKey = "0485D520C8B907F0BC5E03FCBBAC212CCD270764BBFF4990A28653A2FB0D656C342DF143C4D52C43582289E20A81D5D014C1384A1FFFEA1D121903AD7ED35A01EA".hexToBytes()
val expected = "Ly3SZetcgr5gkZMwiNwVrts2z2r3jYieAG"
Truth.assertThat(addressService.makeAddress(walletPublicKey))
.isEqualTo(expected)
}
@Test
fun validateCorrectAddress() {
val address = "Ly3SZetcgr5gkZMwiNwVrts2z2r3jYieAG"
Truth.assertThat(addressService.validate(address))
.isTrue()
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.blockchain.blockchains.binance.BinanceWalletManager
import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager
import com.tangem.blockchain.blockchains.bitcoincash.BitcoinCashWalletManager
import com.tangem.blockchain.blockchains.cardano.CardanoWalletManager
import com.tangem.blockchain.blockchains.ducatus.DucatusWalletManager
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.litecoin.LitecoinWalletManager
import com.tangem.blockchain.blockchains.stellar.StellarWalletManager
@ -18,11 +19,13 @@ import org.junit.Test
internal class WalletManagerFactoryTest {
private val sessionEnvironment = SessionEnvironment()
@Test
fun createBitcoinWalletManager() {
val data = "0108bb00000000000304200754414e47454d00020102800a322e3432642053444b000341040876bdec26b89bd2159a668b9af3d9fe86370f318717c92b8d6c1186fb3648c32a5f9321998cc2d042901c91d40601e79a641e1cbcebe7a2358be6054e1b6e5d0a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b0084034254438640e17ceec48c5be36240c98019f95ad8b6e56acfebe60d11979c6279f715d607d76a860a137da8d109e805753f3f56b0130709f4bbf4cb9974b4c57b8469bf4b873041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd26050a736563703235366b310008040000006407010009020bb8604104752a727e14bba5bd73b6714d72500f61ffd11026ad1196d2e1c54577cbeeac3d11fc68a64700f8d533f4e311964ea8fb3aa26c588295f2133868d69c3e62869362040000005c6304000000090f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -33,7 +36,7 @@ internal class WalletManagerFactoryTest {
fun createEthereumWalletManager() {
val data = "0108bb00000000000536200754414e47454d00020102800a322e3432642053444b000341046c8aea0d5a850b0a608acf9a0c453c39ea86131e88bfa78800de3cfb5bf1007aeaa7b9ffc184212255758605c2461be343c0a661d73cabafa4c9c175b3f0e59a0a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b0084034554488640431b6244acfeac479becdff201a7f720a7d70a97edc4e019fb678596baf52dfe9d0e8faf08ceb4443b82d4e66815541f2dc8ec6dd3ff83eb42f06e5eab07f25f3041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd26050a736563703235366b3100080400000064070100090205dc60410464dddc3f356744aaecfa07427f9eb996ff537d65f20fb5be3abccf0354352a6b5f8a1942e0f8ddeea3a170eda78d060be8162ad60e94e4e91fbbdf0a7054785562040000005b6304000000090f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -44,7 +47,7 @@ internal class WalletManagerFactoryTest {
fun createStellarWalletManager() {
val data = "0108bb00000000000379200754414e47454d00020102800a322e3432642053444b0003410487d7bb51b189213e3cedc3fcfa3fc047b3b71b7805b5b215e14639b3a8ebb1952c9dd5ea4354441b6ada4e8b8327674bb102ddae69df55be69643a2c916edf650a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b008403584c4d86409a4bc2baf0e5836887da21167cf33458d5249d1a610bced0e31dc053f23729ed24d715912bf89e6804669430dfe396ed83274e0031f6803e2bdb8c041fa993413041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd2605086564323535313900080400000064070100090205dc6020e078212d58b2b9d0edc9c936830d10081cd38b90c31778c56dfb1171027e294e62040000003863040000002c0f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -55,7 +58,7 @@ internal class WalletManagerFactoryTest {
fun createCardanoWalletManager() {
val data = "0108bb00000000000502200754414e47454d00020102800a322e3432642053444b0003410402c1e39257d60583489da2d67d35d1cc2a1c005cc05c1021f44838edcaf25d5615cad7c9d11c2e23f5efa93e50904d33c88808d0e169060508df840992e31f4d0a04041e76310c658102ffff8a0101820407e30b0d830b54414e47454d2053444b00840743415244414e4f8640f24ef5c8c6eba0ff97560d5b013edb4a452594270db9647bd0a3543df8104dec75731d4db3ebe0fc493f2afee00195e560b51e3c41189b7c61ba7895d6434b9d3041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd2605086564323535313900080400000064070100090205dc60208a71161cfdf1e0a85d8e7ff372aa4a01136046292aceb5f9ad7ebdb98d3f60a86204000000646304000000000f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -66,7 +69,7 @@ internal class WalletManagerFactoryTest {
fun createXrpWalletManager() {
val data = "0108cb21000000002154200b534d4152542043415348000201028006322e31317200034104bdad63848f97c535da53cf8fd300d24fa33f0516d194aa78ec164a06994d00204bae243a424e316c6ec845e02d9b15eafae8c19018a926b0b7435e6e941cdadb0a0400007e210c5a81020028820407e30502830754414e47454d00840358525086400ed8734b877869722c7d0b37ffb154b9fef21c54bf2c6496feb1fb5c1fc28a2ac28e201dde84f27495fa7f08b3ca2be2fb4954bf0fe78af027d6cdc16c3eee923041048196aa4b410ac44a3b9cce18e7be226aea070acc83a9cf67540fac49af25129f6a538a28ad6341358e3c4f9963064f7e365372a651d374e5c23cdd37fd099bf2050a736563703235366b31000804000f4240070100090205dc604104d2b9fb288540d54e5b32ecaf0381cd571f97f6f1ecd036b66bb11aa52ffe9981110d883080e2e255c6b1640586f7765e6faa325d1340f49b56b83d9de56bc7ed6204000f42406304000000000f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -77,7 +80,7 @@ internal class WalletManagerFactoryTest {
fun createBinanceWalletManager() {
val data = "0108BB00000000000015200754414E47454D00020102800A322E3432642053444B0003410446D4155890B08BE217F0B1FA7DCCB16138C24B3E825A27315D5E4BBD6CAF76A28C7902007052BC1347355A78D54BD73216C9431D555CED827B54FD9255EB3A830A04041E76310C658102FFFF8A0101820407E40410830B54414E47454D2053444B00840742494E414E4345864029F115878EDC7B0CB2A6F4A4009447DCB43BBE922D7629AEBD0C9A910AD1E3BF15AE409C4F579700951ED2FE4D775171A86CFA8E50009A05938CE210D6D4A2583041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A0070100604104E3F3BE3CE3D8284DB3BA073AD0291040093D83C11A277B905D5555C9EC41073E103F4D9D299EDEA8285C51C3356A8681A545618C174251B984DF841F49D2376F62040001869F6304000000010F01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -88,7 +91,7 @@ internal class WalletManagerFactoryTest {
fun createBitcoinCashWalletManager() {
val data = "0108BB00000000000049200754414E47454D00020102800A322E3432642053444B00034104766A1586D164B436E5D420AED01FDAB41B2AE7EDF0C865D7AF1DA995D70AB297E5B94B761CFBB405084C21BC97C02B4A1EA9ED4F515576EAB4D83AD3A0DFAA8A0A04041E76310C618102FFFF8A0101820407E4041B830B54414E47454D2053444B00840342434886408058F0F628C2466B09ECEB13F2A8EFDD4558F5D2DBDA9BD0628EE8C8CC99A778FF0F1AECD35704B9F3518486EA5C1D20F9DFCBAA66184F4CCCD9282E2632882C3041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A0070100604104BE37CD5251C8999EDBBFC759D800EB41E4DCB718289601EB15819404E1B2F2ED90FE50C2A481D06EC790D1EF6184974EB655ABAE4BE56A6D1C9E1A17B1EFDF0262040001869A6304000000060F01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -99,7 +102,7 @@ internal class WalletManagerFactoryTest {
fun createLitecoinWalletManager() {
val data = "0108BB00000000000023200754414E47454D00020102800A322E3432642053444B000341043539F86A40ADD04CE165764A761FD3E4D251028615D2A573B1C3AE652E60AFDBFAF02E3239E89EF2C43FA448A327557ADC5AF36376A0574570F6DBD20113514A0A04041E76310C618102FFFF8A0101820407E40414830B54414E47454D2053444B0084034C5443864004BDEAD0117544886346CB47F7CA84ABA8C34239502F23D28595A4B16CAD72F7DE506BA818B86A649C2BB945986D4574993B3B755B47CBEE31C4FB931F6748183041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A00701006041044A76C9A70422160F515F956D0F50C71BBBA4F9862A22913817D63F0B1EF7C2FAF512E1C91B1BE827560EFE24FB1652B47337E296C778DFB1014D080CDD35EF6562040001869D6304000000030F01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -107,13 +110,24 @@ internal class WalletManagerFactoryTest {
}
@Test
fun createLTezosWalletManager() {
fun createTezosWalletManager() {
val data = "0108BB00000000000080200754414E47454D00020102800A322E3432642053444B0003410436CFC5D0A11353AE6AFEEDC84A2D02B2635C044DEEE47F99913072B8D166D14E557230AC5FB5272F1A0E523332CCE1A744B51DB53102FF7D3FDE023DC3477C460A04041E76310C638102FFFF8A0101820407E40514830B54414E47454D2053444B00840554455A4F538640C752685B29333CFB0DB0A7347579A0AE763F2B5C4BB09FD68E0B81A06CD01EC51347001732815A3ECFFCD78DDE4E53877581B9E4914B069570629D0C40A771B93041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050865643235353139000804000186A0070100602098E0E504F3A5FDE704400302ABB0A2EFB0DF0F95C166C91D7F207DEDCE10CBA362040001869F6304000000010F01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
.isInstanceOf(TezosWalletManager::class.java)
}
@Test
fun createDucatusWalletManager() {
val data = "0108BB00000000000098200754414E47454D00020102800A322E3432642053444B000341041B5FD7C590938E836B388B996AE451FDED54F625FF2CF05E26E5AADF6F690AEF125E3D0F23CB6B8D1F78040DF6F71B40D098F2D8BE504DDEE2E1F99BEADD90500A04041E76310C618102FFFF8A0101820407E40515830B54414E47454D2053444B00840344554386401B453C10A092A3448FA83CAFD4FC3D7EB5EA1BBBDD6A020CAAC8CED36BB2661F41EF0B0C7418214F670DFE1200DCE18597158119BEF6CC52A4FF3B7E021A53B93041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A007010060410485D520C8B907F0BC5E03FCBBAC212CCD270764BBFF4990A28653A2FB0D656C342DF143C4D52C43582289E20A81D5D014C1384A1FFFEA1D121903AD7ED35A01EA62040001869D6304000000030F01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
.isInstanceOf(DucatusWalletManager::class.java)
}
}

View file

@ -11,8 +11,9 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
classpath "com.github.dcendents:android-maven-gradle-plugin:2.1"
classpath 'com.google.gms:google-services:4.3.3'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.0.0-beta04'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.0'
classpath 'com.google.firebase:perf-plugin:1.3.1'
classpath 'com.squareup.sqldelight:gradle-plugin:1.4.0'
}
}
@ -21,6 +22,7 @@ allprojects {
google()
jcenter()
maven { url 'https://jitpack.io' }
maven { url "https://api.bitbucket.org/2.0/repositories/tangem/maven_repository/src/releases" }
}
}

View file

@ -1 +1 @@
include ':app', ':tangem-sdk-old', ':server-android', ':tangem-card-old', ':tangem-core', ':tangem-sdk', ':tangem-devkit', ':blockchain', ':blockchain-demo'
include ':app', ':tangem-sdk-old', ':server-android', ':tangem-card-old', ':blockchain', ':blockchain-demo'

View file

@ -1 +0,0 @@
/build

View file

@ -1,59 +0,0 @@
apply plugin: "kotlin"
apply plugin: 'org.jetbrains.dokka'
apply plugin: 'com.github.dcendents.android-maven'
apply from: '../dependencies.gradle'
apply from: '../jitpack.gradle'
group = "$jitpackSdk.group"
version "$jitpackSdk.version"
dependencies {
// kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
// crypto
implementation "com.madgag.spongycastle:core:1.58.0.0"
implementation "com.madgag.spongycastle:prov:1.58.0.0"
implementation 'net.i2p.crypto:eddsa:0.3.0'
// misc
implementation 'com.google.code.gson:gson:2.8.6'
// tests
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
testImplementation "com.google.truth:truth:1.0"
}
sourceCompatibility = "8"
targetCompatibility = "8"
buildscript {
ext.dokka_version = '0.10.0'
repositories {
mavenCentral()
jcenter()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
}
}
repositories {
mavenCentral()
jcenter()
}
compileKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}
task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
outputFormat = 'markdown'
}

View file

@ -1,14 +0,0 @@
package com.tangem
import com.tangem.common.extensions.CardType
import java.util.*
/**
* Filter that can be used to limit cards that can be interacted with in TangemSdk.
*
* @property allowedCardTypes Type of cards that are allowed to be interacted with in TangemSdk.
*/
data class CardFilter(
var allowedCardTypes: EnumSet<CardType> = EnumSet.allOf(CardType::class.java)
)

View file

@ -1,32 +0,0 @@
package com.tangem
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
/**
* Allows interaction between the phone or any other terminal and Tangem card.
*
* Its default implementation, NfcCardReader, is in our tangem-sdk module.
*/
interface CardReader {
/**
* Sends data to the card and receives the reply.
*
* @param apdu Data to be sent. [CommandApdu] serializes it to a [ByteArray]
* @param callback Returns response from the card,
* [ResponseApdu] Allows to convert raw data to [Tlv]
*/
fun transceiveApdu(apdu: CommandApdu, callback: (response: CompletionResult<ResponseApdu>) -> Unit)
/**
* Signals to [CardReader] to become ready to transceive data.
*/
fun openSession()
/**
* Signals to [CardReader] that no further NFC transition is expected.
*/
fun closeSession()
}

View file

@ -1,260 +0,0 @@
package com.tangem
import com.tangem.commands.Card
import com.tangem.commands.CommandResponse
import com.tangem.commands.OpenSessionCommand
import com.tangem.commands.ReadCommand
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.getType
import com.tangem.crypto.EncryptionHelper
import com.tangem.crypto.FastEncryptionHelper
import com.tangem.crypto.StrongEncryptionHelper
import com.tangem.crypto.pbkdf2Hash
/**
* Basic interface for running tasks and [com.tangem.commands.Command] in a [CardSession]
*/
interface CardSessionRunnable<T : CommandResponse> {
val performPreflightRead: Boolean
/**
* The starting point for custom business logic.
* Implement this interface and use [TangemSdk.startSessionWithRunnable] to run.
* @param session run commands in this [CardSession].
* @param callback trigger the callback to complete the task.
*/
fun run(session: CardSession, callback: (result: CompletionResult<T>) -> Unit)
}
/**
* Allows interaction with Tangem cards. Should be opened before sending commands.
*
* @property environment
* @property reader is an interface that is responsible for NFC connection and
* transfer of data to and from the Tangem Card.
* @property viewDelegate is an interface that allows interaction with users and shows relevant UI.
* @property cardId ID, Unique Tangem card ID number. If not null, the SDK will check that you the card
* with which you tapped a phone has this [cardId] and SDK will return
* the [TangemSdkError.WrongCardNumber] otherwise.
* @property initialMessage A custom description that will be shown at the beginning of the NFC session.
* If null, a default header and text body will be used.
*/
class CardSession(
val environment: SessionEnvironment,
private val reader: CardReader,
val viewDelegate: SessionViewDelegate,
private var cardId: String? = null,
private val initialMessage: Message? = null
) {
private val tag = this.javaClass.simpleName
/**
* True if some operation is still in progress.
*/
private var isBusy = false
private var performPreflightRead = true
/**
* This metod starts a card session, performs preflight [ReadCommand],
* invokes [CardSessionRunnable.run] and closes the session.
* @param runnable [CardSessionRunnable] that will be performed in the session.
* @param callback will be triggered with a [CompletionResult] of a session.
*/
fun <T : CardSessionRunnable<R>, R : CommandResponse> startWithRunnable(
runnable: T, callback: (result: CompletionResult<R>) -> Unit) {
performPreflightRead = runnable.performPreflightRead
start { session, error ->
if (error != null) {
callback(CompletionResult.Failure(error))
return@start
}
if (runnable is ReadCommand) {
callback(CompletionResult.Success(environment.card as R))
return@start
}
runnable.run(this) { result ->
when (result) {
is CompletionResult.Success -> stop()
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.ExtendedLengthNotSupported) {
if (session.environment.terminalKeys != null) {
session.environment.terminalKeys = null
startWithRunnable(runnable, callback)
return@run
}
}
stopWithError(result.error)
}
}
callback(result)
}
}
}
/**
* Starts a card session and performs preflight [ReadCommand].
* @param callback: callback with the card session. Can contain [TangemSdkError] if something goes wrong.
*/
fun start(callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
try {
startSession()
} catch (error: TangemSdkError) {
callback(this, error)
}
if (!performPreflightRead) {
callback(this, null)
return
}
preflightRead() { result ->
when (result) {
is CompletionResult.Failure -> {
callback(this, result.error)
stopWithError(result.error)
}
is CompletionResult.Success -> {
callback(this, null)
}
}
}
}
private fun startSession() {
if (isBusy) throw TangemSdkError.Busy()
isBusy = true
viewDelegate.onNfcSessionStarted(cardId, initialMessage)
reader.openSession()
}
private fun preflightRead(callback: (result: CompletionResult<Card>) -> Unit) {
val readCommand = ReadCommand()
readCommand.run(this) { result ->
when (result) {
is CompletionResult.Failure -> {
tryHandleError(result.error) { handleErrorResult ->
when (handleErrorResult) {
is CompletionResult.Success -> preflightRead(callback)
is CompletionResult.Failure -> {
stopWithError(result.error)
callback(CompletionResult.Failure(result.error))
}
}
}
}
is CompletionResult.Success -> {
val receivedCardId = result.data.cardId
if (cardId != null && receivedCardId != cardId) {
stopWithError(TangemSdkError.WrongCardNumber())
callback(CompletionResult.Failure(TangemSdkError.WrongCardNumber()))
return@run
}
val allowedCardTypes = environment.cardFilter.allowedCardTypes
if (!allowedCardTypes.contains(result.data.getType())) {
stopWithError(TangemSdkError.WrongCardType())
callback(CompletionResult.Failure(TangemSdkError.WrongCardType()))
return@run
}
environment.card = result.data
cardId = receivedCardId
callback(CompletionResult.Success(result.data))
}
}
}
}
/**
* Stops the current session with the text message.
* @param message If null, the default message will be shown.
*/
private fun stop(message: Message? = null) {
reader.closeSession()
viewDelegate.onNfcSessionCompleted(message)
isBusy = false
}
/**
* Stops the current session on error.
* @param error An error that will be shown.
*/
private fun stopWithError(error: TangemSdkError) {
if (!isBusy) return
reader.closeSession()
isBusy = false
val errorMessage = if (error is TangemSdkError) {
"${error::class.simpleName}: ${error.code}"
} else {
error.localizedMessage
}
if (error !is TangemSdkError.UserCancelled) {
Log.e(tag, "Finishing with error: $errorMessage")
viewDelegate.onError(error)
} else {
Log.i(tag, "User cancelled NFC session")
}
}
fun send(apdu: CommandApdu, callback: (result: CompletionResult<ResponseApdu>) -> Unit) {
reader.transceiveApdu(apdu, callback)
}
private fun tryHandleError(
error: TangemSdkError, callback: (result: CompletionResult<Boolean>) -> Unit) {
when (error) {
is TangemSdkError.NeedEncryption -> {
Log.i(tag, "Establishing encryption")
when (environment.encryptionMode) {
EncryptionMode.NONE -> {
environment.encryptionKey = null
environment.encryptionMode = EncryptionMode.FAST
}
EncryptionMode.FAST -> {
environment.encryptionKey = null
environment.encryptionMode = EncryptionMode.STRONG
}
EncryptionMode.STRONG -> {
Log.e(tag, "Encryption doesn't work")
callback(CompletionResult.Failure(TangemSdkError.NeedEncryption()))
}
}
return establishEncryption(callback)
}
else -> callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
}
}
private fun establishEncryption(callback: (result: CompletionResult<Boolean>) -> Unit) {
val encryptionHelper: EncryptionHelper =
if (environment.encryptionMode == EncryptionMode.STRONG) {
StrongEncryptionHelper()
} else {
FastEncryptionHelper()
}
val openSesssionCommand = OpenSessionCommand(encryptionHelper.keyA)
openSesssionCommand.run(this) { result ->
when (result) {
is CompletionResult.Success -> {
val uid = result.data.uid
val protocolKey = environment.pin1.pbkdf2Hash(uid, 50)
val secret = encryptionHelper.generateSecret(result.data.sessionKeyB)
val sessionKey = (secret + protocolKey).calculateSha256()
environment.encryptionKey = sessionKey
callback(CompletionResult.Success(true))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}

View file

@ -1,41 +0,0 @@
package com.tangem
class Config(
/**
* Enables or disables Linked Terminal feature.
App can optionally generate ECDSA key pair Terminal_PrivateKey / Terminal_PublicKey.
And then submit Terminal_PublicKey to the card in any SIGN command.
Once SIGN is successfully executed by COS (Card Operation System),
including PIN2 verification and/or completion of security delay, the submitted
Terminal_PublicKey key is stored by COS. After that, the App instance is deemed trusted
by COS and COS will allow skipping security delay for subsequent SIGN operations
thus improving convenience without sacrificing security.
In order to skip security delay, App should use Terminal_PrivateKey to compute the signature
of the data being submitted to SIGN command for signing and transmit this signature in
Terminal_Transaction_Signature parameter in the same SIGN command. COS will verify
the correctness of Terminal_Transaction_Signature using previously stored Terminal_PublicKey
and, if correct, will skip security delay for the current SIGN operation.
*/
var linkedTerminal: Boolean = true,
/**
* If not null, it will be used to validate Issuer data and issuer extra data.
* If null, issuerPublicKey from current card will be used.
*/
var issuerPublicKey: ByteArray? = null,
/**
* Level of encryption used in communication with a Tangem Card.
*/
var encryptionMode: EncryptionMode = EncryptionMode.NONE,
/**
* Filter that can be used to limit cards that can be interacted with in TangemSdk.
*/
val cardFilter: CardFilter = CardFilter(),
var handleErrors: Boolean = true
)

View file

@ -1,34 +0,0 @@
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 for logging events within the SDK.
*
* It allows to use Android logger or to choose another.
*/
interface LoggerInterface {
fun i(logTag: String, message: String)
fun e(logTag: String, message: String)
fun v(logTag: String, message: String)
}

View file

@ -1,55 +0,0 @@
package com.tangem
import com.tangem.commands.Card
import com.tangem.commands.EllipticCurve
import com.tangem.common.extensions.calculateSha256
import com.tangem.crypto.CryptoUtils.generatePublicKey
/**
* Contains data relating to a Tangem card. It is used in constructing all the commands,
* and commands can return modified [SessionEnvironment].
*
* @property card Current card, read by preflight [com.tangem.commands.ReadCommand].
* @property terminalKeys generated terminal keys used in Linked Terminal feature.
*/
data class SessionEnvironment(
var pin1: ByteArray = DEFAULT_PIN.calculateSha256(),
var pin2: ByteArray = DEFAULT_PIN2.calculateSha256(),
var card: Card? = null,
var terminalKeys: KeyPair? = null,
var encryptionMode: EncryptionMode = EncryptionMode.NONE,
var encryptionKey: ByteArray? = null,
var cvc: ByteArray? = null,
var cardFilter: CardFilter = CardFilter(),
val handleErrors: Boolean = true
) {
fun setPin1(pin1: String) {
this.pin1 = pin1.calculateSha256()
}
fun setPin2(pin2: String) {
this.pin2 = pin2.calculateSha256()
}
companion object {
const val DEFAULT_PIN = "000000"
const val DEFAULT_PIN2 = "000"
}
}
/**
* All possible encryption modes.
*/
enum class EncryptionMode(val code: Byte) {
NONE(0x0),
FAST(0x1),
STRONG(0x2)
}
class KeyPair(val publicKey: ByteArray, val privateKey: ByteArray) {
constructor(privateKey: ByteArray, curve: EllipticCurve = EllipticCurve.Secp256k1) :
this(generatePublicKey(privateKey, curve), privateKey)
}

View file

@ -1,55 +0,0 @@
package com.tangem
import com.tangem.common.CompletionResult
/**
* Allows interaction with users and shows visual elements.
*
* Its default implementation, DefaultCardManagerDelegate, is in our tangem-sdk module.
*/
interface SessionViewDelegate {
/**
* It is called when user is expected to scan a Tangem Card with an Android device.
*/
fun onNfcSessionStarted(cardId: String?, message: Message? = null)
/**
* 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, totalDurationSeconds: Int)
/**
* It is called when long tasks are performed.
* A user is expected to hold the card until the task is complete.
*/
fun onDelay(total: Int, current: Int, step: Int)
/**
* It is called when user takes the card away from the Android device during the scanning
* (for example when security delay is in progress) and the TagLostException is received.
*/
fun onTagLost()
/**
* It is called when NFC session was completed and a user can take the card away from the Android device.
*/
fun onNfcSessionCompleted(message: Message? = null)
/**
* It is called when some error occur during NFC session.
*/
fun onError(error: TangemSdkError)
/**
* It is called when a user is expected to enter pin code.
*/
fun onPinRequested(callback: (result: CompletionResult<String>) -> Unit)
}
/**
* Wrapper for a message that can be shown to user after a start of NFC session.
*/
data class Message(val header: String? = null, val body: String? = null)

View file

@ -1,398 +0,0 @@
package com.tangem
import com.tangem.commands.*
import com.tangem.commands.personalization.DepersonalizeCommand
import com.tangem.commands.personalization.DepersonalizeResponse
import com.tangem.commands.personalization.PersonalizeCommand
import com.tangem.commands.personalization.entities.Acquirer
import com.tangem.commands.personalization.entities.CardConfig
import com.tangem.commands.personalization.entities.Issuer
import com.tangem.commands.personalization.entities.Manufacturer
import com.tangem.common.CompletionResult
import com.tangem.common.TerminalKeysService
import com.tangem.crypto.CryptoUtils
import com.tangem.tasks.CreateWalletTask
import com.tangem.tasks.ScanTask
/**
* The main interface of Tangem SDK that allows your app to communicate with Tangem cards.
*
* @property reader is an interface that is responsible for NFC connection and
* transfer of data to and from the Tangem Card.
* Its default implementation, NfcCardReader, is in our tangem-sdk module.
* @property viewDelegate An interface that allows interaction with users and shows relevant UI.
* Its default implementation, DefaultCardSessionViewDelegate, is in our tangem-sdk module.
* @property config allows to change a number of parameters for communication with Tangem cards.
* Do not change the default values unless you know what you are doing.
*/
class TangemSdk(
private val reader: CardReader,
private val viewDelegate: SessionViewDelegate,
var config: Config = Config()
) {
private var terminalKeysService: TerminalKeysService? = null
init {
CryptoUtils.initCrypto()
}
/**
* This method launches a [ScanTask] on a new thread.
*
* To start using any card, you first need to read it using the scanCard() method.
* This method launches an NFC session, and once its connected with the card,
* it obtains the card data. Optionally, if the card contains a wallet (private and public key pair),
* it proves that the wallet owns a private key that corresponds to a public one.
*
* @param callback is triggered on the completion of the [ScanTask] and provides card response
* in the form of [Card] if the task was performed successfully or [TangemSdkError] in case of an error.
*/
fun scanCard(initialMessage: Message? = null, callback: (result: CompletionResult<Card>) -> Unit) {
startSessionWithRunnable(ScanTask(), null, initialMessage, callback)
}
/**
* This method launches a [SignCommand] on a new thread.
*
* It allows you to sign one or multiple hashes.
* Simultaneous signing of array of hashes in a single [SignCommand] is required to support
* Bitcoin-type multi-input blockchains (UTXO).
* The [SignCommand] will return a corresponding array of signatures.
*
* Please note that Tangem cards usually protect the signing with a security delay
* that may last up to 90 seconds, depending on a card.
* It is for [SessionViewDelegate] to notify users of security delay.
*
* @param hashes Array of transaction hashes. It can be from one or up to ten hashes of the same length.
* @param cardId CID, Unique Tangem card ID number
* @param callback is triggered on the completion of the [SignCommand] and provides card response
* in the form of [SignResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun sign(hashes: Array<ByteArray>, cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<SignResponse>) -> Unit) {
startSessionWithRunnable(SignCommand(hashes), cardId, initialMessage, callback)
}
/**
* This method launches a [ReadIssuerDataCommand] on a new thread.
* This command returns 512-byte Issuer Data field and its issuers signature.
* Issuer Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. For example, this field may contain information about
* wallet balance signed by the issuer or additional issuers attestation data.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [ReadIssuerDataCommand] and provides
* card response in the form of [ReadIssuerDataResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun readIssuerData(cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<ReadIssuerDataResponse>) -> Unit) {
startSessionWithRunnable(ReadIssuerDataCommand(config.issuerPublicKey), cardId, initialMessage, callback)
}
/**
* This method launches a [ReadIssuerExtraDataCommand] on a new thread.
*
* This command retrieves Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. . For example, this field may contain photo or
* biometric information for ID card product. Because of the large size of Issuer_Extra_Data,
* a series of these commands have to be executed to read the entire Issuer_Extra_Data.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [ReadIssuerExtraDataCommand] and provides
* card response in the form of [ReadIssuerExtraDataResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun readIssuerExtraData(cardId: String? = null,
callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit) {
startSessionWithRunnable(ReadIssuerExtraDataCommand(config.issuerPublicKey), cardId, null, callback)
}
/**
* This method launches a [WriteIssuerDataCommand] on a new thread.
*
* This command writes 512-byte Issuer Data field and its issuers signature.
* Issuer Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. For example, this field may contain information about
* wallet balance signed by the issuer or additional issuers attestation data.
*
* @param cardId CID, Unique Tangem card ID number.
* @param issuerData Data provided by issuer.
* @param issuerDataSignature Issuers signature of [issuerData] with Issuer Data Private Key.
* @param issuerDataCounter An optional counter that protect issuer data against replay attack.
* @param callback is triggered on the completion of the [WriteIssuerDataCommand] and provides
* card response in the form of [WriteIssuerDataResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun writeIssuerData(cardId: String? = null,
issuerData: ByteArray,
issuerDataSignature: ByteArray,
issuerDataCounter: Int? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit) {
val command = WriteIssuerDataCommand(
issuerData,
issuerDataSignature,
issuerDataCounter,
config.issuerPublicKey
)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
/**
* This method launches a [WriteIssuerExtraDataCommand] on a new thread.
*
* This command writes Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS.
* The issuer defines purpose of use, format and payload of Issuer Data.
* For example, this field may contain a photo or biometric information for ID card products.
* Because of the large size of IssuerExtraData, a series of these commands have to be executed
* to write entire IssuerExtraData.
*
* @param cardId CID, Unique Tangem card ID number.
* @param issuerData Data provided by issuer.
* @param startingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerDataCounter] (if flags Protect_Issuer_Data_Against_Replay and
* Restrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]) and size of [issuerData].
* @param finalizingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerData] and [issuerDataCounter] (the latter one only if flags Protect_Issuer_Data_Against_Replay
* andRestrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]).
* @param issuerDataCounter An optional counter that protect issuer data against replay attack.
* @param callback is triggered on the completion of the [WriteIssuerExtraDataCommand] and provides
* card response in the form of [WriteIssuerDataResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun writeIssuerExtraData(cardId: String? = null,
issuerData: ByteArray,
startingSignature: ByteArray,
finalizingSignature: ByteArray,
issuerDataCounter: Int? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit) {
val command = WriteIssuerExtraDataCommand(
issuerData,
startingSignature, finalizingSignature,
issuerDataCounter,
config.issuerPublicKey
)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
/**
* This method launches a [WriteUserDataCommand] on a new thread, writing UserData and UserCounter fields.
*
* User_Data is never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and its payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* The initial value of User_Counter can be set by an App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*
* Writing of UserCounter and UserData is protected only by PIN1.
*/
fun writeUserData(
cardId: String? = null,
userData: ByteArray? = null,
userCounter: Int? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<WriteUserDataResponse>) -> Unit
) {
val command = WriteUserDataCommand(userData = userData,userCounter = userCounter)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
/**
* This method launches a [WriteUserDataCommand] on a new thread,
* writing UserProtectedData and UserProtectedCounter fields.
*
* User_ProtectedData is never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and its payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* The initial value of User_ProtectedCounter can be set by an App and increased on every signing
* of a new transaction (on SIGN command that calculate new signatures). The App defines the purpose of use.
* For example, this fields may contain blockchain nonce value.
*
* UserProtectedCounter and UserProtectedData require PIN2 for confirmation.
*/
fun writeProtectedUserData(
cardId: String? = null,
userProtectedData: ByteArray? = null,
userProtectedCounter: Int? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<WriteUserDataResponse>) -> Unit
) {
val command = WriteUserDataCommand(
userProtectedData = userProtectedData, userProtectedCounter = userProtectedCounter
)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
/**
* This method launches a [ReadUserDataCommand] on a new thread.
*
* This command returns two up to 512-byte User_Data, User_Protected_Data and two counters User_Counter and
* User_Protected_Counter fields.
* User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and it's payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [ReadUserDataCommand] and provides
* card response in the form of [ReadUserDataResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun readUserData(cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<ReadUserDataResponse>) -> Unit) {
startSessionWithRunnable(ReadUserDataCommand(), cardId, initialMessage, callback)
}
/**
* This method launches a [CreateWalletTask] on a new thread.
*
* This this will create a new wallet on the card having Empty state with [CreateWalletCommand]
* and will check the success of the operation by performing [CheckWalletCommand].
* A key pair WalletPublicKey / WalletPrivateKey is generated and securely stored in the card.
* App will need to obtain Wallet_PublicKey from the [CreateWalletResponse] or from the
* response of [ReadCommand] and then transform it into an address of corresponding
* blockchain wallet according to a specific blockchain algorithm.
* WalletPrivateKey is never revealed by the card and will be used by [SignCommand] and [CheckWalletCommand].
* RemainingSignature is set to MaxSignatures.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [CreateWalletTask] and provides
* card response in the form of [CreateWalletResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun createWallet(cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
startSessionWithRunnable(CreateWalletTask(), cardId, initialMessage, callback)
}
/**
* This method launches a [PurgeWalletCommand] on a new thread.
*
* This command deletes all wallet data. If IsReusable flag is enabled during personalization,
* or [CreateWalletCommand].
* If IsReusable flag is disabled, the card switches to Purged state.
* Purged state is final, it makes the card useless.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [PurgeWalletCommand] and provides
* card response in the form of [PurgeWalletResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun purgeWallet(cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<PurgeWalletResponse>) -> Unit) {
startSessionWithRunnable(PurgeWalletCommand(), cardId, initialMessage, callback)
}
/**
* Command available on SDK cards only
*
* This method launches a [DepersonalizeCommand] on a new thread.
*
* This command resets card to initial state,
* erasing all data written during personalization and usage.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [DepersonalizeCommand] and provides
* card response in the form of [DepersonalizeResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
* */
fun depersonalize(cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<DepersonalizeResponse>) -> Unit) {
startSessionWithRunnable(DepersonalizeCommand(), cardId, initialMessage, callback)
}
/**
* Command available on SDK cards only
*
* This method launches a [PersonalizeCommand] on a new thread.
*
* Personalization is an initialization procedure, required before starting using a card.
* During this procedure a card setting is set up.
* During this procedure all data exchange is encrypted.
* @param config is a configuration file with all the card settings that are written on the card
* during personalization.
* @param issuer Issuer is a third-party team or company wishing to use Tangem cards.
* @param manufacturer Tangem Card Manufacturer.
* @param acquirer Acquirer is a trusted third-party company that operates proprietary
* (non-EMV) POS terminal infrastructure and transaction processing back-end.
* @param callback is triggered on the completion of the [PersonalizeCommand] and provides
* card response in the form of [Card] if the command was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun personalize(config: CardConfig,
issuer: Issuer, manufacturer: Manufacturer, acquirer: Acquirer? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<Card>) -> Unit) {
val command = PersonalizeCommand(config, issuer, manufacturer, acquirer)
startSessionWithRunnable(command, null, initialMessage, callback)
}
/**
* Allows running a custom bunch of commands in one [CardSession] by creating a custom task.
* [TangemSdk] will start a card session, perform preflight [ReadCommand],
* invoke [CardSessionRunnable.run] and close the session.
* You can find the current card in the [CardSession.environment].
* @runnable: A custom task, adopting [CardSessionRunnable] protocol
* @cardId: CID, Unique Tangem card ID number. If not null, the SDK will check that you the card
* with which you tapped a phone has this [cardId] and SDK will return
* the [TangemSdkError.WrongCardNumber] otherwise.
* @initialMessage: A custom description that shows at the beginning of the NFC session.
* If null, default message will be used.
* @callback: Standard [TangemSdk] callback.
*/
fun <T : CommandResponse> startSessionWithRunnable(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<T>) -> Unit) {
val cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId, initialMessage)
Thread().run { cardSession.startWithRunnable(runnable, callback) }
}
/**
* Allows running a custom bunch of commands in one [CardSession] with lightweight closure syntax.
* Tangem SDK will start a card sesion and perform preflight [ReadCommand].
* @cardId: CID, Unique Tangem card ID number. If not null, the SDK will check that you the card
* with which you tapped a phone has this [cardId] and SDK will return
* the [TangemSdkError.WrongCardNumber] otherwise.
* @initialMessage: A custom description that shows at the beginning of the NFC session.
* If null, default message will be used.
* @callback: At first, you should check that the [TangemSdkError] is not null,
* then you can use the [CardSession] to interact with a card.
*/
fun startSession(cardId: String? = null, initialMessage: Message? = null,
callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
val cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId, initialMessage)
Thread().run { cardSession.start(callback) }
}
/**
* 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 buildEnvironment(): SessionEnvironment {
val terminalKeys = if (config.linkedTerminal) terminalKeysService?.getKeys() else null
return SessionEnvironment(
terminalKeys = terminalKeys,
cardFilter = config.cardFilter,
handleErrors = config.handleErrors
)
}
companion object
}

View file

@ -1,162 +0,0 @@
package com.tangem
import com.tangem.commands.Card
import com.tangem.commands.ReadCommand
import com.tangem.common.apdu.StatusWord
import com.tangem.tasks.ScanTask
/**
* An error class that represent typical errors that may occur when performing Tangem SDK tasks.
* Errors are propagated back to the caller in callbacks.
*/
sealed class TangemSdkError(val code: Int) : Exception(code.toString()) {
/**
* This error is returned when Android NFC reader loses a tag
* (e.g. a user detaches card from the phone's NFC module) while the NFC session is in progress.
*/
class TagLost : TangemSdkError(10001)
/**
* This error is returned when NFC driver on an Android device does not support sending more than 261 bytes.
*/
class ExtendedLengthNotSupported : TangemSdkError(10002)
class SerializeCommandError : TangemSdkError(20001)
class DeserializeApduFailed : TangemSdkError(20002)
class EncodingFailedTypeMismatch : TangemSdkError(20003)
class EncodingFailed : TangemSdkError(20004)
class DecodingFailedMissingTag : TangemSdkError(20005)
class DecodingFailedTypeMismatch : TangemSdkError(20006)
class DecodingFailed : TangemSdkError(20007)
/**
* This error is returned when unknown [StatusWord] is received from a card.
*/
class UnknownStatus : TangemSdkError(30001)
/**
* This error is returned when a card's reply is [StatusWord.ErrorProcessingCommand].
* The card sends this status in case of internal card error.
*/
class ErrorProcessingCommand : TangemSdkError(30002)
/**
* This error is returned when a card's reply is [StatusWord.InvalidState].
* The card sends this status when command can not be executed in the current state of a card.
*/
class InvalidState : TangemSdkError(30003)
/**
* This error is returned when a card's reply is [StatusWord.InsNotSupported].
* The card sends this status when the card cannot process the [com.tangem.common.apdu.Instruction].
*/
class InsNotSupported : TangemSdkError(30004)
/**
* This error is returned when a card's reply is [StatusWord.InvalidParams].
* The card sends this status when there are wrong or not sufficient parameters in TLV request,
* or wrong PIN1/PIN2.
* The error may be caused, for example, by wrong parameters of the [Task], [CommandSerializer],
* mapping or serialization errors.
*/
class InvalidParams : TangemSdkError(30005)
/**
* This error is returned when a card's reply is [StatusWord.NeedEncryption]
* and the encryption was not established by TangemSdk.
*/
class NeedEncryption : TangemSdkError(30006)
//Personalization Errors
class AlreadyPersonalized : TangemSdkError(40101)
//Depersonalization Errors
class CannotBeDepersonalized : TangemSdkError(40201)
//Read Errors
class Pin1Required : TangemSdkError(40401)
//CreateWallet Errors
class AlreadyCreated : TangemSdkError(40501)
//PurgeWallet Errors
class PurgeWalletProhibited : TangemSdkError(40601)
//SetPin Errors
class Pin1CannotBeChanged : TangemSdkError(40801)
class Pin2CannotBeChanged : TangemSdkError(40802)
class Pin1CannotBeDefault : TangemSdkError(40803)
//Sign Errors
class NoRemainingSignatures : TangemSdkError(40901)
/**
* This error is returned when a [com.tangem.commands.SignCommand]
* receives only empty hashes for signature.
*/
class EmptyHashes : TangemSdkError(40902)
/**
* This error is returned when a [com.tangem.commands.SignCommand]
* receives hashes of different lengths for signature.
*/
class HashSizeMustBeEqual : TangemSdkError(40903)
class CardIsEmpty : TangemSdkError(40904)
class SignHashesNotAvailable : TangemSdkError(40905)
/**
* Tangem cards can sign currently up to 10 hashes during one [com.tangem.commands.SignCommand].
* This error is returned when a [com.tangem.commands.SignCommand] receives more than 10 hashes to sign.
*/
class TooManyHashesInOneTransaction : TangemSdkError(40906)
//Write Extra Issuer Data Errors
class ExendedDataSizeTooLarge : TangemSdkError(41101)
//General Errors
class NotPersonalized() : TangemSdkError(40001)
class NotActivated : TangemSdkError(40002)
class CardIsPurged : TangemSdkError(40003)
class Pin2OrCvcRequired : TangemSdkError(40004)
/**
* This error is returned when a [Task] checks unsuccessfully either
* a card's ability to sign with its private key, or the validity of issuer data.
*/
class VerificationFailed : TangemSdkError(40005)
class DataSizeTooLarge : TangemSdkError(40006)
/**
* This error is returned when [ReadIssuerDataTask] or [ReadIssuerExtraDataTask] expects a counter
* (when the card's requires it), but the counter is missing.
*/
class MissingCounter : TangemSdkError(40007)
class OverwritingDataIsProhibited : TangemSdkError(40008)
class DataCannotBeWritten : TangemSdkError(40009)
class MissingIssuerPubicKey : TangemSdkError(40010)
//SDK Errors
class UnknownError: TangemSdkError(50001)
/**
* This error is returned when a user manually closes NFC Reading Bottom Sheet Dialog.
*/
class UserCancelled: TangemSdkError(50002)
/**
* This error is returned when [com.tangem.TangemSdk] was called with a new [Task],
* while a previous [Task] is still in progress.
*/
class Busy : TangemSdkError(50003)
/**
* This error is returned when a task (such as [ScanTask]) requires that [ReadCommand]
* is executed before performing other commands.
*/
class MissingPreflightRead : TangemSdkError(50004)
/**
* This error is returned when a [Task] expects a user to use a particular card,
* but the user tries to use a different card.
*/
class WrongCardNumber : TangemSdkError(50005)
/**
* This error is returned when a user scans a card of a [com.tangem.common.extensions.CardType]
* that is not specified in [Config.cardFilter].
*/
class WrongCardType : TangemSdkError(50006)
/**
* This error is returned when a [ScanTask] returns a [Card] without some of the essential fields.
*/
class CardError : TangemSdkError(50007)
}

View file

@ -1,110 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.CryptoUtils
/**
* Deserialized response from the Tangem card after [CheckWalletCommand].
*
* @property cardId Unique Tangem card ID number
* @property salt Random salt generated by the card.
* @property walletSignature Challenge and salt signed with the wallet private key.
*/
class CheckWalletResponse(
val cardId: String,
val salt: ByteArray,
val walletSignature: ByteArray
) : 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.
* Standard challenge/response scheme is used.
*
* @property pin1 Hashed users pin 1 code to access the card. Default unhashed value: 000000.
* @property cardId Unique Tangem card ID number
* @property challenge Random challenge generated by application
*/
class CheckWalletCommand(
private val curve: EllipticCurve, private val publicKey: ByteArray
) : Command<CheckWalletResponse>() {
private val challenge = CryptoUtils.generateRandomBytes(16)
override fun run(session: CardSession, callback: (result: CompletionResult<CheckWalletResponse>) -> Unit) {
super.run(session) { result ->
when (result) {
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
is CompletionResult.Success -> {
val verified = result.data.verify(
curve,
publicKey,
challenge
)
if (verified) {
callback(CompletionResult.Success(result.data))
} else {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
}
}
}
}
}
override fun performPreCheck(
session: CardSession,
callback: (result: CompletionResult<CheckWalletResponse>) -> Unit
): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
}
return false
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Challenge, challenge)
return CommandApdu(
Instruction.CheckWallet, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): CheckWalletResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return CheckWalletResponse(
cardId = decoder.decode(TlvTag.CardId),
salt = decoder.decode(TlvTag.Salt),
walletSignature = decoder.decode(TlvTag.Signature)
)
}
}

View file

@ -1,138 +0,0 @@
package com.tangem.commands
import com.tangem.*
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.apdu.StatusWord
import com.tangem.common.apdu.toTangemSdkError
import com.tangem.common.extensions.toInt
import com.tangem.common.tlv.TlvTag
/**
* Basic interface for a parsed response from [Command].
*/
interface CommandResponse
/**
* Basic class for Tangem card commands
*/
abstract class Command<T : CommandResponse> : CardSessionRunnable<T> {
override val performPreflightRead: Boolean = true
/**
* Serializes data into an array of [com.tangem.common.tlv.Tlv],
* then creates [CommandApdu] with this data.
* @param environment [SessionEnvironment] of the current card
* @return command data converted to [CommandApdu] that allows to convert it to [ByteArray]
* that can be sent to a Tangem card
*/
abstract fun serialize(environment: SessionEnvironment): CommandApdu
/**
* Deserializes data received from a card and stored in [ResponseApdu]
* into an array of [com.tangem.common.tlv.Tlv]. Then maps it into a [CommandResponse].
* @param environment [SessionEnvironment] of the current card.
* @param apdu received data.
* @return Card response converted to a [CommandResponse] of a type [T]
*/
abstract fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): T
override fun run(session: CardSession, callback: (result: CompletionResult<T>) -> Unit) {
Log.i("Command", "Initializing ${this::class.java.simpleName}")
if (session.environment.handleErrors) {
if (performPreCheck(session, callback)) return
}
transceive(session) { result ->
if (session.environment.handleErrors) {
if (performAfterCheck(session, result, callback)) return@transceive
}
callback(result)
}
}
open fun performPreCheck(session: CardSession,
callback: (result: CompletionResult<T>) -> Unit): Boolean {
return false
}
open fun performAfterCheck(session: CardSession,
result: CompletionResult<T>,
callback: (result: CompletionResult<T>) -> Unit): Boolean {
return false
}
fun transceive(session: CardSession, callback: (result: CompletionResult<T>) -> Unit) {
try {
val apdu = serialize(session.environment)
transceiveApdu(apdu, session) { result ->
when (result) {
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
is CompletionResult.Success -> {
val response = deserialize(session.environment, result.data)
callback(CompletionResult.Success(response))
}
}
}
} catch (error: TangemSdkError) {
callback(CompletionResult.Failure(error))
}
}
private fun transceiveApdu(apdu: CommandApdu, session: CardSession, callback: (result: CompletionResult<ResponseApdu>) -> Unit) {
session.send(apdu) { result ->
when (result) {
is CompletionResult.Success -> {
val responseApdu = result.data
when (responseApdu.statusWord) {
StatusWord.ProcessCompleted, StatusWord.Pin1Changed, StatusWord.Pin2Changed, StatusWord.PinsChanged
-> callback(CompletionResult.Success(responseApdu))
StatusWord.NeedPause -> {
// NeedPause is returned from the card whenever security delay is triggered.
val remainingTime = deserializeSecurityDelay(responseApdu, session.environment)
if (remainingTime != null) {
session.viewDelegate.onSecurityDelay(
remainingTime,
session.environment.card?.pauseBeforePin2 ?: 0)
}
Log.i(this::class.simpleName!!, "Nfc command ${this::class.simpleName!!} " +
"triggered security delay of $remainingTime milliseconds")
transceiveApdu(apdu, session, callback)
}
else -> {
val error = responseApdu.statusWord.toTangemSdkError()
if (error != null && !tryHandleError(error)) {
callback(CompletionResult.Failure(error))
} else {
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
}
}
}
}
is CompletionResult.Failure ->
if (result.error is TangemSdkError.TagLost) {
session.viewDelegate.onTagLost()
} else {
callback(CompletionResult.Failure(result.error))
}
}
}
}
/**
* Helper method to parse security delay information received from a card.
*
* @return Remaining security delay in milliseconds.
*/
private fun deserializeSecurityDelay(responseApdu: ResponseApdu, environment: SessionEnvironment): Int? {
val tlv = responseApdu.getTlvData()
return tlv?.find { it.tag == TlvTag.Pause }?.value?.toInt()
}
private fun tryHandleError(error: TangemSdkError): Boolean {
return false
}
}

View file

@ -1,100 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class CreateWalletResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Current status of the card [1 - Empty, 2 - Loaded, 3- Purged]
*/
val status: CardStatus,
/**
*/
val walletPublicKey: ByteArray
) : CommandResponse
/**
* This command will create a new wallet on the card having Empty state.
* A key pair WalletPublicKey / WalletPrivateKey is generated and securely stored in the card.
* App will need to obtain Wallet_PublicKey from the response of [CreateWalletCommand] or [ReadCommand]
* and then transform it into an address of corresponding blockchain wallet
* according to a specific blockchain algorithm.
* WalletPrivateKey is never revealed by the card and will be used by [SignCommand] and [CheckWalletCommand].
* RemainingSignature is set to MaxSignatures.
*
* @property cardId CID, Unique Tangem card ID number.
*/
class CreateWalletCommand : Command<CreateWalletResponse>() {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
}
if (session.environment.card?.status == CardStatus.Purged) {
callback(CompletionResult.Failure(TangemSdkError.CardIsPurged()))
return true
}
if (session.environment.card?.status == CardStatus.Loaded) {
callback(CompletionResult.Failure(TangemSdkError.AlreadyCreated()))
return true
}
return false
}
override fun performAfterCheck(session: CardSession,
result: CompletionResult<CreateWalletResponse>,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams) {
callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired()))
return true
}
return false
}
else -> return false
}
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
tlvBuilder.append(TlvTag.Cvc, environment.cvc)
return CommandApdu(
Instruction.CreateWallet, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): CreateWalletResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return CreateWalletResponse(
cardId = decoder.decode(TlvTag.CardId),
status = decoder.decode(TlvTag.Status),
walletPublicKey = decoder.decode(TlvTag.WalletPublicKey)
)
}
}

View file

@ -1,43 +0,0 @@
package com.tangem.commands
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class OpenSessionResponse(
val sessionKeyB: ByteArray,
val uid: ByteArray
) : CommandResponse
/**
* In case of encrypted communication, App should setup a session before calling any further command.
* [OpenSessionCommand] generates secret session_key that is used by both host and card
* to encrypt and decrypt commands payload.
*/
class OpenSessionCommand(private val sessionKeyA: ByteArray) : Command<OpenSessionResponse>() {
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.SessionKeyA, sessionKeyA)
return CommandApdu(
Instruction.OpenSession, tlvBuilder.serialize(),
encryptionMode = environment.encryptionMode
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): OpenSessionResponse {
val tlvData = apdu.getTlvData()
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return OpenSessionResponse(
sessionKeyB = decoder.decode(TlvTag.SessionKeyB),
uid = decoder.decode(TlvTag.Uid)
)
}
}

View file

@ -1,85 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class PurgeWalletResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Current status of the card [1 - Empty, 2 - Loaded, 3- Purged]
*/
val status: CardStatus
) : CommandResponse
/**
* 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 : Command<PurgeWalletResponse>() {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<PurgeWalletResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
}
if (session.environment.card?.settingsMask?.contains(Settings.ProhibitPurgeWallet) == true) {
callback(CompletionResult.Failure(TangemSdkError.PurgeWalletProhibited()))
return true
}
return false
}
override fun performAfterCheck(session: CardSession,
result: CompletionResult<PurgeWalletResponse>,
callback: (result: CompletionResult<PurgeWalletResponse>) -> Unit): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams) {
callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired()))
return true
}
return false
}
else -> return false
}
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
return CommandApdu(
Instruction.PurgeWallet, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): PurgeWalletResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return PurgeWalletResponse(
cardId = decoder.decode(TlvTag.CardId),
status = decoder.decode(TlvTag.Status))
}
}

View file

@ -1,454 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import java.util.*
/**
* Determines which type of data is required for signing.
*/
data class SigningMethodMask(val rawValue: Int) {
fun contains(signingMethod: SigningMethod): Boolean {
return if (rawValue and 0x80 == 0) {
signingMethod.code == rawValue
} else {
rawValue and (0x01 shl signingMethod.code) != 0
}
}
}
enum class SigningMethod(val code: Int) {
SignHash(0),
SignRaw(1),
SignHashValidateByIssuer(2),
SignRawValidateByIssuer(3),
SignHashValidateByIssuerWriteIssuerData(4),
SignRawValidateByIssuerWriteIssuerData(5),
SignPos(6)
}
class SigningMethodMaskBuilder() {
private val signingMethods = mutableSetOf<SigningMethod>()
fun add(signingMethod: SigningMethod) {
signingMethods.add(signingMethod)
}
fun build(): SigningMethodMask {
val rawValue: Int = when {
signingMethods.count() == 0 -> {
0
}
signingMethods.count() == 1 -> {
signingMethods.iterator().next().code
}
else -> {
signingMethods.fold(
0x80, { acc, singingMethod -> acc + (0x01 shl singingMethod.code) }
)
}
}
return SigningMethodMask(rawValue)
}
}
/**
* Elliptic curve used for wallet key operations.
*/
enum class EllipticCurve(val curve: String) {
Secp256k1("secp256k1"),
Ed25519("ed25519");
companion object {
private val values = values()
fun byName(curve: String): EllipticCurve? = values.find { it.curve == curve }
}
}
/**
* Status of the card and its wallet.
*/
enum class CardStatus(val code: Int) {
NotPersonalized(0),
Empty(1),
Loaded(2),
Purged(3);
companion object {
private val values = values()
fun byCode(code: Int): CardStatus? = values.find { it.code == code }
}
}
/**
* Mask of products enabled on card
* @property rawValue Products mask values,
* while flags definitions and values are in [ProductMask.Companion] as constants.
*/
data class ProductMask(val rawValue: Int) {
fun contains(product: Product): Boolean = (rawValue and product.code) != 0
}
enum class Product(val code: Int) {
Note(0x01),
Tag(0x02),
IdCard(0x04),
IdIssuer(0x08)
}
class ProductMaskBuilder() {
private var productMaskValue = 0
fun add(product: Product) {
productMaskValue = productMaskValue or product.code
}
fun build() = ProductMask(productMaskValue)
}
/**
* Stores and maps Tangem card settings.
*
* @property rawValue Card settings in a form of flags,
* while flags definitions and possible values are in [Settings].
*/
data class SettingsMask(val rawValue: Int) {
fun contains(settings: Settings): Boolean = (rawValue and settings.code) != 0
}
enum class Settings(val code: Int) {
IsReusable(0x0001),
UseActivation(0x0002),
ProhibitPurgeWallet(0x0004),
UseBlock(0x0008),
AllowSwapPIN(0x0010),
AllowSwapPIN2(0x0020),
UseCVC(0x0040),
ForbidDefaultPIN(0x0080),
UseOneCommandAtTime(0x0100),
UseNdef(0x0200),
UseDynamicNdef(0x0400),
SmartSecurityDelay(0x0800),
ProtocolAllowUnencrypted(0x1000),
ProtocolAllowStaticEncryption(0x2000),
ProtectIssuerDataAgainstReplay(0x4000),
RestrictOverwriteIssuerDataEx(0x00100000),
AllowSelectBlockchain(0x8000),
DisablePrecomputedNdef(0x00010000),
SkipSecurityDelayIfValidatedByLinkedTerminal(0x00080000),
SkipCheckPin2andCvcIfValidatedByIssuer(0x00040000),
SkipSecurityDelayIfValidatedByIssuer(0x00020000),
RequireTermTxSignature(0x01000000),
RequireTermCertSignature(0x02000000),
CheckPIN3onCard(0x04000000)
}
class SettingsMaskBuilder() {
private var settingsMaskValue = 0
fun add(settings: Settings) {
settingsMaskValue = settingsMaskValue or settings.code
}
fun build() = SettingsMask(settingsMaskValue)
}
/**
* Detailed information about card contents.
*/
class CardData(
/**
* Tangem internal manufacturing batch ID.
*/
val batchId: String?,
/**
* Timestamp of manufacturing.
*/
val manufactureDateTime: Date?,
/**
* Name of the issuer.
*/
val issuerName: String?,
/**
* Name of the blockchain.
*/
val blockchainName: String?,
/**
* Signature of CardId with manufacturers private key.
*/
val manufacturerSignature: ByteArray?,
/**
* Mask of products enabled on card.
*/
val productMask: ProductMask?,
/**
* Name of the token.
*/
val tokenSymbol: String?,
/**
* Smart contract address.
*/
val tokenContractAddress: String?,
/**
* Number of decimals in token value.
*/
val tokenDecimal: Int?
)
/**
* Response for [ReadCommand]. Contains detailed card information.
*/
class Card(
/**
* Unique Tangem card ID number.
*/
val cardId: String,
/**
* Name of Tangem card manufacturer.
*/
val manufacturerName: String,
/**
* Current status of the card.
*/
val status: CardStatus?,
/**
* Version of Tangem COS.
*/
val firmwareVersion: String?,
/**
* Public key that is used to authenticate the card against manufacturers database.
* It is generated one time during card manufacturing.
*/
val cardPublicKey: ByteArray?,
/**
* Card settings defined by personalization (bit mask: 0 Enabled, 1 Disabled).
*/
val settingsMask: SettingsMask?,
/**
* Public key that is used by the card issuer to sign IssuerData field.
*/
val issuerPublicKey: ByteArray?,
/**
* Explicit text name of the elliptic curve used for all wallet key operations.
* Supported curves: secp256k1 and ed25519.
*/
val curve: EllipticCurve?,
/**
* Total number of signatures allowed for the wallet when the card was personalized.
*/
val maxSignatures: Int?,
/**
* Defines what data should be submitted to SIGN command.
*/
val signingMethods: SigningMethodMask?,
/**
* Delay in seconds before COS executes commands protected by PIN2.
*/
val pauseBeforePin2: Int?,
/**
* Public key of the blockchain wallet.
*/
val walletPublicKey: ByteArray?,
/**
* Remaining number of [SignCommand] operations before the wallet will stop signing transactions.
*/
val walletRemainingSignatures: Int?,
/**
* Total number of signed single hashes returned by the card in
* [SignCommand] responses since card personalization.
* Sums up array elements within all [SignCommand].
*/
val walletSignedHashes: Int?,
/**
* Any non-zero value indicates that the card experiences some hardware problems.
* User should withdraw the value to other blockchain wallet as soon as possible.
* Non-zero Health tag will also appear in responses of all other commands.
*/
val health: Int?,
/**
* Whether the card requires issuers confirmation of activation.
*/
val isActivated: Boolean,
/**
* A random challenge generated by personalisation that should be signed and returned
* to COS by the issuer to confirm the card has been activated.
* This field will not be returned if the card is activated.
*/
val activationSeed: ByteArray?,
/**
* Returned only if [SigningMethod.SignPos] enabling POS transactions is supported by card.
*/
val paymentFlowVersion: ByteArray?,
/**
* This value can be initialized by terminal and will be increased by COS on execution of every [SignCommand].
* For example, this field can store blockchain nonce for quick one-touch transaction on POS terminals.
* Returned only if [SigningMethod.SignPos] enabling POS transactions is supported by card.
*/
val userCounter: Int?,
/**
* This value can be initialized by App (with PIN2 confirmation) and will be increased by COS
* with the execution of each [SignCommand]. For example, this field can store blockchain nonce
* for a quick one-touch transaction on POS terminals. Returned only if [SigningMethod.SignPos].
*/
val userProtectedCounter: Int?,
/**
* When this value is true, it means that the application is linked to the card,
* and COS will not enforce security delay if [SignCommand] will be called
* with [TlvTag.TerminalTransactionSignature] parameter containing a correct signature of raw data
* to be signed made with [TlvTag.TerminalPublicKey].
*/
val terminalIsLinked: Boolean,
/**
* Detailed information about card contents. Format is defined by the card issuer.
* Cards complaint with Tangem Wallet application should have TLV format.
*/
val cardData: CardData?
) : CommandResponse
/**
* This command receives from the Tangem Card all the data about the card and the wallet,
* including unique card number (CID or cardId) that has to be submitted while calling all other commands.
*/
class ReadCommand : Command<Card>() {
override fun performAfterCheck(session: CardSession, result: CompletionResult<Card>, callback: (result: CompletionResult<Card>) -> Unit): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams) {
callback(CompletionResult.Failure(TangemSdkError.Pin1Required()))
return true
}
return false
}
else -> return false
}
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
/**
* [SessionEnvironment] stores the pin1 value. If no pin1 value was set, it will contain
* default value of 000000.
* In order to obtain cards data, [ReadCommand] should use the correct pin 1 value.
* The card will not respond if wrong pin 1 has been submitted.
*/
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.TerminalPublicKey, environment.terminalKeys?.publicKey)
return CommandApdu(
Instruction.Read, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): Card {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return Card(
cardId = decoder.decodeOptional(TlvTag.CardId) ?: "",
manufacturerName = decoder.decodeOptional(TlvTag.ManufactureId) ?: "",
status = decoder.decodeOptional(TlvTag.Status),
firmwareVersion = decoder.decodeOptional(TlvTag.Firmware),
cardPublicKey = decoder.decodeOptional(TlvTag.CardPublicKey),
settingsMask = decoder.decodeOptional(TlvTag.SettingsMask),
issuerPublicKey = decoder.decodeOptional(TlvTag.IssuerDataPublicKey),
curve = decoder.decodeOptional(TlvTag.CurveId),
maxSignatures = decoder.decodeOptional(TlvTag.MaxSignatures),
signingMethods = decoder.decodeOptional(TlvTag.SigningMethod),
pauseBeforePin2 = decoder.decodeOptional(TlvTag.PauseBeforePin2),
walletPublicKey = decoder.decodeOptional(TlvTag.WalletPublicKey),
walletRemainingSignatures = decoder.decodeOptional(TlvTag.RemainingSignatures),
walletSignedHashes = decoder.decodeOptional(TlvTag.SignedHashes),
health = decoder.decodeOptional(TlvTag.Health),
isActivated = decoder.decode(TlvTag.IsActivated),
activationSeed = decoder.decodeOptional(TlvTag.ActivationSeed),
paymentFlowVersion = decoder.decodeOptional(TlvTag.PaymentFlowVersion),
userCounter = decoder.decodeOptional(TlvTag.UserCounter),
userProtectedCounter = decoder.decodeOptional(TlvTag.UserProtectedCounter),
terminalIsLinked = decoder.decode(TlvTag.TerminalIsLinked),
cardData = deserializeCardData(tlvData)
)
}
private fun deserializeCardData(tlvData: List<Tlv>): CardData? {
val cardDataTlvs = tlvData.find { it.tag == TlvTag.CardData }?.let {
Tlv.deserialize(it.value)
}
if (cardDataTlvs.isNullOrEmpty()) return null
val decoder = TlvDecoder(cardDataTlvs)
return CardData(
batchId = decoder.decodeOptional(TlvTag.Batch),
manufactureDateTime = decoder.decodeOptional(TlvTag.ManufactureDateTime),
issuerName = decoder.decodeOptional(TlvTag.IssuerId),
blockchainName = decoder.decodeOptional(TlvTag.BlockchainId),
manufacturerSignature = decoder.decodeOptional(TlvTag.ManufacturerSignature),
productMask = decoder.decodeOptional(TlvTag.ProductMask),
tokenSymbol = decoder.decodeOptional(TlvTag.TokenSymbol),
tokenContractAddress = decoder.decodeOptional(TlvTag.TokenContractAddress),
tokenDecimal = decoder.decodeOptional(TlvTag.TokenDecimal)
)
}
}

View file

@ -1,124 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.common.DefaultIssuerDataVerifier
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.commands.common.IssuerDataVerifier
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class ReadIssuerDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Data defined by issuer.
*/
val issuerData: ByteArray,
/**
* Issuers signature of [issuerData] with Issuer Data Private Key (which is kept on card).
* Issuers signature of SHA256-hashed [cardId] concatenated with [issuerData]:
* SHA256([cardId] | [issuerData]).
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
*/
val issuerDataSignature: ByteArray,
/**
* An optional counter that protect issuer data against replay attack.
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask]
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
*/
val issuerDataCounter: Int?
) : CommandResponse
/**
* This command returns 512-byte Issuer Data field and its issuers signature.
* Issuer Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. For example, this field may contain information about
* wallet balance signed by the issuer or additional issuers attestation data.
* @property cardId CID, Unique Tangem card ID number.
*/
class ReadIssuerDataCommand(
val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : Command<ReadIssuerDataResponse>(), IssuerDataVerifier by verifier {
override fun run(session: CardSession, callback: (result: CompletionResult<ReadIssuerDataResponse>) -> Unit) {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
val publicKey = issuerPublicKey ?: card.issuerPublicKey
if (publicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey()))
return
}
super.run(session) { result ->
when (result) {
is CompletionResult.Failure -> callback(result)
is CompletionResult.Success -> {
if (result.data.issuerData.isEmpty()) {
callback(result)
return@run
}
val issuerDataToVerify = IssuerDataToVerify(
card.cardId, result.data.issuerData, result.data.issuerDataCounter
)
if (verify(publicKey, result.data.issuerDataSignature, issuerDataToVerify)) {
callback(result)
} else {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
}
}
}
}
}
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<ReadIssuerDataResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
return false
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadData)
return CommandApdu(
Instruction.ReadIssuerData, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): ReadIssuerDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return ReadIssuerDataResponse(
cardId = decoder.decode(TlvTag.CardId),
issuerData = decoder.decode(TlvTag.IssuerData),
issuerDataSignature = decoder.decode(TlvTag.IssuerDataSignature),
issuerDataCounter = decoder.decodeOptional(TlvTag.IssuerDataCounter)
)
}
}

View file

@ -1,183 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.common.DefaultIssuerDataVerifier
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.commands.common.IssuerDataVerifier
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import java.io.ByteArrayOutputStream
class ReadIssuerExtraDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Size of all Issuer_Extra_Data field.
*/
val size: Int?,
/**
* Data defined by issuer.
*/
val issuerData: ByteArray,
/**
* Issuers signature of [issuerData] with Issuer Data Private Key (which is kept on card).
* Issuers signature of SHA256-hashed [cardId] concatenated with [issuerData]:
* SHA256([cardId] | [issuerData]).
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
*/
val issuerDataSignature: ByteArray?,
/**
* An optional counter that protects issuer data against replay attack.
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask]
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
*/
val issuerDataCounter: Int?
) : CommandResponse
/**
* This command retrieves Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. . For example, this field may contain photo or
* biometric information for ID card product. Because of the large size of Issuer_Extra_Data,
* a series of these commands have to be executed to read the entire Issuer_Extra_Data.
*/
class ReadIssuerExtraDataCommand(
private val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : Command<ReadIssuerExtraDataResponse>(), IssuerDataVerifier by verifier {
private val issuerData = ByteArrayOutputStream()
private var offset: Int = 0
private var issuerDataSize: Int = 0
override fun run(session: CardSession, callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit) {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
val publicKey = issuerPublicKey ?: card.issuerPublicKey
if (publicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey()))
return
}
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return
}
readIssuerData(session, card.cardId, publicKey, callback)
}
private fun readIssuerData(
session: CardSession,
cardId: String, publicKey: ByteArray,
callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit) {
if (issuerDataSize != 0) {
session.viewDelegate.onDelay(
issuerDataSize, offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE
)
}
transceive(session) { result ->
when (result) {
is CompletionResult.Success -> {
if (result.data.size != null) {
if (result.data.size == 0) {
callback(CompletionResult.Success(result.data))
return@transceive
}
issuerDataSize = result.data.size
}
issuerData.write(result.data.issuerData)
if (result.data.issuerDataSignature == null) {
offset = issuerData.size()
readIssuerData(session, cardId, publicKey, callback)
} else {
completeTask(result.data, cardId, publicKey, callback)
}
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
private fun completeTask(data: ReadIssuerExtraDataResponse,
cardId: String, publicKey: ByteArray,
callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit) {
val dataToVerify = IssuerDataToVerify(
cardId,
issuerData.toByteArray(),
data.issuerDataCounter
)
if (verify(publicKey, data.issuerDataSignature!!, dataToVerify)) {
val finalResult = ReadIssuerExtraDataResponse(
data.cardId,
issuerDataSize,
issuerData.toByteArray(),
data.issuerDataSignature,
data.issuerDataCounter
)
callback(CompletionResult.Success(finalResult))
} else {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
}
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadExtraData)
tlvBuilder.append(TlvTag.Offset, offset)
return CommandApdu(
Instruction.ReadIssuerData, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): ReadIssuerExtraDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return ReadIssuerExtraDataResponse(
cardId = decoder.decode(TlvTag.CardId),
size = decoder.decodeOptional(TlvTag.Size),
issuerData = decoder.decodeOptional(TlvTag.IssuerData) ?: byteArrayOf(),
issuerDataSignature = decoder.decodeOptional(TlvTag.IssuerDataSignature),
issuerDataCounter = decoder.decodeOptional(TlvTag.IssuerDataCounter)
)
}
companion object {
/**
* This mode value specifies that this command retrieves Issuer EXTRA data from the card
* (with value 0 the command will get instead simple Issuer Data from the card).
*/
const val EXTRA_DATA_MODE = 1
}
}

View file

@ -1,90 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class ReadUserDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Data defined by user's App.
*/
val userData: ByteArray,
/**
* Data defined by user's App (confirmed by PIN2).
*/
val userProtectedData: ByteArray,
/**
* Counter initialized by user's App and increased on every signing of new transaction
*/
val userCounter: Int,
/**
* Counter initialized by user's App (confirmed by PIN2) and increased on every signing of new transaction
*/
val userProtectedCounter: Int
) : CommandResponse
/**
* This command returns two up to 512-byte User_Data, User_Protected_Data and two counters User_Counter and
* User_Protected_Counter fields.
* User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and it's payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*/
class ReadUserDataCommand : Command<ReadUserDataResponse>() {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<ReadUserDataResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
}
return false
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val builder = TlvBuilder()
builder.append(TlvTag.CardId, environment.card?.cardId)
builder.append(TlvTag.Pin, environment.pin1)
return CommandApdu(
Instruction.ReadUserData, builder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): ReadUserDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return ReadUserDataResponse(
cardId = decoder.decode(TlvTag.CardId),
userData = decoder.decode(TlvTag.UserData),
userProtectedData = decoder.decode(TlvTag.UserProtectedData),
userCounter = decoder.decode(TlvTag.UserCounter),
userProtectedCounter = decoder.decode(TlvTag.UserProtectedCounter)
)
}
}

View file

@ -1,147 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.sign
/**
* @param cardId CID, Unique Tangem card ID number
* @param signature Signed hashes (array of resulting signatures)
* @param walletRemainingSignatures Remaining number of sign operations before the wallet will stop signing transactions.
* @param walletSignedHashes Total number of signed single hashes returned by the card in sign command responses.
* Sums up array elements within all SIGN commands
*/
class SignResponse(
val cardId: String,
val signature: ByteArray,
val walletRemainingSignatures: Int,
val walletSignedHashes: Int
) : CommandResponse
/**
* Signs transaction hashes using a wallet private key, stored on the card.
*
* @property hashes Array of transaction hashes.
* @property cardId CID, Unique Tangem card ID number
*/
class SignCommand(private val hashes: Array<ByteArray>)
: Command<SignResponse>() {
private val hashSizes = if (hashes.isNotEmpty()) hashes.first().size else 0
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<SignResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
}
if (session.environment.card?.status == CardStatus.Purged) {
callback(CompletionResult.Failure(TangemSdkError.CardIsPurged()))
return true
}
if (session.environment.card?.status == CardStatus.Empty) {
callback(CompletionResult.Failure(TangemSdkError.CardIsEmpty()))
return true
}
if (session.environment.card?.walletRemainingSignatures == 0) {
callback(CompletionResult.Failure(TangemSdkError.NoRemainingSignatures()))
return true
}
if (session.environment.card?.signingMethods?.contains(SigningMethod.SignHash) != true) {
callback(CompletionResult.Failure(TangemSdkError.SignHashesNotAvailable()))
return true
}
if (hashSizes == 0) {
callback(CompletionResult.Failure(TangemSdkError.EmptyHashes()))
return true
}
if (hashes.any { it.size != hashSizes }) {
callback(CompletionResult.Failure(TangemSdkError.HashSizeMustBeEqual()))
return true
}
return false
}
override fun performAfterCheck(session: CardSession,
result: CompletionResult<SignResponse>,
callback: (result: CompletionResult<SignResponse>) -> Unit): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams) {
callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired()))
return true
}
return false
}
else -> return false
}
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val dataToSign = flattenHashes()
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.TransactionOutHashSize, byteArrayOf(hashSizes.toByte()))
tlvBuilder.append(TlvTag.TransactionOutHash, dataToSign)
tlvBuilder.append(TlvTag.Cvc, environment.cvc)
addTerminalSignature(environment, dataToSign, tlvBuilder)
return CommandApdu(
Instruction.Sign, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
private fun flattenHashes(): ByteArray {
checkForErrors()
return hashes.reduce { arr1, arr2 -> arr1 + arr2 }
}
private fun checkForErrors() {
if (hashes.isEmpty()) throw TangemSdkError.EmptyHashes()
if (hashes.size > 10) throw TangemSdkError.TooManyHashesInOneTransaction()
if (hashes.any { it.size != hashSizes }) throw TangemSdkError.HashSizeMustBeEqual()
}
/**
* Application can optionally submit a public key Terminal_PublicKey in [SignCommand].
* Submitted key is stored by the Tangem card if it differs from a previous submitted Terminal_PublicKey.
* The Tangem card will not enforce security delay if [SignCommand] will be called with
* 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(
environment: SessionEnvironment, dataToSign: ByteArray, tlvBuilder: TlvBuilder) {
environment.terminalKeys?.let { terminalKeyPair ->
val signedData = dataToSign.sign(terminalKeyPair.privateKey)
tlvBuilder.append(TlvTag.TerminalTransactionSignature, signedData)
tlvBuilder.append(TlvTag.TerminalPublicKey, terminalKeyPair.publicKey)
}
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): SignResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return SignResponse(
cardId = decoder.decode(TlvTag.CardId),
signature = decoder.decode(TlvTag.Signature),
walletRemainingSignatures = decoder.decode(TlvTag.RemainingSignatures),
walletSignedHashes = decoder.decode(TlvTag.SignedHashes)
)
}
}

View file

@ -1,136 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.common.DefaultIssuerDataVerifier
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.commands.common.IssuerDataVerifier
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class WriteIssuerDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String
) : CommandResponse
/**
* This command writes 512-byte Issuer Data field and its issuers signature.
* Issuer Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. For example, this field may contain information about
* wallet balance signed by the issuer or additional issuers attestation data.
* @property cardId CID, Unique Tangem card ID number.
* @property issuerData Data provided by issuer.
* @property issuerDataSignature Issuers signature of [issuerData] with Issuer Data Private Key (which is kept on card).
* @property issuerDataCounter An optional counter that protect issuer data against replay attack.
*/
class WriteIssuerDataCommand(
private val issuerData: ByteArray,
private val issuerDataSignature: ByteArray,
private val issuerDataCounter: Int? = null,
private val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : Command<WriteIssuerDataResponse>(), IssuerDataVerifier by verifier {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit): Boolean {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return true
}
val publicKey = issuerPublicKey ?: card.issuerPublicKey
if (publicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey()))
return true
}
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
}
if (issuerData.size > MAX_SIZE) {
callback(CompletionResult.Failure(TangemSdkError.DataSizeTooLarge()))
return true
}
if (!isCounterValid(issuerDataCounter, card)) {
callback(CompletionResult.Failure(TangemSdkError.MissingCounter()))
return true
}
if (!verifySignature(publicKey, card.cardId)) {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
return true
}
return false
}
override fun performAfterCheck(session: CardSession,
result: CompletionResult<WriteIssuerDataResponse>,
callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit
): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams &&
isCounterRequired(session.environment.card)) {
callback(CompletionResult.Failure(TangemSdkError.DataCannotBeWritten()))
return true
}
return false
}
else -> return false
}
}
private fun isCounterValid(issuerDataCounter: Int?, card: Card): Boolean =
if (isCounterRequired(card)) issuerDataCounter != null else true
private fun isCounterRequired(card: Card?): Boolean =
card?.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false
private fun verifySignature(publicKey: ByteArray, cardId: String): Boolean {
return verify(
publicKey,
issuerDataSignature,
IssuerDataToVerify(cardId, issuerData, issuerDataCounter)
)
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.WriteData)
tlvBuilder.append(TlvTag.IssuerData, issuerData)
tlvBuilder.append(TlvTag.IssuerDataSignature, issuerDataSignature)
tlvBuilder.append(TlvTag.IssuerDataCounter, issuerDataCounter)
return CommandApdu(
Instruction.WriteIssuerData, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): WriteIssuerDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return WriteIssuerDataResponse(
cardId = decoder.decode(TlvTag.CardId)
)
}
companion object {
const val MAX_SIZE = 512
}
}

View file

@ -1,213 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.common.DefaultIssuerDataVerifier
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.commands.common.IssuerDataVerifier
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
/**
* This command writes Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS.
* The issuer defines purpose of use, format and payload of Issuer Data.
* For example, this field may contain a photo or biometric information for ID card products.
* Because of the large size of Issuer_Extra_Data, a series of these commands have to be executed
* to write entire Issuer_Extra_Data.
* @param issuerData Data provided by issuer.
* @param startingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerDataCounter] (if flags Protect_Issuer_Data_Against_Replay and
* Restrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]) and size of [issuerData].
* @param finalizingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerData] and [issuerDataCounter] (the latter one only if flags Protect_Issuer_Data_Against_Replay
* andRestrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]).
* @param issuerDataCounter An optional counter that protect issuer data against replay attack.
*/
class WriteIssuerExtraDataCommand(
private val issuerData: ByteArray,
private val startingSignature: ByteArray,
private val finalizingSignature: ByteArray,
private val issuerDataCounter: Int? = null,
private val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : Command<WriteIssuerDataResponse>(), IssuerDataVerifier by verifier {
var mode: IssuerDataMode = IssuerDataMode.InitializeWritingExtraData
var offset: Int = 0
override fun run(session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit) {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
val publicKey = issuerPublicKey ?: card.issuerPublicKey
if (publicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey()))
return
}
writeIssuerData(session, card.cardId, publicKey) { response ->
when (response) {
is CompletionResult.Success -> callback(response)
is CompletionResult.Failure -> {
if (response.error is TangemSdkError.InvalidParams && isCounterRequired(card)) {
callback(CompletionResult.Failure(TangemSdkError.DataCannotBeWritten()))
return@writeIssuerData
}
if (response.error is TangemSdkError.InvalidState &&
card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false) {
callback(CompletionResult.Failure(TangemSdkError.OverwritingDataIsProhibited()))
return@writeIssuerData
}
}
}
}
}
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit): Boolean {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return true
}
val publicKey = issuerPublicKey ?: card.issuerPublicKey
if (publicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey()))
return true
}
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
}
if (issuerData.size > MAX_SIZE) {
callback(CompletionResult.Failure(TangemSdkError.ExendedDataSizeTooLarge()))
return true
}
if (!isCounterValid(issuerDataCounter, card)) {
callback(CompletionResult.Failure(TangemSdkError.MissingCounter()))
return true
}
if (!verifySignatures(card.cardId, publicKey)) {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
return true
}
return false
}
private fun isCounterValid(issuerDataCounter: Int?, card: Card): Boolean =
if (isCounterRequired(card)) issuerDataCounter != null else true
private fun isCounterRequired(card: Card): Boolean =
card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false
private fun verifySignatures(cardId: String, publicKey: ByteArray): Boolean {
val firstData = IssuerDataToVerify(cardId, null, issuerDataCounter, issuerData.size)
val secondData = IssuerDataToVerify(cardId, issuerData, issuerDataCounter)
return verify(publicKey, startingSignature, firstData) &&
verify(publicKey, finalizingSignature, secondData)
}
private fun writeIssuerData(
session: CardSession,
cardId: String, publicKey: ByteArray,
callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit
) {
if (mode == IssuerDataMode.WriteExtraData) {
session.viewDelegate.onDelay(issuerData.size, offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE)
}
transceive(session) { result ->
when (result) {
is CompletionResult.Success -> {
when (mode) {
IssuerDataMode.InitializeWritingExtraData -> {
mode = IssuerDataMode.WriteExtraData
writeIssuerData(session, cardId, publicKey, callback)
return@transceive
}
IssuerDataMode.WriteExtraData -> {
offset += WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE
if (offset >= issuerData.size) {
mode = IssuerDataMode.FinalizeExtraData
}
writeIssuerData(session, cardId, publicKey, callback)
return@transceive
}
IssuerDataMode.FinalizeExtraData -> {
callback(CompletionResult.Success(result.data))
}
}
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, mode)
when (mode) {
IssuerDataMode.InitializeWritingExtraData -> {
tlvBuilder.append(TlvTag.Size, issuerData.size)
tlvBuilder.append(TlvTag.IssuerDataSignature, startingSignature)
tlvBuilder.append(TlvTag.IssuerDataCounter, issuerDataCounter)
}
IssuerDataMode.WriteExtraData -> {
tlvBuilder.append(TlvTag.IssuerData, getDataToWrite())
tlvBuilder.append(TlvTag.Offset, offset)
}
IssuerDataMode.FinalizeExtraData -> {
tlvBuilder.append(TlvTag.IssuerDataSignature, finalizingSignature)
}
}
return CommandApdu(
Instruction.WriteIssuerData, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
private fun getDataToWrite(): ByteArray =
issuerData.copyOfRange(offset, offset + calculatePartSize())
private fun calculatePartSize(): Int {
val bytesLeft = issuerData.size - offset
return if (bytesLeft < SINGLE_WRITE_SIZE) bytesLeft else SINGLE_WRITE_SIZE
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): WriteIssuerDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
return WriteIssuerDataResponse(cardId = TlvDecoder(tlvData).decode(TlvTag.CardId)
)
}
companion object {
const val SINGLE_WRITE_SIZE = 1524
const val MAX_SIZE = 32 * 1024
}
}

View file

@ -1,95 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class WriteUserDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String
) : CommandResponse
/**
* This command writes to the card any of User_Data, User_ProtectedData, User_Counter and User_ProtectedCounter fields.
* User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and it's payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*
* Writing of User_Counter and User_Data protected only by PIN1.
* User_ProtectedCounter and User_ProtectedData additionaly need PIN2 to confirmation.
*/
class WriteUserDataCommand(private val userData: ByteArray? = null, private val userProtectedData: ByteArray? = null,
private val userCounter: Int? = null,
private val userProtectedCounter: Int? = null) : Command<WriteUserDataResponse>() {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<WriteUserDataResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
}
if (userData?.size ?: 0 > MAX_SIZE || userProtectedData?.size ?: 0 > MAX_SIZE) {
callback(CompletionResult.Failure(TangemSdkError.DataSizeTooLarge()))
return true
}
return false
}
override fun performAfterCheck(session: CardSession,
result: CompletionResult<WriteUserDataResponse>,
callback: (result: CompletionResult<WriteUserDataResponse>) -> Unit
): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams) {
callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired()))
return true
}
return false
}
else -> return false
}
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val builder = TlvBuilder()
builder.append(TlvTag.CardId, environment.card?.cardId)
builder.append(TlvTag.Pin, environment.pin1)
builder.append(TlvTag.UserData, userData)
builder.append(TlvTag.UserCounter, userCounter)
builder.append(TlvTag.UserProtectedData, userProtectedData)
builder.append(TlvTag.UserProtectedCounter, userProtectedCounter)
if (userProtectedCounter != null || userProtectedData != null)
builder.append(TlvTag.Pin2, environment.pin2)
return CommandApdu(
Instruction.WriteUserData, builder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): WriteUserDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
return WriteUserDataResponse(TlvDecoder(tlvData).decode(TlvTag.CardId))
}
companion object{
const val MAX_SIZE = 512
}
}

View file

@ -1,44 +0,0 @@
package com.tangem.commands.common
import com.tangem.commands.WriteIssuerExtraDataCommand
/**
* This enum specifies modes for [WriteIssuerExtraDataCommand].
*/
enum class IssuerDataMode(val code: Byte) {
/**
* This mode is required to read issuer data from the card.
*/
ReadData(0),
/**
* This mode is required to write issuer data to the card.
*/
WriteData(0),
/**
* This mode is required to read issuer extra data from the card.
*/
ReadExtraData(1),
/**
* This mode is required to initiate writing issuer extra data to the card.
*/
InitializeWritingExtraData(1),
/**
* With this mode, the command writes part of issuer extra data
* (block of a size [WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE]) to the card.
*/
WriteExtraData(2),
/**
* This mode is used after the issuer extra data was fully written to the card.
* Under this mode the command provides the issuer signature
* to confirm the validity of data that was written to card.
*/
FinalizeExtraData(3);
companion object {
private val values = values()
fun byCode(code: Byte): IssuerDataMode? = values.find { it.code == code }
}
}

View file

@ -1,40 +0,0 @@
package com.tangem.commands.common
import com.tangem.common.tlv.TlvEncoder
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.CryptoUtils
import java.io.ByteArrayOutputStream
interface IssuerDataVerifier {
fun verify(
issuerPublicKey: ByteArray, signature: ByteArray, issuerDataToVerify: IssuerDataToVerify
): Boolean
}
class IssuerDataToVerify(
val cardId: String,
val issuerData: ByteArray?,
val issuerDataCounter: Int? = null,
val issuerExtraDataSize: Int? = null
)
class DefaultIssuerDataVerifier : IssuerDataVerifier {
override fun verify(
issuerPublicKey: ByteArray,
signature: ByteArray,
issuerDataToVerify: IssuerDataToVerify
): Boolean {
val tlvEncoder = TlvEncoder()
val dataToVerify = ByteArrayOutputStream()
dataToVerify.write(tlvEncoder.encodeValue(TlvTag.CardId, issuerDataToVerify.cardId))
issuerDataToVerify.issuerData?.let { dataToVerify.write(it) }
issuerDataToVerify.issuerDataCounter?.let { counter ->
dataToVerify.write(tlvEncoder.encodeValue(TlvTag.IssuerDataCounter, counter))
}
issuerDataToVerify.issuerExtraDataSize?.let {
dataToVerify.write(tlvEncoder.encodeValue(TlvTag.Size, it))
}
return CryptoUtils.verify(issuerPublicKey, dataToVerify.toByteArray(), signature)
}
}

View file

@ -1,119 +0,0 @@
package com.tangem.commands.common
import com.google.gson.*
import com.tangem.commands.*
import com.tangem.common.extensions.print
import com.tangem.common.extensions.toHexString
import java.lang.reflect.Type
import java.text.DateFormat
import java.util.*
/**
[REDACTED_AUTHOR]
*/
class ResponseConverter {
val gson: Gson by lazy { init() }
private val fieldConverter = ResponseFieldConverter()
private fun init(): Gson {
val builder = GsonBuilder().apply {
registerTypeAdapter(ByteArray::class.java, ByteTypeAdapter(fieldConverter))
registerTypeAdapter(SigningMethodMask::class.java, SigningMethodTypeAdapter(fieldConverter))
registerTypeAdapter(SettingsMask::class.java, SettingsMaskTypeAdapter(fieldConverter))
registerTypeAdapter(ProductMask::class.java, ProductMaskTypeAdapter(fieldConverter))
registerTypeAdapter(Date::class.java, DateTypeAdapter())
}
builder.setPrettyPrinting()
return builder.create()
}
fun convertResponse(response: CommandResponse?): String = gson.toJson(response)
}
class ByteTypeAdapter(
private val fieldConverter: ResponseFieldConverter
) : JsonSerializer<ByteArray> {
override fun serialize(src: ByteArray, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
return JsonPrimitive(fieldConverter.byteArrayToHex(src))
}
}
class SettingsMaskTypeAdapter(
private val fieldConverter: ResponseFieldConverter
) : JsonSerializer<SettingsMask> {
override fun serialize(src: SettingsMask, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
return JsonArray().apply {
fieldConverter.settingsMaskList(src).forEach { add(it) }
}
}
}
class ProductMaskTypeAdapter(
private val fieldConverter: ResponseFieldConverter
) : JsonSerializer<ProductMask> {
override fun serialize(src: ProductMask, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
return JsonArray().apply {
fieldConverter.productMaskList(src).forEach { add(it) }
}
}
}
class SigningMethodTypeAdapter(
private val fieldConverter: ResponseFieldConverter
) : JsonSerializer<SigningMethodMask> {
override fun serialize(src: SigningMethodMask, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
return JsonArray().apply {
fieldConverter.signingMethodList(src).forEach { add(it) }
}
}
}
class DateTypeAdapter : JsonSerializer<Date> {
override fun serialize(src: Date, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
val formatter = DateFormat.getDateInstance(DateFormat.DEFAULT, Locale("en_US"))
return JsonPrimitive(formatter.format(src).toString())
}
}
class ResponseFieldConverter {
fun productMask(productMask: ProductMask?): String {
return productMaskList(productMask).print(wrap = false)
}
fun productMaskList(productMask: ProductMask?): List<String> {
val mask = productMask ?: return emptyList()
return Product.values().filter { mask.contains(it) }.map { it.name }
}
fun signingMethod(signingMask: SigningMethodMask?): String {
return signingMethodList(signingMask).print(wrap = false)
}
fun signingMethodList(signingMask: SigningMethodMask?): List<String> {
val mask = signingMask ?: return emptyList()
return SigningMethod.values().filter { mask.contains(it) }.map { it.name }
}
fun settingsMask(settingsMask: SettingsMask?): String {
return settingsMaskList(settingsMask).print(wrap = false)
}
fun settingsMaskList(settingsMask: SettingsMask?): List<String> {
val masks = settingsMask ?: return emptyList()
return Settings.values().filter { masks.contains(it) }.map { it.name }
}
fun byteArrayToHex(byteArray: ByteArray?): String? {
return byteArray?.toHexString()
}
fun byteArrayToString(byteArray: ByteArray?): String? {
return if (byteArray == null) null else String(byteArray)
}
}

View file

@ -1,43 +0,0 @@
package com.tangem.commands.personalization
import com.tangem.SessionEnvironment
import com.tangem.commands.Command
import com.tangem.commands.CommandResponse
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
data class DepersonalizeResponse(val success: Boolean) : CommandResponse
/**
* Command available on SDK cards only
*
* This command resets card to initial state,
* erasing all data written during personalization and usage.
*/
class DepersonalizeCommand : Command<DepersonalizeResponse>() {
override val performPreflightRead = false
// override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<DepersonalizeResponse>) -> Unit): Boolean {
// if (session.environment.card?.status == CardStatus.NotPersonalized) {
// callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
// return true
// }
// if (session.environment.card?.firmwareVersion?.contains("SDK") == false) {
// callback(CompletionResult.Failure(TangemSdkError.CannotBeDepersonalized()))
// return true
// }
// return false
// }
override fun serialize(environment: SessionEnvironment): CommandApdu {
return CommandApdu(
Instruction.Depersonalize, byteArrayOf()
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): DepersonalizeResponse {
return DepersonalizeResponse(true)
}
}

View file

@ -1,88 +0,0 @@
package com.tangem.commands.personalization
import com.tangem.commands.personalization.entities.NdefRecord
import java.io.ByteArrayOutputStream
import java.nio.charset.StandardCharsets
/**
* Encodes information that is to be written on the card as an Ndef Tag.
*/
class NdefEncoder(private val ndefRecords: List<NdefRecord>, private val useDinamicNdef: Boolean) {
fun encode(): ByteArray {
val bs = ByteArrayOutputStream()
// space for size
bs.write(0)
bs.write(0)
for (i in ndefRecords.indices) {
val headerValue = (if (i == 0) 0x80 else 0x00) or (if (!useDinamicNdef && i == ndefRecords.size - 1) 0x40 else 0x00)
var value: ByteArray = ndefRecords[i].value.toByteArray(StandardCharsets.UTF_8)
encodeValue(ndefRecords[i], headerValue, bs)
}
val result = bs.toByteArray()
result[0] = (result.size - 2 shr 8).toByte()
result[1] = (result.size - 2 and 0xFF).toByte()
return result
}
private fun encodeValue(ndefRecord: NdefRecord, headerValue: Int, bs: ByteArrayOutputStream) {
when (ndefRecord.type) {
NdefRecord.Type.AAR -> {
bs.write((headerValue or 0x14)) // NDEF Header
bs.write(0x0F) // Length of the record type
bs.write(ndefRecord.valueInBytes.size) // Length of the payload data
bs.write(byteArrayOf(0x61.toByte(), 0x6E.toByte(), 0x64.toByte(), 0x72.toByte(), 0x6F.toByte(), 0x69.toByte(), 0x64.toByte(), 0x2E.toByte(), 0x63.toByte(), 0x6F.toByte(), 0x6D.toByte(), 0x3A.toByte(),
0x70.toByte(), 0x6B.toByte(), 0x67.toByte())) // type name
bs.write(ndefRecord.valueInBytes)
}
NdefRecord.Type.URI -> {
bs.write((headerValue or 0x11)) // NDEF Header
bs.write(0x01) // Length of the record type
val uriIdentifierCode: Byte
val prefix: String
when {
ndefRecord.value.startsWith("http://www.") -> {
uriIdentifierCode = 0x01.toByte()
prefix = "http://www."
}
ndefRecord.value.startsWith("https://www.") -> {
uriIdentifierCode = 0x02.toByte()
prefix = "https://www."
}
ndefRecord.value.startsWith("http://") -> {
uriIdentifierCode = 0x03.toByte()
prefix = "http://"
}
ndefRecord.value.startsWith("https://") -> {
uriIdentifierCode = 0x04.toByte()
prefix = "https://"
}
else -> {
throw Exception()
}
}
val value = ndefRecord.value.substring(prefix.length).toByteArray()
bs.write(value.size + 1) // Length of the payload data
bs.write(0x55) // URI
bs.write(uriIdentifierCode.toInt()) // ?
bs.write(value)
}
NdefRecord.Type.TEXT -> {
bs.write((headerValue or 0x11)) // NDEF Header
bs.write(0x01) // Length of the record type
bs.write(ndefRecord.valueInBytes.size.toByte() + 1 + "en".length) // Length of the payload data
bs.write(0x54) // Text
bs.write(0x02) // UTF8(MSB=0)|"en".length
bs.write("en".toByteArray(StandardCharsets.US_ASCII))
bs.write(ndefRecord.valueInBytes)
}
else -> throw Exception("Invalid NDEF record in config!")
}
}
}

View file

@ -1,168 +0,0 @@
package com.tangem.commands.personalization
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.Card
import com.tangem.commands.CardData
import com.tangem.commands.CardStatus
import com.tangem.commands.Command
import com.tangem.commands.personalization.entities.*
import com.tangem.common.CompletionResult
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.TlvDecoder
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.sign
/**
* Command available on SDK cards only
*
* Personalization is an initialization procedure, required before starting using a card.
* During this procedure a card setting is set up.
* During this procedure all data exchange is encrypted.
* @property config is a configuration file with all the card settings that are written on the card
* during personalization.
* @property issuer Issuer is a third-party team or company wishing to use Tangem cards.
* @property manufacturer Tangem Card Manufacturer.
* @property acquirer Acquirer is a trusted third-party company that operates proprietary
* (non-EMV) POS terminal infrastructure and transaction processing back-end.
*/
class PersonalizeCommand(
private val config: CardConfig,
private val issuer: Issuer, private val manufacturer: Manufacturer,
private val acquirer: Acquirer? = null
) : Command<Card>() {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit): Boolean {
if (session.environment.card?.status != CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.AlreadyPersonalized()))
return true
}
return false
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
return CommandApdu(
Instruction.Personalize,
serializePersonalizationData(config),
encryptionKey = devPersonalizationKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): Card {
val tlvData = apdu.getTlvData(devPersonalizationKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return Card(
cardId = decoder.decodeOptional(TlvTag.CardId) ?: "",
manufacturerName = decoder.decodeOptional(TlvTag.ManufactureId) ?: "",
status = decoder.decodeOptional(TlvTag.Status),
firmwareVersion = decoder.decodeOptional(TlvTag.Firmware),
cardPublicKey = decoder.decodeOptional(TlvTag.CardPublicKey),
settingsMask = decoder.decodeOptional(TlvTag.SettingsMask),
issuerPublicKey = decoder.decodeOptional(TlvTag.IssuerDataPublicKey),
curve = decoder.decodeOptional(TlvTag.CurveId),
maxSignatures = decoder.decodeOptional(TlvTag.MaxSignatures),
signingMethods = decoder.decodeOptional(TlvTag.SigningMethod),
pauseBeforePin2 = decoder.decodeOptional(TlvTag.PauseBeforePin2),
walletPublicKey = decoder.decodeOptional(TlvTag.WalletPublicKey),
walletRemainingSignatures = decoder.decodeOptional(TlvTag.RemainingSignatures),
walletSignedHashes = decoder.decodeOptional(TlvTag.SignedHashes),
health = decoder.decodeOptional(TlvTag.Health),
isActivated = decoder.decode(TlvTag.IsActivated),
activationSeed = decoder.decodeOptional(TlvTag.ActivationSeed),
paymentFlowVersion = decoder.decodeOptional(TlvTag.PaymentFlowVersion),
userCounter = decoder.decodeOptional(TlvTag.UserCounter),
userProtectedCounter = decoder.decodeOptional(TlvTag.UserProtectedCounter),
terminalIsLinked = decoder.decode(TlvTag.TerminalIsLinked),
cardData = deserializeCardData(tlvData)
)
}
private fun deserializeCardData(tlvData: List<Tlv>): CardData? {
val cardDataTlvs = tlvData.find { it.tag == TlvTag.CardData }?.let {
Tlv.deserialize(it.value)
}
if (cardDataTlvs.isNullOrEmpty()) return null
val decoder = TlvDecoder(cardDataTlvs)
return CardData(
batchId = decoder.decodeOptional(TlvTag.Batch),
manufactureDateTime = decoder.decodeOptional(TlvTag.ManufactureDateTime),
issuerName = decoder.decodeOptional(TlvTag.IssuerId),
blockchainName = decoder.decodeOptional(TlvTag.BlockchainId),
manufacturerSignature = decoder.decodeOptional(TlvTag.ManufacturerSignature),
productMask = decoder.decodeOptional(TlvTag.ProductMask),
tokenSymbol = decoder.decodeOptional(TlvTag.TokenSymbol),
tokenContractAddress = decoder.decodeOptional(TlvTag.TokenContractAddress),
tokenDecimal = decoder.decodeOptional(TlvTag.TokenDecimal)
)
}
private fun serializePersonalizationData(config: CardConfig): ByteArray {
val cardId = config.createCardId() ?: throw TangemSdkError.SerializeCommandError()
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.CardId, cardId)
tlvBuilder.append(TlvTag.CurveId, config.curveID)
tlvBuilder.append(TlvTag.MaxSignatures, config.maxSignatures)
tlvBuilder.append(TlvTag.SigningMethod, config.signingMethods)
tlvBuilder.append(TlvTag.SettingsMask, config.createSettingsMask())
tlvBuilder.append(TlvTag.PauseBeforePin2, config.pauseBeforePin2 / 10)
tlvBuilder.append(TlvTag.Cvc, config.cvc.toByteArray())
if (!config.ndefRecords.isNullOrEmpty())
tlvBuilder.append(TlvTag.NdefData, serializeNdef(config))
tlvBuilder.append(TlvTag.CreateWalletAtPersonalize, config.createWallet)
tlvBuilder.append(TlvTag.NewPin, config.pin)
tlvBuilder.append(TlvTag.NewPin2, config.pin2)
tlvBuilder.append(TlvTag.NewPin3, config.pin3)
tlvBuilder.append(TlvTag.CrExKey, config.hexCrExKey)
tlvBuilder.append(TlvTag.IssuerDataPublicKey, issuer.dataKeyPair.publicKey)
tlvBuilder.append(TlvTag.IssuerTransactionPublicKey, issuer.transactionKeyPair.publicKey)
tlvBuilder.append(TlvTag.AcquirerPublicKey, acquirer?.keyPair?.publicKey)
tlvBuilder.append(TlvTag.CardData, serializeCardData(cardId, config.cardData))
return tlvBuilder.serialize()
}
private fun serializeNdef(config: CardConfig): ByteArray {
return NdefEncoder(config.ndefRecords, config.useDynamicNdef).encode()
}
private fun serializeCardData(cardId: String, cardData: CardData): ByteArray {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Batch, cardData.batchId)
tlvBuilder.append(TlvTag.ProductMask, cardData.productMask)
tlvBuilder.append(TlvTag.ManufactureDateTime, cardData.manufactureDateTime)
tlvBuilder.append(TlvTag.IssuerId, issuer.id)
tlvBuilder.append(TlvTag.BlockchainId, cardData.blockchainName)
if (cardData.tokenSymbol != null) {
tlvBuilder.append(TlvTag.TokenSymbol, cardData.tokenSymbol)
tlvBuilder.append(TlvTag.TokenContractAddress, cardData.tokenContractAddress)
tlvBuilder.append(TlvTag.TokenDecimal, cardData.tokenDecimal)
}
tlvBuilder.append(
TlvTag.CardIdManufacturerSignature,
cardId.hexToBytes().sign(manufacturer.keyPair.privateKey)
)
return tlvBuilder.serialize()
}
companion object {
val devPersonalizationKey = "1234".calculateSha256().copyOf(32)
}
}

View file

@ -1,9 +0,0 @@
package com.tangem.commands.personalization.entities
import com.tangem.KeyPair
data class Acquirer(
val keyPair: KeyPair,
val name: String? = null,
val id: String? = null
)

View file

@ -1,71 +0,0 @@
package com.tangem.commands.personalization.entities
import com.tangem.commands.CardData
import com.tangem.commands.EllipticCurve
import com.tangem.commands.SigningMethodMask
data class NdefRecord(
val type: Type,
val value: String
) {
enum class Type {
URI, AAR, TEXT
}
@delegate:Transient
val valueInBytes: ByteArray by lazy { value.toByteArray() }
}
/**
* It is a configuration file with all the card settings that are written on the card
* during [PersonalizeCommand].
*/
data class CardConfig(
val issuerName: String? = null,
val acquirerName: String? = null,
val series: String? = null,
val startNumber: Long = 0,
val count: Int = 0,
val pin: String,
val pin2: String,
val pin3: String,
val hexCrExKey: String?,
val cvc: String,
val pauseBeforePin2: Int,
val smartSecurityDelay: Boolean,
val curveID: EllipticCurve,
val signingMethods: SigningMethodMask,
val maxSignatures: Int,
val isReusable: Boolean,
val allowSwapPin: Boolean,
val allowSwapPin2: Boolean,
val useActivation: Boolean,
val useCvc: Boolean,
val useNdef: Boolean,
val useDynamicNdef: Boolean,
val useOneCommandAtTime: Boolean,
val useBlock: Boolean,
val allowSelectBlockchain: Boolean,
val forbidPurgeWallet: Boolean,
val protocolAllowUnencrypted: Boolean,
val protocolAllowStaticEncryption: Boolean,
val protectIssuerDataAgainstReplay: Boolean,
val forbidDefaultPin: Boolean,
val disablePrecomputedNdef: Boolean,
val skipSecurityDelayIfValidatedByIssuer: Boolean,
val skipCheckPIN2andCVCIfValidatedByIssuer: Boolean,
val skipSecurityDelayIfValidatedByLinkedTerminal: Boolean,
val restrictOverwriteIssuerDataEx: Boolean,
val requireTerminalTxSignature: Boolean,
val requireTerminalCertSignature: Boolean,
val checkPin3onCard: Boolean,
val createWallet: Boolean,
val cardData: CardData,
val ndefRecords: List<NdefRecord>
) {
companion object
}

View file

@ -1,81 +0,0 @@
package com.tangem.commands.personalization.entities
import com.tangem.commands.Settings
import com.tangem.commands.SettingsMask
import com.tangem.commands.SettingsMaskBuilder
internal fun CardConfig.createSettingsMask(): SettingsMask {
val builder = SettingsMaskBuilder()
if (allowSwapPin) builder.add(Settings.AllowSwapPIN)
if (allowSwapPin2) builder.add(Settings.AllowSwapPIN2)
if (useCvc) builder.add(Settings.UseCVC)
if (isReusable) builder.add(Settings.IsReusable)
if (useOneCommandAtTime) builder.add(Settings.UseOneCommandAtTime)
if (useNdef) builder.add(Settings.UseNdef)
if (useDynamicNdef) builder.add(Settings.UseDynamicNdef)
if (disablePrecomputedNdef) builder.add(Settings.DisablePrecomputedNdef)
if (protocolAllowUnencrypted) builder.add(Settings.ProtocolAllowUnencrypted)
if (protocolAllowStaticEncryption) builder.add(Settings.ProtocolAllowStaticEncryption)
if (forbidDefaultPin) builder.add(Settings.ForbidDefaultPIN)
if (useActivation) builder.add(Settings.UseActivation)
if (useBlock) builder.add(Settings.UseBlock)
if (smartSecurityDelay) builder.add(Settings.SmartSecurityDelay)
if (protectIssuerDataAgainstReplay) builder.add(Settings.ProtectIssuerDataAgainstReplay)
if (forbidPurgeWallet) builder.add(Settings.ProhibitPurgeWallet)
if (allowSelectBlockchain) builder.add(Settings.AllowSelectBlockchain)
if (skipCheckPIN2andCVCIfValidatedByIssuer) builder.add(Settings.SkipCheckPin2andCvcIfValidatedByIssuer)
if (skipSecurityDelayIfValidatedByIssuer) builder.add(Settings.SkipSecurityDelayIfValidatedByIssuer)
if (skipSecurityDelayIfValidatedByLinkedTerminal) builder.add(Settings.SkipSecurityDelayIfValidatedByLinkedTerminal)
if (restrictOverwriteIssuerDataEx) builder.add(Settings.RestrictOverwriteIssuerDataEx)
if (requireTerminalTxSignature) builder.add(Settings.RequireTermTxSignature)
if (requireTerminalCertSignature) builder.add(Settings.RequireTermCertSignature)
if (checkPin3onCard) builder.add(Settings.CheckPIN3onCard)
return builder.build()
}
internal fun CardConfig.createCardId(): String? {
if (series == null) return null
if (startNumber <= 0 || (series.length != 2 && series.length != 4)) return null
val Alf = "ABCDEF0123456789"
fun checkSeries(series: String): Boolean {
val containsList = series.filter { Alf.contains(it) }
return containsList.length == series.length
}
if (!checkSeries(series)) return null
val tail = if (series.length == 2) String.format("%013d", startNumber) else String.format("%011d", startNumber)
var cardId = (series + tail).replace(" ", "")
if (cardId.length != 15 || Alf.indexOf(cardId[0]) == -1 || Alf.indexOf(cardId[1]) == -1)
return null
cardId += "0"
val length = cardId.length
var sum = 0
for (i in 0 until length) {
// get digits in reverse order
var digit: Int
val cDigit = cardId[length - i - 1]
digit = if (cDigit in '0'..'9') cDigit - '0' else cDigit - 'A'
// every 2nd number multiply with 2
if (i % 2 == 1) digit *= 2
sum += if (digit > 9) digit - 9 else digit
}
val lunh = (10 - sum % 10) % 10
return cardId.substring(0, 15) + String.format("%d", lunh)
}

View file

@ -1,10 +0,0 @@
package com.tangem.commands.personalization.entities
import com.tangem.KeyPair
data class Issuer(
val name: String,
val id: String,
val dataKeyPair: KeyPair,
val transactionKeyPair: KeyPair
)

View file

@ -1,8 +0,0 @@
package com.tangem.commands.personalization.entities
import com.tangem.KeyPair
data class Manufacturer(
val keyPair: KeyPair,
val name: String? = null
)

View file

@ -1,13 +0,0 @@
package com.tangem.common
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult.Success
/**
* Response class encapsulating successful and failed results.
* @param T Type of data that is returned in [Success].
*/
sealed class CompletionResult<T> {
class Success<T>(val data: T) : CompletionResult<T>()
class Failure<T>(val error: TangemSdkError) : CompletionResult<T>()
}

View file

@ -1,14 +0,0 @@
package com.tangem.common
import com.tangem.KeyPair
/**
* Interface for a service for managing Terminal keypair, used for Linked Terminal feature.
* Its implementation Needs to be provided to [com.tangem.TangemSdk]
* by calling [com.tangem.TangemSdk.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
}

View file

@ -1,98 +0,0 @@
package com.tangem.common.apdu
import com.tangem.EncryptionMode
import com.tangem.common.extensions.calculateCrc16
import com.tangem.common.extensions.toByteArray
import com.tangem.crypto.encrypt
import java.io.ByteArrayOutputStream
/**
* Class that provides conversion of serialized request and Instruction code
* to a raw data that can be sent to the card.
*
* @property ins Instruction code that determines the type of request for the card.
* @property tlvs Tlvs encoded to a [ByteArray] that are to be sent to the card.
*/
class CommandApdu(
private val ins: Int,
private val tlvs: ByteArray,
private val le: Int = 0x00,
private val encryptionMode: EncryptionMode = EncryptionMode.NONE,
private val encryptionKey: ByteArray? = null,
private val cla: Int = ISO_CLA) {
constructor(
instruction: Instruction,
tlvs: ByteArray,
encryptionMode: EncryptionMode = EncryptionMode.NONE,
encryptionKey: ByteArray? = null
) : this(
instruction.code,
tlvs,
encryptionMode = encryptionMode,
encryptionKey = encryptionKey
)
private val p1: Int
private val p2: Int
init {
if (ins == Instruction.OpenSession.code) {
p1 = 0x00
p2 = encryptionMode.code.toInt()
} else {
p1 = encryptionMode.code.toInt()
p2 = 0x00
}
}
/**
* Request converted to a raw data
*/
val apduData: ByteArray
init {
apduData = toBytes()
}
private fun toBytes(): ByteArray {
val data = if (encryptionKey != null) tlvs.encrypt() else tlvs
val byteStream = ByteArrayOutputStream()
byteStream.write(cla)
byteStream.write(ins)
byteStream.write(p1)
byteStream.write(p2)
if (data.isNotEmpty()) {
byteStream.writeLength(data.size)
byteStream.write(data)
}
return byteStream.toByteArray()
}
private fun ByteArrayOutputStream.writeLength(lc: Int) {
this.write(0)
this.write(lc shr 8)
this.write(lc and 0xFF)
}
private fun ByteArray.encrypt(): ByteArray {
val crc: ByteArray = tlvs.calculateCrc16()
val stream = ByteArrayOutputStream()
stream.write(this.size.toByteArray(2))
stream.write(crc)
stream.write(this)
return stream.toByteArray().encrypt(encryptionKey!!)
}
companion object {
const val ISO_CLA = 0x00
}
}

View file

@ -1,32 +0,0 @@
package com.tangem.common.apdu
/**
* Instruction code that determines the type of the command that is sent to the Tangem card.
* It is used in the construction of [com.tangem.common.apdu.CommandApdu].
*/
enum class Instruction(var code: Int) {
Unknown(0x00),
Personalize(0xF1),
Read(0xF2),
VerifyCard(0xF3),
ValidateCard(0xF4),
VerifyCode(0xF5),
WriteIssuerData(0xF6),
ReadIssuerData(0xF7),
CreateWallet(0xF8),
CheckWallet(0xF9),
SwapPIN(0xFA),
Sign(0xFB),
PurgeWallet(0xFC),
Activate(0xFE),
OpenSession(0xFF),
WriteUserData(0xE0),
ReadUserData(0xE1),
Depersonalize(0xE3);
companion object {
private val values = values()
fun byCode(code: Int): Instruction = values.find { it.code == code } ?: Unknown
}
}

View file

@ -1,66 +0,0 @@
package com.tangem.common.apdu
import com.tangem.common.extensions.calculateCrc16
import com.tangem.common.tlv.Tlv
import com.tangem.crypto.decrypt
import java.io.ByteArrayInputStream
/**
* Stores response data from the card and parses it to [Tlv] and [StatusWord].
*
* @property data Raw response from the card.
* @property sw Status word code, reflecting the status of the response.
* @property statusWord Parsed status word.
*/
class ResponseApdu(private 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)
/**
* Converts raw response data to the list of TLVs.
*
* @param encryptionKey key to decrypt response.
* (Encryption / decryption functionality is not implemented yet.)
*/
fun getTlvData(encryptionKey: ByteArray? = null): List<Tlv>? {
return if (data.size <= 2) {
null
} else {
val responseData = data.copyOf(data.size - 2)
return if (encryptionKey != null) {
if (data.size >= 18) {
val decryptedData = decrypt(responseData, encryptionKey)
Tlv.deserialize(decryptedData)
} else {
null
}
} else {
Tlv.deserialize(responseData)
}
}
}
private fun decrypt(responseData: ByteArray, encryptionKey: ByteArray): ByteArray {
val decryptedData: ByteArray = responseData.decrypt(encryptionKey)
val inputStream = ByteArrayInputStream(decryptedData)
val baLength = ByteArray(2)
inputStream.read(baLength)
val length = (baLength[0].toInt() and 0xFF) * 256 + (baLength[1].toInt() and 0xFF)
if (length > decryptedData.size - 4) throw Exception("Can't decrypt - data size invalid")
val baCRC = ByteArray(2)
inputStream.read(baCRC)
val answerData = ByteArray(length)
inputStream.read(answerData)
val crc: ByteArray = answerData.calculateCrc16()
if (!baCRC.contentEquals(crc)) throw Exception("Can't decrypt - crc invalid")
return answerData
}
}

View file

@ -1,40 +0,0 @@
package com.tangem.common.apdu
import com.tangem.TangemSdkError
/**
* Part of a response from the card, shows the status of the operation
*/
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"),
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"),
Unknown(0x0000, "SW_UNKNOWN");
companion object {
private val values = values()
fun byCode(code: Int): StatusWord = values.find { it.code == code } ?: Unknown
}
}
fun StatusWord.toTangemSdkError(): TangemSdkError? {
return when (this) {
StatusWord.ProcessCompleted, StatusWord.Pin1Changed,
StatusWord.Pin2Changed, StatusWord.PinsChanged -> null
StatusWord.NeedPause -> null
StatusWord.InvalidParams -> TangemSdkError.InvalidParams()
StatusWord.ErrorProcessingCommand -> TangemSdkError.ErrorProcessingCommand()
StatusWord.InvalidState -> TangemSdkError.InvalidState()
StatusWord.InsNotSupported -> TangemSdkError.InsNotSupported()
StatusWord.NeedEncryption -> TangemSdkError.NeedEncryption()
StatusWord.Unknown -> TangemSdkError.UnknownStatus()
}
}

View file

@ -1,7 +0,0 @@
package com.tangem.common.extensions
import java.math.BigDecimal
fun BigDecimal.isZero() : Boolean {
return this.compareTo(BigDecimal.ZERO) == 0
}

View file

@ -1,74 +0,0 @@
package com.tangem.common.extensions
import org.spongycastle.crypto.digests.RIPEMD160Digest
import org.spongycastle.jce.ECNamedCurveTable
import java.nio.ByteBuffer
import java.security.MessageDigest
import java.util.*
import kotlin.experimental.and
import kotlin.experimental.xor
/**
* Extension functions for [ByteArray].
*/
fun ByteArray.toHexString(): String = joinToString("") { "%02x".format(it) }
fun ByteArray.toUtf8(): String = String(this).removeSuffix("\u0000")
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 = copyOfRange(0, 2).toInt()
val month = if (this.size > 2) this[2] - 1 else 0
val day = if (this.size > 3) this[3].toInt() else 0
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)
fun ByteArray.calculateSha256(): ByteArray = MessageDigest.getInstance("SHA-256").digest(this)
fun ByteArray.calculateRipemd160(): ByteArray {
val digest = RIPEMD160Digest()
digest.update(this, 0, this.size)
val out = ByteArray(20)
digest.doFinal(out, 0)
return out
}
fun ByteArray.toCompressedPublicKey(): ByteArray {
return if (this.size == 65) {
val spec = ECNamedCurveTable.getParameterSpec("secp256k1")
val publicKeyPoint = spec.curve.decodePoint(this)
publicKeyPoint.getEncoded(true)
} else {
this
}
}
fun ByteArray.calculateCrc16(): ByteArray {
var chBlock: Byte
// STEP 1 Initialize the CRC-16 value
var wCRC = 0x6363 // ITU-V.41
var i = 0
// STEP 2 Update data and Calucuate their CRC
do {
chBlock = this.get(i++)
chBlock = chBlock xor (wCRC and 0x00FF).toByte()
val chBlockInt = (chBlock.toInt() xor (chBlock.toInt() shl 4))
wCRC = wCRC shr 8 xor (chBlockInt and 0xFF shl 8) and 0xFFFF xor (chBlockInt and 0xFF shl 3 and 0xFFFF) xor (chBlockInt and 0xFF shr 4 and 0xFFFF)
// (wCRC>>8)^((int)chBlock<<8)^((int) chBlock<<3)^((int)chBlock>>4);
} while (i < this.size)
return byteArrayOf((wCRC and 0xFF).toByte(), (wCRC and 0xFFFF shr 8).toByte())
}

View file

@ -1,22 +0,0 @@
package com.tangem.common.extensions
import com.tangem.commands.Card
fun Card.getType(): CardType {
val firmware = this.firmwareVersion ?: return CardType.Unknown
return when {
firmware.endsWith("d SDK") -> {
CardType.Sdk
}
firmware.endsWith("r") -> {
CardType.Release
}
else -> {
CardType.Unknown
}
}
}
enum class CardType {
Sdk, Release, Unknown
}

View file

@ -1,16 +0,0 @@
package com.tangem.common.extensions
import java.nio.ByteBuffer
fun Int.toByteArray(size: Int = Int.SIZE_BYTES): ByteArray {
if (size == Int.SIZE_BYTES) {
val buffer = ByteBuffer.allocate(size)
buffer.putInt(this)
return buffer.array()
} else if (size == Short.SIZE_BYTES){
return byteArrayOf(
(this ushr 8).toByte(),
this.toByte())
}
return byteArrayOf()
}

View file

@ -1,13 +0,0 @@
package com.tangem.common.extensions
fun <T> List<T>.print(delimiter: String = ", ", wrap: Boolean = true): String {
val builder = StringBuilder()
forEach { builder.append(it).append(delimiter) }
val length = builder.length
if (length > delimiter.length) {
builder.delete(length - delimiter.length, length)
}
val result = builder.toString()
return if (wrap) "[$result]" else result
}

View file

@ -1,26 +0,0 @@
package com.tangem.common.extensions
import java.nio.charset.Charset
import java.security.MessageDigest
/**
* Extension functions for [String].
*/
fun String.calculateSha256(): ByteArray {
val sha256 = MessageDigest.getInstance("SHA-256")
val data = this.toByteArray(Charset.forName("UTF-8"))
return sha256.digest(data)
}
fun String.calculateSha512(): ByteArray {
val sha = MessageDigest.getInstance("SHA-512")
val data = this.toByteArray(Charset.forName("UTF-8"))
return sha.digest(data)
}
fun String.hexToBytes(): ByteArray {
return ByteArray(this.length / 2)
{ i ->
Integer.parseInt(this.substring(2 * i, 2 * i + 2), 16).toByte()
}
}

View file

@ -1,105 +0,0 @@
package com.tangem.common.tlv
import com.tangem.Log
import com.tangem.common.extensions.toHexString
import java.io.ByteArrayInputStream
import java.io.IOException
/**
* The data converted to the Tag Length Value protocol.
*/
class Tlv {
val tag: TlvTag
val value: ByteArray
val tagRaw: Int
constructor(tagCode: Int, value: ByteArray = byteArrayOf()) {
this.tag = TlvTag.byCode(tagCode)
this.tagRaw = tagCode
this.value = value
}
constructor(tag: TlvTag, value: ByteArray = byteArrayOf()) {
this.tag = tag
this.tagRaw = tag.code
this.value = value
}
companion object {
private 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 deserialize(data: ByteArray, nfcV: Boolean = false): List<Tlv>? {
val tlvList = mutableListOf<Tlv>()
val stream = ByteArrayInputStream(data)
var tlv: Tlv?
do {
try {
tlv = tlvFromBytes(stream)
if (tlv != null) tlvList.add(tlv)
} catch (e: IOException) {
Log.e(this::class.java.simpleName,"TLVError: " + e.message)
if (nfcV) break else return null
}
} while (tlv != null)
return tlvList
}
}
override fun toString(): String {
return "${this.tag} ($tagRaw): ${value.toHexString()}"
}
}
fun List<Tlv>.serialize(): ByteArray =
this.map { it.serialize() }.reduce { arr1, arr2 -> arr1 + arr2 }
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)
return tag + length + value
}
private fun getLengthInBytes(tlvLength: Int): ByteArray {
return if (tlvLength > 0) {
if (tlvLength > 0xFE) {
byteArrayOf(
0xFF.toByte(),
(tlvLength shr 8 and 0xFF).toByte(),
(tlvLength and 0xFF).toByte()
)
} else {
byteArrayOf((tlvLength and 0xFF).toByte())
}
} else {
byteArrayOf()
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.common.tlv
import com.tangem.Log
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 {
Log.v("TLV",
"Data encoded to TLVs:\n${tlvs.joinToString("\n")}")
return tlvs.serialize()
}
}

View file

@ -1,162 +0,0 @@
package com.tangem.common.tlv
import com.tangem.Log
import com.tangem.TangemSdkError
import com.tangem.commands.*
import com.tangem.commands.common.IssuerDataMode
import com.tangem.common.extensions.toDate
import com.tangem.common.extensions.toHexString
import com.tangem.common.extensions.toInt
import com.tangem.common.extensions.toUtf8
import java.util.*
/**
* Maps value fields in [Tlv] from raw [ByteArray] to concrete classes
* according to their [TlvTag] and corresponding [TlvValueType].
*
* @property tlvList List of TLVs, which values are to be converted to particular classes.
*/
class TlvDecoder(val tlvList: List<Tlv>) {
init {
Log.v("TLV",
"Decoding data from TLV:\n${tlvList.joinToString("\n")}")
}
/**
* Finds [Tlv] by its [TlvTag].
* Returns null if [Tlv] is not found, otherwise converts its value to [T].
*
* @param tag [TlvTag] of a [Tlv] which value is to be returned.
*
* @return Value converted to a nullable type [T].
*/
inline fun <reified T> decodeOptional(tag: TlvTag): T? =
try {
decode<T>(tag, false)
} catch (exception: TangemSdkError.DecodingFailedMissingTag) {
null
}
/**
* Finds [Tlv] by its [TlvTag].
* Throws [TaskError.MissingTag] if [Tlv] is not found,
* otherwise converts [Tlv] value to [T].
*
* @param tag [TlvTag] of a [Tlv] which value is to be returned.
*
* @return [Tlv] value converted to a nullable type [T].
*
* @throws [TangemSdkError.DecodingFailedMissingTag] exception if no [Tlv] is found by the Tag.
*/
inline fun <reified T> decode(tag: TlvTag, logError: Boolean = true): T {
val tlvValue: ByteArray = tlvList.find { it.tag == tag }?.value
?: if (tag.valueType() == TlvValueType.BoolValue && T::class == Boolean::class) {
return false as T
} else {
if (logError) {
Log.e(this::class.simpleName!!, "TLV $tag not found")
} else {
Log.v(this::class.simpleName!!, "TLV $tag not found, but it is not required")
}
throw TangemSdkError.DecodingFailedMissingTag()
}
return when (tag.valueType()) {
TlvValueType.HexString, TlvValueType.HexStringToHash -> {
typeCheck<T, String>(tag)
tlvValue.toHexString() as T
}
TlvValueType.Utf8String -> {
typeCheck<T, String>(tag)
tlvValue.toUtf8() as T
}
TlvValueType.Uint16, TlvValueType.Uint32 -> {
typeCheck<T, Int>(tag)
try {
tlvValue.toInt() as T
} catch (exception: IllegalArgumentException) {
Log.e(this::class.simpleName!!, exception.message ?: "")
throw TangemSdkError.DecodingFailed()
}
}
TlvValueType.BoolValue -> {
typeCheck<T, Boolean>(tag)
true as T
}
TlvValueType.ByteArray -> {
typeCheck<T, ByteArray>(tag)
tlvValue as T
}
TlvValueType.EllipticCurve -> {
typeCheck<T, EllipticCurve>(tag)
try {
EllipticCurve.byName(tlvValue.toUtf8()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.toUtf8(), exception)
throw TangemSdkError.DecodingFailed()
}
}
TlvValueType.DateTime -> {
typeCheck<T, Date>(tag)
try {
tlvValue.toDate() as T
} catch (exception: Exception) {
logException(tag, tlvValue.toHexString(), exception)
throw TangemSdkError.DecodingFailed()
}
}
TlvValueType.ProductMask -> {
typeCheck<T, ProductMask>(tag)
ProductMask(tlvValue.toInt()) as T
}
TlvValueType.SettingsMask -> {
typeCheck<T, SettingsMask>(tag)
SettingsMask(tlvValue.toInt()) as T
}
TlvValueType.CardStatus -> {
typeCheck<T, CardStatus>(tag)
try {
CardStatus.byCode(tlvValue.toInt()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.toInt().toString(), exception)
throw TangemSdkError.DecodingFailed()
}
}
TlvValueType.SigningMethod -> {
typeCheck<T, SigningMethodMask>(tag)
try {
SigningMethodMask(tlvValue.toInt()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.toInt().toString(), exception)
throw TangemSdkError.DecodingFailed()
}
}
TlvValueType.IssuerDataMode -> {
typeCheck<T, IssuerDataMode>(tag)
try {
IssuerDataMode.byCode(tlvValue.toInt().toByte()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.toInt().toString(), exception)
throw TangemSdkError.DecodingFailed()
}
}
}
}
fun logException(tag: TlvTag, value: String, exception: Exception) {
Log.e(this::class.simpleName!!,
"Unknown ${tag.name} with value of: value, \n${exception.message}")
}
inline fun <reified T, reified ExpectedT> typeCheck(tag: TlvTag) {
if (T::class != ExpectedT::class) {
Log.e(this::class.simpleName!!,
"Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
throw TangemSdkError.DecodingFailedTypeMismatch()
}
}
}

View file

@ -1,112 +0,0 @@
package com.tangem.common.tlv
import com.tangem.Log
import com.tangem.TangemSdkError
import com.tangem.commands.*
import com.tangem.commands.common.IssuerDataMode
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toByteArray
import java.util.*
/**
* Encodes information that is to be written on the card from parsed classes into [ByteArray]
* (according to the provided [TlvTag] and corresponding [TlvValueType])
* and then forms [Tlv] with the encoded values.
*/
class TlvEncoder {
/**
* @param value information that is to be encoded into [Tlv].
*/
internal inline fun <reified T> encode(tag: TlvTag, value: T?): Tlv {
if (value != null) {
return Tlv(tag, encodeValue(tag, value))
} else {
Log.e(this::class.simpleName!!, "Encoding error. Value for tag $tag is null")
throw TangemSdkError.EncodingFailed()
}
}
internal inline fun <reified T> encodeValue(tag: TlvTag, value: T): ByteArray {
return when (tag.valueType()) {
TlvValueType.HexString -> {
typeCheck<T, String>(tag)
(value as String).hexToBytes()
}
TlvValueType.HexStringToHash -> {
typeCheck<T, String>(tag)
(value as String).calculateSha256()
}
TlvValueType.Utf8String -> {
typeCheck<T, String>(tag)
(value as String).toByteArray()
}
TlvValueType.Uint16 -> {
typeCheck<T, Int>(tag)
(value as Int).toByteArray(2)
}
TlvValueType.Uint32 -> {
typeCheck<T, Int>(tag)
(value as Int).toByteArray()
}
TlvValueType.BoolValue -> {
typeCheck<T, Boolean>(tag)
val booleanValue = value as Boolean
if (booleanValue) byteArrayOf(1) else byteArrayOf(0)
}
TlvValueType.ByteArray -> {
typeCheck<T, ByteArray>(tag)
value as ByteArray
}
TlvValueType.EllipticCurve -> {
typeCheck<T, EllipticCurve>(tag)
(value as EllipticCurve).curve.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(2) + month.toByte() + day.toByte()
}
TlvValueType.ProductMask -> {
typeCheck<T, ProductMask>(tag)
byteArrayOf(
(value as ProductMask).rawValue.toByte()
)
}
TlvValueType.SettingsMask -> {
typeCheck<T, SettingsMask>(tag)
val rawValue = (value as SettingsMask).rawValue
rawValue.toByteArray(determineByteArraySize(rawValue))
}
TlvValueType.CardStatus -> {
typeCheck<T, CardStatus>(tag)
(value as CardStatus).code.toByteArray()
}
TlvValueType.SigningMethod -> {
typeCheck<T, SigningMethodMask>(tag)
byteArrayOf((value as SigningMethodMask).rawValue.toByte())
}
TlvValueType.IssuerDataMode -> {
typeCheck<T, IssuerDataMode>(tag)
byteArrayOf((value as IssuerDataMode).code)
}
}
}
private fun determineByteArraySize(value: Int): Int {
val mask = 0xFFFF0000.toInt()
return if ((value and mask) != 0) 4 else 2
}
private inline fun <reified T, reified ExpectedT> typeCheck(tag: TlvTag) {
if (T::class != ExpectedT::class) {
Log.e(this::class.simpleName!!,
"Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
throw TangemSdkError.EncodingFailedTypeMismatch()
}
}
}

View file

@ -1,151 +0,0 @@
package com.tangem.common.tlv
/**
* Contains all possible value types that value for [TlvTag] can contain.
*/
enum class TlvValueType {
HexString,
HexStringToHash,
Utf8String,
Uint16,
Uint32,
BoolValue,
ByteArray,
EllipticCurve,
DateTime,
ProductMask,
SettingsMask,
CardStatus,
SigningMethod,
IssuerDataMode
}
/**
* Contains all TLV tags, with their code and descriptive name.
*/
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),
CreateWalletAtPersonalize(0x0E),
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),
NewPin3(0x1E),
CrExKey(0x1F),
Uid(0x0B),
ManufactureId(0x20),
ManufacturerSignature(0x86),
IssuerDataPublicKey(0x30),
IssuerTransactionPublicKey(0x31),
IssuerData(0x32),
IssuerDataSignature(0x33),
IssuerTransactionSignature(0x34),
IssuerDataCounter(0x35),
AcquirerPublicKey(0x37),
Size(0x25),
Mode(0x23),
Offset(0x24),
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),
TokenSymbol(0xA0),
TokenContractAddress(0xA1),
TokenDecimal(0xA2),
Denomination(0xC0),
ValidatedBalance(0xC1),
LastSignDate(0xC2),
DenominationText(0xC3),
TerminalIsLinked(0x58),
TerminalPublicKey(0x5C),
TerminalTransactionSignature(0x57),
UserData(0x2A),
UserProtectedData(0x2B),
UserCounter(0x2C),
UserProtectedCounter(0x2D);
/**
* @return [TlvValueType] associated with a [TlvTag]
*/
fun valueType(): TlvValueType {
return when (this) {
CardId, Batch, CrExKey -> TlvValueType.HexString
NewPin, NewPin2, NewPin3 -> TlvValueType.HexStringToHash
ManufactureId, Firmware, IssuerId, BlockchainId, TokenSymbol, TokenContractAddress ->
TlvValueType.Utf8String
CurveId -> TlvValueType.EllipticCurve
PauseBeforePin2, RemainingSignatures, SignedHashes, Health, TokenDecimal,
Offset, Size -> TlvValueType.Uint16
MaxSignatures, UserCounter, UserProtectedCounter, IssuerDataCounter -> TlvValueType.Uint32
IsActivated, TerminalIsLinked, CreateWalletAtPersonalize -> TlvValueType.BoolValue
ManufactureDateTime -> TlvValueType.DateTime
ProductMask -> TlvValueType.ProductMask
SettingsMask -> TlvValueType.SettingsMask
Status -> TlvValueType.CardStatus
SigningMethod -> TlvValueType.SigningMethod
Mode -> TlvValueType.IssuerDataMode
else -> TlvValueType.ByteArray
}
}
companion object {
private val values = values()
fun byCode(code: Int): TlvTag = values.find { it.code == code } ?: Unknown
}
}

View file

@ -1,121 +0,0 @@
package com.tangem.crypto
import com.tangem.commands.EllipticCurve
import net.i2p.crypto.eddsa.EdDSASecurityProvider
import org.spongycastle.jce.provider.BouncyCastleProvider
import java.security.PublicKey
import java.security.SecureRandom
import java.security.Security
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object CryptoUtils {
fun initCrypto() {
Security.insertProviderAt(BouncyCastleProvider(), 1)
Security.addProvider(EdDSASecurityProvider())
}
/**
* Generates ByteArray of random bytes.
* It is used, among other things, to generate helper private keys
* (not the one for the blockchains, that one is generated on the card and does not leave the card).
*
* @param length length of the ByteArray that is to be generated.
*/
fun generateRandomBytes(length: Int): ByteArray {
val bytes = ByteArray(length)
SecureRandom().nextBytes(bytes)
return bytes
}
/**
* Helper function to verify that the data was signed with a private key that corresponds
* to the provided public key.
*
* @param publicKey Corresponding to the private key that was used to sing a message
* @param message The data that was signed
* @param signature Signed data
* @param curve Elliptic curve used
*
* @return Result of a verification
*/
fun verify(publicKey: ByteArray, message: ByteArray, signature: ByteArray,
curve: EllipticCurve = EllipticCurve.Secp256k1): Boolean {
return when (curve) {
EllipticCurve.Secp256k1 -> Secp256k1.verify(publicKey, message, signature)
EllipticCurve.Ed25519 -> Ed25519.verify(publicKey, message, signature)
}
}
/**
* Helper function that generates public key from a private key.
*
* @param privateKeyArray A private key from which a public key is generated
* @param curve Elliptic curve used
*
* @return Public key [ByteArray]
*/
fun generatePublicKey(
privateKeyArray: ByteArray,
curve: EllipticCurve = EllipticCurve.Secp256k1
): ByteArray {
return when (curve) {
EllipticCurve.Secp256k1 -> Secp256k1.generatePublicKey(privateKeyArray)
EllipticCurve.Ed25519 -> Ed25519.generatePublicKey(privateKeyArray)
}
}
fun loadPublicKey(
publicKey: ByteArray,
curve: EllipticCurve = EllipticCurve.Secp256k1
): PublicKey {
return when (curve) {
EllipticCurve.Secp256k1 -> Secp256k1.loadPublicKey(publicKey)
EllipticCurve.Ed25519 -> Ed25519.loadPublicKey(publicKey)
}
}
}
/**
* Extension function to sign a ByteArray with an elliptic curve cryptography.
*
* @param privateKeyArray Key to sign data
* @param curve Elliptic curve that is used to sign data
*
* @return Signed data
*/
fun ByteArray.sign(privateKeyArray: ByteArray, curve: EllipticCurve = EllipticCurve.Secp256k1): ByteArray {
return when (curve) {
EllipticCurve.Secp256k1 -> Secp256k1.sign(this, privateKeyArray)
EllipticCurve.Ed25519 -> Ed25519.sign(this, privateKeyArray)
}
}
fun ByteArray.encrypt(key: ByteArray, usePkcs7: Boolean = true): ByteArray {
val spec = if (usePkcs7) ENCRYPTION_SPEC_PKCS7 else ENCRYPTION_SPEC_NO_PADDING
val secretKeySpec = SecretKeySpec(key, spec)
val cipher = Cipher.getInstance(spec, "SC")
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, IvParameterSpec(ByteArray(16)))
return cipher.doFinal(this)
}
fun ByteArray.decrypt(key: ByteArray, usePkcs7: Boolean = true): ByteArray {
val spec = if (usePkcs7) ENCRYPTION_SPEC_PKCS7 else ENCRYPTION_SPEC_NO_PADDING
val secretKeySpec = SecretKeySpec(key, spec)
val cipher = Cipher.getInstance(spec)
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, IvParameterSpec(ByteArray(16)))
return cipher.doFinal(this.copyOfRange(0, this.size))
}
fun ByteArray.pbkdf2Hash(salt: ByteArray, iterations: Int): ByteArray {
return Pbkdf2().deriveKey(this, salt, iterations)
}
private const val ENCRYPTION_SPEC_PKCS7 = "AES/CBC/PKCS7PADDING"
private const val ENCRYPTION_SPEC_NO_PADDING = "AES/CBC/NOPADDING"

View file

@ -1,55 +0,0 @@
package com.tangem.crypto
import com.tangem.common.extensions.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
object Ed25519 {
internal fun verify(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)
}
internal fun loadPublicKey(publicKeyArray: ByteArray): PublicKey {
val spec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519)
val pubKey = EdDSAPublicKeySpec(publicKeyArray, spec)
return EdDSAPublicKey(pubKey)
}
internal fun sign(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()
}
internal fun generatePublicKey(privateKeyArray: ByteArray): ByteArray {
val spec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519)
val privateKeySpec = EdDSAPrivateKeySpec(privateKeyArray, spec)
val publicKeySpec = EdDSAPublicKeySpec(privateKeySpec.a, spec)
val publicKey = EdDSAPublicKey(publicKeySpec)
return publicKey.abyte
}
}

View file

@ -1,50 +0,0 @@
package com.tangem.crypto
import org.spongycastle.jce.interfaces.ECPublicKey
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.SecureRandom
import java.security.spec.ECGenParameterSpec
import javax.crypto.KeyAgreement
interface EncryptionHelper {
val keyA: ByteArray
fun generateSecret(keyB: ByteArray): ByteArray
}
class StrongEncryptionHelper : EncryptionHelper {
private val keyPair = generateKeyPair()
private val keyAgreement = generateKeyAgreement(keyPair)
override val keyA = provideKeyA(keyPair)
override fun generateSecret(keyB: ByteArray): ByteArray {
keyAgreement.doPhase(CryptoUtils.loadPublicKey(keyB), true)
return keyAgreement.generateSecret()
}
private fun generateKeyPair(): KeyPair {
val kpgen = KeyPairGenerator.getInstance("ECDH", "SC")
kpgen.initialize(ECGenParameterSpec("secp256k1"), SecureRandom())
return kpgen.generateKeyPair()
}
private fun generateKeyAgreement(keyPair: KeyPair): KeyAgreement {
val keyAgreement = KeyAgreement.getInstance("ECDH", "SC")
keyAgreement.init(keyPair.private)
return keyAgreement
}
private fun provideKeyA(keyPair: KeyPair): ByteArray {
val eckey = keyPair.public as ECPublicKey
return eckey.q.getEncoded(false)
}
}
class FastEncryptionHelper : EncryptionHelper {
override val keyA = CryptoUtils.generateRandomBytes(16)
override fun generateSecret(keyB: ByteArray): ByteArray {
return keyA + keyB
}
}

View file

@ -1,88 +0,0 @@
package com.tangem.crypto
import org.spongycastle.crypto.CipherParameters
import org.spongycastle.crypto.digests.SHA256Digest
import org.spongycastle.crypto.macs.HMac
import org.spongycastle.crypto.params.KeyParameter
import java.security.InvalidKeyException
import java.util.*
import kotlin.experimental.xor
import kotlin.math.min
import kotlin.math.pow
class Pbkdf2 {
private val F: HMac = HMac(SHA256Digest())
fun deriveKey(password: ByteArray, salt: ByteArray, iterations: Int): ByteArray {
val macSize = F.macSize
// Check key length
if (macSize > (2.0.pow(32.0) - 1) * macSize) throw InvalidKeyException("Derived key to long")
val derivedKey = ByteArray(macSize)
val J = 0
val K: Int = macSize
val U: Int = macSize shl 1
val B = K + U
val workingArray = ByteArray(K + U + 4)
// Initialize F
val macParams: CipherParameters = KeyParameter(password)
F.init(macParams)
// Perform iterations
var kpos = 0
var blk = 1
while (kpos < macSize) {
storeInt32BE(blk, workingArray, B)
F.update(salt, 0, salt.size)
F.reset()
F.update(salt, 0, salt.size)
F.update(workingArray, B, 4)
F.doFinal(workingArray, U)
System.arraycopy(workingArray, U, workingArray, J, K)
var i = 1
var j = J
var k = K
while (i < iterations) {
F.init(macParams)
F.update(workingArray, j, K)
F.doFinal(workingArray, k)
var u = U
var v = k
while (u < B) {
workingArray[u] = workingArray[u] xor workingArray[v]
u++
v++
}
val swp = k
k = j
j = swp
i++
}
val tocpy = min(macSize - kpos, K)
System.arraycopy(workingArray, U, derivedKey, kpos, tocpy)
kpos += K
blk++
}
Arrays.fill(workingArray, 0.toByte())
return derivedKey
}
/**
* Convert a 32-bit integer value into a big-endian byte array
*
* @param value The integer value to convert
* @param bytes The byte array to store the converted value
* @param offSet The offset in the output byte array
*/
private fun storeInt32BE(value: Int, bytes: ByteArray, offSet: Int) {
bytes[offSet + 3] = value.toByte()
bytes[offSet + 2] = (value ushr 8).toByte()
bytes[offSet + 1] = (value ushr 16).toByte()
bytes[offSet] = (value ushr 24).toByte()
}
}

View file

@ -1,118 +0,0 @@
package com.tangem.crypto
import com.tangem.common.extensions.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
object Secp256k1 {
internal fun verify(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)
}
internal 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 sign(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 (!verify(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
}
internal fun generatePublicKey(privateKeyArray: ByteArray): ByteArray {
val spec = ECNamedCurveTable.getParameterSpec("secp256k1")
return spec.g.multiply(BigInteger(1, privateKeyArray)).getEncoded(false)
}
}

View file

@ -1,45 +0,0 @@
package com.tangem.tasks
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.TangemSdkError
import com.tangem.commands.CardStatus
import com.tangem.commands.CheckWalletCommand
import com.tangem.commands.CreateWalletCommand
import com.tangem.commands.CreateWalletResponse
import com.tangem.common.CompletionResult
class CreateWalletTask : CardSessionRunnable<CreateWalletResponse> {
override val performPreflightRead = true
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
val curve = session.environment.card?.curve
if (curve == null) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
val command = CreateWalletCommand()
command.run(session) { createWalletResult ->
when (createWalletResult) {
is CompletionResult.Failure -> callback(createWalletResult)
is CompletionResult.Success -> {
if (createWalletResult.data.status != CardStatus.Loaded) {
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
} else {
val checkWalletCommand = CheckWalletCommand(
curve, createWalletResult.data.walletPublicKey
)
checkWalletCommand.run(session) { result ->
when (result) {
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
is CompletionResult.Success -> callback(createWalletResult)
}
}
}
}
}
}
}
}

View file

@ -1,45 +0,0 @@
package com.tangem.tasks
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.TangemSdkError
import com.tangem.commands.*
import com.tangem.common.CompletionResult
/**
* Task that allows to read Tangem card and verify its private key.
*
* It performs two commands, [ReadCommand] and [CheckWalletCommand], subsequently.
*/
internal class ScanTask : CardSessionRunnable<Card> {
override val performPreflightRead = true
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
} else if (card.cardData?.productMask?.contains(Product.Tag) != false) {
callback(CompletionResult.Success(card))
} else if (card.status != CardStatus.Loaded) {
callback(CompletionResult.Success(card))
} else if (card.curve == null || card.walletPublicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
} else {
val checkWalletCommand = CheckWalletCommand(card.curve, card.walletPublicKey)
checkWalletCommand.run(session) { result ->
when (result) {
is CompletionResult.Success -> callback(CompletionResult.Success(card))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}
}

View file

@ -1,54 +0,0 @@
package com.tangem.common.apdu
import com.google.common.truth.Truth.assertThat
import com.tangem.SessionEnvironment
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvTag
import org.junit.Test
class CommandApduTest {
@Test
fun `simple READ command to bytes`() {
val sessionEnvironment = SessionEnvironment()
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, sessionEnvironment.pin1)
val commandApdu = CommandApdu(
Instruction.Read,
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)
assertThat(commandApdu.apduData)
.isEqualTo(expected)
}
@Test
fun `READ with terminal key to bytes`() {
val sessionEnvironment = SessionEnvironment()
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, sessionEnvironment.pin1)
tlvBuilder.append(TlvTag.TerminalPublicKey, terminalPublicKey)
val commandApdu = CommandApdu(
Instruction.Read,
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,
-10, -45, 19, -124, -123, -55, -94, 3, 92, 65, 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)
assertThat(commandApdu.apduData)
.isEqualTo(expected)
}
}

View file

@ -1,44 +0,0 @@
package com.tangem.common.apdu
import com.google.common.truth.Truth.assertThat
import com.tangem.common.tlv.TlvTag
import org.junit.Test
class ResponseApduTest {
@Test
fun `get StatusWord returns Unknown`() {
val corruptData = byteArrayOf(0, 0, 0, 0)
val responseApdu = ResponseApdu(corruptData)
assertThat(responseApdu.statusWord)
.isEqualTo(StatusWord.Unknown)
}
@Test
fun `get StatusWord returns ProcessCompleted`() {
val data = byteArrayOf(0, 0, 0, 0, -112, 0)
val responseApdu = ResponseApdu(data)
assertThat(responseApdu.statusWord)
.isEqualTo(StatusWord.ProcessCompleted)
}
@Test
fun `corrupt response, getTlvData returns null`() {
val corruptData = byteArrayOf(0, 0, 0)
val responseApdu = ResponseApdu(corruptData)
assertThat(responseApdu.getTlvData())
.isNull()
}
@Test
fun `response, getTlvData returns cardId`() {
val data = 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, -112, 0)
val responseApdu = ResponseApdu(data)
assertThat(responseApdu.getTlvData())
.isNotNull()
assertThat(responseApdu.getTlvData())
.isNotEmpty()
assertThat(responseApdu.getTlvData()?.filter { it.tag == TlvTag.Unknown })
.isEmpty()
}
}

View file

@ -1,105 +0,0 @@
package com.tangem.common.extensions
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import java.util.*
class ByteArrayExtensionsTest {
@Test
fun `card Id to Hex String`() {
val hex = "cb22000000027374"
val bytes = byteArrayOf(-53, 34, 0, 0, 0, 2, 115, 116)
assertThat(bytes.toHexString())
.matches(hex)
}
@Test
fun `batch Id to Hex String`() {
val hex = "0029"
val bytes = byteArrayOf(0, 41)
assertThat(bytes.toHexString())
.matches(hex)
}
@Test
fun `curve name to Utf8`() {
val bytes = byteArrayOf(115, 101, 99, 112, 50, 53, 54, 107, 49, 0)
val expected = "secp256k1"
val converted = bytes.toUtf8()
assertThat(converted)
.matches(expected)
}
@Test
fun `empty byteArray to Utf8 returns empty String`() {
val bytes = byteArrayOf()
val expected = ""
assertThat(bytes.toUtf8())
.matches(expected)
}
@Test
fun `blockchain name to Utf8`() {
val bytes = byteArrayOf(69, 84, 72)
val expected = "ETH"
val converted = bytes.toUtf8()
assertThat(converted)
.matches(expected)
}
@Test
fun `bytes to int`() {
val bytes = byteArrayOf(0, 2, 106, 3)
val expected = 158211
assertThat(bytes.toInt())
.isEqualTo(expected)
val bytes1 = byteArrayOf(0, 0, 0, 13)
val expected1 = 13
assertThat(bytes1.toInt())
.isEqualTo(expected1)
}
@Test
fun `zero to int`() {
val bytes = byteArrayOf(0)
val expected = 0
assertThat(bytes.toInt())
.isEqualTo(expected)
}
@Test
fun toDate() {
val bytes1 = byteArrayOf(7, -30, 7, 27)
val expected1 = Calendar.getInstance().apply { this.set(2018, 6, 27, 0, 0, 0) }.time
val converted1 = bytes1.toDate()
assertThat(converted1.toString())
.isEqualTo(expected1.toString())
val bytes2 = byteArrayOf(7, -30, 7, 27, 30)
val expected2 = Calendar.getInstance().apply { this.set(2018, 6, 27, 0, 0, 0) }.time
val converted2 = bytes2.toDate()
assertThat(converted2.toString())
.isEqualTo(expected2.toString())
val bytes3 = byteArrayOf(7, -30, 7)
val expected3 = Calendar.getInstance().apply { this.set(2018, 6, 0, 0, 0, 0) }.time
val converted3 = bytes3.toDate()
assertThat(converted3.toString())
.isEqualTo(expected3.toString())
}
@Test
fun `calculate sha512`() {
val bytes = ByteArray(64) { 5 }
val expected = byteArrayOf(
-123, 96, 121, 57, -117, -23, -108, 57, 25, -119, -22, 97, 11, -91,
74, -19, -88, 21, -108, -116, -100, 111, 6, -78, 114, -115, 70, -121, 29, 102, 104, 65,
-21, -68, -111, 121, -51, 109, -94, -24, -40, 108, -25, 70, -26, 61, 38, 12, -127, -34,
-77, -81, 81, -32, -89, -112, -31, -33, 91, 114, 89, 127, -123, -58)
assertThat(bytes.calculateSha512())
.isEqualTo(expected)
}
}

View file

@ -1,24 +0,0 @@
package com.tangem.common.extensions
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Test
class IntExtensionsTest {
@Test
fun `small int toByteArray`() {
val int = 13
val expected = byteArrayOf(0, 0, 0, 13)
assertThat(int.toByteArray())
.isEqualTo(expected)
}
@Test
fun `int toByteArray`() {
val int = 999
val expected = byteArrayOf(0, 0, 3, -25)
assertThat(int.toByteArray())
.isEqualTo(expected)
}
}

View file

@ -1,53 +0,0 @@
package com.tangem.common.extensions
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class StringExtensionsTest {
@Test
fun `calculate SHA 256 for default PIN 1`() {
val pin = "000000"
val expected = 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)
assertThat(pin.calculateSha256())
.isEqualTo(expected)
}
@Test
fun `calculate SHA 256 for default PIN 2`() {
val pin = "000"
val expected = byteArrayOf(42, -55, -90, 116, 106, -54, 84, 58, -8, -33, -13, -104, -108, -49,
-24, 23, 58, -5, -94, 30, -80, 28, 111, -82, 51, -43, 41, 71, 34, 40, 85, -17)
assertThat(pin.calculateSha256())
.isEqualTo(expected)
}
@Test
fun `calculate SHA 256 for a sample PIN 1`() {
val pin = "999999"
val expected = byteArrayOf(-109, 115, 119, -16, 86, 22, 15, -60, -79, 94, 11, 119, 12, 103,
19, 106, 95, 3, -63, 82, 5, -76, -45, -65, -111, -126, 104, -2, -6, 44, 109, 10)
assertThat(pin.calculateSha256())
.isEqualTo(expected)
}
@Test
fun `calculate SHA 256 for a sample PIN 2`() {
val pin = "999"
val expected = byteArrayOf(-125, -49, -117, 96, -99, -26, 0, 54, -88, 39, 123, -48, -23, 97,
53, 117, 27, -68, 7, -21, 35, 66, 86, -44, -74, 91, -119, 51, 96, 101, 27, -14)
assertThat(pin.calculateSha256())
.isEqualTo(expected)
}
@Test
fun `card ID hex to bytes`() {
val cardId = "cb22000000027374"
val expected = byteArrayOf(-53, 34, 0, 0, 0, 2, 115, 116)
assertThat(cardId.hexToBytes())
.isEqualTo(expected)
}
}

View file

@ -1,192 +0,0 @@
package com.tangem.common.tlv
import com.google.common.truth.Truth.assertThat
import com.tangem.TangemSdkError
import com.tangem.commands.*
import com.tangem.common.extensions.hexToBytes
import org.junit.Test
import org.junit.jupiter.api.assertThrows
import java.util.*
class TlvDecoderTest {
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.deserialize(rawData)
private val tlvMapper = TlvDecoder(tlvData!!)
private val cardDataRaw: ByteArray = tlvMapper.decode(TlvTag.CardData)
private val cardDataMapper = TlvDecoder(Tlv.deserialize(cardDataRaw)!!)
@Test
fun `map optional when value is present`() {
val settingsMask: SettingsMask? = tlvMapper.decodeOptional(TlvTag.SettingsMask)
assertThat(settingsMask)
.isNotNull()
}
@Test
fun `map optional when no tag returns null`() {
val tokenSymbol: String? = tlvMapper.decodeOptional(TlvTag.TokenSymbol)
assertThat(tokenSymbol)
.isNull()
}
@Test
fun `map when value is null throws MissingTagException`() {
assertThrows<TangemSdkError.DecodingFailedMissingTag> {
tlvMapper.decode<String>(TlvTag.TokenSymbol)
}
}
@Test
fun `map optional to wrong type throws WrongTypeException`() {
assertThrows<TangemSdkError.DecodingFailedTypeMismatch> {
tlvMapper.decodeOptional<String?>(TlvTag.CardData)
}
}
@Test
fun `map to wrong type throws WrongTypeException`() {
assertThrows<TangemSdkError.DecodingFailedTypeMismatch> {
tlvMapper.decode<String>(TlvTag.CardData)
}
}
@Test
fun `map boolean missing flag returns false`() {
val terminalIsLinked: Boolean = tlvMapper.decode(TlvTag.TerminalIsLinked)
assertThat(terminalIsLinked)
.isFalse()
}
@Test
fun `map SettingsMask returns correct value`() {
val settingsMask: SettingsMask = tlvMapper.decode(TlvTag.SettingsMask)
assertThat(settingsMask)
.isNotNull()
assertThat(settingsMask.rawValue)
.isEqualTo(32289)
assertThat(settingsMask.contains(Settings.SkipSecurityDelayIfValidatedByLinkedTerminal))
.isFalse()
assertThat(settingsMask.contains(Settings.IsReusable))
.isTrue()
assertThat(settingsMask.contains(Settings.AllowSwapPIN2))
.isTrue()
assertThat(settingsMask.contains(Settings.UseDynamicNdef))
.isTrue()
assertThat(settingsMask.contains(Settings.ProhibitPurgeWallet))
.isFalse()
}
@Test
fun `map SigningMethods single value returns correct value`() {
val signingMethods: SigningMethodMask = tlvMapper.decode(TlvTag.SigningMethod)
assertThat(signingMethods.contains(SigningMethod.SignHash))
.isTrue()
}
@Test
fun `map SigningMethods set of methods returns correct value`() {
val localMapper = TlvDecoder(Tlv.deserialize("070195".hexToBytes())!!)
val signingMethods: SigningMethodMask = localMapper.decode(TlvTag.SigningMethod)
assertThat(signingMethods.contains(SigningMethod.SignHash))
.isTrue()
assertThat(signingMethods.contains(SigningMethod.SignHashValidateByIssuer))
.isTrue()
assertThat(signingMethods.contains(SigningMethod.SignHashValidateByIssuerWriteIssuerData))
.isTrue()
assertThat(signingMethods.contains(SigningMethod.SignRaw))
.isFalse()
assertThat(signingMethods.contains(SigningMethod.SignRawValidateByIssuer))
.isFalse()
assertThat(signingMethods.contains(SigningMethod.SignRawValidateByIssuerWriteIssuerData))
.isFalse()
assertThat(signingMethods.contains(SigningMethod.SignPos))
.isFalse()
}
@Test
fun `map CardStatus returns correct value`() {
val cardStatus: CardStatus = tlvMapper.decode(TlvTag.Status)
assertThat(cardStatus)
.isEqualTo(CardStatus.Loaded)
}
@Test
fun `map ProductMask with raw value 5 returns correct value`() {
val localMapper = TlvDecoder(listOf(Tlv(TlvTag.ProductMask, byteArrayOf(5))))
val productMask: ProductMask = localMapper.decode(TlvTag.ProductMask)
assertThat(productMask.contains(Product.Note) && productMask.contains(Product.IdCard))
.isTrue()
}
@Test
fun `map ProductMask with raw value 1 returns correct value`() {
val localMapper = TlvDecoder(listOf(Tlv(TlvTag.ProductMask, byteArrayOf(1))))
val productMask: ProductMask = localMapper.decode(TlvTag.ProductMask)
assertThat(productMask.contains(Product.Note))
.isTrue()
}
@Test
fun `map Enum with unknown code throws ConversionException error`() {
val localMapper = TlvDecoder(listOf(Tlv(TlvTag.CurveId, "test".toByteArray())))
assertThrows<TangemSdkError.DecodingFailed> {
localMapper.decode<EllipticCurve>(TlvTag.CurveId)
}
}
@Test
fun `map DateTime returns correct value`() {
val date: Date = cardDataMapper.decode(TlvTag.ManufactureDateTime)
val expected = Calendar.getInstance().apply { this.set(2019, 4, 2, 0, 0, 0) }.time
assertThat(date.toString())
.isEqualTo(expected.toString())
}
@Test
fun `map EllipticCurve returns correct value`() {
val ellipticCurve: EllipticCurve = tlvMapper.decode(TlvTag.CurveId)
assertThat(ellipticCurve)
.isEqualTo(EllipticCurve.Secp256k1)
}
@Test
fun `map ByteArray returns correctly`() {
val cardPublicKey: ByteArray = tlvMapper.decode(TlvTag.CardPublicKey)
assertThat(cardPublicKey)
.isInstanceOf(ByteArray::class.java)
}
@Test
fun `map Int returns correct value`() {
val signedHashes: Int = tlvMapper.decode(TlvTag.SignedHashes)
assertThat(signedHashes)
.isEqualTo(13)
}
@Test
fun `map Int with wrong value throws ConversionException`() {
val localMapper = TlvDecoder(listOf(Tlv(TlvTag.SignedHashes, byteArrayOf(1, 2, 3, 4, 5))))
assertThrows<TangemSdkError.DecodingFailed> {
localMapper.decode<Int>(TlvTag.SignedHashes)
}
}
@Test
fun `map UTF8 returns correct value`() {
val blockchainId: String = cardDataMapper.decode(TlvTag.BlockchainId)
assertThat(blockchainId)
.isEqualTo("ETH")
}
@Test
fun `map Hex returns correct value`() {
val cardId: String = tlvMapper.decode(TlvTag.CardId)
assertThat(cardId)
.isEqualTo("cb22000000027374")
}
}

View file

@ -1,111 +0,0 @@
package com.tangem.common.tlv
import com.google.common.truth.Truth.assertThat
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import org.junit.Test
class TlvTest {
@Test
fun `TLVs to bytes, only PIN`() {
val tlvs = listOf(
Tlv(TlvTag.Pin, "000000".calculateSha256())
)
val expected = byteArrayOf(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)
assertThat(tlvs.serialize())
.isEqualTo(expected)
}
@Test
fun `TLVs to bytes, check wallet`() {
val tlvs = listOf(
Tlv(TlvTag.Pin, "000000".calculateSha256()),
Tlv(TlvTag.CardId, "cb22000000027374".hexToBytes()),
Tlv(TlvTag.Challenge, byteArrayOf(-82, -78, -31, 34, 66, -19, -86, -1, 26, 8, 100, -126, -74, 20, -28, 83))
)
val expected = byteArrayOf(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, 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.serialize())
.isEqualTo(expected)
}
@Test
fun `Bytes to Tlvs, only PIN`() {
val bytes = byteArrayOf(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)
val tlvs = Tlv.deserialize(bytes)
assertThat(tlvs)
.isNotNull()
assertThat(tlvs)
.isNotEmpty()
val pin = tlvs!!.find { it.tag == TlvTag.Pin }?.value
val pinExpected = "000000".calculateSha256()
assertThat(pin)
.isEqualTo(pinExpected)
}
@Test
fun `Bytes to TLVs, check wallet TLVs`() {
val bytes = byteArrayOf(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, 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.deserialize(bytes)
assertThat(tlvs)
.isNotNull()
assertThat(tlvs)
.isNotEmpty()
val pin = tlvs!!.find { it.tag == TlvTag.Pin }?.value
val pinExpected = "000000".calculateSha256()
assertThat(pin)
.isEqualTo(pinExpected)
val cardId = tlvs.find { it.tag == TlvTag.CardId }?.value
val cardIdExpected = "cb22000000027374".hexToBytes()
assertThat(cardId)
.isEqualTo(cardIdExpected)
val challenge = tlvs.find { it.tag == TlvTag.Challenge }?.value
val challengeExpected = byteArrayOf(-82, -78, -31, 34, 66, -19, -86, -1, 26, 8, 100, -126, -74, 20, -28, 83)
assertThat(challenge)
.isEqualTo(challengeExpected)
}
@Test
fun `Bytes to TLVs, wrong values`() {
val bytes = byteArrayOf(0)
val tlvs = Tlv.deserialize(bytes)
assertThat(tlvs)
.isNull()
val bytes1 = byteArrayOf(0, 0, 0, 0, 0, 0, 0)
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()
}
}

View file

@ -1,50 +0,0 @@
package com.tangem.crypto
import com.google.common.truth.Truth.assertThat
import com.tangem.commands.EllipticCurve
import com.tangem.crypto.CryptoUtils.generatePublicKey
import com.tangem.crypto.CryptoUtils.generateRandomBytes
import com.tangem.crypto.CryptoUtils.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class CryptoUtilsTest {
@BeforeEach
internal fun setUp() {
CryptoUtils.initCrypto()
}
@Test
fun generateRandomBytesTest() {
val privateKey: ByteArray = generateRandomBytes(32)
assertThat(privateKey)
.hasLength(32)
assertThat(privateKey.sum())
.isNotEqualTo(0)
}
@Test
internal fun verifyEd25519Test() {
val verified = verifySignature_withSampleData(EllipticCurve.Ed25519)
assertThat(verified)
.isTrue()
}
@Test
internal fun verifySecp256k1Test() {
val verified = verifySignature_withSampleData(EllipticCurve.Secp256k1)
assertThat(verified)
.isTrue()
}
private fun verifySignature_withSampleData(curve: EllipticCurve): Boolean {
val privateKey = ByteArray(32) { 1 }
val publicKey = generatePublicKey(privateKey, curve)
val message = ByteArray(64) { 5 }
val signature = message.sign(privateKey, curve)
return verify(publicKey, message, signature, curve)
}
}

View file

@ -1 +0,0 @@
/build

View file

@ -1,56 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "com.tangem.devkit"
minSdkVersion 21
targetSdkVersion 29
versionCode 4
versionName "1.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation project(':tangem-core')
implementation project(':tangem-sdk')
implementation fileTree(dir: 'libs', include: ['*.jar'])
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-stdlib-jdk8:$versions.kotlin"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0'
implementation "androidx.constraintlayout:constraintlayout:2.0.0-beta4"
implementation "androidx.navigation:navigation-fragment-ktx:2.2.1"
implementation "androidx.navigation:navigation-ui-ktx:2.2.1"
implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
implementation "com.google.android.material:material:1.2.0-alpha05"
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.github.gbIxaHue:eu4d:0.3.8'
}

View file

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -1,78 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tangem.devkit">
<uses-feature
android:name="android.hardware.nfc"
android:required="true" />
<uses-permission android:name="android.permission.NFC" />
<application
android:name="com.tangem.devkit.AppTangemDemo"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name="com.tangem.devkit._main.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="www.tangem.com"
android:scheme="http" />
<data
android:host="www.tangem.com"
android:scheme="https" />
<data
android:host="tangem.com"
android:scheme="http" />
<data
android:host="tangem.com"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
</activity>
<activity android:name="com.tangem.devkit.TestUserDataActivity">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="www.tangem.com"
android:scheme="http" />
<data
android:host="www.tangem.com"
android:scheme="https" />
<data
android:host="tangem.com"
android:scheme="http" />
<data
android:host="tangem.com"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
</activity>
</application>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,35 +0,0 @@
package com.tangem.devkit
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import com.tangem.devkit._arch.structure.ILog
import com.tangem.devkit._arch.structure.ItemLogger
import com.tangem.devkit.commons.TangemLogger
import ru.dev.gbixahue.eu4d.lib.android.global.log.Log
/**
[REDACTED_AUTHOR]
*/
class AppTangemDemo : Application() {
override fun onCreate() {
super.onCreate()
AppTangemDemo.appInstance = this
setupLoggers()
}
private fun setupLoggers() {
Log.setLogger(TangemLogger())
ILog.setLogger(ItemLogger())
}
fun sharedPreferences(name: String = "DevKitApp", mode: Int = Context.MODE_PRIVATE): SharedPreferences {
return getSharedPreferences(name, mode)
}
companion object {
lateinit var appInstance: AppTangemDemo
}
}

View file

@ -1,143 +0,0 @@
package com.tangem.devkit
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.tangem.TangemSdk
import com.tangem.common.CompletionResult
import com.tangem.tangem_sdk_new.extensions.init
import kotlinx.android.synthetic.main.old_activity_main.*
class Old_MainActivity : AppCompatActivity() {
private lateinit var tangemSdk: TangemSdk
private lateinit var cardId: String
private lateinit var issuerData: ByteArray
private lateinit var issuerDataSignature: ByteArray
private var issuerDataCounter: Int = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.old_activity_main)
tangemSdk = TangemSdk.init(this)
btn_scan?.setOnClickListener { _ ->
tangemSdk.scanCard { taskEvent ->
when (taskEvent) {
is CompletionResult.Success -> {
// Handle returned card data
val card = taskEvent.data
cardId = card.cardId
runOnUiThread {
tv_card_cid?.text = cardId
btn_create_wallet.isEnabled = true
tv_card_cid?.text = cardId
btn_sign.isEnabled = true
btn_read_issuer_data.isEnabled = true
btn_read_issuer_extra_data.isEnabled = true
btn_write_issuer_data.isEnabled = true
btn_purge_wallet.isEnabled = true
btn_create_wallet.isEnabled = true
}
}
}
}
}
btn_sign?.setOnClickListener { _ ->
tangemSdk.sign(
createSampleHashes(),
cardId) {
when (it) {
is CompletionResult.Failure -> {
runOnUiThread { tv_card_cid?.text = it.error::class.simpleName }
}
is CompletionResult.Success -> runOnUiThread { tv_card_cid?.text = cardId + "was used to sign sample hashes." }
}
}
}
btn_read_issuer_data?.setOnClickListener { _ ->
tangemSdk.readIssuerData(cardId) {
when (it) {
is CompletionResult.Failure -> {
runOnUiThread { tv_card_cid?.text = it.error::class.simpleName }
}
is CompletionResult.Success -> runOnUiThread {
btn_write_issuer_data.isEnabled = true
tv_card_cid?.text = it.data.issuerData.contentToString()
issuerData = it.data.issuerData
issuerDataSignature = it.data.issuerDataSignature
}
}
}
}
btn_write_issuer_data?.setOnClickListener { _ ->
tangemSdk.writeIssuerData(
cardId,
issuerData,
issuerDataSignature) {
when (it) {
is CompletionResult.Failure -> {
runOnUiThread { tv_card_cid?.text = it.error::class.simpleName }
}
is CompletionResult.Success -> runOnUiThread {
tv_card_cid?.text = it.data.cardId
}
}
}
}
btn_read_issuer_extra_data?.setOnClickListener { _ ->
tangemSdk.readIssuerExtraData(cardId) {
when (it) {
is CompletionResult.Failure -> {
runOnUiThread { tv_card_cid?.text = it.error::class.simpleName }
}
is CompletionResult.Success -> runOnUiThread {
issuerDataCounter = (it.data.issuerDataCounter ?: 0) + 1
btn_write_issuer_data.isEnabled = true
tv_card_cid?.text = "Read ${it.data.issuerData.size} bytes of data."
}
}
}
}
btn_purge_wallet?.setOnClickListener { _ ->
tangemSdk.purgeWallet(
cardId) {
when (it) {
is CompletionResult.Failure -> {
runOnUiThread { tv_card_cid?.text = it.error::class.simpleName }
}
is CompletionResult.Success -> runOnUiThread {
tv_card_cid?.text = it.data.status.name
}
}
}
}
btn_create_wallet?.setOnClickListener { _ ->
tangemSdk.createWallet(
cardId) {
when (it) {
is CompletionResult.Failure -> {
runOnUiThread { tv_card_cid?.text = it.error::class.simpleName }
}
is CompletionResult.Success -> 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
}
}
}
}
btn_read_write_user_data?.setOnClickListener { startActivity(Intent(this, TestUserDataActivity::class.java)) }
}
private fun createSampleHashes(): Array<ByteArray> {
val hash1 = ByteArray(32) { 1 }
val hash2 = ByteArray(32) { 2 }
return arrayOf(hash1, hash2)
}
}

View file

@ -1,147 +0,0 @@
package com.tangem.devkit
import android.os.Bundle
import android.view.View
import android.widget.CompoundButton
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.tangem.SessionEnvironment
import com.tangem.TangemSdk
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.tangem_sdk_new.extensions.init
import kotlinx.android.synthetic.main.activity_test_user_data.*
import java.nio.charset.StandardCharsets
/**
[REDACTED_AUTHOR]
*/
class TestUserDataActivity : AppCompatActivity() {
private lateinit var tangemSdk: TangemSdk
private lateinit var writeOptions: WriteOptions
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test_user_data)
init()
initWriteOptions()
}
private fun init() {
tangemSdk = TangemSdk.init(this)
btn_scan?.setOnClickListener { _ ->
tangemSdk.scanCard { taskEvent ->
when (taskEvent) {
is CompletionResult.Success -> {
// Handle returned card data
writeOptions.cardId = taskEvent.data.cardId
runOnUiThread { showReadWriteSection(true) }
}
}
}
}
btn_write.setOnClickListener {
if (writeOptions.cardId == null) return@setOnClickListener
tangemSdk.writeUserData(
writeOptions.cardId!!,
writeOptions.userData,
writeOptions.userCounter
) {
when (it) {
is CompletionResult.Failure -> handleError(tv_write_result, it.error)
is CompletionResult.Success -> {
runOnUiThread { tv_write_result?.text = "Success" }
}
}
}
}
btn_read.setOnClickListener {
if (writeOptions.cardId == null) return@setOnClickListener
tangemSdk.readUserData(writeOptions.cardId!!) {
when (it) {
is CompletionResult.Failure -> handleError(tv_write_result, it.error)
is CompletionResult.Success -> {
runOnUiThread {
tv_read_result?.text = "Success"
writeOptions.userData = it.data.userData
writeOptions.userProtectedData = it.data.userProtectedData
writeOptions.userCounter = it.data.userCounter
writeOptions.userProtectedCounter = it.data.userProtectedCounter
tv_card_cid.text = it.data.cardId
tv_data.text = String(it.data.userData, StandardCharsets.US_ASCII)
tv_protected_data.text = String(it.data.userProtectedData, StandardCharsets.US_ASCII)
tv_counter.text = it.data.userCounter.toString()
tv_protected_counter.text = it.data.userProtectedCounter.toString()
}
}
}
}
}
}
private fun handleError(tv: TextView, error: TangemSdkError) {
if (error is TangemSdkError.UserCancelled) return
runOnUiThread { tv.text = error::class.simpleName }
}
private fun initWriteOptions() {
writeOptions = WriteOptions()
chb_with_ud.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateData(buttonView) }
chb_with_ud_protected.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateProtectedData(buttonView) }
chb_with_counter.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateCounter(buttonView) }
chb_with_protected_counter.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateProtectedCounter(buttonView) }
chb_with_pin2.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updatePin2(buttonView) }
}
private fun showReadWriteSection(show: Boolean) {
val state = if (show) View.VISIBLE else View.GONE
cl_read_write.visibility = state
}
}
class WriteOptions {
var cardId: String? = null
var userData: ByteArray? = null
var userProtectedData: ByteArray? = null
var userCounter: Int? = null
var userProtectedCounter: Int? = null
var pin2: String? = null
fun updateData(chbx: CompoundButton) {
val value = "simple user data".toByteArray()
userData = if (chbx.isChecked) value else null
}
fun updateProtectedData(chbx: CompoundButton) {
val value = "protected user data".toByteArray()
userProtectedData = if (chbx.isChecked) value else null
}
fun updateCounter(chbx: CompoundButton) {
val value = if (userCounter == null) 0 else userCounter!! + 1
userCounter = if (chbx.isChecked) value else null
}
fun updateProtectedCounter(chbx: CompoundButton) {
val value = if (userProtectedCounter == null) 0 else userProtectedCounter!! + 1
userProtectedCounter = if (chbx.isChecked) value else null
}
fun updatePin2(chbx: CompoundButton) {
val value = SessionEnvironment.DEFAULT_PIN2
pin2 = if (chbx.isChecked) value else null
}
}

View file

@ -1,44 +0,0 @@
package com.tangem.devkit._arch
import android.util.Log
import androidx.annotation.MainThread
import androidx.annotation.Nullable
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean
/**
[REDACTED_AUTHOR]
*/
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val mPending: AtomicBoolean = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer {
if (mPending.compareAndSet(true, false)) {
observer.onChanged(it)
}
})
}
@MainThread
override fun setValue(@Nullable t: T?) {
mPending.set(true)
super.setValue(t)
}
@MainThread
fun call() {
setValue(null)
}
companion object {
private const val TAG = "SingleLiveEvent"
}
}

Some files were not shown because too many files have changed in this diff Show more