Updated on 2026-08-14
This commit is contained in:
commit
7c6a986089
61 changed files with 1454 additions and 248 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, "", "");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class ReadIssuerDataResponse(
|
|||
* Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card).
|
||||
* Issuer’s 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)
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class ReadIssuerExtraDataResponse(
|
|||
* Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card).
|
||||
* Issuer’s 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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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!")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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!!,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
88
tangem-core/src/main/java/com/tangem/crypto/Pbkdf2.kt
Normal file
88
tangem-core/src/main/java/com/tangem/crypto/Pbkdf2.kt
Normal 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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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!!
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
0
tangem-demo/src/main/res/layout/activity_main.xml
Normal file
0
tangem-demo/src/main/res/layout/activity_main.xml
Normal 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),
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue