Updated on 2026-08-14

This commit is contained in:
Tangem 2020-07-10 08:27:58 +00:00
commit db06852c97
40 changed files with 771 additions and 258 deletions

View file

@ -19,11 +19,13 @@ import org.junit.Test
internal class WalletManagerFactoryTest {
private val sessionEnvironment = SessionEnvironment()
@Test
fun createBitcoinWalletManager() {
val data = "0108bb00000000000304200754414e47454d00020102800a322e3432642053444b000341040876bdec26b89bd2159a668b9af3d9fe86370f318717c92b8d6c1186fb3648c32a5f9321998cc2d042901c91d40601e79a641e1cbcebe7a2358be6054e1b6e5d0a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b0084034254438640e17ceec48c5be36240c98019f95ad8b6e56acfebe60d11979c6279f715d607d76a860a137da8d109e805753f3f56b0130709f4bbf4cb9974b4c57b8469bf4b873041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd26050a736563703235366b310008040000006407010009020bb8604104752a727e14bba5bd73b6714d72500f61ffd11026ad1196d2e1c54577cbeeac3d11fc68a64700f8d533f4e311964ea8fb3aa26c588295f2133868d69c3e62869362040000005c6304000000090f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -34,7 +36,7 @@ internal class WalletManagerFactoryTest {
fun createEthereumWalletManager() {
val data = "0108bb00000000000536200754414e47454d00020102800a322e3432642053444b000341046c8aea0d5a850b0a608acf9a0c453c39ea86131e88bfa78800de3cfb5bf1007aeaa7b9ffc184212255758605c2461be343c0a661d73cabafa4c9c175b3f0e59a0a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b0084034554488640431b6244acfeac479becdff201a7f720a7d70a97edc4e019fb678596baf52dfe9d0e8faf08ceb4443b82d4e66815541f2dc8ec6dd3ff83eb42f06e5eab07f25f3041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd26050a736563703235366b3100080400000064070100090205dc60410464dddc3f356744aaecfa07427f9eb996ff537d65f20fb5be3abccf0354352a6b5f8a1942e0f8ddeea3a170eda78d060be8162ad60e94e4e91fbbdf0a7054785562040000005b6304000000090f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -45,7 +47,7 @@ internal class WalletManagerFactoryTest {
fun createStellarWalletManager() {
val data = "0108bb00000000000379200754414e47454d00020102800a322e3432642053444b0003410487d7bb51b189213e3cedc3fcfa3fc047b3b71b7805b5b215e14639b3a8ebb1952c9dd5ea4354441b6ada4e8b8327674bb102ddae69df55be69643a2c916edf650a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b008403584c4d86409a4bc2baf0e5836887da21167cf33458d5249d1a610bced0e31dc053f23729ed24d715912bf89e6804669430dfe396ed83274e0031f6803e2bdb8c041fa993413041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd2605086564323535313900080400000064070100090205dc6020e078212d58b2b9d0edc9c936830d10081cd38b90c31778c56dfb1171027e294e62040000003863040000002c0f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -56,7 +58,7 @@ internal class WalletManagerFactoryTest {
fun createCardanoWalletManager() {
val data = "0108bb00000000000502200754414e47454d00020102800a322e3432642053444b0003410402c1e39257d60583489da2d67d35d1cc2a1c005cc05c1021f44838edcaf25d5615cad7c9d11c2e23f5efa93e50904d33c88808d0e169060508df840992e31f4d0a04041e76310c658102ffff8a0101820407e30b0d830b54414e47454d2053444b00840743415244414e4f8640f24ef5c8c6eba0ff97560d5b013edb4a452594270db9647bd0a3543df8104dec75731d4db3ebe0fc493f2afee00195e560b51e3c41189b7c61ba7895d6434b9d3041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd2605086564323535313900080400000064070100090205dc60208a71161cfdf1e0a85d8e7ff372aa4a01136046292aceb5f9ad7ebdb98d3f60a86204000000646304000000000f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -67,7 +69,7 @@ internal class WalletManagerFactoryTest {
fun createXrpWalletManager() {
val data = "0108cb21000000002154200b534d4152542043415348000201028006322e31317200034104bdad63848f97c535da53cf8fd300d24fa33f0516d194aa78ec164a06994d00204bae243a424e316c6ec845e02d9b15eafae8c19018a926b0b7435e6e941cdadb0a0400007e210c5a81020028820407e30502830754414e47454d00840358525086400ed8734b877869722c7d0b37ffb154b9fef21c54bf2c6496feb1fb5c1fc28a2ac28e201dde84f27495fa7f08b3ca2be2fb4954bf0fe78af027d6cdc16c3eee923041048196aa4b410ac44a3b9cce18e7be226aea070acc83a9cf67540fac49af25129f6a538a28ad6341358e3c4f9963064f7e365372a651d374e5c23cdd37fd099bf2050a736563703235366b31000804000f4240070100090205dc604104d2b9fb288540d54e5b32ecaf0381cd571f97f6f1ecd036b66bb11aa52ffe9981110d883080e2e255c6b1640586f7765e6faa325d1340f49b56b83d9de56bc7ed6204000f42406304000000000f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -78,7 +80,7 @@ internal class WalletManagerFactoryTest {
fun createBinanceWalletManager() {
val data = "0108BB00000000000015200754414E47454D00020102800A322E3432642053444B0003410446D4155890B08BE217F0B1FA7DCCB16138C24B3E825A27315D5E4BBD6CAF76A28C7902007052BC1347355A78D54BD73216C9431D555CED827B54FD9255EB3A830A04041E76310C658102FFFF8A0101820407E40410830B54414E47454D2053444B00840742494E414E4345864029F115878EDC7B0CB2A6F4A4009447DCB43BBE922D7629AEBD0C9A910AD1E3BF15AE409C4F579700951ED2FE4D775171A86CFA8E50009A05938CE210D6D4A2583041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A0070100604104E3F3BE3CE3D8284DB3BA073AD0291040093D83C11A277B905D5555C9EC41073E103F4D9D299EDEA8285C51C3356A8681A545618C174251B984DF841F49D2376F62040001869F6304000000010F01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -89,7 +91,7 @@ internal class WalletManagerFactoryTest {
fun createBitcoinCashWalletManager() {
val data = "0108BB00000000000049200754414E47454D00020102800A322E3432642053444B00034104766A1586D164B436E5D420AED01FDAB41B2AE7EDF0C865D7AF1DA995D70AB297E5B94B761CFBB405084C21BC97C02B4A1EA9ED4F515576EAB4D83AD3A0DFAA8A0A04041E76310C618102FFFF8A0101820407E4041B830B54414E47454D2053444B00840342434886408058F0F628C2466B09ECEB13F2A8EFDD4558F5D2DBDA9BD0628EE8C8CC99A778FF0F1AECD35704B9F3518486EA5C1D20F9DFCBAA66184F4CCCD9282E2632882C3041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A0070100604104BE37CD5251C8999EDBBFC759D800EB41E4DCB718289601EB15819404E1B2F2ED90FE50C2A481D06EC790D1EF6184974EB655ABAE4BE56A6D1C9E1A17B1EFDF0262040001869A6304000000060F01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -100,7 +102,7 @@ internal class WalletManagerFactoryTest {
fun createLitecoinWalletManager() {
val data = "0108BB00000000000023200754414E47454D00020102800A322E3432642053444B000341043539F86A40ADD04CE165764A761FD3E4D251028615D2A573B1C3AE652E60AFDBFAF02E3239E89EF2C43FA448A327557ADC5AF36376A0574570F6DBD20113514A0A04041E76310C618102FFFF8A0101820407E40414830B54414E47454D2053444B0084034C5443864004BDEAD0117544886346CB47F7CA84ABA8C34239502F23D28595A4B16CAD72F7DE506BA818B86A649C2BB945986D4574993B3B755B47CBEE31C4FB931F6748183041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A00701006041044A76C9A70422160F515F956D0F50C71BBBA4F9862A22913817D63F0B1EF7C2FAF512E1C91B1BE827560EFE24FB1652B47337E296C778DFB1014D080CDD35EF6562040001869D6304000000030F01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -111,7 +113,7 @@ internal class WalletManagerFactoryTest {
fun createTezosWalletManager() {
val data = "0108BB00000000000080200754414E47454D00020102800A322E3432642053444B0003410436CFC5D0A11353AE6AFEEDC84A2D02B2635C044DEEE47F99913072B8D166D14E557230AC5FB5272F1A0E523332CCE1A744B51DB53102FF7D3FDE023DC3477C460A04041E76310C638102FFFF8A0101820407E40514830B54414E47454D2053444B00840554455A4F538640C752685B29333CFB0DB0A7347579A0AE763F2B5C4BB09FD68E0B81A06CD01EC51347001732815A3ECFFCD78DDE4E53877581B9E4914B069570629D0C40A771B93041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050865643235353139000804000186A0070100602098E0E504F3A5FDE704400302ABB0A2EFB0DF0F95C166C91D7F207DEDCE10CBA362040001869F6304000000010F01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -122,7 +124,7 @@ internal class WalletManagerFactoryTest {
fun createDucatusWalletManager() {
val data = "0108BB00000000000098200754414E47454D00020102800A322E3432642053444B000341041B5FD7C590938E836B388B996AE451FDED54F625FF2CF05E26E5AADF6F690AEF125E3D0F23CB6B8D1F78040DF6F71B40D098F2D8BE504DDEE2E1F99BEADD90500A04041E76310C618102FFFF8A0101820407E40515830B54414E47454D2053444B00840344554386401B453C10A092A3448FA83CAFD4FC3D7EB5EA1BBBDD6A020CAAC8CED36BB2661F41EF0B0C7418214F670DFE1200DCE18597158119BEF6CC52A4FF3B7E021A53B93041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A007010060410485D520C8B907F0BC5E03FCBBAC212CCD270764BBFF4990A28653A2FB0D656C342DF143C4D52C43582289E20A81D5D014C1384A1FFFEA1D121903AD7ED35A01EA62040001869D6304000000030F01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)

View file

@ -11,8 +11,9 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
classpath "com.github.dcendents:android-maven-gradle-plugin:2.1"
classpath 'com.google.gms:google-services:4.3.3'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.0.0-beta04'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.0'
classpath 'com.google.firebase:perf-plugin:1.3.1'
classpath 'com.squareup.sqldelight:gradle-plugin:1.4.0'
}
}

View file

@ -4,6 +4,7 @@ apply plugin: 'com.github.dcendents.android-maven'
apply from: '../dependencies.gradle'
apply from: '../jitpack.gradle'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.squareup.sqldelight'
group = "$jitpackSdk.group"
version "$jitpackSdk.version"
@ -11,7 +12,7 @@ version "$jitpackSdk.version"
dependencies {
// kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlidn"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.7"
@ -22,6 +23,7 @@ dependencies {
// misc
implementation 'com.google.code.gson:gson:2.8.6'
implementation "com.squareup.sqldelight:sqlite-driver:1.4.0"
//network
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
@ -36,6 +38,12 @@ dependencies {
testImplementation "com.google.truth:truth:1.0.1"
}
sqldelight {
Database {
packageName = "com.tangem"
}
}
sourceCompatibility = "8"
targetCompatibility = "8"

View file

@ -1,15 +1,18 @@
package com.tangem
import com.tangem.commands.Command
import com.tangem.commands.CommandResponse
import com.tangem.commands.OpenSessionCommand
import com.tangem.commands.ReadCommand
import com.tangem.common.CompletionResult
import com.tangem.common.PinCode
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.getType
import com.tangem.crypto.EncryptionHelper
import com.tangem.crypto.pbkdf2Hash
import com.tangem.tasks.PinType
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@ -18,7 +21,7 @@ import kotlinx.coroutines.flow.*
*/
interface CardSessionRunnable<T : CommandResponse> {
val performPreflightRead: Boolean
val requiresPin2: Boolean
/**
* The starting point for custom business logic.
@ -53,7 +56,7 @@ enum class TagType {
* If null, a default header and text body will be used.
*/
class CardSession(
val environment: SessionEnvironment,
private val environmentService: SessionEnvironmentService,
private val reader: CardReader,
val viewDelegate: SessionViewDelegate,
private var cardId: String? = null,
@ -73,8 +76,13 @@ class CardSession(
private val tag = this.javaClass.simpleName
private var performPreflightRead = true
private var pin2Required = false
val environment = environmentService.createEnvironment(cardId)
/**
* This metod starts a card session, performs preflight [ReadCommand],
* This method starts a card session, performs preflight [ReadCommand],
* invokes [CardSessionRunnable.run] and closes the session.
* @param runnable [CardSessionRunnable] that will be performed in the session.
* @param callback will be triggered with a [CompletionResult] of a session.
@ -83,7 +91,10 @@ class CardSession(
runnable: T, callback: (result: CompletionResult<R>) -> Unit
) {
start(runnable.performPreflightRead) { session, error ->
if ((runnable as? Command<*>)?.performPreflightRead == false) performPreflightRead = false
pin2Required = runnable.requiresPin2
start() { session, error ->
if (error != null) {
callback(CompletionResult.Failure(error))
return@start
@ -113,7 +124,6 @@ class CardSession(
* @param callback: callback with the card session. Can contain [TangemSdkError] if something goes wrong.
*/
fun start(
performPreflightRead: Boolean = true,
callback: (session: CardSession, error: TangemSdkError?) -> Unit
) {
@ -121,6 +131,26 @@ class CardSession(
callback(this, TangemSdkError.Busy())
return
}
if (environment.pin1 == null) {
viewDelegate.onSessionStarted(cardId)
viewDelegate.onPinRequested(PinType.Pin1) {
environment.pin1 = PinCode(it)
start(callback)
}
return
}
if (pin2Required && environment.pin2 == null) {
viewDelegate.onSessionStarted(cardId)
viewDelegate.onPinRequested(PinType.Pin2) {
environment.pin2 = PinCode(it)
start(callback)
}
return
}
state = CardSessionState.Active
viewDelegate.onSessionStarted(cardId)
@ -174,6 +204,7 @@ class CardSession(
return@run
}
environment.card = result.data
environmentService.updateEnvironment(environment, result.data.cardId)
cardId = receivedCardId
callback(this, null)
}
@ -209,6 +240,7 @@ class CardSession(
}
private fun stopSession() {
environmentService.saveEnvironmentValues(environment, cardId)
reader.stopSession()
state = CardSessionState.Inactive
scope.cancel()
@ -259,7 +291,7 @@ class CardSession(
return CompletionResult.Failure(error)
}
val uid = result.uid
val protocolKey = environment.pin1.pbkdf2Hash(uid, 50)
val protocolKey = environment.pin1!!.value.pbkdf2Hash(uid, 50)
val secret = encryptionHelper.generateSecret(result.sessionKeyB)
val sessionKey = (secret + protocolKey).calculateSha256()
environment.encryptionKey = sessionKey

View file

@ -36,6 +36,18 @@ class Config(
*/
val cardFilter: CardFilter = CardFilter(),
var handleErrors: Boolean = true
var handleErrors: Boolean = true,
)
var defaultPin1: String = DEFAULT_PIN_1,
var defaultPin2: String = DEFAULT_PIN_2,
var savePin1InStaticField: Boolean = true,
var savePin2InStaticField: Boolean = false
) {
companion object {
const val DEFAULT_PIN_1 = "000000"
const val DEFAULT_PIN_2 = "000"
}
}

View file

@ -2,7 +2,7 @@ package com.tangem
import com.tangem.commands.Card
import com.tangem.commands.EllipticCurve
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.PinCode
import com.tangem.crypto.CryptoUtils.generatePublicKey
@ -10,42 +10,38 @@ import com.tangem.crypto.CryptoUtils.generatePublicKey
* Contains data relating to a Tangem card. It is used in constructing all the commands,
* and commands can return modified [SessionEnvironment].
*
* @property card Current card, read by preflight [com.tangem.commands.ReadCommand].
* @property terminalKeys generated terminal keys used in Linked Terminal feature.
* @param cardId Card ID, if it is known before tapping the card.
* @property config sets a number of parameters for communication with Tangem cards.
* @param terminalKeysService is used to retrieve terminal keys used in Linked Terminal feature.
* @param cardValuesStorage is used to save and retrieve some values relating to a particular card.
*
* @property pin1 An access Code, required to get access to a card. A default value is set in [Config]
* @property pin2 A code, required to perform particular operations with a card. A default value is set in [Config]
* @property terminalKeys generated terminal keys used in Linked Terminal feature
* @property cardFilter a property that defines types of card that this SDK will be able to interact with
* @property handleErrors if true, the SDK parses internal card errors into concrete [TangemSdkError]
* @property encryptionMode preferred [EncryptionMode] for interaction with cards
* @property encryptionKey is used for encrypted communication with a card
*
*/
data class SessionEnvironment(
var pin2: ByteArray = DEFAULT_PIN2.calculateSha256(),
var card: Card? = null,
class SessionEnvironment(
var pin1: PinCode? = PinCode(Config.DEFAULT_PIN_1, true),
var pin2: PinCode? = PinCode(Config.DEFAULT_PIN_2, true),
var cvc: ByteArray? = null,
var terminalKeys: KeyPair? = null,
var cardFilter: CardFilter = CardFilter(),
val handleErrors: Boolean = true,
var encryptionMode: EncryptionMode = EncryptionMode.NONE,
var encryptionKey: ByteArray? = null,
var cvc: ByteArray? = null,
var cardFilter: CardFilter = CardFilter(),
val handleErrors: Boolean = true
) {
var pin1: ByteArray = SessionEnvironment.pin1
private set
get() = SessionEnvironment.pin1
var cardVerification: VerificationState = VerificationState.NotVerified,
var cardValidation: VerificationState = VerificationState.NotVerified,
var codeVerification: VerificationState = VerificationState.NotVerified,
fun isCurrentPin1Default(): Boolean = Companion.pin1.contentEquals(DEFAULT_PIN.calculateSha256())
fun isCurrentPin2Default(): Boolean = pin2.contentEquals(DEFAULT_PIN2.calculateSha256())
fun setPin1(pin1: String) {
SessionEnvironment.pin1 = pin1.calculateSha256()
}
fun setPin2(pin2: String) {
this.pin2 = pin2.calculateSha256()
}
companion object {
const val DEFAULT_PIN = "000000"
const val DEFAULT_PIN2 = "000"
var pin1: ByteArray = DEFAULT_PIN.calculateSha256()
}
}
var card: Card? = null
)
/**
* All possible encryption modes.
@ -60,4 +56,8 @@ class KeyPair(val publicKey: ByteArray, val privateKey: ByteArray) {
constructor(privateKey: ByteArray, curve: EllipticCurve = EllipticCurve.Secp256k1) :
this(generatePublicKey(privateKey, curve), privateKey)
}
enum class VerificationState {
Passed, Offline, Failed, NotVerified, Cancelled
}

View file

@ -0,0 +1,77 @@
package com.tangem
import com.tangem.common.CardValuesStorage
import com.tangem.common.PinCode
import com.tangem.common.TerminalKeysService
class SessionEnvironmentService(
private val config: Config,
private val terminalKeysService: TerminalKeysService?,
private val cardValuesStorage: CardValuesStorage?
) {
fun createEnvironment(cardId: String?): SessionEnvironment {
val terminalKeys = if (config.linkedTerminal) terminalKeysService?.getKeys() else null
val cardValues = cardId?.let { cardValuesStorage?.getValues(cardId) }
val cardVerification = cardValues?.cardVerification ?: VerificationState.NotVerified
val cardValidation = cardValues?.cardValidation ?: VerificationState.NotVerified
val codeVerification = cardValues?.codeVerification ?: VerificationState.NotVerified
val pin1 = TangemSdk.pin1
?: if (cardValues?.isPin1Default != false) {
PinCode(config.defaultPin1, true)
} else {
null
}
val pin2 = cardId?.let { TangemSdk.pin2[it] }
?: if (cardValues?.isPin2Default != false) {
PinCode(config.defaultPin2, true)
} else {
null
}
return SessionEnvironment(
terminalKeys = terminalKeys,
cardFilter = config.cardFilter,
handleErrors = config.handleErrors,
encryptionMode = config.encryptionMode,
cardVerification = cardVerification,
cardValidation = cardValidation,
codeVerification = codeVerification,
pin1 = pin1,
pin2 = pin2
)
}
fun updateEnvironment(environment: SessionEnvironment, cardId: String) {
val cardValues = cardId.let { cardValuesStorage?.getValues(it) }
environment.cardVerification = cardValues?.cardVerification ?: VerificationState.NotVerified
environment.cardValidation = cardValues?.cardValidation ?: VerificationState.NotVerified
environment.codeVerification = cardValues?.codeVerification ?: VerificationState.NotVerified
if (cardValues?.isPin1Default == false && environment.pin1?.isDefault == true) environment.pin1 = null
if (cardValues?.isPin2Default == false && environment.pin1?.isDefault == true) environment.pin2 = null
}
fun saveEnvironmentValues(environment: SessionEnvironment, cardId: String?) {
if (config.savePin1InStaticField) {
TangemSdk.pin1 = environment.pin1
}
if (config.savePin2InStaticField) {
cardId?.let { cardId -> TangemSdk.pin2[cardId] = environment.pin2 }
}
cardId?.let { cardId ->
cardValuesStorage?.saveValues(cardId,
environment.pin1?.isDefault ?: false,
environment.pin2?.isDefault ?: false,
environment.cardVerification, environment.cardVerification,
environment.codeVerification)
}
}
}

View file

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

View file

@ -10,10 +10,14 @@ import com.tangem.commands.personalization.entities.Issuer
import com.tangem.commands.personalization.entities.Manufacturer
import com.tangem.commands.verifycard.VerifyCardCommand
import com.tangem.commands.verifycard.VerifyCardResponse
import com.tangem.common.CardValuesStorage
import com.tangem.common.CompletionResult
import com.tangem.common.PinCode
import com.tangem.common.TerminalKeysService
import com.tangem.crypto.CryptoUtils
import com.tangem.tasks.ChangePinTask
import com.tangem.tasks.CreateWalletTask
import com.tangem.tasks.PinType
import com.tangem.tasks.ScanTask
/**
@ -26,14 +30,19 @@ import com.tangem.tasks.ScanTask
* Its default implementation, DefaultCardSessionViewDelegate, is in our tangem-sdk module.
* @property config allows to change a number of parameters for communication with Tangem cards.
* Do not change the default values unless you know what you are doing.
* @property terminalKeysService allows to retrieve saved terminal keys.
*/
class TangemSdk(
private val reader: CardReader,
private val viewDelegate: SessionViewDelegate,
var config: Config = Config()
var config: Config = Config(),
cardValuesStorage: CardValuesStorage,
terminalKeysService: TerminalKeysService? = null
) {
private var terminalKeysService: TerminalKeysService? = null
private val environmentService = SessionEnvironmentService(
config, terminalKeysService, cardValuesStorage
)
init {
CryptoUtils.initCrypto()
@ -203,7 +212,7 @@ class TangemSdk(
initialMessage: Message? = null,
callback: (result: CompletionResult<WriteUserDataResponse>) -> Unit
) {
val command = WriteUserDataCommand(userData = userData,userCounter = userCounter)
val command = WriteUserDataCommand(userData = userData, userCounter = userCounter)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
@ -361,6 +370,30 @@ class TangemSdk(
startSessionWithRunnable(command, null, initialMessage, callback)
}
fun changePin1(cardId: String? = null,
pin: ByteArray? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<SetPinResponse>) -> Unit) {
val command = ChangePinTask(PinType.Pin1, pin)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
fun changePin2(cardId: String? = null,
pin: ByteArray? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<SetPinResponse>) -> Unit) {
val command = ChangePinTask(PinType.Pin2, pin)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
fun changePin3(cardId: String? = null,
pin: ByteArray? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<SetPinResponse>) -> Unit) {
val command = ChangePinTask(PinType.Pin3, pin)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
/**
* Allows running a custom bunch of commands in one [CardSession] by creating a custom task.
* [TangemSdk] will start a card session, perform preflight [ReadCommand],
@ -378,13 +411,13 @@ class TangemSdk(
fun <T : CommandResponse> startSessionWithRunnable(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<T>) -> Unit) {
val cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId, initialMessage)
val cardSession = CardSession(environmentService, reader, viewDelegate, cardId, initialMessage)
Thread().run { cardSession.startWithRunnable(runnable, callback) }
}
/**
* Allows running a custom bunch of commands in one [CardSession] with lightweight closure syntax.
* Tangem SDK will start a card sesion and perform preflight [ReadCommand].
* Tangem SDK will start a card session and perform preflight [ReadCommand].
* @cardId: CID, Unique Tangem card ID number. If not null, the SDK will check that you the card
* with which you tapped a phone has this [cardId] and SDK will return
@ -395,27 +428,13 @@ class TangemSdk(
* then you can use the [CardSession] to interact with a card.
*/
fun startSession(cardId: String? = null, initialMessage: Message? = null,
callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
val cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId, initialMessage)
callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
val cardSession = CardSession(environmentService, reader, viewDelegate, cardId, initialMessage)
Thread().run { cardSession.start(callback = callback) }
}
/**
* Allows to set a particular [TerminalKeysService] to retrieve terminal keys.
* Default implementation is provided in tangem-sdk module: [TerminalKeysStorage].
*/
fun setTerminalKeysService(terminalKeysService: TerminalKeysService) {
this.terminalKeysService = terminalKeysService
companion object {
var pin1: PinCode? = null
val pin2: MutableMap<String, PinCode?> = mutableMapOf()
}
private fun buildEnvironment(): SessionEnvironment {
val terminalKeys = if (config.linkedTerminal) terminalKeysService?.getKeys() else null
return SessionEnvironment(
terminalKeys = terminalKeys,
cardFilter = config.cardFilter,
handleErrors = config.handleErrors
)
}
companion object
}

View file

@ -82,7 +82,7 @@ class CheckWalletCommand(
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Challenge, challenge)
return CommandApdu(Instruction.CheckWallet, tlvBuilder.serialize())

View file

@ -2,9 +2,11 @@ package com.tangem.commands
import com.tangem.*
import com.tangem.common.CompletionResult
import com.tangem.common.PinCode
import com.tangem.common.apdu.*
import com.tangem.common.extensions.toInt
import com.tangem.common.tlv.TlvTag
import com.tangem.tasks.PinType
interface ApduSerializable<T : CommandResponse> {
@ -37,8 +39,8 @@ interface CommandResponse
*/
abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRunnable<T> {
override val performPreflightRead: Boolean = true
open val requiresPin2: Boolean = false
open val performPreflightRead: Boolean = true
override val requiresPin2: Boolean = false
override fun run(session: CardSession, callback: (result: CompletionResult<T>) -> Unit) {
Log.i("Command", "Initializing ${this::class.java.simpleName}")
@ -50,7 +52,6 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
open fun mapError(card: Card?, error: TangemSdkError): TangemSdkError = error
fun transceive(session: CardSession, callback: (result: CompletionResult<T>) -> Unit) {
val card = session.environment.card
if (session.environment.handleErrors && card != null) {
performPreCheck(card)?.let { error ->
@ -58,12 +59,14 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
return
}
}
if (requiresPin2 && session.environment.isCurrentPin2Default()) {
if (requiresPin2 && session.environment.pin2?.isDefault == true) {
handlePin2(session, callback)
return
}
transceiveInternal(session, callback)
}
private fun transceiveInternal(session: CardSession, callback: (result: CompletionResult<T>) -> Unit) {
val apdu = serialize(session.environment)
transceiveApdu(apdu, session) { result ->
when (result) {
@ -71,9 +74,15 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
if (session.environment.handleErrors) {
val error = mapError(session.environment.card, result.error)
if (error is TangemSdkError.Pin1Required) {
session.environment.pin1 = null
handlePin1(session, callback)
return@transceiveApdu
}
if (error is TangemSdkError.Pin2OrCvcRequired) {
session.environment.pin2 = null
handlePin2(session, callback)
return@transceiveApdu
}
callback(CompletionResult.Failure(error))
return@transceiveApdu
@ -93,9 +102,9 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
}
private fun transceiveApdu(
apdu: CommandApdu,
session: CardSession,
callback: (result: CompletionResult<ResponseApdu>) -> Unit
apdu: CommandApdu,
session: CardSession,
callback: (result: CompletionResult<ResponseApdu>) -> Unit
) {
Log.i(this::class.simpleName!!, "transieve: ${Instruction.byCode(apdu.ins)}")
@ -114,17 +123,17 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
StatusWord.NeedPause -> {
// NeedPause is returned from the card whenever security delay is triggered.
val remainingTime =
deserializeSecurityDelay(responseApdu)
deserializeSecurityDelay(responseApdu)
if (remainingTime != null) {
session.viewDelegate.onSecurityDelay(
remainingTime,
session.environment.card?.pauseBeforePin2 ?: 0
remainingTime,
session.environment.card?.pauseBeforePin2 ?: 0
)
}
Log.i(
this::class.simpleName!!,
"Nfc command ${this::class.simpleName!!} " +
"triggered security delay of $remainingTime milliseconds"
this::class.simpleName!!,
"Nfc command ${this::class.simpleName!!} " +
"triggered security delay of $remainingTime milliseconds"
)
transceiveApdu(apdu, session, callback)
}
@ -149,7 +158,7 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
}
else -> {
val error = responseApdu.statusWord.toTangemSdkError()
if (error != null && !tryHandleError(error)) {
if (error != null) {
callback(CompletionResult.Failure(error))
} else {
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
@ -173,61 +182,56 @@ abstract class Command<T : CommandResponse> : ApduSerializable<T>, CardSessionRu
* @return Remaining security delay in milliseconds.
*/
private fun deserializeSecurityDelay(
responseApdu: ResponseApdu
responseApdu: ResponseApdu
): Int? {
val tlv = responseApdu.getTlvData()
return tlv?.find { it.tag == TlvTag.Pause }?.value?.toInt()
}
private fun tryHandleError(error: TangemSdkError): Boolean {
return false
}
private fun handlePin1(
session: CardSession,
callback: (result: CompletionResult<T>) -> Unit
session: CardSession,
callback: (result: CompletionResult<T>) -> Unit
) {
if (!session.environment.isCurrentPin1Default()) {
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)
}
session.viewDelegate.onPinRequested(PinType.Pin1) { pin1 ->
session.environment.pin1 = PinCode(pin1)
session.resume()
transceive(session, callback)
}
}
private fun handlePin2(
session: CardSession,
callback: (result: CompletionResult<T>) -> Unit
session: CardSession,
callback: (result: CompletionResult<T>) -> Unit
) {
val checkPinCommand = SetPinCommand(session.environment.pin1, session.environment.pin2)
val currentPin1 = session.environment.pin1?.value ?: run {
callback(CompletionResult.Failure(TangemSdkError.Pin1Required()))
return
}
val currentPin2 = session.environment.pin2?.value ?: run {
callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired()))
return
}
val checkPinCommand = SetPinCommand(currentPin1, currentPin2)
checkPinCommand.run(session) { result ->
when (result) {
is CompletionResult.Failure -> {
session.viewDelegate.onPinRequested { pin2 ->
if (!pin2.isNullOrEmpty()) {
session.environment.setPin2(pin2)
transceive(session, callback)
} else {
session.environment.setPin2(SessionEnvironment.DEFAULT_PIN2)
callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired()))
}
}
session.environment.pin2 = null
getPin2FromDelegate(session, callback)
}
is CompletionResult.Success -> {
transceive(session, callback)
transceiveInternal(session, callback)
}
}
}
}
private fun getPin2FromDelegate(session: CardSession,
callback: (result: CompletionResult<T>) -> Unit) {
session.viewDelegate.onPinRequested(PinType.Pin2) { pin2 ->
session.environment.pin2 = PinCode(pin2)
transceive(session, callback)
}
}
}

View file

@ -62,9 +62,9 @@ class CreateWalletCommand : Command<CreateWalletResponse>() {
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
tlvBuilder.append(TlvTag.Pin2, environment.pin2?.value)
tlvBuilder.append(TlvTag.Cvc, environment.cvc)
return CommandApdu(Instruction.CreateWallet, tlvBuilder.serialize())
}

View file

@ -61,9 +61,9 @@ class PurgeWalletCommand : Command<PurgeWalletResponse>() {
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
tlvBuilder.append(TlvTag.Pin2, environment.pin2?.value)
return CommandApdu(Instruction.PurgeWallet, tlvBuilder.serialize())
}

View file

@ -382,7 +382,7 @@ class ReadCommand : Command<Card>() {
* In order to obtain cards data, [ReadCommand] should use the correct pin 1 value.
* The card will not respond if wrong pin 1 has been submitted.
*/
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.TerminalPublicKey, environment.terminalKeys?.publicKey)
return CommandApdu(Instruction.Read, tlvBuilder.serialize())
}

View file

@ -96,7 +96,7 @@ class ReadIssuerDataCommand(
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadData)
return CommandApdu(Instruction.ReadIssuerData, tlvBuilder.serialize())

View file

@ -145,7 +145,7 @@ class ReadIssuerExtraDataCommand(
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadExtraData)
tlvBuilder.append(TlvTag.Offset, offset)

View file

@ -62,7 +62,7 @@ class ReadUserDataCommand : Command<ReadUserDataResponse>() {
override fun serialize(environment: SessionEnvironment): CommandApdu {
val builder = TlvBuilder()
builder.append(TlvTag.CardId, environment.card?.cardId)
builder.append(TlvTag.Pin, environment.pin1)
builder.append(TlvTag.Pin, environment.pin1?.value)
return CommandApdu(Instruction.ReadUserData, builder.serialize())
}

View file

@ -6,7 +6,6 @@ import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.apdu.StatusWord
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
@ -40,28 +39,27 @@ enum class SetPinStatus {
}
class SetPinResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
*
*/
val status: SetPinStatus
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
*
*/
val status: SetPinStatus
) : CommandResponse
class SetPinCommand(
private val newPin1: ByteArray = SessionEnvironment.DEFAULT_PIN.calculateSha256(),
private val newPin2: ByteArray = SessionEnvironment.DEFAULT_PIN2.calculateSha256(),
private val newPin3: ByteArray? = null
private var newPin1: ByteArray,
private var newPin2: ByteArray,
private var newPin3: ByteArray? = null
) : Command<SetPinResponse>() {
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
tlvBuilder.append(TlvTag.Pin2, environment.pin2?.value)
tlvBuilder.append(TlvTag.Cvc, environment.cvc)
tlvBuilder.append(TlvTag.NewPin, newPin1)
tlvBuilder.append(TlvTag.NewPin2, newPin2)
@ -72,12 +70,13 @@ class SetPinCommand(
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): SetPinResponse {
val tlvData = apdu.getTlvData() ?: throw TangemSdkError.DeserializeApduFailed()
val status = SetPinStatus.fromStatusWord(apdu.statusWord) ?: throw TangemSdkError.DecodingFailed()
val status = SetPinStatus.fromStatusWord(apdu.statusWord)
?: throw TangemSdkError.DecodingFailed()
val decoder = TlvDecoder(tlvData)
return SetPinResponse(
cardId = decoder.decode(TlvTag.CardId),
status = status
cardId = decoder.decode(TlvTag.CardId),
status = status
)
}
}

View file

@ -75,8 +75,8 @@ class SignCommand(private val hashes: Array<ByteArray>) : Command<SignResponse>(
override fun serialize(environment: SessionEnvironment): CommandApdu {
val dataToSign = flattenHashes()
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.Pin2, environment.pin2?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.TransactionOutHashSize, byteArrayOf(hashSizes.toByte()))
tlvBuilder.append(TlvTag.TransactionOutHash, dataToSign)

View file

@ -83,7 +83,7 @@ class WriteIssuerDataCommand(
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.WriteData)
tlvBuilder.append(TlvTag.IssuerData, issuerData)

View file

@ -149,7 +149,7 @@ class WriteIssuerExtraDataCommand(
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, mode)

View file

@ -57,7 +57,7 @@ class WriteUserDataCommand(private val userData: ByteArray? = null, private val
override fun serialize(environment: SessionEnvironment): CommandApdu {
val builder = TlvBuilder()
builder.append(TlvTag.CardId, environment.card?.cardId)
builder.append(TlvTag.Pin, environment.pin1)
builder.append(TlvTag.Pin, environment.pin1?.value)
builder.append(TlvTag.UserData, userData)
builder.append(TlvTag.UserCounter, userCounter)
builder.append(TlvTag.UserProtectedData, userProtectedData)

View file

@ -19,18 +19,6 @@ class DepersonalizeCommand : Command<DepersonalizeResponse>() {
override val performPreflightRead = false
// override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<DepersonalizeResponse>) -> Unit): Boolean {
// if (session.environment.card?.status == CardStatus.NotPersonalized) {
// callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
// return true
// }
// if (session.environment.card?.firmwareVersion?.contains("SDK") == false) {
// callback(CompletionResult.Failure(TangemSdkError.CannotBeDepersonalized()))
// return true
// }
// return false
// }
override fun serialize(environment: SessionEnvironment): CommandApdu {
return CommandApdu(
Instruction.Depersonalize, byteArrayOf()

View file

@ -3,6 +3,7 @@ package com.tangem.commands.personalization.entities
import com.tangem.commands.CardData
import com.tangem.commands.EllipticCurve
import com.tangem.commands.SigningMethodMask
import com.tangem.common.extensions.calculateSha256
data class NdefRecord(
val type: Type,
@ -20,15 +21,15 @@ data class NdefRecord(
* It is a configuration file with all the card settings that are written on the card
* during [PersonalizeCommand].
*/
data class CardConfig(
class CardConfig(
val issuerName: String? = null,
val acquirerName: String? = null,
val series: String? = null,
val startNumber: Long = 0,
val count: Int = 0,
val pin: String,
val pin2: String,
val pin3: String,
pin: String,
pin2: String,
pin3: String,
val hexCrExKey: String?,
val cvc: String,
val pauseBeforePin2: Int,
@ -67,5 +68,10 @@ data class CardConfig(
val cardData: CardData,
val ndefRecords: List<NdefRecord>
) {
val pin: ByteArray = pin.calculateSha256()
val pin2: ByteArray = pin2.calculateSha256()
val pin3: ByteArray = pin3.calculateSha256()
companion object
}

View file

@ -137,7 +137,7 @@ class VerifyCardCommand(private val onlineVerification: Boolean) : Command<Verif
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin, environment.pin1?.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Challenge, challenge)
return CommandApdu(Instruction.VerifyCard, tlvBuilder.serialize())

View file

@ -0,0 +1,56 @@
package com.tangem.common
import com.squareup.sqldelight.EnumColumnAdapter
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
import com.tangem.CardValues
import com.tangem.CardValuesEntityQueries
import com.tangem.Database
import com.tangem.VerificationState
interface CardValuesStorage {
fun saveValues(cardId: String,
isPin1Default: Boolean, isPin2Default: Boolean,
cardVerification: VerificationState?,
cardValidation: VerificationState?,
codeVerification: VerificationState?)
fun getValues(cardId: String): CardValues?
}
class CardValuesDbStorage(driver: SqlDriver) : CardValuesStorage {
private val cardValuesQueries: CardValuesEntityQueries
init {
val database = Database(driver, cardValuesAdapter = CardValues.Adapter(
cardVerificationAdapter = EnumColumnAdapter(),
cardValidationAdapter = EnumColumnAdapter(),
codeVerificationAdapter = EnumColumnAdapter()
))
cardValuesQueries = database.cardValuesEntityQueries
}
override fun saveValues(cardId: String,
isPin1Default: Boolean, isPin2Default: Boolean,
cardVerification: VerificationState?,
cardValidation: VerificationState?,
codeVerification: VerificationState?) {
cardValuesQueries.insertOrReplace(
cardId,
isPin1Default, isPin2Default,
cardVerification, cardValidation, codeVerification
)
}
override fun getValues(cardId: String): CardValues? =
cardValuesQueries.selectByCardId(cardId).executeAsOneOrNull()
companion object {
fun initJvm() = CardValuesDbStorage(
JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY).also { Database.Schema.create(it) }
)
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.common
import com.tangem.common.extensions.calculateSha256
class PinCode(
val value: ByteArray,
val isDefault: Boolean
) {
constructor(value: String, isDefault: Boolean = false): this(value.calculateSha256(), isDefault)
}

View file

@ -0,0 +1,101 @@
package com.tangem.tasks
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.SessionEnvironment
import com.tangem.SessionViewDelegate
import com.tangem.commands.SetPinCommand
import com.tangem.commands.SetPinResponse
import com.tangem.common.CompletionResult
import com.tangem.common.PinCode
import com.tangem.common.extensions.calculateSha256
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
enum class PinType {
Pin1,
Pin2,
Pin3,
;
}
class ChangePinTask(
private val pinType: PinType,
private val pin: ByteArray? = null
) : CardSessionRunnable<SetPinResponse> {
override val requiresPin2 = false
override fun run(session: CardSession, callback: (result: CompletionResult<SetPinResponse>) -> Unit) {
session.scope.launch {
val pin = this@ChangePinTask.pin
?: requestNewPin(pinType, session.viewDelegate).calculateSha256()
runSetPin(pin, session, callback)
}
}
private suspend fun runSetPin(pin: ByteArray, session: CardSession, callback: (result: CompletionResult<SetPinResponse>) -> Unit) {
val pin1: ByteArray
val pin2: ByteArray
val pin3: ByteArray?
if (session.environment.pin1 == null) {
session.environment.pin1 = PinCode(requestPin(PinType.Pin1, session.viewDelegate))
}
if (session.environment.pin2 == null) {
session.environment.pin2 = PinCode(requestPin(PinType.Pin2, session.viewDelegate))
}
when (pinType) {
PinType.Pin1 -> {
pin1 = pin
pin2 = session.environment.pin2!!.value
pin3 = null
}
PinType.Pin2 -> {
pin1 = session.environment.pin1!!.value
pin2 = pin
pin3 = null
}
PinType.Pin3 -> {
pin1 = session.environment.pin1!!.value
pin2 = session.environment.pin2!!.value
pin3 = pin
}
}
val command = SetPinCommand(pin1, pin2, pin3)
command.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
savePin(pin, session.environment)
callback(result)
}
is CompletionResult.Failure -> callback(result)
}
}
}
private suspend fun requestPin(pinType: PinType, viewDelegate: SessionViewDelegate): String =
suspendCancellableCoroutine { continuation ->
viewDelegate.onPinRequested(pinType) { result ->
if (continuation.isActive) continuation.resume(result)
}
}
private suspend fun requestNewPin(pinType: PinType, viewDelegate: SessionViewDelegate): String =
suspendCancellableCoroutine { continuation ->
viewDelegate.onPinChangeRequested(pinType) { result ->
if (continuation.isActive) continuation.resume(result)
}
}
private fun savePin(pin: ByteArray, environment: SessionEnvironment) {
when (pinType) {
PinType.Pin1 -> environment.pin1 = PinCode(pin, false)
PinType.Pin2 -> environment.pin2 = PinCode(pin, false)
PinType.Pin3 -> {}
}
}
}

View file

@ -11,7 +11,7 @@ import com.tangem.common.CompletionResult
class CreateWalletTask : CardSessionRunnable<CreateWalletResponse> {
override val performPreflightRead = true
override val requiresPin2 = false
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
val curve = session.environment.card?.curve

View file

@ -15,41 +15,54 @@ import com.tangem.common.CompletionResult
*/
internal class ScanTask : CardSessionRunnable<Card> {
override val performPreflightRead = false
override val requiresPin2 = false
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
if (session.connectedTag == TagType.Slix) {
readSlixTag(session, callback)
return
}
ReadCommand().run(session) { readResult ->
when(readResult) {
is CompletionResult.Failure -> callback(readResult)
is CompletionResult.Success -> {
val card = readResult.data
session.environment.card = card
if (card.cardData?.productMask?.contains(Product.Tag) != false) {
callback(CompletionResult.Success(card))
} else if (card.status != CardStatus.Loaded) {
callback(CompletionResult.Success(card))
} else if (card.curve == null || card.walletPublicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
} else {
val checkWalletCommand = CheckWalletCommand(card.curve, card.walletPublicKey)
checkWalletCommand.run(session) { result ->
when (result) {
is CompletionResult.Success -> callback(CompletionResult.Success(card))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
if (session.environment.pin1 != null && session.environment.pin2 != null) {
val checkPinCommand = SetPinCommand(session.environment.pin1!!.value, session.environment.pin2!!.value)
checkPinCommand.run(session) { result ->
when (result) {
is CompletionResult.Failure -> {
session.environment.pin2 = null
}
}
runCheckWalletIfNeeded(card, session, callback)
}
} else {
runCheckWalletIfNeeded(card, session, callback)
}
}
private fun runCheckWalletIfNeeded(
card: Card, session: CardSession,
callback: (result: CompletionResult<Card>) -> Unit
) {
if (card.cardData?.productMask?.contains(Product.Tag) != false) {
callback(CompletionResult.Success(card))
} else if (card.status != CardStatus.Loaded) {
callback(CompletionResult.Success(card))
} else if (card.curve == null || card.walletPublicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
} else {
val checkWalletCommand = CheckWalletCommand(card.curve, card.walletPublicKey)
checkWalletCommand.run(session) { result ->
when (result) {
is CompletionResult.Success -> callback(CompletionResult.Success(card))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}

View file

@ -0,0 +1,34 @@
import com.tangem.VerificationState;
CREATE TABLE cardValues (
cardId TEXT NOT NULL UNIQUE PRIMARY KEY,
isPin1Default INTEGER AS Boolean DEFAULT 1,
isPin2Default INTEGER AS Boolean DEFAULT 1,
cardVerification TEXT AS VerificationState,
cardValidation TEXT AS VerificationState,
codeVerification TEXT AS VerificationState
);
insertOrReplace:
INSERT OR REPLACE INTO cardValues(
cardId,
isPin1Default,
isPin2Default,
cardVerification,
cardValidation,
codeVerification
)
VALUES (?, ?, ?, ?, ?, ?);
selectByCardId:
SELECT *
FROM cardValues
WHERE cardId = ?;
deleteAll:
DELETE FROM cardValues;
deleteByCardId:
DELETE
FROM cardValues
WHERE cardId = ?;

View file

@ -4,20 +4,17 @@ import com.google.common.truth.Truth.assertThat
import com.tangem.SessionEnvironment
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvTag
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collect
import org.junit.Test
class CommandApduTest {
private val sessionEnvironment = SessionEnvironment()
@Test
fun `simple READ command to bytes`() {
val sessionEnvironment = SessionEnvironment()
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, sessionEnvironment.pin1)
tlvBuilder.append(TlvTag.Pin, sessionEnvironment.pin1?.value)
val commandApdu = CommandApdu(
Instruction.Read,
tlvBuilder.serialize()
@ -28,18 +25,17 @@ class CommandApduTest {
assertThat(commandApdu.apduData)
.isEqualTo(expected)
assertThat(listOf(1, 2)).containsExactlyElementsIn(listOf(1,2))
assertThat(listOf(1, 2)).containsExactlyElementsIn(listOf(1, 2))
}
@Test
fun `READ with terminal key to bytes`() {
val sessionEnvironment = SessionEnvironment()
val terminalPublicKey = byteArrayOf(4, 80, -122, 58, -42, 74, -121, -82, -118, 47, -24, 60,
26, -15, -88, 64, 60, -75, 63, 83, -28, -122, -40, 81, 29, -83, -118, 4, -120, 126,
91, 35, 82, 44, -44, 112, 36, 52, 83, -94, -103, -6, -98, 119, 35, 119, 22, 16, 58,
-68, 17, -95, -33, 56, -123, 94, -42, -14, -18, 24, 126, -100, 88, 43, -90)
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, sessionEnvironment.pin1)
tlvBuilder.append(TlvTag.Pin, sessionEnvironment.pin1?.value)
tlvBuilder.append(TlvTag.TerminalPublicKey, terminalPublicKey)
val commandApdu = CommandApdu(
Instruction.Read,

View file

@ -5,7 +5,6 @@ import android.view.View
import android.widget.CompoundButton
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.tangem.SessionEnvironment
import com.tangem.TangemSdk
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
@ -103,7 +102,7 @@ class TestUserDataActivity : AppCompatActivity() {
chb_with_ud_protected.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateProtectedData(buttonView) }
chb_with_counter.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateCounter(buttonView) }
chb_with_protected_counter.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updateProtectedCounter(buttonView) }
chb_with_pin2.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updatePin2(buttonView) }
// chb_with_pin2.setOnCheckedChangeListener { buttonView, isChecked -> writeOptions.updatePin2(buttonView) }
}
private fun showReadWriteSection(show: Boolean) {
@ -140,8 +139,8 @@ class WriteOptions {
userProtectedCounter = if (chbx.isChecked) value else null
}
fun updatePin2(chbx: CompoundButton) {
val value = SessionEnvironment.DEFAULT_PIN2
pin2 = if (chbx.isChecked) value else null
}
// fun updatePin2(chbx: CompoundButton) {
// val value = SessionEnvironment.pin1.DEFAULT_PIN2
// pin2 = if (chbx.isChecked) value else null
// }
}

View file

@ -62,6 +62,7 @@ dependencies {
// misc
implementation 'com.skyfishjy.ripplebackground:library:1.0.1'
implementation 'at.favre.lib:armadillo:0.9.0'
implementation "com.squareup.sqldelight:android-driver:1.4.0"
// testing
testImplementation 'junit:junit:4.12'

View file

@ -1,9 +1,10 @@
package com.tangem.tangem_sdk_new
import androidx.fragment.app.FragmentActivity
import android.app.Activity
import com.tangem.*
import com.tangem.tangem_sdk_new.nfc.NfcReader
import com.tangem.tangem_sdk_new.ui.NfcSessionDialog
import com.tangem.tasks.PinType
/**
* Default implementation of [SessionViewDelegate].
@ -11,7 +12,7 @@ import com.tangem.tangem_sdk_new.ui.NfcSessionDialog
*/
class DefaultSessionViewDelegate(private val reader: NfcReader) : SessionViewDelegate {
lateinit var activity: FragmentActivity
lateinit var activity: Activity
private var readingDialog: NfcSessionDialog? = null
init {
@ -19,20 +20,19 @@ class DefaultSessionViewDelegate(private val reader: NfcReader) : SessionViewDel
}
override fun onSessionStarted(cardId: String?, message: Message?) {
postUI { showReadingDialog(activity, cardId, message) }
postUI {
if (readingDialog == null) createReadingDialog(activity)
readingDialog?.show(SessionViewDelegateState.Ready(cardId, message))
}
}
private fun showReadingDialog(activity: FragmentActivity, cardId: String?, message: Message?) {
private fun createReadingDialog(activity: Activity) {
val dialogView = activity.layoutInflater.inflate(R.layout.nfc_bottom_sheet, null)
readingDialog = NfcSessionDialog(activity)
readingDialog?.setContentView(dialogView)
readingDialog?.dismissWithAnimation = true
readingDialog?.create()
readingDialog?.setOnShowListener {
readingDialog?.show(SessionViewDelegateState.Ready(cardId, message))
}
readingDialog?.setOnCancelListener { reader.stopSession(true) }
readingDialog?.show()
}
override fun onSecurityDelay(ms: Int, totalDurationSeconds: Int) {
@ -68,8 +68,22 @@ class DefaultSessionViewDelegate(private val reader: NfcReader) : SessionViewDel
postUI { readingDialog?.show(SessionViewDelegateState.Error(error)) }
}
override fun onPinRequested(callback: (pin: String?) -> Unit) {
postUI { readingDialog?.show(SessionViewDelegateState.PinRequested(callback)) }
override fun onPinRequested(pinType: PinType, callback: (pin: String) -> Unit) {
val message = when (pinType) {
PinType.Pin1 -> activity.getString(R.string.pin_enter_pin_1)
PinType.Pin2 -> activity.getString(R.string.pin_enter_pin_2)
PinType.Pin3 -> activity.getString(R.string.pin_enter_pin_3)
}
postUI { readingDialog?.show(SessionViewDelegateState.PinRequested(message, callback)) }
}
override fun onPinChangeRequested(pinType: PinType, callback: (pin: String) -> Unit) {
val message = when (pinType) {
PinType.Pin1 -> activity.getString(R.string.pin_change_pin_1)
PinType.Pin2 -> activity.getString(R.string.pin_change_pin_2)
PinType.Pin3 -> activity.getString(R.string.pin_change_pin_3)
}
postUI { readingDialog?.show(SessionViewDelegateState.PinChangeRequested(message, callback)) }
}
private fun setLogger() {

View file

@ -9,7 +9,8 @@ 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: (pin: String?) -> Unit) : SessionViewDelegateState()
data class PinRequested(val message: String, val callback: (pin: String) -> Unit) : SessionViewDelegateState()
data class PinChangeRequested(val message: String, val callback: (pin: String) -> Unit) : SessionViewDelegateState()
object TagLost : SessionViewDelegateState()
object TagConnected : SessionViewDelegateState()
object WrongCard : SessionViewDelegateState()

View file

@ -1,45 +1,48 @@
package com.tangem.tangem_sdk_new.extensions
import androidx.fragment.app.FragmentActivity
import androidx.activity.ComponentActivity
import com.squareup.sqldelight.android.AndroidSqliteDriver
import com.tangem.Config
import com.tangem.Database
import com.tangem.SessionViewDelegate
import com.tangem.TangemSdk
import com.tangem.common.CardValuesDbStorage
import com.tangem.tangem_sdk_new.DefaultSessionViewDelegate
import com.tangem.tangem_sdk_new.NfcLifecycleObserver
import com.tangem.tangem_sdk_new.TerminalKeysStorage
import com.tangem.tangem_sdk_new.nfc.NfcManager
fun TangemSdk.Companion.init(activity: FragmentActivity, config: Config = Config()): TangemSdk {
fun TangemSdk.Companion.init(activity: ComponentActivity, config: Config = Config()): TangemSdk {
val nfcManager = TangemSdk.initNfcManager(activity)
val viewDelegate = DefaultSessionViewDelegate(nfcManager.reader)
viewDelegate.activity = activity
val tangemSdk = TangemSdk(nfcManager.reader, viewDelegate, config)
tangemSdk.setTerminalKeysService(TerminalKeysStorage(activity.application))
return tangemSdk
val databaseDriver = AndroidSqliteDriver(Database.Schema, activity.applicationContext, "cards.db")
return TangemSdk(
nfcManager.reader, viewDelegate, config,
CardValuesDbStorage(databaseDriver), TerminalKeysStorage(activity.application)
)
}
fun TangemSdk.Companion.customInit(
activity: FragmentActivity,
activity: ComponentActivity,
viewDelegate: SessionViewDelegate? = null,
config: Config = Config()
): TangemSdk {
val nfcManager = TangemSdk.initNfcManager(activity)
val databaseDriver = AndroidSqliteDriver(Database.Schema, activity.applicationContext, "cards.db")
val tangemSdk = TangemSdk(
return TangemSdk(
nfcManager.reader,
viewDelegate ?: DefaultSessionViewDelegate(nfcManager.reader)
.apply { this.activity = activity },
config
config,
CardValuesDbStorage(databaseDriver), TerminalKeysStorage(activity.application)
)
tangemSdk.setTerminalKeysService(TerminalKeysStorage(activity.application))
return tangemSdk
}
fun TangemSdk.Companion.initNfcManager(activity: FragmentActivity): NfcManager {
fun TangemSdk.Companion.initNfcManager(activity: ComponentActivity): NfcManager {
val nfcManager = NfcManager()
nfcManager.setCurrentActivity(activity)
activity.lifecycle.addObserver(NfcLifecycleObserver(nfcManager))

View file

@ -8,9 +8,7 @@ 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
@ -19,11 +17,14 @@ import com.tangem.tangem_sdk_new.postUI
import kotlinx.android.synthetic.main.layout_touch_card.*
import kotlinx.android.synthetic.main.nfc_bottom_sheet.*
class NfcSessionDialog(val activity: FragmentActivity) : BottomSheetDialog(activity) {
class NfcSessionDialog(val activity: Activity) : BottomSheetDialog(activity) {
private var currentState: SessionViewDelegateState? = null
fun show(state: SessionViewDelegateState) {
if (!this.isShowing) {
this.show()
}
when (state) {
is SessionViewDelegateState.Ready -> onReady(state)
is SessionViewDelegateState.Success -> onSuccess(state)
@ -31,6 +32,7 @@ class NfcSessionDialog(val activity: FragmentActivity) : BottomSheetDialog(activ
is SessionViewDelegateState.SecurityDelay -> onSecurityDelay(state)
is SessionViewDelegateState.Delay -> onDelay(state)
is SessionViewDelegateState.PinRequested -> onPinRequested(state)
is SessionViewDelegateState.PinChangeRequested -> onPinChangeRequested(state)
is SessionViewDelegateState.TagLost -> onTagLost()
is SessionViewDelegateState.TagConnected -> onTagConnected()
is SessionViewDelegateState.WrongCard -> onWrongCard()
@ -50,9 +52,17 @@ class NfcSessionDialog(val activity: FragmentActivity) : BottomSheetDialog(activ
tvCardId?.show()
tvCardId?.text = cardId
}
state.message?.let { message ->
if (message.body != null) tvTaskText?.text = message.body
if (message.header != null) tvTaskTitle?.text = message.header
if (state.message?.header != null) {
tvTaskTitle?.text = state.message.header
} else {
tvTaskTitle?.text = activity.getText(R.string.dialog_ready_to_scan)
}
if (state.message?.body != null) {
tvTaskText?.text = state.message.body
} else {
tvTaskText?.text = activity.getText(R.string.dialog_scan_text)
}
}
@ -130,19 +140,76 @@ class NfcSessionDialog(val activity: FragmentActivity) : BottomSheetDialog(activ
tvTaskText?.visibility = View.INVISIBLE
tvTaskTitle?.visibility = View.INVISIBLE
show(flPin)
tilPin.hint = state.message
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)
if (v?.text.isNullOrBlank()) {
etPin?.error = activity.getString(R.string.pin_enter_error_empty)
} else {
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) }
v?.text.toString().let { pin ->
v?.text = ""
state.callback(pin)
}
}
}
}
true
}
}
private fun onPinChangeRequested(state: SessionViewDelegateState.PinChangeRequested) {
tvTaskText?.visibility = View.INVISIBLE
tvTaskTitle?.visibility = View.INVISIBLE
show(llChangePin)
tilChangePin.hint = state.message
tilChangePinConfirm.hint = activity.getString(R.string.pin_change_confirm)
performHapticFeedback()
etPin?.setOnEditorActionListener { v: TextView?, actionId: Int, event: KeyEvent? ->
postUI {
if (actionId == KeyEvent.KEYCODE_ENDCALL) {
if (v?.text.isNullOrBlank()) {
etChangePin?.error = activity.getString(R.string.pin_enter_error_empty)
}
}
}
true
}
etChangePinConfirm?.setOnEditorActionListener { v: TextView?, actionId: Int, event: KeyEvent? ->
postUI {
if (actionId == KeyEvent.KEYCODE_ENDCALL) {
if (v?.text.isNullOrBlank()) {
etChangePin?.error = activity.getString(R.string.pin_enter_error_empty)
} else if (v?.text.toString() != etChangePin?.text.toString()) {
etChangePinConfirm?.error = activity.getString(R.string.pin_change_error)
etChangePin?.error = activity.getString(R.string.pin_change_error)
} else {
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 { pin ->
v?.text = ""
etChangePin?.setText("")
state.callback(pin)
}
}
}
}
true
@ -151,7 +218,7 @@ class NfcSessionDialog(val activity: FragmentActivity) : BottomSheetDialog(activ
private fun onTagLost() {
if (currentState is SessionViewDelegateState.Success ||
currentState is SessionViewDelegateState.PinRequested) {
currentState is SessionViewDelegateState.PinRequested) {
return
}
show(lTouchCard)
@ -173,7 +240,7 @@ class NfcSessionDialog(val activity: FragmentActivity) : BottomSheetDialog(activ
)
performHapticFeedback()
postUI (2000){
postUI(2000) {
show(lTouchCard)
tvTaskTitle?.text = activity.getText(R.string.dialog_ready_to_scan)
tvTaskText?.text = activity.getText(R.string.dialog_scan_text)
@ -189,6 +256,7 @@ class NfcSessionDialog(val activity: FragmentActivity) : BottomSheetDialog(activ
flError?.show(view.id == flError.id)
flCompletion?.show(view.id == flCompletion.id)
flPin?.show(view.id == flPin.id)
llChangePin?.show(view.id == llChangePin.id)
}
private fun performHapticFeedback() {

View file

@ -79,6 +79,7 @@
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_gravity="center"
android:theme="@style/Theme.MaterialComponents"
android:visibility="gone">
<com.google.android.material.textfield.TextInputLayout
@ -98,12 +99,64 @@
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>
<LinearLayout
android:id="@+id/llChangePin"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_gravity="center"
android:orientation="vertical"
android:visibility="gone">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilChangePin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp"
android:backgroundTint="#f3f5f6"
android:layout_gravity="center"
android:gravity="center"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etChangePin"
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>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilChangePinConfirm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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/etChangePinConfirm"
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>
</LinearLayout>
<FrameLayout
android:id="@+id/flSecurityDelay"

View file

@ -10,5 +10,14 @@
<string name="dialog_ready_to_scan">Ready to Scan</string>
<string name="dialog_scan_text">Scan card with your phone as shown above</string>
<string name="header_card">Card</string>
<string name="pin_change_pin_1">Enter New Access Code</string>
<string name="pin_change_pin_2">Enter New Pass Code</string>
<string name="pin_change_pin_3">Enter New PIN 3</string>
<string name="pin_change_confirm">Confirm New Code</string>
<string name="pin_change_error">Codes do not match</string>
<string name="pin_enter_error_empty">Enter code</string>
<string name="pin_enter_pin_1">Enter Access Code</string>
<string name="pin_enter_pin_2">Enter Pass Code</string>
<string name="pin_enter_pin_3">Enter PIN 3</string>
</resources>