Updated on 2026-08-14
This commit is contained in:
commit
a72f5e67ff
12 changed files with 137 additions and 33 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)) }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue