Updated on 2026-08-14

This commit is contained in:
Tangem 2020-03-25 09:01:59 +03:00
commit 7c6a986089
61 changed files with 1454 additions and 248 deletions

View file

@ -150,12 +150,13 @@ public class Server {
public static class ApiBlockchair {
public static final String URL_BLOCKCHAIR = ServerURL.API_BLOCKCHAIR + "{blockchain}/";
private static final String API_KEY = "?key=A___0Shpsu4KagE7oSabrw20DfXAqWlT";
public static class Method {
static final String ADDRESS = URL_BLOCKCHAIR + "dashboards/address/{address}";
static final String TRANSACTION = URL_BLOCKCHAIR + "dashboards/transaction/{transaction}";
static final String STATS = URL_BLOCKCHAIR + "stats";
static final String PUSH = URL_BLOCKCHAIR + "push/transaction";
static final String ADDRESS = URL_BLOCKCHAIR + "dashboards/address/{address}" + API_KEY;
static final String TRANSACTION = URL_BLOCKCHAIR + "dashboards/transaction/{transaction}" + API_KEY;
static final String STATS = URL_BLOCKCHAIR + "stats" + API_KEY;
static final String PUSH = URL_BLOCKCHAIR + "push/transaction" + API_KEY;
}
}
}

View file

@ -189,7 +189,7 @@ public class ServerApiAdalite {
}
private void retryRequest(String method, String wallet, String tx) {
currentURL = adaliteURL2;
// currentURL = adaliteURL2;
requestData(method, wallet, tx, true);
}
}

View file

@ -472,10 +472,14 @@ class VerifyCardFragment : BaseFragment(), NavigationResultListener, NfcAdapter.
private fun doPurge() {
requestPIN2Count = 0
val engine = CoinEngineFactory.create(ctx)
if (!engine!!.hasBalanceInfo()) {
val engine = CoinEngineFactory.create(ctx) ?: return
if (!engine.hasBalanceInfo()) {
return
} else if (engine.isBalanceNotZero) {
}
if (engine.isBalanceNotZero) {
Toast.makeText(context, R.string.general_error_cannot_erase_wallet_with_non_zero_balance, Toast.LENGTH_LONG).show()
return
} else if (engine.awaitingConfirmation()) {
Toast.makeText(context, R.string.general_error_cannot_erase_wallet_with_non_zero_balance, Toast.LENGTH_LONG).show()
return
}

View file

@ -163,7 +163,7 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
when (items[which]) {
getString(R.string.loaded_wallet_load_via_app) -> {
try {
val intent = Intent(Intent.ACTION_VIEW, engine.shareWalletUri)
val intent = Intent(Intent.ACTION_VIEW, engine.shareWalletUriEx)
intent.addCategory(Intent.CATEGORY_DEFAULT)
startActivity(intent)
} catch (e: ActivityNotFoundException) {
@ -176,7 +176,7 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
}
getString(R.string.loaded_wallet_load_via_qr) -> {
ShowQRCodeDialog.show(activity as AppCompatActivity?, engine.shareWalletUri.toString())
ShowQRCodeDialog.show(activity as AppCompatActivity?, engine.shareWalletUriEx.toString())
}
getString(R.string.loaded_wallet_load_via_cryptonit) -> {
@ -297,16 +297,15 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
serverApiTangem.setArtworkListener(artworkListener)
refresh()
startVerify(lastTag)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(LoadedWalletViewModel::class.java)
// set rate info to CoinData
viewModel.getRateInfo().observe(this, Observer<Float> { rate ->
ctx.coinData.rate = rate
ctx.coinData.rateAlter = rate
updateViews()
})
viewModel.requestRateInfo(ctx)
}
@ -886,8 +885,7 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
private fun doShareWallet(useURI: Boolean) {
if (useURI) {
val engine = CoinEngineFactory.create(ctx)
val txtShare = engine?.shareWalletUri.toString()
val txtShare = ctx.coinData.wallet
val intent = Intent(Intent.ACTION_SEND)
intent.type = Constant.INTENT_TYPE_TEXT_PLAIN
intent.putExtra(Intent.EXTRA_SUBJECT, Constant.WALLET_ADDRESS)

View file

@ -3,6 +3,7 @@ package com.tangem.wallet;
import android.net.Uri;
import android.text.InputFilter;
import com.tangem.data.Blockchain;
import com.tangem.tangem_card.reader.CardProtocol;
import com.tangem.tangem_card.tasks.SignTask;
@ -195,6 +196,13 @@ public abstract class CoinEngine {
public abstract Uri getShareWalletUri();
public Uri getShareWalletUriEx(){
if (ctx.getBlockchain() == Blockchain.BitcoinCash)
return getShareWalletUri();
else
return Uri.parse(ctx.getBlockchain().name().toLowerCase() + ":" + getShareWalletUri().toString());
}
public abstract boolean checkNewTransactionAmount(Amount amount);
public abstract boolean checkNewTransactionAmountAndFee(Amount amount, Amount fee, Boolean isFeeIncluded);

View file

@ -141,7 +141,7 @@ public class BtcCashEngine extends CoinEngine {
@Override
public Uri getWalletExplorerUri() {
return Uri.parse("https://explorer.bitcoin.com/bch/address/" + ctx.getCoinData().getWallet());
return Uri.parse("https://blockchair.com/bitcoin-cash/address/" + ctx.getCoinData().getWallet());
}
@Override

View file

@ -219,9 +219,9 @@ public class BtcEngine extends CoinEngine {
@Override
public Uri getShareWalletUri() {
if (ctx.getCard().getDenomination() != null && !ctx.getCard().getDenominationText().equals("0.00")) {
return Uri.parse("bitcoin:" + ctx.getCoinData().getWallet() + "?amount=" + convertToAmount(convertToInternalAmount(ctx.getCard().getDenomination())).toValueString(8));
return Uri.parse(ctx.getCoinData().getWallet() + "?amount=" + convertToAmount(convertToInternalAmount(ctx.getCard().getDenomination())).toValueString(8));
} else {
return Uri.parse("bitcoin:" + ctx.getCoinData().getWallet());
return Uri.parse(ctx.getCoinData().getWallet());
}
}

View file

@ -228,10 +228,12 @@ public class EthEngine extends CoinEngine {
@Override
public Uri getShareWalletUri() {
if (ctx.getCard().getDenomination() != null) {
return Uri.parse("ethereum:" + ctx.getCoinData().getWallet());// + "?value=" + mCard.getDenomination() +"e18");
// if (ctx.getCard().getDenomination() != null) {
// return Uri.parse("ethereum:" + ctx.getCoinData().getWallet());// + "?value=" + mCard.getDenomination() +"e18");
if (ctx.getBlockchain() == Blockchain.Ethereum) {
return Uri.parse(ctx.getCoinData().getWallet());
} else {
return Uri.parse("ethereum:" + ctx.getCoinData().getWallet());
return Uri.parse(ctx.getCoinData().getWallet());
}
}

View file

@ -27,7 +27,6 @@ import com.tangem.wallet.EthTransaction;
import com.tangem.wallet.Keccak256;
import com.tangem.wallet.R;
import com.tangem.wallet.TangemContext;
import com.tangem.wallet.eth.EthData;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.SignatureDecodeException;
@ -185,9 +184,9 @@ public class EthIdEngine extends CoinEngine {
@Override
public Uri getShareWalletUri() {
if (ctx.getCard().getDenomination() != null) {
return Uri.parse("ethereum:" + ctx.getCoinData().getWallet());// + "?value=" + mCard.getDenomination() +"e18");
return Uri.parse(ctx.getCoinData().getWallet());// + "?value=" + mCard.getDenomination() +"e18");
} else {
return Uri.parse("ethereum:" + ctx.getCoinData().getWallet());
return Uri.parse(ctx.getCoinData().getWallet());
}
}

View file

@ -179,9 +179,9 @@ public class LtcEngine extends BtcEngine {
@Override
public Uri getShareWalletUri() {
if (ctx.getCard().getDenomination() != null) {
return Uri.parse("litecoin:" + ctx.getCoinData().getWallet() + "?amount=" + convertToAmount(convertToInternalAmount(ctx.getCard().getDenomination())).toValueString(8));
return Uri.parse(ctx.getCoinData().getWallet() + "?amount=" + convertToAmount(convertToInternalAmount(ctx.getCard().getDenomination())).toValueString(8));
} else {
return Uri.parse("litecoin:" + ctx.getCoinData().getWallet());
return Uri.parse(ctx.getCoinData().getWallet());
}
}

View file

@ -189,7 +189,7 @@ public class NftTokenEngine extends CoinEngine {
@Override
public Uri getShareWalletUri() {
return Uri.parse("ethereum:" + ctx.getCoinData().getWallet());
return Uri.parse(ctx.getCoinData().getWallet());
}
@Override

View file

@ -305,9 +305,9 @@ public class TokenEngine extends CoinEngine {
@Override
public Uri getShareWalletUri() {
if (ctx.getCard().getDenomination() != null) {
return Uri.parse("ethereum:" + ctx.getCoinData().getWallet());// + "?value=" + mCard.getDenomination() +"e18");
return Uri.parse(ctx.getCoinData().getWallet());// + "?value=" + mCard.getDenomination() +"e18");
} else {
return Uri.parse("ethereum:" + ctx.getCoinData().getWallet());
return Uri.parse(ctx.getCoinData().getWallet());
}
}

View file

@ -361,10 +361,14 @@ public class XlmEngine extends CoinEngine {
}
Operation operation;
if (coinData.isTargetAccountCreated())
if (coinData.isTargetAccountCreated()) {
operation = new PaymentOperation.Builder(KeyPair.fromAccountId(targetAddress), new AssetTypeNative(), amountValue.toValueString()).build();
else
} else {
if (amountValue.compareTo(coinData.getReserve()) < 0) {
throw new IllegalArgumentException("Target account is not created. Send " + coinData.getReserve().toDescriptionString(getDecimals()) + " or more to create");
}
operation = new CreateAccountOperation.Builder(KeyPair.fromAccountId(targetAddress), amountValue.toValueString()).build();
}
TransactionEx transaction = TransactionEx.buildEx(120, coinData.getAccountResponse(), operation);
@ -432,7 +436,7 @@ public class XlmEngine extends CoinEngine {
if (request.errorResponse != null && request.errorResponse.getCode() == 404) {
coinData.setTargetAccountCreated(false);
if (amount.compareTo(coinData.getReserve()) >= 0) { //TODO: take fee inclusion in account, now 1 XLM with fee included will fail after transaction is sent
if (amount.compareTo(coinData.getReserve()) >= 0) {
blockchainRequestsCallbacks.onComplete(true);
} else {
ctx.setError(R.string.confirm_transaction_error_not_enough_xlm_for_create);

View file

@ -17,7 +17,7 @@ public class XrpData extends CoinData {
private Long reserve = 20000000L;
private Boolean accountNotFound = false;
private Boolean accountNotFound, targetAccountCreated = false;
@Override
public void loadFromBundle(Bundle B) {
@ -32,7 +32,9 @@ public class XrpData extends CoinData {
if (B.containsKey("Reserve")) reserve = B.getLong("Reserve");
else reserve = 20000000L;
if (B.containsKey("AccoundNotFound")) accountNotFound = B.getBoolean("AccoundNotFound");
else reserve = 20000000L;
else accountNotFound = false;
if (B.containsKey("TargetAccountCreated")) targetAccountCreated = B.getBoolean("TargetAccountCreated");
else targetAccountCreated = false;
}
@Override
@ -44,6 +46,7 @@ public class XrpData extends CoinData {
if (sequence != null) B.putLong("Sequence", sequence);
if (reserve != null) B.putLong("Reserve", reserve);
if (accountNotFound != null) B.putBoolean("AccoundNotFound", accountNotFound);
if (targetAccountCreated != null) B.putBoolean("TargetAccountCreated", targetAccountCreated);
} catch (Exception e) {
Log.e("Can't save to bundle ", e.getMessage());
}
@ -57,6 +60,7 @@ public class XrpData extends CoinData {
sequence = null;
reserve = 20000000L;
accountNotFound = false;
targetAccountCreated = false;
}
// balanceUnconfirmed is just the latest balance, it equals balanceConfirmed if no unconfirmed transaction present
@ -105,6 +109,14 @@ public class XrpData extends CoinData {
this.accountNotFound = accountFound;
}
public Boolean isTargetAccountCreated() {
return targetAccountCreated;
}
public void setTargetAccountCreated(boolean targetAccountCreated) {
this.targetAccountCreated = targetAccountCreated;
}
public boolean hasBalanceInfo() {
return balanceConfirmed != null || balanceUnconfirmed != null;
}
@ -112,5 +124,4 @@ public class XrpData extends CoinData {
public boolean hasUnconfirmed() {
return !balanceConfirmed.equals(balanceUnconfirmed);
}
}

View file

@ -148,7 +148,7 @@ public class XrpEngine extends CoinEngine {
}
public Uri getShareWalletUri() {
return Uri.parse("ripple:" + ctx.getCoinData().getWallet());
return Uri.parse(ctx.getCoinData().getWallet());
}
@Override
@ -347,11 +347,18 @@ public class XrpEngine extends CoinEngine {
if (IncFee) {
amount = convertToInternalAmount(amountValue).subtract(convertToInternalAmount(feeValue)).setScale(0).toPlainString();
} else {
amount = Long.toString(convertToInternalAmount(amountValue).longValueExact());
amount = Long.toString(convertToInternalAmount(amountValue).longValue());
}
fee = Long.toString(convertToInternalAmount(feeValue).longValueExact());
if (!coinData.isTargetAccountCreated() &&
Long.valueOf(amount).compareTo(coinData.getReserveInInternalUnits().longValue()) < 0
) {
String reserveDescription = convertToAmount(coinData.getReserveInInternalUnits()).toDescriptionString(getDecimals());
throw new IllegalArgumentException("Target account is not created. Send " + reserveDescription + " or more to create");
}
XrpPayment payment = new XrpPayment();
// Put `as` AccountID field Account, `Object` o
@ -522,19 +529,50 @@ public class XrpEngine extends CoinEngine {
ServerApiRipple.ResponseListener rippleListener = new ServerApiRipple.ResponseListener() {
@Override
public void onSuccess(String method, RippleResponse rippleResponse) {
try {
InternalAmount minFee = new InternalAmount(Long.valueOf(rippleResponse.getResult().getDrops().getMinimum_fee()), "Drops");
InternalAmount normalFee = new InternalAmount(Long.valueOf(rippleResponse.getResult().getDrops().getOpen_ledger_fee()), "Drops");
InternalAmount maxFee = new InternalAmount(Long.valueOf(rippleResponse.getResult().getDrops().getMedian_fee()), "Drops");
Log.i(TAG, "onSuccess: " + method);
switch (method) {
case ServerApiRipple.RIPPLE_ACCOUNT_INFO: {
try {
if (rippleResponse.getResult().getError_code().equals(19)) { // "Account not found"
coinData.setTargetAccountCreated(false);
} else {
coinData.setTargetAccountCreated(true);
}
} catch (Exception e) {
coinData.setTargetAccountCreated(true); //expected behaviour, if account exists, there should be no error code -> null pointer
}
coinData.minFee = convertToAmount(minFee);
coinData.normalFee = convertToAmount(normalFee);
coinData.maxFee = convertToAmount(maxFee);
if (serverApiRipple.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
break;
blockchainRequestsCallbacks.onComplete(true);
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "FAIL RIPPLE_FEE Exception");
case ServerApiRipple.RIPPLE_FEE: {
try {
InternalAmount minFee = new InternalAmount(Long.valueOf(rippleResponse.getResult().getDrops().getMinimum_fee()), "Drops");
InternalAmount normalFee = new InternalAmount(Long.valueOf(rippleResponse.getResult().getDrops().getOpen_ledger_fee()), "Drops");
InternalAmount maxFee = new InternalAmount(Long.valueOf(rippleResponse.getResult().getDrops().getMedian_fee()), "Drops");
coinData.minFee = convertToAmount(minFee);
coinData.normalFee = convertToAmount(normalFee);
coinData.maxFee = convertToAmount(maxFee);
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "FAIL RIPPLE_FEE Exception");
ctx.setError(e.getMessage());
}
if (serverApiRipple.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
break;
}
}
@ -548,6 +586,7 @@ public class XrpEngine extends CoinEngine {
serverApiRipple.setResponseListener(rippleListener);
serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_INFO, targetAddress, "");
serverApiRipple.requestData(ServerApiRipple.RIPPLE_FEE, "", "");
}

View file

@ -13,6 +13,7 @@ import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import com.tangem.Constant
import com.tangem.data.Blockchain
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.fragment.qr.CameraPermissionManager
@ -151,6 +152,9 @@ class PrepareTransactionFragment : BaseFragment(), NavigationResultListener, Nfc
// etAmount?.setText(amount)
// rgIncFee.check(R.id.rbFeeOut)
// }
} else if (ctx.blockchain == Blockchain.Ripple && schemeSplit[0] == "ripple") {
val uri = Uri.parse(schemeSplit[1])
etWallet?.setText(uri.path)
} else {
etWallet?.setText(code)
}

View file

@ -34,6 +34,7 @@ import com.tangem.wallet.TangemContext
import kotlinx.android.synthetic.main.layout_progress_horizontal.*
import kotlinx.android.synthetic.main.layout_touch_card.*
import kotlinx.android.synthetic.tangemAccess.fragment_sign_transaction.*
import java.lang.IllegalArgumentException
class SignTransactionFragment : BaseFragment(), NavigationResultListener,
@ -148,6 +149,10 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener,
} catch (e: Exception) {
e.printStackTrace()
}
} catch(e: IllegalArgumentException) {
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, e.message)
navigateBackWithResult(Activity.RESULT_CANCELED, data, R.id.loadedWalletFragment)
} catch (e: Exception) {
e.printStackTrace()
}

View file

@ -12,9 +12,9 @@ dependencies {
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'
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
testImplementation "com.google.truth:truth:1.0"
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
}
sourceCompatibility = "8"

View file

@ -1,11 +1,14 @@
package com.tangem
import com.tangem.commands.*
import com.tangem.commands.personalization.CardConfig
import com.tangem.commands.personalization.DepersonalizeCommand
import com.tangem.commands.personalization.DepersonalizeResponse
import com.tangem.commands.personalization.PersonalizeCommand
import com.tangem.common.CardEnvironment
import com.tangem.common.TerminalKeysService
import com.tangem.crypto.CryptoUtils
import com.tangem.tasks.*
import java.util.concurrent.Executors
/**
* The main interface of Tangem SDK that allows your app to communicate with Tangem cards.
@ -24,7 +27,6 @@ class CardManager(
private var terminalKeysService: TerminalKeysService? = null
private var isBusy = false
private val cardManagerExecutor = Executors.newSingleThreadExecutor()
init {
CryptoUtils.initCrypto()
@ -69,18 +71,7 @@ class CardManager(
*/
fun sign(hashes: Array<ByteArray>, cardId: String,
callback: (result: TaskEvent<SignResponse>) -> Unit) {
val signCommand: SignCommand
try {
signCommand = SignCommand(hashes)
} catch (error: Exception) {
if (error is TaskError) {
callback(TaskEvent.Completion(error))
} else {
Log.e(this::class.simpleName!!, error.message ?: "")
callback(TaskEvent.Completion(TaskError.UnknownError()))
}
return
}
val signCommand = SignCommand(hashes)
val task = SingleCommandTask(signCommand)
runTask(task, cardId, callback)
}
@ -189,16 +180,16 @@ class CardManager(
* User_ProtectedCounter and User_ProtectedData additionaly need PIN2 to confirmation.
*/
fun writeUserData(
cardId: String,
userData: ByteArray? = null,
userProtectedData: ByteArray? = null,
userCounter: Int? = null,
userProtectedCounter: Int? = null,
callback: (result: TaskEvent<WriteUserDataResponse>) -> Unit
cardId: String,
userData: ByteArray? = null,
userProtectedData: ByteArray? = null,
userCounter: Int? = null,
userProtectedCounter: Int? = null,
callback: (result: TaskEvent<WriteUserDataResponse>) -> Unit
) {
val writeUserDataCommand = WriteUserDataCommand(userData, userProtectedData, userCounter, userProtectedCounter)
val task = SingleCommandTask(writeUserDataCommand)
runTask(task, cardId, callback)
val writeUserDataCommand = WriteUserDataCommand(userData, userProtectedData, userCounter, userProtectedCounter)
val task = SingleCommandTask(writeUserDataCommand)
runTask(task, cardId, callback)
}
/**
@ -212,8 +203,8 @@ class CardManager(
* For example, this fields may contain blockchain nonce value.
*/
fun readUserData(cardId: String, callback: (result: TaskEvent<ReadUserDataResponse>) -> Unit) {
val task = SingleCommandTask(ReadUserDataCommand())
runTask(task, cardId, callback)
val task = SingleCommandTask(ReadUserDataCommand())
runTask(task, cardId, callback)
}
/**
@ -247,6 +238,43 @@ class CardManager(
runTask(task, cardId, callback)
}
/**
* Command available on SDK cards only
*
* This command resets card to initial state,
* erasing all data written during personalization and usage.
* @param cardId CID, Unique Tangem card ID number.
*/
fun depersonalize(cardId: String,
callback: (result: TaskEvent<DepersonalizeResponse>) -> Unit) {
val depersonalizeCommand = DepersonalizeCommand()
val task = SingleCommandTask(depersonalizeCommand)
runTask(task, cardId, callback)
}
/**
* 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.
* @param config is a configuration file with all the card settings that are written on the card
* during personalization.
* @param cardId this parameter will set up CID, Unique Tangem card ID.
*/
fun personalize(config: CardConfig,
cardId: String,
callback: (result: TaskEvent<Card>) -> Unit) {
if (this.config.issuer == null) {
callback(TaskEvent.Completion(TaskError.IssuerIsRequired()))
return
}
val personalizationCommand = PersonalizeCommand(config, cardId)
val task = SingleCommandTask(personalizationCommand)
task.performPreflightRead = false
runTask(task, callback = callback)
}
/**
*/
@ -262,7 +290,7 @@ class CardManager(
task.reader = reader
task.delegate = cardManagerDelegate
cardManagerExecutor.execute {
Thread().run {
task.run(environment) { taskEvent ->
if (taskEvent is TaskEvent.Completion) isBusy = false
callback(taskEvent)
@ -292,7 +320,10 @@ class CardManager(
val terminalKeys = if (config.linkedTerminal) terminalKeysService?.getKeys() else null
return CardEnvironment(
cardId = cardId,
terminalKeys = terminalKeys
terminalKeys = terminalKeys,
manufacturerKeyPair = config.manufacturerKeyPair,
acquirerKeyPair = config.acquirerKeyPair,
issuer = config.issuer
)
}

View file

@ -1,6 +1,12 @@
package com.tangem
import com.tangem.commands.personalization.entities.Issuer
import com.tangem.common.KeyPair
class Config(
val linkedTerminal: Boolean = true,
val issuerPublicKey: ByteArray? = null
val issuerPublicKey: ByteArray? = null,
val manufacturerKeyPair: KeyPair? = null,
val acquirerKeyPair: KeyPair? = null,
val issuer: Issuer? = null
)

View file

@ -4,9 +4,6 @@ import com.tangem.common.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
@ -52,11 +49,14 @@ class CheckWalletCommand : CommandSerializer<CheckWalletResponse>() {
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
tlvBuilder.append(TlvTag.Challenge, challenge)
return CommandApdu(Instruction.CheckWallet, tlvBuilder.serialize())
return CommandApdu(
Instruction.CheckWallet, tlvBuilder.serialize(),
cardEnvironment.encryptionMode, cardEnvironment.encryptionKey
)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): CheckWalletResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null
return try {
val mapper = TlvMapper(tlvData)

View file

@ -38,7 +38,7 @@ abstract class CommandSerializer<T : CommandResponse> {
* @return Remaining security delay in milliseconds.
*/
fun deserializeSecurityDelay(responseApdu: ResponseApdu, cardEnvironment: CardEnvironment): Int? {
val tlv = responseApdu.getTlvData(cardEnvironment.encryptionKey)
val tlv = responseApdu.getTlvData()
return tlv?.find { it.tag == TlvTag.Pause }?.value?.toInt()
}
}

View file

@ -4,9 +4,6 @@ import com.tangem.common.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
@ -46,11 +43,14 @@ class CreateWalletCommand : CommandSerializer<CreateWalletResponse>() {
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
tlvBuilder.append(TlvTag.Pin2, cardEnvironment.pin2)
tlvBuilder.append(TlvTag.Cvc, cardEnvironment.cvc)
return CommandApdu(Instruction.CreateWallet, tlvBuilder.serialize())
return CommandApdu(
Instruction.CreateWallet, tlvBuilder.serialize(),
cardEnvironment.encryptionMode, cardEnvironment.encryptionKey
)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): CreateWalletResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null
return try {
val mapper = TlvMapper(tlvData)

View file

@ -0,0 +1,46 @@
package com.tangem.commands
import com.tangem.common.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.tasks.TaskError
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) : CommandSerializer<OpenSessionResponse>() {
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.SessionKeyA, sessionKeyA)
return CommandApdu(
Instruction.OpenSession, tlvBuilder.serialize(),
encryptionMode = cardEnvironment.encryptionMode
)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): OpenSessionResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
return try {
val mapper = TlvMapper(tlvData)
OpenSessionResponse(
sessionKeyB = mapper.map(TlvTag.SessionKeyB),
uid = mapper.map(TlvTag.Uid)
)
} catch (exception: Exception) {
throw TaskError.SerializeCommandError()
}
}
}

View file

@ -4,9 +4,6 @@ import com.tangem.common.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
@ -37,11 +34,14 @@ class PurgeWalletCommand : CommandSerializer<PurgeWalletResponse>() {
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
tlvBuilder.append(TlvTag.Pin2, cardEnvironment.pin2)
return CommandApdu(Instruction.PurgeWallet, tlvBuilder.serialize())
return CommandApdu(
Instruction.PurgeWallet, tlvBuilder.serialize(),
cardEnvironment.encryptionMode, cardEnvironment.encryptionKey
)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): PurgeWalletResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null
return try {
val mapper = TlvMapper(tlvData)

View file

@ -32,6 +32,49 @@ data class SigningMethod(val rawValue: Int) {
const val signHashValidatedByIssuerAndWriteIssuerData = 4
const val signRawValidatedByIssuerAndWriteIssuerData = 5
const val signPos = 6
fun build(
signHash: Boolean = false,
signRaw: Boolean = false,
signHashValidatedByIssuer: Boolean = false,
signRawValidatedByIssuer: Boolean = false,
signHashValidatedByIssuerAndWriteIssuerData: Boolean = false,
signRawValidatedByIssuerAndWriteIssuerData: Boolean = false,
signPos: Boolean = false
): SigningMethod {
fun Boolean.toInt() = if (this) 1 else 0
val signingMethodsCount = 0 +
signHash.toInt() +
signRaw.toInt() +
signHashValidatedByIssuer.toInt() +
signRawValidatedByIssuer.toInt() +
signHashValidatedByIssuerAndWriteIssuerData.toInt() +
signRawValidatedByIssuerAndWriteIssuerData.toInt() +
signPos.toInt()
var signingMethod: Int = 0
if (signingMethodsCount == 1) {
if (signHash) signingMethod += SigningMethod.signHash
if (signRaw) signingMethod += SigningMethod.signRaw
if (signHashValidatedByIssuer) signingMethod += SigningMethod.signHashValidatedByIssuer
if (signRawValidatedByIssuer) signingMethod += SigningMethod.signRawValidatedByIssuer
if (signHashValidatedByIssuerAndWriteIssuerData) signingMethod += SigningMethod.signHashValidatedByIssuerAndWriteIssuerData
if (signRawValidatedByIssuerAndWriteIssuerData) signingMethod += SigningMethod.signRawValidatedByIssuerAndWriteIssuerData
if (signPos) signingMethod += SigningMethod.signPos
} else if (signingMethodsCount > 1) {
signingMethod = 0x80
if (signHash) signingMethod += 0x01
if (signRaw) signingMethod += 0x01 shl SigningMethod.signRaw
if (signHashValidatedByIssuer) signingMethod += 0x01 shl SigningMethod.signHashValidatedByIssuer
if (signRawValidatedByIssuer) signingMethod += 0x01 shl SigningMethod.signRawValidatedByIssuer
if (signHashValidatedByIssuerAndWriteIssuerData) signingMethod += 0x01 shl SigningMethod.signHashValidatedByIssuerAndWriteIssuerData
if (signRawValidatedByIssuerAndWriteIssuerData) signingMethod += 0x01 shl SigningMethod.signRawValidatedByIssuerAndWriteIssuerData
if (signPos) signingMethod += 0x01 shl SigningMethod.signPos
}
return SigningMethod(signingMethod)
}
}
}
@ -79,43 +122,74 @@ data class ProductMask(val rawValue: Int) {
}
}
class ProductMaskBuilder() {
private var productMaskValue = 0
fun add(productCode: Int) {
productMaskValue = productMaskValue or productCode
}
fun build() = ProductMask(productMaskValue)
}
/**
* Stores and maps Tangem card settings.
*
* @property rawValue Card settings in a form of flags,
* while flags definitions and values are in [SettingsMask.Companion] as constants.
* 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
}
fun contains(value: Int): Boolean = (rawValue and value) != 0
enum class Settings(val code: Int) {
IsReusable(0x0001),
UseActivation(0x0002),
ForbidPurgeWallet(0x0004),
UseBlock(0x0008),
companion object {
const val isReusable = 0x0001
const val useActivation = 0x0002
const val forbidPurgeWallet = 0x0004
const val useBlock = 0x0008
AllowSwapPIN(0x0010),
AllowSwapPIN2(0x0020),
UseCVC(0x0040),
ForbidDefaultPIN(0x0080),
const val allowSwapPIN = 0x0010
const val allowSwapPIN2 = 0x0020
const val useCVC = 0x0040
const val forbidDefaultPIN = 0x0080
UseOneCommandAtTime(0x0100),
UseNdef(0x0200),
UseDynamicNdef(0x0400),
SmartSecurityDelay(0x0800),
const val useOneCommandAtTime = 0x0100
const val useNdef = 0x0200
const val useDynamicNdef = 0x0400
const val smartSecurityDelay = 0x0800
ProtocolAllowUnencrypted(0x1000),
ProtocolAllowStaticEncryption(0x2000),
const val protocolAllowUnencrypted = 0x1000
const val protocolAllowStaticEncryption = 0x2000
ProtectIssuerDataAgainstReplay(0x4000),
RestrictOverwriteIssuerDataEx(0x00100000),
const val protectIssuerDataAgainstReplay = 0x4000
AllowSelectBlockchain(0x8000),
const val allowSelectBlockchain = 0x8000
DisablePrecomputedNdef(0x00010000),
const val disablePrecomputedNdef = 0x00010000
SkipSecurityDelayIfValidatedByLinkedTerminal(0x00080000),
SkipCheckPin2andCvcIfValidatedByIssuer(0x00040000),
SkipSecurityDelayIfValidatedByIssuer(0x00020000),
const val skipSecurityDelayIfValidatedByLinkedTerminal = 0x00080000
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)
}
/**
@ -310,11 +384,14 @@ class ReadCommand : CommandSerializer<Card>() {
*/
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
tlvBuilder.append(TlvTag.TerminalPublicKey, cardEnvironment.terminalKeys?.publicKey)
return CommandApdu(Instruction.Read, tlvBuilder.serialize())
return CommandApdu(
Instruction.Read, tlvBuilder.serialize(),
cardEnvironment.encryptionMode, cardEnvironment.encryptionKey
)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): Card? {
val tlvData = responseApdu.getTlvData() ?: return null
val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null
return try {
val tlvMapper = TlvMapper(tlvData)

View file

@ -28,7 +28,7 @@ class ReadIssuerDataResponse(
* 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 [SettingsMask.protectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
*/
@ -36,7 +36,7 @@ class ReadIssuerDataResponse(
/**
* An optional counter that protect issuer data against replay attack.
* When flag [SettingsMask.protectIssuerDataAgainstReplay] set in [SettingsMask]
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask]
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
*/
val issuerDataCounter: Int?
@ -60,11 +60,14 @@ class ReadIssuerDataCommand(
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadData)
return CommandApdu(Instruction.ReadIssuerData, tlvBuilder.serialize())
return CommandApdu(
Instruction.ReadIssuerData, tlvBuilder.serialize(),
cardEnvironment.encryptionMode, cardEnvironment.encryptionKey
)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): ReadIssuerDataResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null
return try {
val mapper = TlvMapper(tlvData)

View file

@ -33,7 +33,7 @@ class ReadIssuerExtraDataResponse(
* 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 [SettingsMask.protectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
*/
@ -41,7 +41,7 @@ class ReadIssuerExtraDataResponse(
/**
* An optional counter that protect issuer data against replay attack.
* When flag [SettingsMask.protectIssuerDataAgainstReplay] set in [SettingsMask]
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask]
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
*/
val issuerDataCounter: Int?
@ -67,11 +67,14 @@ class ReadIssuerExtraDataCommand(
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadExtraData)
tlvBuilder.append(TlvTag.Offset, offset)
return CommandApdu(Instruction.ReadIssuerData, tlvBuilder.serialize())
return CommandApdu(
Instruction.ReadIssuerData, tlvBuilder.serialize(),
cardEnvironment.encryptionMode, cardEnvironment.encryptionKey
)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): ReadIssuerExtraDataResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null
return try {
val mapper = TlvMapper(tlvData)

View file

@ -57,11 +57,14 @@ class ReadUserDataCommand: CommandSerializer<ReadUserDataResponse>() {
builder.append(TlvTag.CardId, cardEnvironment.cardId)
builder.append(TlvTag.Pin, cardEnvironment.pin1)
return CommandApdu(Instruction.ReadUserData, builder.serialize())
return CommandApdu(
Instruction.ReadUserData, builder.serialize(),
cardEnvironment.encryptionMode, cardEnvironment.encryptionKey
)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): ReadUserDataResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null
return try {
val mapper = TlvMapper(tlvData)

View file

@ -4,9 +4,6 @@ import com.tangem.common.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
@ -60,7 +57,10 @@ class SignCommand(private val hashes: Array<ByteArray>)
tlvBuilder.append(TlvTag.Cvc, cardEnvironment.cvc)
addTerminalSignature(cardEnvironment, tlvBuilder)
return CommandApdu(Instruction.Sign, tlvBuilder.serialize())
return CommandApdu(
Instruction.Sign, tlvBuilder.serialize(),
cardEnvironment.encryptionMode, cardEnvironment.encryptionKey
)
}
/**
@ -79,7 +79,7 @@ class SignCommand(private val hashes: Array<ByteArray>)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): SignResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null
val tlvMapper = TlvMapper(tlvData)
return SignResponse(

View file

@ -45,11 +45,14 @@ class WriteIssuerDataCommand(
tlvBuilder.append(TlvTag.IssuerDataSignature, issuerDataSignature)
tlvBuilder.append(TlvTag.IssuerDataCounter, issuerDataCounter)
return CommandApdu(Instruction.WriteIssuerData, tlvBuilder.serialize())
return CommandApdu(
Instruction.WriteIssuerData, tlvBuilder.serialize(),
cardEnvironment.encryptionMode, cardEnvironment.encryptionKey
)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): WriteIssuerDataResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null
return try {
val mapper = TlvMapper(tlvData)

View file

@ -60,7 +60,10 @@ class WriteIssuerExtraDataCommand(
tlvBuilder.append(TlvTag.IssuerDataSignature, finalizingSignature)
}
}
return CommandApdu(Instruction.WriteIssuerData, tlvBuilder.serialize())
return CommandApdu(
Instruction.WriteIssuerData, tlvBuilder.serialize(),
cardEnvironment.encryptionMode, cardEnvironment.encryptionKey
)
}
private fun getDataToWrite(): ByteArray =
@ -72,7 +75,7 @@ class WriteIssuerExtraDataCommand(
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): WriteIssuerDataResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null
return try {
val mapper = TlvMapper(tlvData)

View file

@ -47,11 +47,14 @@ class WriteUserDataCommand(private val userData: ByteArray? = null, private val
if (userProtectedCounter != null || userProtectedData != null)
builder.append(TlvTag.Pin2, cardEnvironment.pin2)
return CommandApdu(Instruction.WriteUserData, builder.serialize())
return CommandApdu(
Instruction.WriteUserData, builder.serialize(),
cardEnvironment.encryptionMode, cardEnvironment.encryptionKey
)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): WriteUserDataResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null
return try {
WriteUserDataResponse(TlvMapper(tlvData).map(TlvTag.CardId))

View file

@ -0,0 +1,112 @@
package com.tangem.commands.personalization
import com.tangem.commands.*
data class NdefRecord(
val type: Type,
val value: String
) {
enum class Type {
URI, AAR, TEXT
}
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 signingMethod: SigningMethod,
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>
) {
fun getSettingsMask(): 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.ForbidPurgeWallet)
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()
}
companion object
}

View file

@ -0,0 +1,31 @@
package com.tangem.commands.personalization
import com.tangem.commands.CommandResponse
import com.tangem.commands.CommandSerializer
import com.tangem.common.CardEnvironment
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.
* @param cardId CID, Unique Tangem card ID number.
*/
class DepersonalizeCommand : CommandSerializer<DepersonalizeResponse>() {
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
return CommandApdu(
Instruction.Depersonalize, byteArrayOf(),
cardEnvironment.encryptionMode, cardEnvironment.encryptionKey
)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): DepersonalizeResponse? {
return DepersonalizeResponse(true)
}
}

View file

@ -0,0 +1,87 @@
package com.tangem.commands.personalization
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

@ -0,0 +1,165 @@
package com.tangem.commands.personalization
import com.tangem.commands.Card
import com.tangem.commands.CardData
import com.tangem.commands.CommandSerializer
import com.tangem.commands.personalization.entities.Issuer
import com.tangem.common.CardEnvironment
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.sign
import com.tangem.tasks.TaskError
/**
* 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.
* @param config is a configuration file with all the card settings that are written on the card
* during personalization.
* @param cardId this parameter will set up CID, Unique Tangem card ID.
*/
class PersonalizeCommand(private val config: CardConfig, private val cardId: String) : CommandSerializer<Card>() {
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
if (cardEnvironment.issuer == null || cardEnvironment.manufacturerKeyPair == null) {
throw TaskError.SerializeCommandError()
}
return CommandApdu(
Instruction.Personalize,
serializePersonalizationData(
cardId, config,
cardEnvironment.issuer, cardEnvironment.manufacturerKeyPair.privateKey,
cardEnvironment.acquirerKeyPair?.publicKey),
encryptionKey = devPersonalizationKey
)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): Card? {
val tlvData = responseApdu.getTlvData(devPersonalizationKey) ?: return null
return try {
val tlvMapper = TlvMapper(tlvData)
Card(
cardId = tlvMapper.mapOptional(TlvTag.CardId) ?: "",
manufacturerName = tlvMapper.mapOptional(TlvTag.ManufactureId) ?: "",
status = tlvMapper.mapOptional(TlvTag.Status),
firmwareVersion = tlvMapper.mapOptional(TlvTag.Firmware),
cardPublicKey = tlvMapper.mapOptional(TlvTag.CardPublicKey),
settingsMask = tlvMapper.mapOptional(TlvTag.SettingsMask),
issuerPublicKey = tlvMapper.mapOptional(TlvTag.IssuerDataPublicKey),
curve = tlvMapper.mapOptional(TlvTag.CurveId),
maxSignatures = tlvMapper.mapOptional(TlvTag.MaxSignatures),
signingMethod = tlvMapper.mapOptional(TlvTag.SigningMethod),
pauseBeforePin2 = tlvMapper.mapOptional(TlvTag.PauseBeforePin2),
walletPublicKey = tlvMapper.mapOptional(TlvTag.WalletPublicKey),
walletRemainingSignatures = tlvMapper.mapOptional(TlvTag.RemainingSignatures),
walletSignedHashes = tlvMapper.mapOptional(TlvTag.SignedHashes),
health = tlvMapper.mapOptional(TlvTag.Health),
isActivated = tlvMapper.map(TlvTag.IsActivated),
activationSeed = tlvMapper.mapOptional(TlvTag.ActivationSeed),
paymentFlowVersion = tlvMapper.mapOptional(TlvTag.PaymentFlowVersion),
userCounter = tlvMapper.mapOptional(TlvTag.UserCounter),
terminalIsLinked = tlvMapper.map(TlvTag.TerminalIsLinked),
cardData = deserializeCardData(tlvData)
)
} catch (exception: Exception) {
throw TaskError.SerializeCommandError()
}
}
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 tlvMapper = TlvMapper(cardDataTlvs)
return CardData(
batchId = tlvMapper.mapOptional(TlvTag.Batch),
manufactureDateTime = tlvMapper.mapOptional(TlvTag.ManufactureDateTime),
issuerName = tlvMapper.mapOptional(TlvTag.IssuerId),
blockchainName = tlvMapper.mapOptional(TlvTag.BlockchainId),
manufacturerSignature = tlvMapper.mapOptional(TlvTag.ManufacturerSignature),
productMask = tlvMapper.mapOptional(TlvTag.ProductMask),
tokenSymbol = tlvMapper.mapOptional(TlvTag.TokenSymbol),
tokenContractAddress = tlvMapper.mapOptional(TlvTag.TokenContractAddress),
tokenDecimal = tlvMapper.mapOptional(TlvTag.TokenDecimal)
)
}
private fun serializePersonalizationData(cardId: String, config: CardConfig,
issuer: Issuer, manufacturerPrivateKey: ByteArray,
acquirePublicKey: ByteArray?
): ByteArray {
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.signingMethod)
tlvBuilder.append(TlvTag.SettingsMask, config.getSettingsMask())
tlvBuilder.append(TlvTag.PauseBeforePin2, config.pauseBeforePin2 / 10)
tlvBuilder.append(TlvTag.Cvc, config.cvc.toByteArray())
if (!config.ndefRecords.isNullOrEmpty()) tlvBuilder.append(TlvTag.NdefData, serializeNdef(config.ndefRecords))
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, acquirePublicKey)
tlvBuilder.append(
TlvTag.CardData, serializeCardData(cardId, config.cardData, issuer, manufacturerPrivateKey)
)
return tlvBuilder.serialize()
}
private fun serializeCardData(
cardId: String, cardData: CardData,
issuer: Issuer, manufacturerPrivateKey: ByteArray): 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(manufacturerPrivateKey)
)
return tlvBuilder.serialize()
}
private fun serializeNdef(ndefRecords: List<NdefRecord>): ByteArray {
return NdefEncoder(ndefRecords, config.useDynamicNdef).encode()
}
companion object {
val devPersonalizationKey = "1234".calculateSha256().copyOf(32)
}
}

View file

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

View file

@ -1,5 +1,7 @@
package com.tangem.common
import com.tangem.commands.personalization.entities.Issuer
/**
* Contains data relating to a Tangem card. It is used in constructing all the commands,
@ -10,8 +12,12 @@ data class CardEnvironment(
val pin2: String = DEFAULT_PIN2,
val cardId: String? = null,
val terminalKeys: KeyPair? = null,
val encryptionKey: ByteArray? = null,
val cvc: ByteArray? = null
var encryptionMode: EncryptionMode = EncryptionMode.NONE,
var encryptionKey: ByteArray? = null,
val cvc: ByteArray? = null,
val manufacturerKeyPair: KeyPair? = null,
val acquirerKeyPair: KeyPair? = null,
val issuer: Issuer? = null
) {
companion object {

View file

@ -1,6 +1,9 @@
package com.tangem.common.apdu
import com.tangem.common.EncryptionMode
import com.tangem.common.extensions.calculateCrc16
import com.tangem.common.extensions.toByteArray
import com.tangem.crypto.encrypt
import java.io.ByteArrayOutputStream
/**
@ -8,21 +11,19 @@ import java.io.ByteArrayOutputStream
* 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 tlvList A list of TLVs that are to be sent to 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 cla: Byte = ISO_CLA,
private val p1: Byte = 0x00,
private val p2: Byte = 0x00,
private val le: Int = 0x00,
private val encryptionMode: EncryptionMode = EncryptionMode.NONE,
private val encryptionKey: ByteArray? = null) {
private val encryptionKey: ByteArray? = null,
private val cla: Int = ISO_CLA) {
constructor(
instruction: Instruction,
@ -36,6 +37,19 @@ class CommandApdu(
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
@ -48,33 +62,37 @@ class CommandApdu(
private fun toBytes(): ByteArray {
val lc = tlvs.size
val data = if (encryptionKey != null) tlvs.encrypt() else tlvs
val byteStream = ByteArrayOutputStream()
byteStream.write(cla.toInt())
byteStream.write(cla)
byteStream.write(ins)
byteStream.write(p1.toInt())
byteStream.write(p2.toInt())
if (lc != 0) {
writeLength(byteStream, lc)
byteStream.write(tlvs)
byteStream.write(p1)
byteStream.write(p2)
if (data.isNotEmpty()) {
byteStream.writeLength(data.size)
byteStream.write(data)
}
return byteStream.toByteArray()
}
private fun writeLength(stream: ByteArrayOutputStream, lc: Int) {
stream.write(0)
stream.write(lc shr 8)
stream.write(lc and 0xFF)
private fun ByteArrayOutputStream.writeLength(lc: Int) {
this.write(0)
this.write(lc shr 8)
this.write(lc and 0xFF)
}
private fun encrypt() {
TODO("not implemented")
}
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.toByte()
const val ISO_CLA = 0x00
}
}

View file

@ -6,6 +6,7 @@ package com.tangem.common.apdu
*/
enum class Instruction(var code: Int) {
Unknown(0x00),
Personalize(0xF1),
Read(0xF2),
VerifyCard(0xF3),
ValidateCard(0xF4),
@ -20,7 +21,8 @@ enum class Instruction(var code: Int) {
Activate(0xFE),
OpenSession(0xFF),
WriteUserData(0xE0),
ReadUserData(0xE1);
ReadUserData(0xE1),
Depersonalize(0xE3);
companion object {

View file

@ -1,6 +1,9 @@
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].
@ -25,15 +28,39 @@ class ResponseApdu(private val data: ByteArray) {
* (Encryption / decryption functionality is not implemented yet.)
*/
fun getTlvData(encryptionKey: ByteArray? = null): List<Tlv>? {
return when {
data.size <= 2 -> null
else -> Tlv.deserialize(data.copyOf(data.size - 2))
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)
private fun decrypt(encryptionKey: ByteArray) {
TODO("not implemented")
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

@ -6,6 +6,7 @@ import java.nio.ByteBuffer
import java.security.MessageDigest
import java.util.*
import kotlin.experimental.and
import kotlin.experimental.xor
/**
* Extension functions for [ByteArray].
@ -53,4 +54,21 @@ fun ByteArray.toCompressedPublicKey(): ByteArray {
} 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

@ -6,6 +6,7 @@ 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 com.tangem.common.extensions.toHexString
import com.tangem.tasks.TaskError
import java.util.*
@ -32,11 +33,11 @@ class TlvEncoder {
return when (tag.valueType()) {
TlvValueType.HexString -> {
typeCheck<T, String>(tag)
return if (tag == TlvTag.Pin || tag == TlvTag.Pin2) {
(value as String).calculateSha256()
} else {
(value as String).hexToBytes()
}
(value as String).hexToBytes()
}
TlvValueType.HexStringToHash -> {
typeCheck<T, String>(tag)
(value as String).calculateSha256()
}
TlvValueType.Utf8String -> {
typeCheck<T, String>(tag)
@ -52,8 +53,8 @@ class TlvEncoder {
}
TlvValueType.BoolValue -> {
typeCheck<T, Boolean>(tag)
Log.e(this::class.simpleName!!, "Unsupported operation: Boolean to ByteArray for tag $tag")
throw TaskError.ConvertError()
val booleanValue = value as Boolean
if (booleanValue) byteArrayOf(1) else byteArrayOf(0)
}
TlvValueType.ByteArray -> {
typeCheck<T, ByteArray>(tag)
@ -61,7 +62,7 @@ class TlvEncoder {
}
TlvValueType.EllipticCurve -> {
typeCheck<T, EllipticCurve>(tag)
(value as EllipticCurve).curve.plus("\\0").toByteArray()
(value as EllipticCurve).curve.toByteArray()
}
TlvValueType.DateTime -> {
typeCheck<T, Date>(tag)
@ -69,7 +70,7 @@ class TlvEncoder {
val year = calendar.get(Calendar.YEAR)
val month = calendar.get(Calendar.MONTH) + 1
val day = calendar.get(Calendar.DAY_OF_MONTH)
return year.toByteArray() + month.toByteArray() + day.toByteArray()
return year.toByteArray(2) + month.toByte() + day.toByte()
}
TlvValueType.ProductMask -> {
typeCheck<T, ProductMask>(tag)
@ -79,7 +80,8 @@ class TlvEncoder {
}
TlvValueType.SettingsMask -> {
typeCheck<T, SettingsMask>(tag)
(value as SettingsMask).rawValue.toByteArray(2)
val rawValue = (value as SettingsMask).rawValue
rawValue.toByteArray(determineByteArraySize(rawValue))
}
TlvValueType.CardStatus -> {
typeCheck<T, CardStatus>(tag)
@ -87,7 +89,7 @@ class TlvEncoder {
}
TlvValueType.SigningMethod -> {
typeCheck<T, SigningMethod>(tag)
(value as SigningMethod).rawValue.toByteArray()
byteArrayOf((value as SigningMethod).rawValue.toByte())
}
TlvValueType.IssuerDataMode -> {
typeCheck<T, IssuerDataMode>(tag)
@ -96,6 +98,11 @@ class TlvEncoder {
}
}
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!!,

View file

@ -54,7 +54,7 @@ class TlvMapper(val tlvList: List<Tlv>) {
}
return when (tag.valueType()) {
TlvValueType.HexString -> {
TlvValueType.HexString, TlvValueType.HexStringToHash -> {
typeCheck<T, String>(tag)
tlvValue.toHexString() as T
}

View file

@ -5,6 +5,7 @@ package com.tangem.common.tlv
*/
enum class TlvValueType {
HexString,
HexStringToHash,
Utf8String,
Uint16,
Uint32,
@ -36,6 +37,7 @@ enum class TlvTag(val code: Int) {
SettingsMask(0x0A),
CardData(0x0C),
NdefData(0x0D),
CreateWalletAtPersonalize(0x0E),
Health(0x0F),
Pin(0x10),
@ -52,6 +54,10 @@ enum class TlvTag(val code: Int) {
SessionKeyA(0x1A),
SessionKeyB(0x1B),
Pause(0x1C),
NewPin3(0x1E),
CrExKey(0x1F),
Uid(0x0B),
ManufactureId(0x20),
ManufacturerSignature(0x86),
@ -62,11 +68,12 @@ enum class TlvTag(val code: Int) {
IssuerDataSignature(0x33),
IssuerTransactionSignature(0x34),
IssuerDataCounter(0x35),
AcquirerPublicKey(0x37),
Size(0x25),
Mode(0x23),
Offset(0x24),
IsActivated(0x3A),
ActivationSeed(0x3B),
ResetPin(0x36),
@ -95,7 +102,6 @@ enum class TlvTag(val code: Int) {
ProductMask(0x8A),
PaymentFlowVersion(0x54),
TokenSymbol(0xA0),
TokenContractAddress(0xA1),
TokenDecimal(0xA2),
@ -118,15 +124,15 @@ enum class TlvTag(val code: Int) {
*/
fun valueType(): TlvValueType {
return when (this) {
CardId, Pin, Pin2, Batch -> TlvValueType.HexString
CardId, Batch, CrExKey -> TlvValueType.HexString
Pin, Pin2, NewPin, NewPin2, NewPin3 -> TlvValueType.HexStringToHash
ManufactureId, Firmware, IssuerId, BlockchainId, TokenSymbol, TokenContractAddress ->
TlvValueType.Utf8String
CurveId -> TlvValueType.EllipticCurve
MaxSignatures, PauseBeforePin2, RemainingSignatures,
SignedHashes, Health, TokenDecimal,
PauseBeforePin2, RemainingSignatures, SignedHashes, Health, TokenDecimal,
Offset, Size -> TlvValueType.Uint16
UserCounter, UserProtectedCounter, IssuerDataCounter -> TlvValueType.Uint32
IsActivated, TerminalIsLinked -> TlvValueType.BoolValue
MaxSignatures, UserCounter, UserProtectedCounter, IssuerDataCounter -> TlvValueType.Uint32
IsActivated, TerminalIsLinked, CreateWalletAtPersonalize -> TlvValueType.BoolValue
ManufactureDateTime -> TlvValueType.DateTime
ProductMask -> TlvValueType.ProductMask
SettingsMask -> TlvValueType.SettingsMask

View file

@ -2,14 +2,19 @@ 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(org.spongycastle.jce.provider.BouncyCastleProvider(), 1)
Security.insertProviderAt(BouncyCastleProvider(), 1)
Security.addProvider(EdDSASecurityProvider())
}
@ -62,6 +67,16 @@ object CryptoUtils {
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)
}
}
}
/**
@ -79,4 +94,28 @@ fun ByteArray.sign(privateKeyArray: ByteArray, curve: EllipticCurve = EllipticCu
}
}
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

@ -24,7 +24,7 @@ object Ed25519 {
return signatureInstance.verify(signature)
}
private fun loadPublicKey(publicKeyArray: ByteArray): PublicKey {
internal fun loadPublicKey(publicKeyArray: ByteArray): PublicKey {
val spec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519)
val pubKey = EdDSAPublicKeySpec(publicKeyArray, spec)
return EdDSAPublicKey(pubKey)

View file

@ -0,0 +1,50 @@
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

@ -0,0 +1,88 @@
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

@ -29,7 +29,7 @@ object Secp256k1 {
return signatureInstance.verify(sigDer)
}
private fun loadPublicKey(publicKeyArray: ByteArray): PublicKey {
internal fun loadPublicKey(publicKeyArray: ByteArray): PublicKey {
val spec = ECNamedCurveTable.getParameterSpec("secp256k1")
val factory = KeyFactory.getInstance("EC", "SC")

View file

@ -3,57 +3,150 @@ package com.tangem.tasks
import com.tangem.CardManagerDelegate
import com.tangem.CardReader
import com.tangem.Log
import com.tangem.commands.Card
import com.tangem.commands.CommandResponse
import com.tangem.commands.CommandSerializer
import com.tangem.commands.ReadCommand
import com.tangem.commands.*
import com.tangem.common.CardEnvironment
import com.tangem.common.CompletionResult
import com.tangem.common.EncryptionMode
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.StatusWord
import com.tangem.common.extensions.calculateSha256
import com.tangem.crypto.EncryptionHelper
import com.tangem.crypto.FastEncryptionHelper
import com.tangem.crypto.StrongEncryptionHelper
import com.tangem.crypto.pbkdf2Hash
/**
* 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 TaskError(val code: Int): Exception() {
sealed class TaskError(val code: Int) : Exception() {
//Errors in serializing APDU
class SerializeCommandError: TaskError(1001)
class EncodingError: TaskError(1002)
class MissingTag: TaskError(1003)
class WrongType: TaskError(1004)
class ConvertError: TaskError(1005)
/**
* This error is returned when there [CommandSerializer] cannot deserialize [com.tangem.common.tlv.Tlv]
* (this error is a wrapper around internal [com.tangem.common.tlv.TlvMapper] errors).
*/
class SerializeCommandError : TaskError(1001)
//Card errors
class UnknownStatus: TaskError(2001)
class ErrorProcessingCommand: TaskError(2002)
class MissingPreflightRead: TaskError(2003)
class InvalidState: TaskError(2004)
class InsNotSupported: TaskError(2005)
class InvalidParams: TaskError(2006)
class NeedEncryption: TaskError(2007)
class EncodingError : TaskError(1002)
class MissingTag : TaskError(1003)
class WrongType : TaskError(1004)
class ConvertError : TaskError(1005)
/**
* This error is returned when unknown [StatusWord] is received from a card.
*/
class UnknownStatus : TaskError(2001)
/**
* 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 : TaskError(2002)
/**
* This error is returned when a task (such as [ScanTask]) requires that [ReadCommand]
* is executed before performing other commands.
*/
class MissingPreflightRead : TaskError(2003)
/**
* 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 : TaskError(2004)
/**
* 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 : TaskError(2005)
/**
* 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 : TaskError(2006)
/**
* This error is returned when a card's reply is [StatusWord.NeedEncryption]
* and the encryption was not established by TangemSdk.
*/
class NeedEncryption : TaskError(2007)
//Scan errors
class VerificationFailed: TaskError(3000)
class CardError: TaskError(3001)
class WrongCard: TaskError(3002)
class TooMuchHashesInOneTransaction: TaskError(3003)
class EmptyHashes: TaskError(3004)
class HashSizeMustBeEqual: TaskError(3005)
/**
* 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 : TaskError(3000)
class Busy: TaskError(4000)
class UserCancelled: TaskError(4001)
class UnsupportedDevice: TaskError(4002)
/**
* This error is returned when a [ScanTask] returns a [Card] without some of the essential fields.
*/
class CardError : TaskError(3001)
//NFC error
class NfcReaderError: TaskError(5002)
class TagLost: TaskError(5003)
/**
* This error is returned when a [Task] expects a user to use a particular card,
* and a user tries to use a different card.
*/
class WrongCard : TaskError(3002)
class UnknownError: TaskError(6000)
/**
* 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 TooMuchHashesInOneTransaction : TaskError(3003)
//Issuer Data Errors
class MissingCounter: TaskError(7001)
/**
* This error is returned when a [com.tangem.commands.SignCommand]
* receives only empty hashes for signature.
*/
class EmptyHashes : TaskError(3004)
/**
* This error is returned when a [com.tangem.commands.SignCommand]
* receives hashes of different lengths for signature.
*/
class HashSizeMustBeEqual : TaskError(3005)
/**
* This error is returned when [com.tangem.CardManager] was called with a new [Task],
* while a previous [Task] is still in progress.
*/
class Busy : TaskError(4000)
/**
* This error is returned when a user manually closes NFC Reading Bottom Sheet Dialog.
*/
class UserCancelled : TaskError(4001)
//NFC errors
class NfcReaderError : TaskError(5002)
/**
* 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 : TaskError(5003)
class UnknownError : TaskError(6000)
//Specific Command Errors
/**
* This error is returned when [ReadIssuerDataTask] or [ReadIssuerExtraDataTask] expects a counter
* (when the card's requires it), but the counter is missing.
*/
class MissingCounter : TaskError(7001)
/**
* This error is returned when [com.tangem.commands.personalization.PersonalizeCommand] is attempted
* without [com.tangem.commands.personalization.entities.Issuer] being set in the [com.tangem.Config].
*/
class IssuerIsRequired : TaskError(7002)
}
/**
@ -137,8 +230,41 @@ abstract class Task<T> {
Log.i(this::class.simpleName!!, "Nfc command ${command::class.simpleName!!} is initiated")
val commandApdu = command.serialize(cardEnvironment)
sendRequest(command, commandApdu, cardEnvironment, callback)
when (cardEnvironment.encryptionMode) {
EncryptionMode.NONE -> {
val commandApdu = command.serialize(cardEnvironment)
sendRequest(command, commandApdu, cardEnvironment, callback)
}
EncryptionMode.FAST, EncryptionMode.STRONG -> {
if (cardEnvironment.encryptionKey != null ) {
val commandApdu = command.serialize(cardEnvironment)
sendRequest(command, commandApdu, cardEnvironment, callback)
return
}
val encryptionHelper: EncryptionHelper =
if (cardEnvironment.encryptionMode == EncryptionMode.STRONG) {
StrongEncryptionHelper()
} else {
FastEncryptionHelper()
}
val openSessionCommand = OpenSessionCommand(encryptionHelper.keyA)
val openSessionApdu = openSessionCommand.serialize(cardEnvironment)
sendRequest(openSessionCommand, openSessionApdu, cardEnvironment) { result ->
when (result) {
is CompletionResult.Success -> {
val uid = result.data.uid
val protocolKey = cardEnvironment.pin1.calculateSha256().pbkdf2Hash(uid, 50)
val secret = encryptionHelper.generateSecret(result.data.sessionKeyB)
val sessionKey = (secret + protocolKey).calculateSha256()
cardEnvironment.encryptionKey = sessionKey
sendCommand(command, cardEnvironment, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}
}
private fun <T : CommandResponse> sendRequest(command: CommandSerializer<T>,
@ -171,9 +297,26 @@ abstract class Task<T> {
StatusWord.InvalidState -> callback(CompletionResult.Failure(TaskError.InvalidState()))
StatusWord.InsNotSupported -> callback(CompletionResult.Failure(TaskError.InsNotSupported()))
StatusWord.NeedEncryption -> callback(CompletionResult.Failure(TaskError.NeedEncryption()))
StatusWord.NeedEncryption -> {
when (cardEnvironment.encryptionMode) {
EncryptionMode.NONE -> {
cardEnvironment.encryptionKey = null
cardEnvironment.encryptionMode = EncryptionMode.FAST
}
EncryptionMode.FAST -> {
cardEnvironment.encryptionKey = null
cardEnvironment.encryptionMode = EncryptionMode.STRONG
}
EncryptionMode.STRONG -> {
Log.e(this::class.simpleName!!, "Encryption doesn't work")
callback(CompletionResult.Failure(TaskError.NeedEncryption()))
return@transceiveApdu
}
}
sendCommand(command, cardEnvironment, callback)
}
StatusWord.NeedPause -> {
// When NeedPause is returned from the card whenever security delay is triggered.
// NeedPause is returned from the card whenever security delay is triggered.
val remainingTime = command.deserializeSecurityDelay(responseApdu, cardEnvironment)
if (remainingTime != null) delegate?.onSecurityDelay(remainingTime, securityDelayDuration)
Log.i(this::class.simpleName!!, "Nfc command ${command::class.simpleName!!} triggered security delay of $remainingTime milliseconds")
@ -201,7 +344,6 @@ abstract class Task<T> {
callback(TaskEvent.Completion(readResult.error))
}
is CompletionResult.Success -> {
val receivedCardId = readResult.data.cardId
securityDelayDuration = readResult.data.pauseBeforePin2 ?: 0
@ -215,10 +357,7 @@ abstract class Task<T> {
onRun(newEnvironment, readResult.data, callback)
}
}
}
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.tasks
import com.tangem.commands.Card
import com.tangem.commands.SettingsMask
import com.tangem.commands.Settings
import com.tangem.commands.WriteIssuerDataCommand
import com.tangem.commands.WriteIssuerDataResponse
import com.tangem.commands.common.IssuerDataToVerify
@ -55,7 +55,7 @@ class WriteIssuerDataTask(
if (isCounterRequired()) issuerDataCounter != null else true
private fun isCounterRequired(): Boolean =
card.settingsMask?.contains(SettingsMask.protectIssuerDataAgainstReplay) != false
card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false
private fun verifySignature(command: WriteIssuerDataCommand, cardId: String): Boolean {
return command.verify(

View file

@ -1,9 +1,6 @@
package com.tangem.tasks
import com.tangem.commands.Card
import com.tangem.commands.SettingsMask
import com.tangem.commands.WriteIssuerDataResponse
import com.tangem.commands.WriteIssuerExtraDataCommand
import com.tangem.commands.*
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.common.CardEnvironment
@ -98,7 +95,7 @@ internal class WriteIssuerExtraDataTask(
if (isCounterRequired()) issuerDataCounter != null else true
private fun isCounterRequired(): Boolean =
card.settingsMask?.contains(SettingsMask.protectIssuerDataAgainstReplay) != false
card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false
private fun verifySignatures(command: WriteIssuerExtraDataCommand): Boolean {
val publicKey = issuerPublicKey ?: card.issuerPublicKey!!

View file

@ -68,15 +68,15 @@ class TlvMapperTest {
.isNotNull()
assertThat(settingsMask.rawValue)
.isEqualTo(32289)
assertThat(settingsMask.contains(SettingsMask.skipSecurityDelayIfValidatedByLinkedTerminal))
assertThat(settingsMask.contains(Settings.SkipSecurityDelayIfValidatedByLinkedTerminal))
.isFalse()
assertThat(settingsMask.contains(SettingsMask.isReusable))
assertThat(settingsMask.contains(Settings.IsReusable))
.isTrue()
assertThat(settingsMask.contains(SettingsMask.allowSwapPIN2))
assertThat(settingsMask.contains(Settings.AllowSwapPIN2))
.isTrue()
assertThat(settingsMask.contains(SettingsMask.useDynamicNdef))
assertThat(settingsMask.contains(Settings.UseDynamicNdef))
.isTrue()
assertThat(settingsMask.contains(SettingsMask.forbidPurgeWallet))
assertThat(settingsMask.contains(Settings.ForbidPurgeWallet))
.isFalse()
}

View file

@ -4,7 +4,9 @@ import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.tangem.CardManager
import com.tangem.commands.personalization.CardConfig
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tangemtest.extensions.init
import com.tangem.tasks.ScanEvent
import com.tangem.tasks.TaskError
import com.tangem.tasks.TaskEvent
@ -151,6 +153,30 @@ class Old_MainActivity : AppCompatActivity() {
}
}
btn_read_write_user_data?.setOnClickListener { startActivity(Intent(this, TestUserDataActivity::class.java)) }
btn_personalize?.setOnClickListener { _ ->
cardManager.personalize(
CardConfig.init(application), "BB00000000000395"
) {
when (it) {
is TaskEvent.Completion -> {
if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName }
}
is TaskEvent.Event -> runOnUiThread { tv_card_cid?.text = it.data.cardId }
}
}
}
btn_depersonalize?.setOnClickListener { _ ->
cardManager.depersonalize(cardId) {
when (it) {
is TaskEvent.Completion -> {
if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName }
}
is TaskEvent.Event -> runOnUiThread {
tv_card_cid?.text = "Depersonalized: ${it.data.success.toString()}"
}
}
}
}
}
private fun createSampleHashes(): Array<ByteArray> {

View file

@ -0,0 +1,125 @@
package com.tangem.tangemtest.extensions
import android.app.Application
import android.content.Context
import com.tangem.commands.*
import com.tangem.commands.personalization.CardConfig
import com.tangem.commands.personalization.NdefRecord
import java.util.*
fun CardConfig.Companion.init(application: Application): CardConfig {
val preferences = application.getSharedPreferences("prefs", Context.MODE_PRIVATE)
val signingMethod = SigningMethod.build(
signHash = preferences.getBoolean("personalization_SigningMethod_0", false),
signRaw = preferences.getBoolean("personalization_SigningMethod_1", false),
signHashValidatedByIssuer = preferences.getBoolean("personalization_SigningMethod_2", false),
signRawValidatedByIssuer = preferences.getBoolean("personalization_SigningMethod_3", false),
signHashValidatedByIssuerAndWriteIssuerData = preferences.getBoolean("personalization_SigningMethod_4", false),
signRawValidatedByIssuerAndWriteIssuerData = preferences.getBoolean("personalization_SigningMethod_5", false),
signPos = preferences.getBoolean("personalization_SigningMethod_6", false)
)
val isNote = preferences.getBoolean("personalization_ProductMask_IsNote", true)
val isTag = preferences.getBoolean("personalization_ProductMask_IsTag", false)
val isIdCard = preferences.getBoolean("personalization_ProductMask_IsIDCard", false)
val productMaskBuilder = ProductMaskBuilder()
if (isNote) productMaskBuilder.add(ProductMask.note)
if (isTag) productMaskBuilder.add(ProductMask.tag)
if (isIdCard) productMaskBuilder.add(ProductMask.idCard)
val productMask = productMaskBuilder.build()
var tokenSymbol: String? = null
var tokenContractAddress: String? = null
var tokenDecimal: Int? = null
if (preferences.getBoolean("personalization_isToken", false)) {
tokenSymbol = preferences.getString("personalization_token_symbol", "")
tokenContractAddress = preferences.getString("personalization_token_contract_address", "")
tokenDecimal = preferences.getString("personalization_token_decimal", "")!!.toInt()
}
val cardData = CardData(
blockchainName = preferences.getString("personalization_Blockchain", "BTC"),
batchId = preferences.getString("personalization_card_batch", "FFFF"),
productMask = productMask,
tokenSymbol = tokenSymbol,
tokenContractAddress = tokenContractAddress,
tokenDecimal = tokenDecimal,
issuerName = null,
manufactureDateTime = Calendar.getInstance().time,
manufacturerSignature = null)
val ndefAar = preferences.getString("personalization_NDEF_AAR", "Release APP")
val ndefUri = preferences.getString("personalization_NDEF_URI", "https://tangem.com")
val ndefs = mutableListOf<NdefRecord>()
if (!ndefUri.isNullOrEmpty()) {
ndefs.add(NdefRecord(NdefRecord.Type.URI, ndefUri))
}
if (ndefAar != "None") {
val type = NdefRecord.Type.AAR
val value = when (ndefAar) {
"Debug APP" -> {
"com.tangem.wallet.debug"
}
"Release APP" -> {
"com.tangem.wallet"
}
"--- CUSTOM ---" -> {
preferences.getString("personalization_NDEF_CUSTOM_AAR", "com.tangem.wallet")!!
}
else -> ""
}
ndefs.add(NdefRecord(type, value))
}
return CardConfig(
cardData = cardData,
curveID = EllipticCurve.byName(preferences.getString("personalization_CurveId", "secp256k1")!!)
?: EllipticCurve.Secp256k1,
signingMethod = signingMethod,
createWallet = preferences.getBoolean("personalization_CreateWallet", true),
maxSignatures = preferences.getString("personalization_MaxSignatures", "1000")!!.toInt(),
isReusable = preferences.getBoolean("personalization_SettingsMask_IsReusable", true),
protocolAllowUnencrypted = preferences.getBoolean("personalization_SettingsMask_AllowEncryption_None", true),
protocolAllowStaticEncryption = preferences.getBoolean("personalization_SettingsMask_AllowEncryption_Fast", true),
useActivation = preferences.getBoolean("personalization_SettingsMask_NeedActivation", false),
useOneCommandAtTime = preferences.getBoolean("personalization_SettingsMask_OneApduAtOnce", false),
useCvc = preferences.getBoolean("personalization_SettingsMask_UseCVC", false),
useBlock = preferences.getBoolean("personalization_SettingsMask_UseBlock", false),
allowSwapPin = preferences.getBoolean("personalization_SettingsMask_AllowSwapPIN", true),
allowSwapPin2 = preferences.getBoolean("personalization_SettingsMask_AllowSwapPIN2", true),
useNdef = preferences.getBoolean("personalization_SettingsMask_UseNDEF", true),
useDynamicNdef = preferences.getBoolean("personalization_SettingsMask_UseDynamicNDEF", true),
protectIssuerDataAgainstReplay = preferences.getBoolean("personalization_SettingsMask_ProtectIssuerDataAgainstReplay", true),
forbidDefaultPin = preferences.getBoolean("personalization_SettingsMask_ForbidDefaultPIN", false),
smartSecurityDelay = preferences.getBoolean("personalization_SettingsMask_SmartSecurityDelay", false),
pauseBeforePin2 = preferences.getString("personalization_PauseBeforePIN2", "15")!!.toInt() * 1000,
allowSelectBlockchain = preferences.getBoolean("personalization_SettingsMask_AllowSelectBlockchain", false),
forbidPurgeWallet = preferences.getBoolean("personalization_SettingsMask_ForbidPurgeWallet", false)
?: false,
disablePrecomputedNdef = preferences.getBoolean("personalization_SettingsMask_DisablePrecomputedNDEF", false)
?: false,
skipSecurityDelayIfValidatedByIssuer = preferences.getBoolean("personalization_SettingsMask_SkipSecurityDelayIfValidatedByIssuer", true),
skipCheckPIN2andCVCIfValidatedByIssuer = preferences.getBoolean("personalization_SettingsMask_SkipCheckPIN2andCVCIfValidatedByIssuer", true),
skipSecurityDelayIfValidatedByLinkedTerminal = preferences.getBoolean("personalization_SettingsMask_SkipSecurityDelayIfValidatedByLinkedTerminal", true),
restrictOverwriteIssuerDataEx = preferences.getBoolean("personalization_SettingsMask_RestrictOverwriteIssuerDataEx", true),
requireTerminalTxSignature = preferences.getBoolean("personalization_SettingsMask_RequireTerminalTxSignature", false),
requireTerminalCertSignature = preferences.getBoolean("personalization_SettingsMask_RequireTerminalCertSignature", false),
checkPin3onCard = preferences.getBoolean("personalization_SettingsMask_CheckPIN3onCard", true),
cvc = preferences.getString("personalization_cvc", "000") ?: "000",
pin = preferences.getString("personalization_pin", "000000") ?: "000000",
pin2 = preferences.getString("personalization_pin2", "000") ?: "000",
pin3 = preferences.getString("personalization_pin3", "123") ?: "123",
hexCrExKey = preferences.getString("personalization_CrEx_Key", "00112233445566778899AABBCCDDEEFFFFEEDDCCBBAA998877665544332211000000111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFF"),
ndefRecords = ndefs
)
}

View file

@ -43,7 +43,7 @@ enum class NfcLocation(val codename: String, val fullName: String, val orientati
model40("HWVTR", "Huawei P10", 0, 50, 0, 0),
model41("HWWAS-H", "Huawei P10 lite", 0, 50, 0, 0),
model42("HWVKY", "Huawei P10 Plus", 0, 50, 0, 0),
model43("HWEML", "Huawei P20", 0, 50, 50, 0),
model43("HWEML", "Huawei P20", 0, 40, 5, 0),
model44("HWANE", "Huawei P20 Lite", 0, 50, 20, 0),
model45("HWCLT", "Huawei P20 Pro", 0, 50, 50, 0),
model46("HW-01K", "Huawei P20 Pro", 0, 50, 50, 0),
@ -231,8 +231,8 @@ enum class NfcLocation(val codename: String, val fullName: String, val orientati
model228("sagit", "Xiaomi Mi 6", 0, 50, 20, 0),
model229("dipper", "Xiaomi Mi 8", 0, 45, 20, 0),
model230("ursa", "Xiaomi MI 8 Explorer Edition", 0, 50, 40, 0),
model231("cepheus", "Xiaomi MI 9", 0, 40, 20, 0),
model232("grus", "Xiaomi MI 9 SE", 0, 40, 20, 0),
model231("cepheus", "Xiaomi MI 9", 0, 40, 5, 0),
model232("grus", "Xiaomi MI 9 SE", 0, 40, 5, 0),
model233("lithium", "Xiaomi Mi MIX", 0, 20, 20, 0),
model234("chiron", "Xiaomi Mi MIX 2", 0, 45, 20, 0),
model235("polaris", "Xiaomi Mi MIX 2S", 0, 45, 20, 0),

View file

@ -47,11 +47,11 @@ dependencies {
implementation 'com.skyfishjy.ripplebackground:library:1.0.1'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.core:core-ktx:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
implementation "org.jetbrains.kotlin:kotlin-reflect:1.3.61"
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
implementation 'at.favre.lib:armadillo:0.9.0'

View file

@ -40,7 +40,7 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
}
private fun showReadingDialog(activity: FragmentActivity, cardId: String?) {
val dialogView = activity.getLayoutInflater().inflate(R.layout.nfc_bottom_sheet, null)
val dialogView = activity.layoutInflater.inflate(R.layout.nfc_bottom_sheet, null)
readingDialog = BottomSheetDialog(activity)
readingDialog?.setContentView(dialogView)
readingDialog?.dismissWithAnimation = true