Updated on 2026-08-14

This commit is contained in:
Tangem 2020-06-23 15:03:27 +00:00
commit a72f5e67ff
12 changed files with 137 additions and 33 deletions

View file

@ -231,6 +231,14 @@ class CardSession(
}
}
fun pause() {
reader.stopSession()
}
fun resume() {
reader.startSession()
}
private suspend fun establishEncryptionIfNeeded(): CompletionResult<Boolean> {
if (environment.encryptionMode == EncryptionMode.NONE || environment.encryptionKey != null) {
return CompletionResult.Success(true)

View file

@ -14,7 +14,6 @@ import com.tangem.crypto.CryptoUtils.generatePublicKey
* @property terminalKeys generated terminal keys used in Linked Terminal feature.
*/
data class SessionEnvironment(
var pin1: ByteArray = DEFAULT_PIN.calculateSha256(),
var pin2: ByteArray = DEFAULT_PIN2.calculateSha256(),
var card: Card? = null,
var terminalKeys: KeyPair? = null,
@ -25,8 +24,14 @@ data class SessionEnvironment(
val handleErrors: Boolean = true
) {
var isDefaultPin1: Boolean = Companion.pin1.contentEquals(DEFAULT_PIN.calculateSha256())
var pin1: ByteArray = SessionEnvironment.pin1
private set
get() = SessionEnvironment.pin1
fun setPin1(pin1: String) {
this.pin1 = pin1.calculateSha256()
SessionEnvironment.pin1 = pin1.calculateSha256()
}
fun setPin2(pin2: String) {
@ -36,6 +41,8 @@ data class SessionEnvironment(
companion object {
const val DEFAULT_PIN = "000000"
const val DEFAULT_PIN2 = "000"
var pin1: ByteArray = DEFAULT_PIN.calculateSha256()
}
}

View file

@ -1,7 +1,5 @@
package com.tangem
import com.tangem.common.CompletionResult
/**
* Allows interaction with users and shows visual elements.
*
@ -49,8 +47,7 @@ interface SessionViewDelegate {
/**
* It is called when a user is expected to enter pin code.
*/
fun onPinRequested(callback: (result: CompletionResult<String>) -> Unit)
fun onPinRequested(callback: (pin: String?) -> Unit)
}
/**

View file

@ -64,6 +64,10 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
is CompletionResult.Failure -> {
if (session.environment.handleErrors) {
val error = mapError(session.environment.card, result.error)
if (error is TangemSdkError.Pin1Required) {
handlePin1(session, callback)
return@transceiveApdu
}
callback(CompletionResult.Failure(error))
return@transceiveApdu
@ -80,7 +84,6 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
}
}
}
}
private fun transceiveApdu(
@ -96,8 +99,10 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
val responseApdu = result.data
when (responseApdu.statusWord) {
StatusWord.ProcessCompleted, StatusWord.Pin1Changed,
StatusWord.Pin2Changed, StatusWord.PinsChanged -> {
StatusWord.ProcessCompleted,
StatusWord.Pin1Changed, StatusWord.Pin2Changed, StatusWord.Pins12Changed,
StatusWord.Pin3Changed, StatusWord.Pins13Changed, StatusWord.Pins23Changed,
StatusWord.Pins123Changed -> {
callback(CompletionResult.Success(responseApdu))
}
StatusWord.NeedPause -> {
@ -172,4 +177,26 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
return false
}
private fun handlePin1(
session: CardSession,
callback: (result: CompletionResult<T>) -> Unit
) {
if (!session.environment.isDefaultPin1) {
session.environment.setPin1(SessionEnvironment.DEFAULT_PIN)
transceive(session, callback)
return
}
session.pause()
session.viewDelegate.onPinRequested { pin1 ->
if (!pin1.isNullOrEmpty()) {
session.environment.setPin1(pin1)
session.resume()
transceive(session, callback)
} else {
session.environment.setPin1(SessionEnvironment.DEFAULT_PIN)
session.resume()
transceive(session, callback)
}
}
}
}

View file

@ -18,9 +18,8 @@ class OpenSessionResponse(
* In case of encrypted communication, App should setup a session before calling any further command.
* [OpenSessionCommand] generates secret session_key that is used by both host and card
* to encrypt and decrypt commands payload.
*/
class OpenSessionCommand(private val sessionKeyA: ByteArray) : Command<OpenSessionResponse>() {
class OpenSessionCommand(private val sessionKeyA: ByteArray) : ApduSerializable<OpenSessionResponse> {
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.SessionKeyA, sessionKeyA)

View file

@ -43,7 +43,8 @@ class PurgeWalletCommand : Command<PurgeWalletResponse>() {
return when (card.status) {
CardStatus.Loaded -> null
CardStatus.NotPersonalized -> TangemSdkError.NotPersonalized()
CardStatus.Empty, CardStatus.Purged -> TangemSdkError.CardIsEmpty()
CardStatus.Empty -> TangemSdkError.CardIsEmpty()
CardStatus.Purged -> TangemSdkError.CardIsPurged()
null -> TangemSdkError.CardError()
}
}

View file

@ -5,19 +5,27 @@ import com.tangem.TangemSdkError
/**
* Part of a response from the card, shows the status of the operation
*/
enum class StatusWord(val code: Int, val description: String) {
enum class StatusWord(val code: Int) {
ProcessCompleted(0x9000, "SW_PROCESS_COMPLETED"),
InvalidParams(0x6A86, "SW_INVALID_PARAMS"),
ErrorProcessingCommand(0x6286, "SW_ERROR_PROCESSING_COMMAND"),
InvalidState(0x6985, "SW_INVALID_STATE"),
Pin1Changed(ProcessCompleted.code + 0x0001, "SW_PIN1_CHANGED"),
Pin2Changed(ProcessCompleted.code + 0x0002, "SW_PIN2_CHANGED"),
PinsChanged(ProcessCompleted.code + 0x0003, "SW_PINS_CHANGED"),
InsNotSupported(0x6D00, "SW_INS_NOT_SUPPORTED"),
NeedEncryption(0x6982, "SW_NEED_ENCRYPTION"),
NeedPause(0x9789, "SW_NEED_PAUSE"),
Unknown(0x0000, "SW_UNKNOWN");
ProcessCompleted(0x9000),
InvalidParams(0x6A86),
ErrorProcessingCommand(0x6286),
InvalidState(0x6985),
//PinsNotChanged(0x9000) is equal to ProcessCompleted(0x9000)
Pin1Changed(0x9001),
Pin2Changed(0x9002),
Pins12Changed(0x9003),
Pin3Changed(0x9004),
Pins13Changed(0x9005),
Pins23Changed(0x9006),
Pins123Changed(0x9007),
InsNotSupported(0x6D00),
NeedEncryption(0x6982),
NeedPause(0x9789),
Unknown(0x0000);
companion object {
private val values = values()
@ -28,7 +36,8 @@ enum class StatusWord(val code: Int, val description: String) {
fun StatusWord.toTangemSdkError(): TangemSdkError? {
return when (this) {
StatusWord.ProcessCompleted, StatusWord.Pin1Changed,
StatusWord.Pin2Changed, StatusWord.PinsChanged -> null
StatusWord.Pin2Changed, StatusWord.Pins12Changed, StatusWord.Pin3Changed,
StatusWord.Pins13Changed, StatusWord.Pins23Changed, StatusWord.Pins123Changed -> null
StatusWord.NeedPause -> null
StatusWord.InvalidParams -> TangemSdkError.InvalidParams()
StatusWord.ErrorProcessingCommand -> TangemSdkError.ErrorProcessingCommand()

View file

@ -125,7 +125,6 @@ enum class TlvTag(val code: Int) {
fun valueType(): TlvValueType {
return when (this) {
CardId, Batch, CrExKey -> TlvValueType.HexString
NewPin, NewPin2, NewPin3 -> TlvValueType.HexStringToHash
ManufactureId, Firmware, IssuerId, BlockchainId, TokenSymbol, TokenContractAddress ->
TlvValueType.Utf8String
CurveId -> TlvValueType.EllipticCurve

View file

@ -2,7 +2,6 @@ package com.tangem.tangem_sdk_new
import androidx.fragment.app.FragmentActivity
import com.tangem.*
import com.tangem.common.CompletionResult
import com.tangem.tangem_sdk_new.nfc.NfcReader
import com.tangem.tangem_sdk_new.ui.NfcSessionDialog
@ -69,7 +68,7 @@ class DefaultSessionViewDelegate(private val reader: NfcReader) : SessionViewDel
postUI { readingDialog?.show(SessionViewDelegateState.Error(error)) }
}
override fun onPinRequested(callback: (result: CompletionResult<String>) -> Unit) {
override fun onPinRequested(callback: (pin: String?) -> Unit) {
postUI { readingDialog?.show(SessionViewDelegateState.PinRequested(callback)) }
}

View file

@ -2,7 +2,6 @@ package com.tangem.tangem_sdk_new
import com.tangem.Message
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
sealed class SessionViewDelegateState() {
data class Error(val error: TangemSdkError) : SessionViewDelegateState()
@ -10,7 +9,7 @@ sealed class SessionViewDelegateState() {
data class SecurityDelay(val ms: Int, val totalDurationSeconds: Int) : SessionViewDelegateState()
data class Delay(val total: Int, val current: Int, val step: Int) : SessionViewDelegateState()
data class Ready(val cardId: String?, val message: Message?) : SessionViewDelegateState()
data class PinRequested(val callback: (result: CompletionResult<String>) -> Unit) : SessionViewDelegateState()
data class PinRequested(val callback: (pin: String?) -> Unit) : SessionViewDelegateState()
object TagLost : SessionViewDelegateState()
object TagConnected : SessionViewDelegateState()
object WrongCard : SessionViewDelegateState()

View file

@ -1,11 +1,16 @@
package com.tangem.tangem_sdk_new.ui
import android.animation.ObjectAnimator
import android.app.Activity
import android.view.HapticFeedbackConstants
import android.view.KeyEvent
import android.view.View
import android.view.animation.DecelerateInterpolator
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.fragment.app.FragmentActivity
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.Log
import com.tangem.tangem_sdk_new.R
import com.tangem.tangem_sdk_new.SessionViewDelegateState
import com.tangem.tangem_sdk_new.extensions.localizedDescription
@ -121,11 +126,34 @@ class NfcSessionDialog(val activity: FragmentActivity) : BottomSheetDialog(activ
}
private fun onPinRequested(state: SessionViewDelegateState.PinRequested) {
TODO("To be implemented")
tvTaskText?.visibility = View.INVISIBLE
tvTaskTitle?.visibility = View.INVISIBLE
show(flPin)
performHapticFeedback()
etPin?.setOnEditorActionListener { v: TextView?, actionId: Int, event: KeyEvent? ->
postUI {
if (actionId == KeyEvent.KEYCODE_ENDCALL) {
show(lTouchCard)
tvTaskTitle?.show()
tvTaskText?.show()
val imm: InputMethodManager =
context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(v?.windowToken, 0)
v?.text?.toString()?.let { state.callback(it) }
}
}
true
}
}
private fun onTagLost() {
if (currentState is SessionViewDelegateState.Success) return
if (currentState is SessionViewDelegateState.Success ||
currentState is SessionViewDelegateState.PinRequested) {
return
}
show(lTouchCard)
tvTaskTitle?.text = activity.getText(R.string.dialog_ready_to_scan)
tvTaskText?.text = activity.getText(R.string.dialog_scan_text)
@ -160,6 +188,7 @@ class NfcSessionDialog(val activity: FragmentActivity) : BottomSheetDialog(activ
flReading?.show(view.id == flReading.id)
flError?.show(view.id == flError.id)
flCompletion?.show(view.id == flCompletion.id)
flPin?.show(view.id == flPin.id)
}
private fun performHapticFeedback() {

View file

@ -57,7 +57,6 @@
</LinearLayout>
<TextView
android:id="@+id/tvTaskTitle"
android:layout_width="wrap_content"
@ -75,6 +74,37 @@
android:layout_height="200dp"
android:layout_gravity="center" />
<FrameLayout
android:id="@+id/flPin"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_gravity="center"
android:visibility="gone">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilPin"
android:layout_width="match_parent"
android:layout_height="345dp"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:backgroundTint="#f3f5f6"
android:layout_gravity="center"
android:gravity="center"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="#f3f5f6"
android:hint="Enter PIN"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
<FrameLayout
android:id="@+id/flSecurityDelay"
android:layout_width="wrap_content"