Updated on 2026-08-14

This commit is contained in:
Tangem 2020-03-10 12:13:24 +03:00
parent da0d072fcc
commit effd4025ee
25 changed files with 431 additions and 90 deletions

View file

@ -69,18 +69,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)
}

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,6 +10,7 @@ data class CardEnvironment(
val pin2: String = DEFAULT_PIN2,
val cardId: String? = null,
val terminalKeys: KeyPair? = null,
val encryptionMode: EncryptionMode = EncryptionMode.NONE,
val 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,14 +3,17 @@ 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.
@ -85,6 +88,7 @@ abstract class Task<T> {
var reader: CardReader? = null
var performPreflightRead: Boolean = true
var securityDelayDuration: Int = 0
private var openSessionRequired = false
/**
* This method should be called to run the [Task] and perform all its operations.
@ -137,8 +141,43 @@ 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)
if (cardEnvironment.encryptionKey != null && !openSessionRequired) {
val commandApdu = command.serialize(cardEnvironment)
sendRequest(command, commandApdu, cardEnvironment, callback)
return
}
when (cardEnvironment.encryptionMode) {
EncryptionMode.NONE -> {
val commandApdu = command.serialize(cardEnvironment)
sendRequest(command, commandApdu, cardEnvironment, callback)
}
EncryptionMode.FAST, EncryptionMode.STRONG -> {
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()
val newEnvironment = cardEnvironment.copy(encryptionKey = sessionKey)
openSessionRequired = false
sendCommand(command, newEnvironment, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}
}
private fun <T : CommandResponse> sendRequest(command: CommandSerializer<T>,
@ -171,9 +210,25 @@ 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 -> {
openSessionRequired = true
val newEnvironment = when (cardEnvironment.encryptionMode) {
EncryptionMode.NONE -> {
cardEnvironment.copy(encryptionMode = EncryptionMode.FAST)
}
EncryptionMode.FAST -> {
cardEnvironment.copy(encryptionMode = EncryptionMode.STRONG)
}
EncryptionMode.STRONG -> {
Log.e(this::class.simpleName!!, "Encryption doesn't work")
callback(CompletionResult.Failure(TaskError.NeedEncryption()))
return@transceiveApdu
}
}
sendCommand(command, newEnvironment, 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 +256,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 +269,7 @@ abstract class Task<T> {
onRun(newEnvironment, readResult.data, callback)
}
}
}
}
}