Updated on 2026-08-14

This commit is contained in:
Tangem 2020-03-17 13:32:45 +00:00
commit 2cbf8c03ee
39 changed files with 638 additions and 192 deletions

28
.idea/gradle.xml generated
View file

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="delegatedBuild" value="false" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/blockchain" />
<option value="$PROJECT_DIR$/server-android" />
<option value="$PROJECT_DIR$/tangem-card-old" />
<option value="$PROJECT_DIR$/tangem-core" />
<option value="$PROJECT_DIR$/tangem-demo" />
<option value="$PROJECT_DIR$/tangem-sdk" />
<option value="$PROJECT_DIR$/tangem-sdk-old" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
<option name="testRunner" value="PLATFORM" />
</GradleProjectSettings>
</option>
</component>
</project>

View file

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

View file

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

View file

@ -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)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -148,7 +148,7 @@ public class XrpEngine extends CoinEngine {
}
public Uri getShareWalletUri() {
return Uri.parse("ripple:" + ctx.getCoinData().getWallet());
return Uri.parse(ctx.getCoinData().getWallet());
}
@Override
@ -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, "", "");
}

View file

@ -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<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)
}
@ -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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -310,11 +310,14 @@ class ReadCommand : CommandSerializer<Card>() {
*/
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
tlvBuilder.append(TlvTag.TerminalPublicKey, cardEnvironment.terminalKeys?.publicKey)
return CommandApdu(Instruction.Read, tlvBuilder.serialize())
return CommandApdu(
Instruction.Read, tlvBuilder.serialize(),
cardEnvironment.encryptionMode, cardEnvironment.encryptionKey
)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): Card? {
val tlvData = responseApdu.getTlvData() ?: return null
val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null
return try {
val tlvMapper = TlvMapper(tlvData)

View file

@ -60,11 +60,14 @@ class ReadIssuerDataCommand(
tlvBuilder.append(TlvTag.Pin, cardEnvironment.pin1)
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadData)
return CommandApdu(Instruction.ReadIssuerData, tlvBuilder.serialize())
return CommandApdu(
Instruction.ReadIssuerData, tlvBuilder.serialize(),
cardEnvironment.encryptionMode, cardEnvironment.encryptionKey
)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): ReadIssuerDataResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null
return try {
val mapper = TlvMapper(tlvData)

View file

@ -67,11 +67,14 @@ class ReadIssuerExtraDataCommand(
tlvBuilder.append(TlvTag.CardId, cardEnvironment.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadExtraData)
tlvBuilder.append(TlvTag.Offset, offset)
return CommandApdu(Instruction.ReadIssuerData, tlvBuilder.serialize())
return CommandApdu(
Instruction.ReadIssuerData, tlvBuilder.serialize(),
cardEnvironment.encryptionMode, cardEnvironment.encryptionKey
)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): ReadIssuerExtraDataResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
val tlvData = responseApdu.getTlvData(cardEnvironment.encryptionKey) ?: return null
return try {
val mapper = TlvMapper(tlvData)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
) {

View file

@ -1,6 +1,9 @@
package com.tangem.common.apdu
import com.tangem.common.EncryptionMode
import com.tangem.common.extensions.calculateCrc16
import com.tangem.common.extensions.toByteArray
import com.tangem.crypto.encrypt
import java.io.ByteArrayOutputStream
/**
@ -8,21 +11,19 @@ import java.io.ByteArrayOutputStream
* to a raw data that can be sent to the card.
*
* @property ins Instruction code that determines the type of request for the card.
* @property tlvList A list of TLVs that are to be sent to the card
* @property tlvs Tlvs encoded to a [ByteArray] that are to be sent to the card.
*/
class CommandApdu(
private val ins: Int,
private val tlvs: ByteArray,
private val cla: Byte = ISO_CLA,
private val p1: Byte = 0x00,
private val p2: Byte = 0x00,
private val le: Int = 0x00,
private val encryptionMode: EncryptionMode = EncryptionMode.NONE,
private val encryptionKey: ByteArray? = null) {
private val encryptionKey: ByteArray? = null,
private val cla: Int = ISO_CLA) {
constructor(
instruction: Instruction,
@ -36,6 +37,19 @@ class CommandApdu(
encryptionKey = encryptionKey
)
private val p1: Int
private val p2: Int
init {
if (ins == Instruction.OpenSession.code) {
p1 = 0x00
p2 = encryptionMode.code.toInt()
} else {
p1 = encryptionMode.code.toInt()
p2 = 0x00
}
}
/**
* Request converted to a raw data
@ -48,33 +62,37 @@ class CommandApdu(
private fun toBytes(): ByteArray {
val lc = tlvs.size
val data = if (encryptionKey != null) tlvs.encrypt() else tlvs
val byteStream = ByteArrayOutputStream()
byteStream.write(cla.toInt())
byteStream.write(cla)
byteStream.write(ins)
byteStream.write(p1.toInt())
byteStream.write(p2.toInt())
if (lc != 0) {
writeLength(byteStream, lc)
byteStream.write(tlvs)
byteStream.write(p1)
byteStream.write(p2)
if (data.isNotEmpty()) {
byteStream.writeLength(data.size)
byteStream.write(data)
}
return byteStream.toByteArray()
}
private fun writeLength(stream: ByteArrayOutputStream, lc: Int) {
stream.write(0)
stream.write(lc shr 8)
stream.write(lc and 0xFF)
private fun ByteArrayOutputStream.writeLength(lc: Int) {
this.write(0)
this.write(lc shr 8)
this.write(lc and 0xFF)
}
private fun encrypt() {
TODO("not implemented")
}
private fun ByteArray.encrypt(): ByteArray {
val crc: ByteArray = tlvs.calculateCrc16()
val stream = ByteArrayOutputStream()
stream.write(this.size.toByteArray(2))
stream.write(crc)
stream.write(this)
return stream.toByteArray().encrypt(encryptionKey!!)
}
companion object {
const val ISO_CLA = 0x00.toByte()
const val ISO_CLA = 0x00
}
}

View file

@ -1,6 +1,9 @@
package com.tangem.common.apdu
import com.tangem.common.extensions.calculateCrc16
import com.tangem.common.tlv.Tlv
import com.tangem.crypto.decrypt
import java.io.ByteArrayInputStream
/**
* Stores response data from the card and parses it to [Tlv] and [StatusWord].
@ -25,15 +28,39 @@ class ResponseApdu(private val data: ByteArray) {
* (Encryption / decryption functionality is not implemented yet.)
*/
fun getTlvData(encryptionKey: ByteArray? = null): List<Tlv>? {
return when {
data.size <= 2 -> null
else -> Tlv.deserialize(data.copyOf(data.size - 2))
return if (data.size <= 2) {
null
} else {
val responseData = data.copyOf(data.size - 2)
return if (encryptionKey != null) {
if (data.size >= 18) {
val decryptedData = decrypt(responseData, encryptionKey)
Tlv.deserialize(decryptedData)
} else {
null
}
} else {
Tlv.deserialize(responseData)
}
}
}
private fun decrypt(responseData: ByteArray, encryptionKey: ByteArray): ByteArray {
val decryptedData: ByteArray = responseData.decrypt(encryptionKey)
private fun decrypt(encryptionKey: ByteArray) {
TODO("not implemented")
val inputStream = ByteArrayInputStream(decryptedData)
val baLength = ByteArray(2)
inputStream.read(baLength)
val length = (baLength[0].toInt() and 0xFF) * 256 + (baLength[1].toInt() and 0xFF)
if (length > decryptedData.size - 4) throw Exception("Can't decrypt - data size invalid")
val baCRC = ByteArray(2)
inputStream.read(baCRC)
val answerData = ByteArray(length)
inputStream.read(answerData)
val crc: ByteArray = answerData.calculateCrc16()
if (!baCRC.contentEquals(crc)) throw Exception("Can't decrypt - crc invalid")
return answerData
}
}

View file

@ -6,6 +6,7 @@ import java.nio.ByteBuffer
import java.security.MessageDigest
import java.util.*
import kotlin.experimental.and
import kotlin.experimental.xor
/**
* Extension functions for [ByteArray].
@ -53,4 +54,21 @@ fun ByteArray.toCompressedPublicKey(): ByteArray {
} else {
this
}
}
fun ByteArray.calculateCrc16(): ByteArray {
var chBlock: Byte
// STEP 1 Initialize the CRC-16 value
var wCRC = 0x6363 // ITU-V.41
var i = 0
// STEP 2 Update data and Calucuate their CRC
do {
chBlock = this.get(i++)
chBlock = chBlock xor (wCRC and 0x00FF).toByte()
val chBlockInt = (chBlock.toInt() xor (chBlock.toInt() shl 4))
wCRC = wCRC shr 8 xor (chBlockInt and 0xFF shl 8) and 0xFFFF xor (chBlockInt and 0xFF shl 3 and 0xFFFF) xor (chBlockInt and 0xFF shr 4 and 0xFFFF)
// (wCRC>>8)^((int)chBlock<<8)^((int) chBlock<<3)^((int)chBlock>>4);
} while (i < this.size)
return byteArrayOf((wCRC and 0xFF).toByte(), (wCRC and 0xFFFF shr 8).toByte())
}

View file

@ -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),

View file

@ -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"

View file

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

View file

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

View file

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

View file

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

View file

@ -3,57 +3,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<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 +291,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 +338,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 +351,7 @@ abstract class Task<T> {
onRun(newEnvironment, readResult.data, callback)
}
}
}
}
}

View file

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