diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 280cc6ed9d..0000000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/network/Server.java b/app/src/main/java/com/tangem/data/network/Server.java index 97fb0ab7e3..c31e4cf46b 100644 --- a/app/src/main/java/com/tangem/data/network/Server.java +++ b/app/src/main/java/com/tangem/data/network/Server.java @@ -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; } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/ui/fragment/VerifyCardFragment.kt b/app/src/main/java/com/tangem/ui/fragment/VerifyCardFragment.kt index 307a9186b7..421f780cc1 100644 --- a/app/src/main/java/com/tangem/ui/fragment/VerifyCardFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/VerifyCardFragment.kt @@ -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 } diff --git a/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletFragment.kt b/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletFragment.kt index ca0505d7aa..2fdf9776d8 100644 --- a/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletFragment.kt @@ -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 { rate -> ctx.coinData.rate = rate ctx.coinData.rateAlter = rate + updateViews() }) viewModel.requestRateInfo(ctx) } diff --git a/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java b/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java index edc471e86b..2be3abdcac 100644 --- a/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java +++ b/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java @@ -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://api.blockchair.com/bitcoin-cash/dashboards/address/" + ctx.getCoinData().getWallet()); } @Override diff --git a/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java b/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java index 4bfa331eda..05b489ba07 100644 --- a/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java +++ b/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java @@ -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()); } } diff --git a/app/src/main/java/com/tangem/wallet/eth/EthEngine.java b/app/src/main/java/com/tangem/wallet/eth/EthEngine.java index 7175f68f73..8c24e30480 100644 --- a/app/src/main/java/com/tangem/wallet/eth/EthEngine.java +++ b/app/src/main/java/com/tangem/wallet/eth/EthEngine.java @@ -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()); } } diff --git a/app/src/main/java/com/tangem/wallet/ethID/EthIdEngine.java b/app/src/main/java/com/tangem/wallet/ethID/EthIdEngine.java index ebf42f4e21..3c9e6c0017 100644 --- a/app/src/main/java/com/tangem/wallet/ethID/EthIdEngine.java +++ b/app/src/main/java/com/tangem/wallet/ethID/EthIdEngine.java @@ -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()); } } diff --git a/app/src/main/java/com/tangem/wallet/ltc/LtcEngine.java b/app/src/main/java/com/tangem/wallet/ltc/LtcEngine.java index 801ed7673e..e8ebfa3fd3 100644 --- a/app/src/main/java/com/tangem/wallet/ltc/LtcEngine.java +++ b/app/src/main/java/com/tangem/wallet/ltc/LtcEngine.java @@ -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()); } } diff --git a/app/src/main/java/com/tangem/wallet/nftToken/NftTokenEngine.java b/app/src/main/java/com/tangem/wallet/nftToken/NftTokenEngine.java index 859d8f3d24..151e631fbd 100644 --- a/app/src/main/java/com/tangem/wallet/nftToken/NftTokenEngine.java +++ b/app/src/main/java/com/tangem/wallet/nftToken/NftTokenEngine.java @@ -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 diff --git a/app/src/main/java/com/tangem/wallet/token/TokenEngine.java b/app/src/main/java/com/tangem/wallet/token/TokenEngine.java index ac453d5ff9..d36abf0480 100644 --- a/app/src/main/java/com/tangem/wallet/token/TokenEngine.java +++ b/app/src/main/java/com/tangem/wallet/token/TokenEngine.java @@ -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()); } } diff --git a/app/src/main/java/com/tangem/wallet/xrp/XrpData.java b/app/src/main/java/com/tangem/wallet/xrp/XrpData.java index 3df5e26532..1b10a221a3 100644 --- a/app/src/main/java/com/tangem/wallet/xrp/XrpData.java +++ b/app/src/main/java/com/tangem/wallet/xrp/XrpData.java @@ -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); } - } diff --git a/app/src/main/java/com/tangem/wallet/xrp/XrpEngine.java b/app/src/main/java/com/tangem/wallet/xrp/XrpEngine.java index 73db5449a5..f6941d71fc 100644 --- a/app/src/main/java/com/tangem/wallet/xrp/XrpEngine.java +++ b/app/src/main/java/com/tangem/wallet/xrp/XrpEngine.java @@ -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 @@ -342,6 +342,11 @@ public class XrpEngine extends CoinEngine { public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception { checkBlockchainDataExists(); + Amount reserve = convertToAmount(coinData.getReserveInInternalUnits()); + if (!coinData.isTargetAccountCreated() && amountValue.compareTo(reserve) < 0) { + throw new Exception("Target account is not created. Amount should be " + reserve.toDescriptionString(getDecimals()) + " or more"); + } + String amount, fee; if (IncFee) { @@ -522,19 +527,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 +584,7 @@ public class XrpEngine extends CoinEngine { serverApiRipple.setResponseListener(rippleListener); + serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_INFO, targetAddress, ""); serverApiRipple.requestData(ServerApiRipple.RIPPLE_FEE, "", ""); } diff --git a/tangem-core/src/main/java/com/tangem/CardManager.kt b/tangem-core/src/main/java/com/tangem/CardManager.kt index 4094d935bc..83fcd22aeb 100644 --- a/tangem-core/src/main/java/com/tangem/CardManager.kt +++ b/tangem-core/src/main/java/com/tangem/CardManager.kt @@ -5,7 +5,6 @@ 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 +23,6 @@ class CardManager( private var terminalKeysService: TerminalKeysService? = null private var isBusy = false - private val cardManagerExecutor = Executors.newSingleThreadExecutor() init { CryptoUtils.initCrypto() @@ -69,18 +67,7 @@ class CardManager( */ fun sign(hashes: Array, cardId: String, callback: (result: TaskEvent) -> 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) } @@ -262,7 +249,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) diff --git a/tangem-core/src/main/java/com/tangem/commands/CheckWalletCommand.kt b/tangem-core/src/main/java/com/tangem/commands/CheckWalletCommand.kt index e5376f2cc7..28f0cd2d43 100644 --- a/tangem-core/src/main/java/com/tangem/commands/CheckWalletCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/CheckWalletCommand.kt @@ -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() { 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) diff --git a/tangem-core/src/main/java/com/tangem/commands/CommandSerializer.kt b/tangem-core/src/main/java/com/tangem/commands/CommandSerializer.kt index 92ee3d0d90..134474fb23 100644 --- a/tangem-core/src/main/java/com/tangem/commands/CommandSerializer.kt +++ b/tangem-core/src/main/java/com/tangem/commands/CommandSerializer.kt @@ -38,7 +38,7 @@ abstract class CommandSerializer { * @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() } } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/CreateWalletCommand.kt b/tangem-core/src/main/java/com/tangem/commands/CreateWalletCommand.kt index 0c142ee296..18b20f1394 100644 --- a/tangem-core/src/main/java/com/tangem/commands/CreateWalletCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/CreateWalletCommand.kt @@ -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() { 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) diff --git a/tangem-core/src/main/java/com/tangem/commands/OpenSessionCommand.kt b/tangem-core/src/main/java/com/tangem/commands/OpenSessionCommand.kt new file mode 100644 index 0000000000..9b78cb8bea --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/OpenSessionCommand.kt @@ -0,0 +1,41 @@ +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 + + +class OpenSessionCommand(private val sessionKeyA: ByteArray) : CommandSerializer() { + 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() + } + } +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/PurgeWalletCommand.kt b/tangem-core/src/main/java/com/tangem/commands/PurgeWalletCommand.kt index cef8aca229..4eb379e521 100644 --- a/tangem-core/src/main/java/com/tangem/commands/PurgeWalletCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/PurgeWalletCommand.kt @@ -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() { 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) diff --git a/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt b/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt index 0a161f392c..b735e54d8f 100644 --- a/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt @@ -310,11 +310,14 @@ class ReadCommand : CommandSerializer() { */ 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) diff --git a/tangem-core/src/main/java/com/tangem/commands/ReadIssuerDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/ReadIssuerDataCommand.kt index c681f5fed9..fea83e979f 100644 --- a/tangem-core/src/main/java/com/tangem/commands/ReadIssuerDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/ReadIssuerDataCommand.kt @@ -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) diff --git a/tangem-core/src/main/java/com/tangem/commands/ReadIssuerExtraDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/ReadIssuerExtraDataCommand.kt index 4e3f757395..3a6e8309ff 100644 --- a/tangem-core/src/main/java/com/tangem/commands/ReadIssuerExtraDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/ReadIssuerExtraDataCommand.kt @@ -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) diff --git a/tangem-core/src/main/java/com/tangem/commands/ReadUserDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/ReadUserDataCommand.kt index 3628d9cad7..ba5954fe01 100644 --- a/tangem-core/src/main/java/com/tangem/commands/ReadUserDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/ReadUserDataCommand.kt @@ -57,11 +57,14 @@ class ReadUserDataCommand: CommandSerializer() { 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) diff --git a/tangem-core/src/main/java/com/tangem/commands/SignCommand.kt b/tangem-core/src/main/java/com/tangem/commands/SignCommand.kt index d76722e96c..6f0b7bfacc 100644 --- a/tangem-core/src/main/java/com/tangem/commands/SignCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/SignCommand.kt @@ -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) 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) } 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( diff --git a/tangem-core/src/main/java/com/tangem/commands/WriteIssuerDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/WriteIssuerDataCommand.kt index 2ca018f2a0..dbc7f15640 100644 --- a/tangem-core/src/main/java/com/tangem/commands/WriteIssuerDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/WriteIssuerDataCommand.kt @@ -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) diff --git a/tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt index 7a1b987cd5..753607f0a9 100644 --- a/tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt @@ -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) diff --git a/tangem-core/src/main/java/com/tangem/commands/WriteUserDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/WriteUserDataCommand.kt index d512c4ded7..804abf6601 100644 --- a/tangem-core/src/main/java/com/tangem/commands/WriteUserDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/WriteUserDataCommand.kt @@ -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)) diff --git a/tangem-core/src/main/java/com/tangem/common/CardEnvironment.kt b/tangem-core/src/main/java/com/tangem/common/CardEnvironment.kt index 21728a8c44..c556f427b3 100644 --- a/tangem-core/src/main/java/com/tangem/common/CardEnvironment.kt +++ b/tangem-core/src/main/java/com/tangem/common/CardEnvironment.kt @@ -10,7 +10,8 @@ data class CardEnvironment( val pin2: String = DEFAULT_PIN2, val cardId: String? = null, val terminalKeys: KeyPair? = null, - val encryptionKey: ByteArray? = null, + var encryptionMode: EncryptionMode = EncryptionMode.NONE, + var encryptionKey: ByteArray? = null, val cvc: ByteArray? = null ) { diff --git a/tangem-core/src/main/java/com/tangem/common/apdu/CommandApdu.kt b/tangem-core/src/main/java/com/tangem/common/apdu/CommandApdu.kt index 450618ced5..1a2c2be076 100644 --- a/tangem-core/src/main/java/com/tangem/common/apdu/CommandApdu.kt +++ b/tangem-core/src/main/java/com/tangem/common/apdu/CommandApdu.kt @@ -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 } } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/common/apdu/ResponseApdu.kt b/tangem-core/src/main/java/com/tangem/common/apdu/ResponseApdu.kt index e98c78874b..e7e8625384 100644 --- a/tangem-core/src/main/java/com/tangem/common/apdu/ResponseApdu.kt +++ b/tangem-core/src/main/java/com/tangem/common/apdu/ResponseApdu.kt @@ -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? { - 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 } } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/common/extensions/ByteArray.kt b/tangem-core/src/main/java/com/tangem/common/extensions/ByteArray.kt index f7e416d700..ac201f5fcb 100644 --- a/tangem-core/src/main/java/com/tangem/common/extensions/ByteArray.kt +++ b/tangem-core/src/main/java/com/tangem/common/extensions/ByteArray.kt @@ -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()) } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt b/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt index d37500e221..c9f3275dfe 100644 --- a/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt +++ b/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt @@ -51,6 +51,7 @@ enum class TlvTag(val code: Int) { SessionKeyA(0x1A), SessionKeyB(0x1B), + Uid(0x0B), Pause(0x1C), ManufactureId(0x20), @@ -66,7 +67,6 @@ enum class TlvTag(val code: Int) { Mode(0x23), Offset(0x24), - IsActivated(0x3A), ActivationSeed(0x3B), ResetPin(0x36), @@ -95,7 +95,6 @@ enum class TlvTag(val code: Int) { ProductMask(0x8A), PaymentFlowVersion(0x54), - TokenSymbol(0xA0), TokenContractAddress(0xA1), TokenDecimal(0xA2), diff --git a/tangem-core/src/main/java/com/tangem/crypto/CryptoUtils.kt b/tangem-core/src/main/java/com/tangem/crypto/CryptoUtils.kt index 734b3d8f7a..d7643bdaae 100644 --- a/tangem-core/src/main/java/com/tangem/crypto/CryptoUtils.kt +++ b/tangem-core/src/main/java/com/tangem/crypto/CryptoUtils.kt @@ -2,8 +2,12 @@ package com.tangem.crypto import com.tangem.commands.EllipticCurve import net.i2p.crypto.eddsa.EdDSASecurityProvider +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 { @@ -62,6 +66,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 +93,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" + + diff --git a/tangem-core/src/main/java/com/tangem/crypto/Ed25519.kt b/tangem-core/src/main/java/com/tangem/crypto/Ed25519.kt index fd0cf14cc1..5c07b5a4e9 100644 --- a/tangem-core/src/main/java/com/tangem/crypto/Ed25519.kt +++ b/tangem-core/src/main/java/com/tangem/crypto/Ed25519.kt @@ -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) diff --git a/tangem-core/src/main/java/com/tangem/crypto/EncryptionHelper.kt b/tangem-core/src/main/java/com/tangem/crypto/EncryptionHelper.kt new file mode 100644 index 0000000000..ab3d97445c --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/crypto/EncryptionHelper.kt @@ -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 + } +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/crypto/Pbkdf2.kt b/tangem-core/src/main/java/com/tangem/crypto/Pbkdf2.kt new file mode 100644 index 0000000000..4c532b3692 --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/crypto/Pbkdf2.kt @@ -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() + } + + +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/crypto/Secp256k1.kt b/tangem-core/src/main/java/com/tangem/crypto/Secp256k1.kt index 2a62979bed..97eba2a81c 100644 --- a/tangem-core/src/main/java/com/tangem/crypto/Secp256k1.kt +++ b/tangem-core/src/main/java/com/tangem/crypto/Secp256k1.kt @@ -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") diff --git a/tangem-core/src/main/java/com/tangem/tasks/Task.kt b/tangem-core/src/main/java/com/tangem/tasks/Task.kt index 2e193ecc5e..1f901a543e 100644 --- a/tangem-core/src/main/java/com/tangem/tasks/Task.kt +++ b/tangem-core/src/main/java/com/tangem/tasks/Task.kt @@ -3,57 +3,144 @@ 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) + + /** + * 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) //Issuer Data Errors - class MissingCounter: TaskError(7001) + /** + * 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) } /** @@ -137,8 +224,41 @@ abstract class Task { 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 sendRequest(command: CommandSerializer, @@ -171,9 +291,26 @@ abstract class Task { 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 +338,6 @@ abstract class Task { callback(TaskEvent.Completion(readResult.error)) } is CompletionResult.Success -> { - val receivedCardId = readResult.data.cardId securityDelayDuration = readResult.data.pauseBeforePin2 ?: 0 @@ -215,10 +351,7 @@ abstract class Task { onRun(newEnvironment, readResult.data, callback) } } - - } - } } diff --git a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/nfc/NfcLocation.kt b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/nfc/NfcLocation.kt index d87b466385..ed3ba8bf1c 100644 --- a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/nfc/NfcLocation.kt +++ b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/nfc/NfcLocation.kt @@ -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),