Updated on 2026-08-14
This commit is contained in:
commit
4538d65393
28 changed files with 807 additions and 77 deletions
|
|
@ -12,9 +12,9 @@ dependencies {
|
||||||
implementation "com.madgag.spongycastle:core:1.58.0.0"
|
implementation "com.madgag.spongycastle:core:1.58.0.0"
|
||||||
implementation "com.madgag.spongycastle:prov:1.58.0.0"
|
implementation "com.madgag.spongycastle:prov:1.58.0.0"
|
||||||
implementation 'net.i2p.crypto:eddsa:0.3.0'
|
implementation 'net.i2p.crypto:eddsa:0.3.0'
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
|
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
|
||||||
testImplementation "com.google.truth:truth:1.0"
|
testImplementation "com.google.truth:truth:1.0"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceCompatibility = "8"
|
sourceCompatibility = "8"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
package com.tangem
|
package com.tangem
|
||||||
|
|
||||||
import com.tangem.commands.*
|
import com.tangem.commands.*
|
||||||
|
import com.tangem.commands.personalization.CardConfig
|
||||||
|
import com.tangem.commands.personalization.DepersonalizeCommand
|
||||||
|
import com.tangem.commands.personalization.DepersonalizeResponse
|
||||||
|
import com.tangem.commands.personalization.PersonalizeCommand
|
||||||
import com.tangem.common.CardEnvironment
|
import com.tangem.common.CardEnvironment
|
||||||
import com.tangem.common.TerminalKeysService
|
import com.tangem.common.TerminalKeysService
|
||||||
import com.tangem.crypto.CryptoUtils
|
import com.tangem.crypto.CryptoUtils
|
||||||
|
|
@ -176,16 +180,16 @@ class CardManager(
|
||||||
* User_ProtectedCounter and User_ProtectedData additionaly need PIN2 to confirmation.
|
* User_ProtectedCounter and User_ProtectedData additionaly need PIN2 to confirmation.
|
||||||
*/
|
*/
|
||||||
fun writeUserData(
|
fun writeUserData(
|
||||||
cardId: String,
|
cardId: String,
|
||||||
userData: ByteArray? = null,
|
userData: ByteArray? = null,
|
||||||
userProtectedData: ByteArray? = null,
|
userProtectedData: ByteArray? = null,
|
||||||
userCounter: Int? = null,
|
userCounter: Int? = null,
|
||||||
userProtectedCounter: Int? = null,
|
userProtectedCounter: Int? = null,
|
||||||
callback: (result: TaskEvent<WriteUserDataResponse>) -> Unit
|
callback: (result: TaskEvent<WriteUserDataResponse>) -> Unit
|
||||||
) {
|
) {
|
||||||
val writeUserDataCommand = WriteUserDataCommand(userData, userProtectedData, userCounter, userProtectedCounter)
|
val writeUserDataCommand = WriteUserDataCommand(userData, userProtectedData, userCounter, userProtectedCounter)
|
||||||
val task = SingleCommandTask(writeUserDataCommand)
|
val task = SingleCommandTask(writeUserDataCommand)
|
||||||
runTask(task, cardId, callback)
|
runTask(task, cardId, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -199,8 +203,8 @@ class CardManager(
|
||||||
* For example, this fields may contain blockchain nonce value.
|
* For example, this fields may contain blockchain nonce value.
|
||||||
*/
|
*/
|
||||||
fun readUserData(cardId: String, callback: (result: TaskEvent<ReadUserDataResponse>) -> Unit) {
|
fun readUserData(cardId: String, callback: (result: TaskEvent<ReadUserDataResponse>) -> Unit) {
|
||||||
val task = SingleCommandTask(ReadUserDataCommand())
|
val task = SingleCommandTask(ReadUserDataCommand())
|
||||||
runTask(task, cardId, callback)
|
runTask(task, cardId, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -234,6 +238,43 @@ class CardManager(
|
||||||
runTask(task, cardId, callback)
|
runTask(task, cardId, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command available on SDK cards only
|
||||||
|
*
|
||||||
|
* This command resets card to initial state,
|
||||||
|
* erasing all data written during personalization and usage.
|
||||||
|
* @param cardId CID, Unique Tangem card ID number.
|
||||||
|
*/
|
||||||
|
fun depersonalize(cardId: String,
|
||||||
|
callback: (result: TaskEvent<DepersonalizeResponse>) -> Unit) {
|
||||||
|
val depersonalizeCommand = DepersonalizeCommand()
|
||||||
|
val task = SingleCommandTask(depersonalizeCommand)
|
||||||
|
runTask(task, cardId, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command available on SDK cards only
|
||||||
|
*
|
||||||
|
* Personalization is an initialization procedure, required before starting using a card.
|
||||||
|
* During this procedure a card setting is set up.
|
||||||
|
* During this procedure all data exchange is encrypted.
|
||||||
|
* @param config is a configuration file with all the card settings that are written on the card
|
||||||
|
* during personalization.
|
||||||
|
* @param cardId this parameter will set up CID, Unique Tangem card ID.
|
||||||
|
*/
|
||||||
|
fun personalize(config: CardConfig,
|
||||||
|
cardId: String,
|
||||||
|
callback: (result: TaskEvent<Card>) -> Unit) {
|
||||||
|
if (this.config.issuer == null) {
|
||||||
|
callback(TaskEvent.Completion(TaskError.IssuerIsRequired()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val personalizationCommand = PersonalizeCommand(config, cardId)
|
||||||
|
val task = SingleCommandTask(personalizationCommand)
|
||||||
|
task.performPreflightRead = false
|
||||||
|
runTask(task, callback = callback)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
@ -279,7 +320,10 @@ class CardManager(
|
||||||
val terminalKeys = if (config.linkedTerminal) terminalKeysService?.getKeys() else null
|
val terminalKeys = if (config.linkedTerminal) terminalKeysService?.getKeys() else null
|
||||||
return CardEnvironment(
|
return CardEnvironment(
|
||||||
cardId = cardId,
|
cardId = cardId,
|
||||||
terminalKeys = terminalKeys
|
terminalKeys = terminalKeys,
|
||||||
|
manufacturerKeyPair = config.manufacturerKeyPair,
|
||||||
|
acquirerKeyPair = config.acquirerKeyPair,
|
||||||
|
issuer = config.issuer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
package com.tangem
|
package com.tangem
|
||||||
|
|
||||||
|
import com.tangem.commands.personalization.entities.Issuer
|
||||||
|
import com.tangem.common.KeyPair
|
||||||
|
|
||||||
class Config(
|
class Config(
|
||||||
val linkedTerminal: Boolean = true,
|
val linkedTerminal: Boolean = true,
|
||||||
val issuerPublicKey: ByteArray? = null
|
val issuerPublicKey: ByteArray? = null,
|
||||||
|
val manufacturerKeyPair: KeyPair? = null,
|
||||||
|
val acquirerKeyPair: KeyPair? = null,
|
||||||
|
val issuer: Issuer? = null
|
||||||
)
|
)
|
||||||
|
|
@ -14,7 +14,12 @@ class OpenSessionResponse(
|
||||||
val uid: ByteArray
|
val uid: ByteArray
|
||||||
) : CommandResponse
|
) : CommandResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) : CommandSerializer<OpenSessionResponse>() {
|
class OpenSessionCommand(private val sessionKeyA: ByteArray) : CommandSerializer<OpenSessionResponse>() {
|
||||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||||
val tlvBuilder = TlvBuilder()
|
val tlvBuilder = TlvBuilder()
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,49 @@ data class SigningMethod(val rawValue: Int) {
|
||||||
const val signHashValidatedByIssuerAndWriteIssuerData = 4
|
const val signHashValidatedByIssuerAndWriteIssuerData = 4
|
||||||
const val signRawValidatedByIssuerAndWriteIssuerData = 5
|
const val signRawValidatedByIssuerAndWriteIssuerData = 5
|
||||||
const val signPos = 6
|
const val signPos = 6
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
signHash: Boolean = false,
|
||||||
|
signRaw: Boolean = false,
|
||||||
|
signHashValidatedByIssuer: Boolean = false,
|
||||||
|
signRawValidatedByIssuer: Boolean = false,
|
||||||
|
signHashValidatedByIssuerAndWriteIssuerData: Boolean = false,
|
||||||
|
signRawValidatedByIssuerAndWriteIssuerData: Boolean = false,
|
||||||
|
signPos: Boolean = false
|
||||||
|
|
||||||
|
): SigningMethod {
|
||||||
|
fun Boolean.toInt() = if (this) 1 else 0
|
||||||
|
|
||||||
|
val signingMethodsCount = 0 +
|
||||||
|
signHash.toInt() +
|
||||||
|
signRaw.toInt() +
|
||||||
|
signHashValidatedByIssuer.toInt() +
|
||||||
|
signRawValidatedByIssuer.toInt() +
|
||||||
|
signHashValidatedByIssuerAndWriteIssuerData.toInt() +
|
||||||
|
signRawValidatedByIssuerAndWriteIssuerData.toInt() +
|
||||||
|
signPos.toInt()
|
||||||
|
|
||||||
|
var signingMethod: Int = 0
|
||||||
|
if (signingMethodsCount == 1) {
|
||||||
|
if (signHash) signingMethod += SigningMethod.signHash
|
||||||
|
if (signRaw) signingMethod += SigningMethod.signRaw
|
||||||
|
if (signHashValidatedByIssuer) signingMethod += SigningMethod.signHashValidatedByIssuer
|
||||||
|
if (signRawValidatedByIssuer) signingMethod += SigningMethod.signRawValidatedByIssuer
|
||||||
|
if (signHashValidatedByIssuerAndWriteIssuerData) signingMethod += SigningMethod.signHashValidatedByIssuerAndWriteIssuerData
|
||||||
|
if (signRawValidatedByIssuerAndWriteIssuerData) signingMethod += SigningMethod.signRawValidatedByIssuerAndWriteIssuerData
|
||||||
|
if (signPos) signingMethod += SigningMethod.signPos
|
||||||
|
} else if (signingMethodsCount > 1) {
|
||||||
|
signingMethod = 0x80
|
||||||
|
if (signHash) signingMethod += 0x01
|
||||||
|
if (signRaw) signingMethod += 0x01 shl SigningMethod.signRaw
|
||||||
|
if (signHashValidatedByIssuer) signingMethod += 0x01 shl SigningMethod.signHashValidatedByIssuer
|
||||||
|
if (signRawValidatedByIssuer) signingMethod += 0x01 shl SigningMethod.signRawValidatedByIssuer
|
||||||
|
if (signHashValidatedByIssuerAndWriteIssuerData) signingMethod += 0x01 shl SigningMethod.signHashValidatedByIssuerAndWriteIssuerData
|
||||||
|
if (signRawValidatedByIssuerAndWriteIssuerData) signingMethod += 0x01 shl SigningMethod.signRawValidatedByIssuerAndWriteIssuerData
|
||||||
|
if (signPos) signingMethod += 0x01 shl SigningMethod.signPos
|
||||||
|
}
|
||||||
|
return SigningMethod(signingMethod)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,43 +122,74 @@ data class ProductMask(val rawValue: Int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ProductMaskBuilder() {
|
||||||
|
|
||||||
|
private var productMaskValue = 0
|
||||||
|
|
||||||
|
fun add(productCode: Int) {
|
||||||
|
productMaskValue = productMaskValue or productCode
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build() = ProductMask(productMaskValue)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores and maps Tangem card settings.
|
* Stores and maps Tangem card settings.
|
||||||
*
|
*
|
||||||
* @property rawValue Card settings in a form of flags,
|
* @property rawValue Card settings in a form of flags,
|
||||||
* while flags definitions and values are in [SettingsMask.Companion] as constants.
|
* while flags definitions and possible values are in [Settings].
|
||||||
*/
|
*/
|
||||||
data class SettingsMask(val rawValue: Int) {
|
data class SettingsMask(val rawValue: Int) {
|
||||||
|
fun contains(settings: Settings): Boolean = (rawValue and settings.code) != 0
|
||||||
|
}
|
||||||
|
|
||||||
fun contains(value: Int): Boolean = (rawValue and value) != 0
|
enum class Settings(val code: Int) {
|
||||||
|
IsReusable(0x0001),
|
||||||
|
UseActivation(0x0002),
|
||||||
|
ForbidPurgeWallet(0x0004),
|
||||||
|
UseBlock(0x0008),
|
||||||
|
|
||||||
companion object {
|
AllowSwapPIN(0x0010),
|
||||||
const val isReusable = 0x0001
|
AllowSwapPIN2(0x0020),
|
||||||
const val useActivation = 0x0002
|
UseCVC(0x0040),
|
||||||
const val forbidPurgeWallet = 0x0004
|
ForbidDefaultPIN(0x0080),
|
||||||
const val useBlock = 0x0008
|
|
||||||
|
|
||||||
const val allowSwapPIN = 0x0010
|
UseOneCommandAtTime(0x0100),
|
||||||
const val allowSwapPIN2 = 0x0020
|
UseNdef(0x0200),
|
||||||
const val useCVC = 0x0040
|
UseDynamicNdef(0x0400),
|
||||||
const val forbidDefaultPIN = 0x0080
|
SmartSecurityDelay(0x0800),
|
||||||
|
|
||||||
const val useOneCommandAtTime = 0x0100
|
ProtocolAllowUnencrypted(0x1000),
|
||||||
const val useNdef = 0x0200
|
ProtocolAllowStaticEncryption(0x2000),
|
||||||
const val useDynamicNdef = 0x0400
|
|
||||||
const val smartSecurityDelay = 0x0800
|
|
||||||
|
|
||||||
const val protocolAllowUnencrypted = 0x1000
|
ProtectIssuerDataAgainstReplay(0x4000),
|
||||||
const val protocolAllowStaticEncryption = 0x2000
|
RestrictOverwriteIssuerDataEx(0x00100000),
|
||||||
|
|
||||||
const val protectIssuerDataAgainstReplay = 0x4000
|
AllowSelectBlockchain(0x8000),
|
||||||
|
|
||||||
const val allowSelectBlockchain = 0x8000
|
DisablePrecomputedNdef(0x00010000),
|
||||||
|
|
||||||
const val disablePrecomputedNdef = 0x00010000
|
SkipSecurityDelayIfValidatedByLinkedTerminal(0x00080000),
|
||||||
|
SkipCheckPin2andCvcIfValidatedByIssuer(0x00040000),
|
||||||
|
SkipSecurityDelayIfValidatedByIssuer(0x00020000),
|
||||||
|
|
||||||
const val skipSecurityDelayIfValidatedByLinkedTerminal = 0x00080000
|
RequireTermTxSignature(0x01000000),
|
||||||
|
RequireTermCertSignature(0x02000000),
|
||||||
|
CheckPIN3onCard(0x04000000)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsMaskBuilder() {
|
||||||
|
|
||||||
|
private var settingsMaskValue = 0
|
||||||
|
|
||||||
|
fun add(settings: Settings) {
|
||||||
|
settingsMaskValue = settingsMaskValue or settings.code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun build() = SettingsMask(settingsMaskValue)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ class ReadIssuerDataResponse(
|
||||||
* Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card).
|
* Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card).
|
||||||
* Issuer’s signature of SHA256-hashed [cardId] concatenated with [issuerData]:
|
* Issuer’s signature of SHA256-hashed [cardId] concatenated with [issuerData]:
|
||||||
* SHA256([cardId] | [issuerData]).
|
* SHA256([cardId] | [issuerData]).
|
||||||
* When flag [SettingsMask.protectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
|
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
|
||||||
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
|
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
|
||||||
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
|
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
|
||||||
*/
|
*/
|
||||||
|
|
@ -36,7 +36,7 @@ class ReadIssuerDataResponse(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An optional counter that protect issuer data against replay attack.
|
* An optional counter that protect issuer data against replay attack.
|
||||||
* When flag [SettingsMask.protectIssuerDataAgainstReplay] set in [SettingsMask]
|
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask]
|
||||||
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
|
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
|
||||||
*/
|
*/
|
||||||
val issuerDataCounter: Int?
|
val issuerDataCounter: Int?
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class ReadIssuerExtraDataResponse(
|
||||||
* Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card).
|
* Issuer’s signature of [issuerData] with Issuer Data Private Key (which is kept on card).
|
||||||
* Issuer’s signature of SHA256-hashed [cardId] concatenated with [issuerData]:
|
* Issuer’s signature of SHA256-hashed [cardId] concatenated with [issuerData]:
|
||||||
* SHA256([cardId] | [issuerData]).
|
* SHA256([cardId] | [issuerData]).
|
||||||
* When flag [SettingsMask.protectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
|
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
|
||||||
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
|
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
|
||||||
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
|
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
|
||||||
*/
|
*/
|
||||||
|
|
@ -41,7 +41,7 @@ class ReadIssuerExtraDataResponse(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An optional counter that protect issuer data against replay attack.
|
* An optional counter that protect issuer data against replay attack.
|
||||||
* When flag [SettingsMask.protectIssuerDataAgainstReplay] set in [SettingsMask]
|
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask]
|
||||||
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
|
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
|
||||||
*/
|
*/
|
||||||
val issuerDataCounter: Int?
|
val issuerDataCounter: Int?
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
package com.tangem.commands.personalization
|
||||||
|
|
||||||
|
import com.tangem.commands.*
|
||||||
|
|
||||||
|
data class NdefRecord(
|
||||||
|
val type: Type,
|
||||||
|
val value: String
|
||||||
|
) {
|
||||||
|
enum class Type {
|
||||||
|
URI, AAR, TEXT
|
||||||
|
}
|
||||||
|
|
||||||
|
val valueInBytes: ByteArray by lazy { value.toByteArray() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It is a configuration file with all the card settings that are written on the card
|
||||||
|
* during [PersonalizeCommand].
|
||||||
|
*/
|
||||||
|
data 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,
|
||||||
|
val hexCrExKey: String?,
|
||||||
|
val cvc: String,
|
||||||
|
val pauseBeforePin2: Int,
|
||||||
|
val smartSecurityDelay: Boolean,
|
||||||
|
val curveID: EllipticCurve,
|
||||||
|
val signingMethod: SigningMethod,
|
||||||
|
val maxSignatures: Int,
|
||||||
|
val isReusable: Boolean,
|
||||||
|
val allowSwapPin: Boolean,
|
||||||
|
val allowSwapPin2: Boolean,
|
||||||
|
val useActivation: Boolean,
|
||||||
|
val useCvc: Boolean,
|
||||||
|
val useNdef: Boolean,
|
||||||
|
val useDynamicNdef: Boolean,
|
||||||
|
val useOneCommandAtTime: Boolean,
|
||||||
|
val useBlock: Boolean,
|
||||||
|
val allowSelectBlockchain: Boolean,
|
||||||
|
val forbidPurgeWallet: Boolean,
|
||||||
|
val protocolAllowUnencrypted: Boolean,
|
||||||
|
val protocolAllowStaticEncryption: Boolean,
|
||||||
|
val protectIssuerDataAgainstReplay: Boolean,
|
||||||
|
val forbidDefaultPin: Boolean,
|
||||||
|
val disablePrecomputedNdef: Boolean,
|
||||||
|
val skipSecurityDelayIfValidatedByIssuer: Boolean,
|
||||||
|
val skipCheckPIN2andCVCIfValidatedByIssuer: Boolean,
|
||||||
|
val skipSecurityDelayIfValidatedByLinkedTerminal: Boolean,
|
||||||
|
|
||||||
|
val restrictOverwriteIssuerDataEx: Boolean,
|
||||||
|
|
||||||
|
val requireTerminalTxSignature: Boolean,
|
||||||
|
val requireTerminalCertSignature: Boolean,
|
||||||
|
val checkPin3onCard: Boolean,
|
||||||
|
|
||||||
|
val createWallet: Boolean,
|
||||||
|
|
||||||
|
val cardData: CardData,
|
||||||
|
val ndefRecords: List<NdefRecord>
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun getSettingsMask(): SettingsMask {
|
||||||
|
val builder = SettingsMaskBuilder()
|
||||||
|
|
||||||
|
if (allowSwapPin) builder.add(Settings.AllowSwapPIN)
|
||||||
|
if (allowSwapPin2) builder.add(Settings.AllowSwapPIN2)
|
||||||
|
if (useCvc) builder.add(Settings.UseCVC)
|
||||||
|
if (isReusable) builder.add(Settings.IsReusable)
|
||||||
|
|
||||||
|
if (useOneCommandAtTime) builder.add(Settings.UseOneCommandAtTime)
|
||||||
|
if (useNdef) builder.add(Settings.UseNdef)
|
||||||
|
if (useDynamicNdef) builder.add(Settings.UseDynamicNdef)
|
||||||
|
if (disablePrecomputedNdef) builder.add(Settings.DisablePrecomputedNdef)
|
||||||
|
|
||||||
|
if (protocolAllowUnencrypted) builder.add(Settings.ProtocolAllowUnencrypted)
|
||||||
|
if (protocolAllowStaticEncryption) builder.add(Settings.ProtocolAllowStaticEncryption)
|
||||||
|
|
||||||
|
if (forbidDefaultPin) builder.add(Settings.ForbidDefaultPIN)
|
||||||
|
|
||||||
|
if (useActivation) builder.add(Settings.UseActivation)
|
||||||
|
|
||||||
|
if (useBlock) builder.add(Settings.UseBlock)
|
||||||
|
if (smartSecurityDelay) builder.add(Settings.SmartSecurityDelay)
|
||||||
|
|
||||||
|
if (protectIssuerDataAgainstReplay) builder.add(Settings.ProtectIssuerDataAgainstReplay)
|
||||||
|
|
||||||
|
if (forbidPurgeWallet) builder.add(Settings.ForbidPurgeWallet)
|
||||||
|
if (allowSelectBlockchain) builder.add(Settings.AllowSelectBlockchain)
|
||||||
|
|
||||||
|
if (skipCheckPIN2andCVCIfValidatedByIssuer) builder.add(Settings.SkipCheckPin2andCvcIfValidatedByIssuer)
|
||||||
|
if (skipSecurityDelayIfValidatedByIssuer) builder.add(Settings.SkipSecurityDelayIfValidatedByIssuer)
|
||||||
|
|
||||||
|
if (skipSecurityDelayIfValidatedByLinkedTerminal) builder.add(Settings.SkipSecurityDelayIfValidatedByLinkedTerminal)
|
||||||
|
if (restrictOverwriteIssuerDataEx) builder.add(Settings.RestrictOverwriteIssuerDataEx)
|
||||||
|
|
||||||
|
if (requireTerminalTxSignature) builder.add(Settings.RequireTermTxSignature)
|
||||||
|
|
||||||
|
if (requireTerminalCertSignature) builder.add(Settings.RequireTermCertSignature)
|
||||||
|
|
||||||
|
if (checkPin3onCard) builder.add(Settings.CheckPIN3onCard)
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.tangem.commands.personalization
|
||||||
|
|
||||||
|
import com.tangem.commands.CommandResponse
|
||||||
|
import com.tangem.commands.CommandSerializer
|
||||||
|
import com.tangem.common.CardEnvironment
|
||||||
|
import com.tangem.common.apdu.CommandApdu
|
||||||
|
import com.tangem.common.apdu.Instruction
|
||||||
|
import com.tangem.common.apdu.ResponseApdu
|
||||||
|
|
||||||
|
data class DepersonalizeResponse(val success: Boolean) : CommandResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command available on SDK cards only
|
||||||
|
*
|
||||||
|
* This command resets card to initial state,
|
||||||
|
* erasing all data written during personalization and usage.
|
||||||
|
* @param cardId CID, Unique Tangem card ID number.
|
||||||
|
*/
|
||||||
|
class DepersonalizeCommand : CommandSerializer<DepersonalizeResponse>() {
|
||||||
|
|
||||||
|
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||||
|
return CommandApdu(
|
||||||
|
Instruction.Depersonalize, byteArrayOf(),
|
||||||
|
cardEnvironment.encryptionMode, cardEnvironment.encryptionKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): DepersonalizeResponse? {
|
||||||
|
return DepersonalizeResponse(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
package com.tangem.commands.personalization
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes information that is to be written on the card as an Ndef Tag.
|
||||||
|
*/
|
||||||
|
class NdefEncoder(private val ndefRecords: List<NdefRecord>, private val useDinamicNdef: Boolean) {
|
||||||
|
|
||||||
|
fun encode(): ByteArray {
|
||||||
|
val bs = ByteArrayOutputStream()
|
||||||
|
// space for size
|
||||||
|
bs.write(0)
|
||||||
|
bs.write(0)
|
||||||
|
|
||||||
|
for (i in ndefRecords.indices) {
|
||||||
|
val headerValue = (if (i == 0) 0x80 else 0x00) or (if (!useDinamicNdef && i == ndefRecords.size - 1) 0x40 else 0x00)
|
||||||
|
var value: ByteArray = ndefRecords[i].value.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
encodeValue(ndefRecords[i], headerValue, bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = bs.toByteArray()
|
||||||
|
result[0] = (result.size - 2 shr 8).toByte()
|
||||||
|
result[1] = (result.size - 2 and 0xFF).toByte()
|
||||||
|
return result
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun encodeValue(ndefRecord: NdefRecord, headerValue: Int, bs: ByteArrayOutputStream) {
|
||||||
|
when (ndefRecord.type) {
|
||||||
|
NdefRecord.Type.AAR -> {
|
||||||
|
bs.write((headerValue or 0x14)) // NDEF Header
|
||||||
|
bs.write(0x0F) // Length of the record type
|
||||||
|
bs.write(ndefRecord.valueInBytes.size) // Length of the payload data
|
||||||
|
bs.write(byteArrayOf(0x61.toByte(), 0x6E.toByte(), 0x64.toByte(), 0x72.toByte(), 0x6F.toByte(), 0x69.toByte(), 0x64.toByte(), 0x2E.toByte(), 0x63.toByte(), 0x6F.toByte(), 0x6D.toByte(), 0x3A.toByte(),
|
||||||
|
0x70.toByte(), 0x6B.toByte(), 0x67.toByte())) // type name
|
||||||
|
bs.write(ndefRecord.valueInBytes)
|
||||||
|
}
|
||||||
|
NdefRecord.Type.URI -> {
|
||||||
|
bs.write((headerValue or 0x11)) // NDEF Header
|
||||||
|
bs.write(0x01) // Length of the record type
|
||||||
|
val uriIdentifierCode: Byte
|
||||||
|
val prefix: String
|
||||||
|
when {
|
||||||
|
ndefRecord.value.startsWith("http://www.") -> {
|
||||||
|
uriIdentifierCode = 0x01.toByte()
|
||||||
|
prefix = "http://www."
|
||||||
|
}
|
||||||
|
ndefRecord.value.startsWith("https://www.") -> {
|
||||||
|
uriIdentifierCode = 0x02.toByte()
|
||||||
|
prefix = "https://www."
|
||||||
|
}
|
||||||
|
ndefRecord.value.startsWith("http://") -> {
|
||||||
|
uriIdentifierCode = 0x03.toByte()
|
||||||
|
prefix = "http://"
|
||||||
|
}
|
||||||
|
ndefRecord.value.startsWith("https://") -> {
|
||||||
|
uriIdentifierCode = 0x04.toByte()
|
||||||
|
prefix = "https://"
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw Exception()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val value = ndefRecord.value.substring(prefix.length).toByteArray()
|
||||||
|
bs.write(value.size + 1) // Length of the payload data
|
||||||
|
bs.write(0x55) // URI
|
||||||
|
bs.write(uriIdentifierCode.toInt()) // ?
|
||||||
|
bs.write(value)
|
||||||
|
}
|
||||||
|
NdefRecord.Type.TEXT -> {
|
||||||
|
bs.write((headerValue or 0x11)) // NDEF Header
|
||||||
|
bs.write(0x01) // Length of the record type
|
||||||
|
bs.write(ndefRecord.valueInBytes.size.toByte() + 1 + "en".length) // Length of the payload data
|
||||||
|
bs.write(0x54) // Text
|
||||||
|
bs.write(0x02) // UTF8(MSB=0)|"en".length
|
||||||
|
bs.write("en".toByteArray(StandardCharsets.US_ASCII))
|
||||||
|
bs.write(ndefRecord.valueInBytes)
|
||||||
|
}
|
||||||
|
else -> throw Exception("Invalid NDEF record in config!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
package com.tangem.commands.personalization
|
||||||
|
|
||||||
|
import com.tangem.commands.Card
|
||||||
|
import com.tangem.commands.CardData
|
||||||
|
import com.tangem.commands.CommandSerializer
|
||||||
|
import com.tangem.commands.personalization.entities.Issuer
|
||||||
|
import com.tangem.common.CardEnvironment
|
||||||
|
import com.tangem.common.apdu.CommandApdu
|
||||||
|
import com.tangem.common.apdu.Instruction
|
||||||
|
import com.tangem.common.apdu.ResponseApdu
|
||||||
|
import com.tangem.common.extensions.calculateSha256
|
||||||
|
import com.tangem.common.extensions.hexToBytes
|
||||||
|
import com.tangem.common.tlv.Tlv
|
||||||
|
import com.tangem.common.tlv.TlvBuilder
|
||||||
|
import com.tangem.common.tlv.TlvMapper
|
||||||
|
import com.tangem.common.tlv.TlvTag
|
||||||
|
import com.tangem.crypto.sign
|
||||||
|
import com.tangem.tasks.TaskError
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command available on SDK cards only
|
||||||
|
*
|
||||||
|
* Personalization is an initialization procedure, required before starting using a card.
|
||||||
|
* During this procedure a card setting is set up.
|
||||||
|
* During this procedure all data exchange is encrypted.
|
||||||
|
* @param config is a configuration file with all the card settings that are written on the card
|
||||||
|
* during personalization.
|
||||||
|
* @param cardId this parameter will set up CID, Unique Tangem card ID.
|
||||||
|
*/
|
||||||
|
class PersonalizeCommand(private val config: CardConfig, private val cardId: String) : CommandSerializer<Card>() {
|
||||||
|
|
||||||
|
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||||
|
if (cardEnvironment.issuer == null || cardEnvironment.manufacturerKeyPair == null) {
|
||||||
|
throw TaskError.SerializeCommandError()
|
||||||
|
}
|
||||||
|
return CommandApdu(
|
||||||
|
Instruction.Personalize,
|
||||||
|
serializePersonalizationData(
|
||||||
|
cardId, config,
|
||||||
|
cardEnvironment.issuer, cardEnvironment.manufacturerKeyPair.privateKey,
|
||||||
|
cardEnvironment.acquirerKeyPair?.publicKey),
|
||||||
|
encryptionKey = devPersonalizationKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): Card? {
|
||||||
|
val tlvData = responseApdu.getTlvData(devPersonalizationKey) ?: return null
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val tlvMapper = TlvMapper(tlvData)
|
||||||
|
Card(
|
||||||
|
cardId = tlvMapper.mapOptional(TlvTag.CardId) ?: "",
|
||||||
|
manufacturerName = tlvMapper.mapOptional(TlvTag.ManufactureId) ?: "",
|
||||||
|
status = tlvMapper.mapOptional(TlvTag.Status),
|
||||||
|
|
||||||
|
firmwareVersion = tlvMapper.mapOptional(TlvTag.Firmware),
|
||||||
|
cardPublicKey = tlvMapper.mapOptional(TlvTag.CardPublicKey),
|
||||||
|
settingsMask = tlvMapper.mapOptional(TlvTag.SettingsMask),
|
||||||
|
issuerPublicKey = tlvMapper.mapOptional(TlvTag.IssuerDataPublicKey),
|
||||||
|
curve = tlvMapper.mapOptional(TlvTag.CurveId),
|
||||||
|
maxSignatures = tlvMapper.mapOptional(TlvTag.MaxSignatures),
|
||||||
|
signingMethod = tlvMapper.mapOptional(TlvTag.SigningMethod),
|
||||||
|
pauseBeforePin2 = tlvMapper.mapOptional(TlvTag.PauseBeforePin2),
|
||||||
|
walletPublicKey = tlvMapper.mapOptional(TlvTag.WalletPublicKey),
|
||||||
|
walletRemainingSignatures = tlvMapper.mapOptional(TlvTag.RemainingSignatures),
|
||||||
|
walletSignedHashes = tlvMapper.mapOptional(TlvTag.SignedHashes),
|
||||||
|
health = tlvMapper.mapOptional(TlvTag.Health),
|
||||||
|
isActivated = tlvMapper.map(TlvTag.IsActivated),
|
||||||
|
activationSeed = tlvMapper.mapOptional(TlvTag.ActivationSeed),
|
||||||
|
paymentFlowVersion = tlvMapper.mapOptional(TlvTag.PaymentFlowVersion),
|
||||||
|
userCounter = tlvMapper.mapOptional(TlvTag.UserCounter),
|
||||||
|
terminalIsLinked = tlvMapper.map(TlvTag.TerminalIsLinked),
|
||||||
|
|
||||||
|
cardData = deserializeCardData(tlvData)
|
||||||
|
)
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
throw TaskError.SerializeCommandError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deserializeCardData(tlvData: List<Tlv>): CardData? {
|
||||||
|
val cardDataTlvs = tlvData.find { it.tag == TlvTag.CardData }?.let {
|
||||||
|
Tlv.deserialize(it.value)
|
||||||
|
}
|
||||||
|
if (cardDataTlvs.isNullOrEmpty()) return null
|
||||||
|
|
||||||
|
val tlvMapper = TlvMapper(cardDataTlvs)
|
||||||
|
return CardData(
|
||||||
|
batchId = tlvMapper.mapOptional(TlvTag.Batch),
|
||||||
|
manufactureDateTime = tlvMapper.mapOptional(TlvTag.ManufactureDateTime),
|
||||||
|
issuerName = tlvMapper.mapOptional(TlvTag.IssuerId),
|
||||||
|
blockchainName = tlvMapper.mapOptional(TlvTag.BlockchainId),
|
||||||
|
manufacturerSignature = tlvMapper.mapOptional(TlvTag.ManufacturerSignature),
|
||||||
|
productMask = tlvMapper.mapOptional(TlvTag.ProductMask),
|
||||||
|
|
||||||
|
tokenSymbol = tlvMapper.mapOptional(TlvTag.TokenSymbol),
|
||||||
|
tokenContractAddress = tlvMapper.mapOptional(TlvTag.TokenContractAddress),
|
||||||
|
tokenDecimal = tlvMapper.mapOptional(TlvTag.TokenDecimal)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun serializePersonalizationData(cardId: String, config: CardConfig,
|
||||||
|
issuer: Issuer, manufacturerPrivateKey: ByteArray,
|
||||||
|
acquirePublicKey: ByteArray?
|
||||||
|
): ByteArray {
|
||||||
|
val tlvBuilder = TlvBuilder()
|
||||||
|
tlvBuilder.append(TlvTag.CardId, cardId)
|
||||||
|
|
||||||
|
tlvBuilder.append(TlvTag.CurveId, config.curveID)
|
||||||
|
tlvBuilder.append(TlvTag.MaxSignatures, config.maxSignatures)
|
||||||
|
tlvBuilder.append(TlvTag.SigningMethod, config.signingMethod)
|
||||||
|
tlvBuilder.append(TlvTag.SettingsMask, config.getSettingsMask())
|
||||||
|
tlvBuilder.append(TlvTag.PauseBeforePin2, config.pauseBeforePin2 / 10)
|
||||||
|
tlvBuilder.append(TlvTag.Cvc, config.cvc.toByteArray())
|
||||||
|
if (!config.ndefRecords.isNullOrEmpty()) tlvBuilder.append(TlvTag.NdefData, serializeNdef(config.ndefRecords))
|
||||||
|
|
||||||
|
tlvBuilder.append(TlvTag.CreateWalletAtPersonalize, config.createWallet)
|
||||||
|
|
||||||
|
tlvBuilder.append(TlvTag.NewPin, config.pin)
|
||||||
|
tlvBuilder.append(TlvTag.NewPin2, config.pin2)
|
||||||
|
tlvBuilder.append(TlvTag.NewPin3, config.pin3)
|
||||||
|
tlvBuilder.append(TlvTag.CrExKey, config.hexCrExKey)
|
||||||
|
tlvBuilder.append(TlvTag.IssuerDataPublicKey, issuer.dataKeyPair.publicKey)
|
||||||
|
tlvBuilder.append(TlvTag.IssuerTransactionPublicKey, issuer.transactionKeyPair.publicKey)
|
||||||
|
|
||||||
|
tlvBuilder.append(TlvTag.AcquirerPublicKey, acquirePublicKey)
|
||||||
|
|
||||||
|
tlvBuilder.append(
|
||||||
|
TlvTag.CardData, serializeCardData(cardId, config.cardData, issuer, manufacturerPrivateKey)
|
||||||
|
)
|
||||||
|
return tlvBuilder.serialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun serializeCardData(
|
||||||
|
cardId: String, cardData: CardData,
|
||||||
|
issuer: Issuer, manufacturerPrivateKey: ByteArray): ByteArray {
|
||||||
|
val tlvBuilder = TlvBuilder()
|
||||||
|
tlvBuilder.append(TlvTag.Batch, cardData.batchId)
|
||||||
|
tlvBuilder.append(TlvTag.ProductMask, cardData.productMask)
|
||||||
|
|
||||||
|
tlvBuilder.append(TlvTag.ManufactureDateTime, cardData.manufactureDateTime)
|
||||||
|
|
||||||
|
tlvBuilder.append(TlvTag.IssuerId, issuer.id)
|
||||||
|
|
||||||
|
tlvBuilder.append(TlvTag.BlockchainId, cardData.blockchainName)
|
||||||
|
|
||||||
|
if (cardData.tokenSymbol != null) {
|
||||||
|
tlvBuilder.append(TlvTag.TokenSymbol, cardData.tokenSymbol)
|
||||||
|
tlvBuilder.append(TlvTag.TokenContractAddress, cardData.tokenContractAddress)
|
||||||
|
tlvBuilder.append(TlvTag.TokenDecimal, cardData.tokenDecimal)
|
||||||
|
}
|
||||||
|
tlvBuilder.append(
|
||||||
|
TlvTag.CardIdManufacturerSignature, cardId.hexToBytes().sign(manufacturerPrivateKey)
|
||||||
|
)
|
||||||
|
return tlvBuilder.serialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun serializeNdef(ndefRecords: List<NdefRecord>): ByteArray {
|
||||||
|
return NdefEncoder(ndefRecords, config.useDynamicNdef).encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val devPersonalizationKey = "1234".calculateSha256().copyOf(32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.tangem.commands.personalization.entities
|
||||||
|
|
||||||
|
import com.tangem.common.KeyPair
|
||||||
|
|
||||||
|
data class Issuer(
|
||||||
|
val name: String,
|
||||||
|
val id: String,
|
||||||
|
val dataKeyPair: KeyPair,
|
||||||
|
val transactionKeyPair: KeyPair
|
||||||
|
)
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package com.tangem.common
|
package com.tangem.common
|
||||||
|
|
||||||
|
import com.tangem.commands.personalization.entities.Issuer
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains data relating to a Tangem card. It is used in constructing all the commands,
|
* Contains data relating to a Tangem card. It is used in constructing all the commands,
|
||||||
|
|
@ -12,7 +14,10 @@ data class CardEnvironment(
|
||||||
val terminalKeys: KeyPair? = null,
|
val terminalKeys: KeyPair? = null,
|
||||||
var encryptionMode: EncryptionMode = EncryptionMode.NONE,
|
var encryptionMode: EncryptionMode = EncryptionMode.NONE,
|
||||||
var encryptionKey: ByteArray? = null,
|
var encryptionKey: ByteArray? = null,
|
||||||
val cvc: ByteArray? = null
|
val cvc: ByteArray? = null,
|
||||||
|
val manufacturerKeyPair: KeyPair? = null,
|
||||||
|
val acquirerKeyPair: KeyPair? = null,
|
||||||
|
val issuer: Issuer? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ package com.tangem.common.apdu
|
||||||
*/
|
*/
|
||||||
enum class Instruction(var code: Int) {
|
enum class Instruction(var code: Int) {
|
||||||
Unknown(0x00),
|
Unknown(0x00),
|
||||||
|
Personalize(0xF1),
|
||||||
Read(0xF2),
|
Read(0xF2),
|
||||||
VerifyCard(0xF3),
|
VerifyCard(0xF3),
|
||||||
ValidateCard(0xF4),
|
ValidateCard(0xF4),
|
||||||
|
|
@ -20,7 +21,8 @@ enum class Instruction(var code: Int) {
|
||||||
Activate(0xFE),
|
Activate(0xFE),
|
||||||
OpenSession(0xFF),
|
OpenSession(0xFF),
|
||||||
WriteUserData(0xE0),
|
WriteUserData(0xE0),
|
||||||
ReadUserData(0xE1);
|
ReadUserData(0xE1),
|
||||||
|
Depersonalize(0xE3);
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import com.tangem.commands.common.IssuerDataMode
|
||||||
import com.tangem.common.extensions.calculateSha256
|
import com.tangem.common.extensions.calculateSha256
|
||||||
import com.tangem.common.extensions.hexToBytes
|
import com.tangem.common.extensions.hexToBytes
|
||||||
import com.tangem.common.extensions.toByteArray
|
import com.tangem.common.extensions.toByteArray
|
||||||
|
import com.tangem.common.extensions.toHexString
|
||||||
import com.tangem.tasks.TaskError
|
import com.tangem.tasks.TaskError
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
@ -32,11 +33,11 @@ class TlvEncoder {
|
||||||
return when (tag.valueType()) {
|
return when (tag.valueType()) {
|
||||||
TlvValueType.HexString -> {
|
TlvValueType.HexString -> {
|
||||||
typeCheck<T, String>(tag)
|
typeCheck<T, String>(tag)
|
||||||
return if (tag == TlvTag.Pin || tag == TlvTag.Pin2) {
|
(value as String).hexToBytes()
|
||||||
(value as String).calculateSha256()
|
}
|
||||||
} else {
|
TlvValueType.HexStringToHash -> {
|
||||||
(value as String).hexToBytes()
|
typeCheck<T, String>(tag)
|
||||||
}
|
(value as String).calculateSha256()
|
||||||
}
|
}
|
||||||
TlvValueType.Utf8String -> {
|
TlvValueType.Utf8String -> {
|
||||||
typeCheck<T, String>(tag)
|
typeCheck<T, String>(tag)
|
||||||
|
|
@ -52,8 +53,8 @@ class TlvEncoder {
|
||||||
}
|
}
|
||||||
TlvValueType.BoolValue -> {
|
TlvValueType.BoolValue -> {
|
||||||
typeCheck<T, Boolean>(tag)
|
typeCheck<T, Boolean>(tag)
|
||||||
Log.e(this::class.simpleName!!, "Unsupported operation: Boolean to ByteArray for tag $tag")
|
val booleanValue = value as Boolean
|
||||||
throw TaskError.ConvertError()
|
if (booleanValue) byteArrayOf(1) else byteArrayOf(0)
|
||||||
}
|
}
|
||||||
TlvValueType.ByteArray -> {
|
TlvValueType.ByteArray -> {
|
||||||
typeCheck<T, ByteArray>(tag)
|
typeCheck<T, ByteArray>(tag)
|
||||||
|
|
@ -61,7 +62,7 @@ class TlvEncoder {
|
||||||
}
|
}
|
||||||
TlvValueType.EllipticCurve -> {
|
TlvValueType.EllipticCurve -> {
|
||||||
typeCheck<T, EllipticCurve>(tag)
|
typeCheck<T, EllipticCurve>(tag)
|
||||||
(value as EllipticCurve).curve.plus("\\0").toByteArray()
|
(value as EllipticCurve).curve.toByteArray()
|
||||||
}
|
}
|
||||||
TlvValueType.DateTime -> {
|
TlvValueType.DateTime -> {
|
||||||
typeCheck<T, Date>(tag)
|
typeCheck<T, Date>(tag)
|
||||||
|
|
@ -69,7 +70,7 @@ class TlvEncoder {
|
||||||
val year = calendar.get(Calendar.YEAR)
|
val year = calendar.get(Calendar.YEAR)
|
||||||
val month = calendar.get(Calendar.MONTH) + 1
|
val month = calendar.get(Calendar.MONTH) + 1
|
||||||
val day = calendar.get(Calendar.DAY_OF_MONTH)
|
val day = calendar.get(Calendar.DAY_OF_MONTH)
|
||||||
return year.toByteArray() + month.toByteArray() + day.toByteArray()
|
return year.toByteArray(2) + month.toByte() + day.toByte()
|
||||||
}
|
}
|
||||||
TlvValueType.ProductMask -> {
|
TlvValueType.ProductMask -> {
|
||||||
typeCheck<T, ProductMask>(tag)
|
typeCheck<T, ProductMask>(tag)
|
||||||
|
|
@ -79,7 +80,8 @@ class TlvEncoder {
|
||||||
}
|
}
|
||||||
TlvValueType.SettingsMask -> {
|
TlvValueType.SettingsMask -> {
|
||||||
typeCheck<T, SettingsMask>(tag)
|
typeCheck<T, SettingsMask>(tag)
|
||||||
(value as SettingsMask).rawValue.toByteArray(2)
|
val rawValue = (value as SettingsMask).rawValue
|
||||||
|
rawValue.toByteArray(determineByteArraySize(rawValue))
|
||||||
}
|
}
|
||||||
TlvValueType.CardStatus -> {
|
TlvValueType.CardStatus -> {
|
||||||
typeCheck<T, CardStatus>(tag)
|
typeCheck<T, CardStatus>(tag)
|
||||||
|
|
@ -87,7 +89,7 @@ class TlvEncoder {
|
||||||
}
|
}
|
||||||
TlvValueType.SigningMethod -> {
|
TlvValueType.SigningMethod -> {
|
||||||
typeCheck<T, SigningMethod>(tag)
|
typeCheck<T, SigningMethod>(tag)
|
||||||
(value as SigningMethod).rawValue.toByteArray()
|
byteArrayOf((value as SigningMethod).rawValue.toByte())
|
||||||
}
|
}
|
||||||
TlvValueType.IssuerDataMode -> {
|
TlvValueType.IssuerDataMode -> {
|
||||||
typeCheck<T, IssuerDataMode>(tag)
|
typeCheck<T, IssuerDataMode>(tag)
|
||||||
|
|
@ -96,6 +98,11 @@ class TlvEncoder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun determineByteArraySize(value: Int): Int {
|
||||||
|
val mask = 0xFFFF0000.toInt()
|
||||||
|
return if ((value and mask) != 0) 4 else 2
|
||||||
|
}
|
||||||
|
|
||||||
private inline fun <reified T, reified ExpectedT> typeCheck(tag: TlvTag) {
|
private inline fun <reified T, reified ExpectedT> typeCheck(tag: TlvTag) {
|
||||||
if (T::class != ExpectedT::class) {
|
if (T::class != ExpectedT::class) {
|
||||||
Log.e(this::class.simpleName!!,
|
Log.e(this::class.simpleName!!,
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ class TlvMapper(val tlvList: List<Tlv>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return when (tag.valueType()) {
|
return when (tag.valueType()) {
|
||||||
TlvValueType.HexString -> {
|
TlvValueType.HexString, TlvValueType.HexStringToHash -> {
|
||||||
typeCheck<T, String>(tag)
|
typeCheck<T, String>(tag)
|
||||||
tlvValue.toHexString() as T
|
tlvValue.toHexString() as T
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ package com.tangem.common.tlv
|
||||||
*/
|
*/
|
||||||
enum class TlvValueType {
|
enum class TlvValueType {
|
||||||
HexString,
|
HexString,
|
||||||
|
HexStringToHash,
|
||||||
Utf8String,
|
Utf8String,
|
||||||
Uint16,
|
Uint16,
|
||||||
Uint32,
|
Uint32,
|
||||||
|
|
@ -36,6 +37,7 @@ enum class TlvTag(val code: Int) {
|
||||||
SettingsMask(0x0A),
|
SettingsMask(0x0A),
|
||||||
CardData(0x0C),
|
CardData(0x0C),
|
||||||
NdefData(0x0D),
|
NdefData(0x0D),
|
||||||
|
CreateWalletAtPersonalize(0x0E),
|
||||||
Health(0x0F),
|
Health(0x0F),
|
||||||
|
|
||||||
Pin(0x10),
|
Pin(0x10),
|
||||||
|
|
@ -51,8 +53,11 @@ enum class TlvTag(val code: Int) {
|
||||||
|
|
||||||
SessionKeyA(0x1A),
|
SessionKeyA(0x1A),
|
||||||
SessionKeyB(0x1B),
|
SessionKeyB(0x1B),
|
||||||
Uid(0x0B),
|
|
||||||
Pause(0x1C),
|
Pause(0x1C),
|
||||||
|
NewPin3(0x1E),
|
||||||
|
CrExKey(0x1F),
|
||||||
|
|
||||||
|
Uid(0x0B),
|
||||||
|
|
||||||
ManufactureId(0x20),
|
ManufactureId(0x20),
|
||||||
ManufacturerSignature(0x86),
|
ManufacturerSignature(0x86),
|
||||||
|
|
@ -63,6 +68,8 @@ enum class TlvTag(val code: Int) {
|
||||||
IssuerDataSignature(0x33),
|
IssuerDataSignature(0x33),
|
||||||
IssuerTransactionSignature(0x34),
|
IssuerTransactionSignature(0x34),
|
||||||
IssuerDataCounter(0x35),
|
IssuerDataCounter(0x35),
|
||||||
|
AcquirerPublicKey(0x37),
|
||||||
|
|
||||||
Size(0x25),
|
Size(0x25),
|
||||||
Mode(0x23),
|
Mode(0x23),
|
||||||
Offset(0x24),
|
Offset(0x24),
|
||||||
|
|
@ -117,15 +124,15 @@ enum class TlvTag(val code: Int) {
|
||||||
*/
|
*/
|
||||||
fun valueType(): TlvValueType {
|
fun valueType(): TlvValueType {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
CardId, Pin, Pin2, Batch -> TlvValueType.HexString
|
CardId, Batch, CrExKey -> TlvValueType.HexString
|
||||||
|
Pin, Pin2, NewPin, NewPin2, NewPin3 -> TlvValueType.HexStringToHash
|
||||||
ManufactureId, Firmware, IssuerId, BlockchainId, TokenSymbol, TokenContractAddress ->
|
ManufactureId, Firmware, IssuerId, BlockchainId, TokenSymbol, TokenContractAddress ->
|
||||||
TlvValueType.Utf8String
|
TlvValueType.Utf8String
|
||||||
CurveId -> TlvValueType.EllipticCurve
|
CurveId -> TlvValueType.EllipticCurve
|
||||||
MaxSignatures, PauseBeforePin2, RemainingSignatures,
|
PauseBeforePin2, RemainingSignatures, SignedHashes, Health, TokenDecimal,
|
||||||
SignedHashes, Health, TokenDecimal,
|
|
||||||
Offset, Size -> TlvValueType.Uint16
|
Offset, Size -> TlvValueType.Uint16
|
||||||
UserCounter, UserProtectedCounter, IssuerDataCounter -> TlvValueType.Uint32
|
MaxSignatures, UserCounter, UserProtectedCounter, IssuerDataCounter -> TlvValueType.Uint32
|
||||||
IsActivated, TerminalIsLinked -> TlvValueType.BoolValue
|
IsActivated, TerminalIsLinked, CreateWalletAtPersonalize -> TlvValueType.BoolValue
|
||||||
ManufactureDateTime -> TlvValueType.DateTime
|
ManufactureDateTime -> TlvValueType.DateTime
|
||||||
ProductMask -> TlvValueType.ProductMask
|
ProductMask -> TlvValueType.ProductMask
|
||||||
SettingsMask -> TlvValueType.SettingsMask
|
SettingsMask -> TlvValueType.SettingsMask
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.tangem.crypto
|
||||||
|
|
||||||
import com.tangem.commands.EllipticCurve
|
import com.tangem.commands.EllipticCurve
|
||||||
import net.i2p.crypto.eddsa.EdDSASecurityProvider
|
import net.i2p.crypto.eddsa.EdDSASecurityProvider
|
||||||
|
import org.spongycastle.jce.provider.BouncyCastleProvider
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
|
|
@ -13,7 +14,7 @@ import javax.crypto.spec.SecretKeySpec
|
||||||
object CryptoUtils {
|
object CryptoUtils {
|
||||||
|
|
||||||
fun initCrypto() {
|
fun initCrypto() {
|
||||||
Security.insertProviderAt(org.spongycastle.jce.provider.BouncyCastleProvider(), 1)
|
Security.insertProviderAt(BouncyCastleProvider(), 1)
|
||||||
Security.addProvider(EdDSASecurityProvider())
|
Security.addProvider(EdDSASecurityProvider())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,12 +135,18 @@ sealed class TaskError(val code: Int) : Exception() {
|
||||||
|
|
||||||
class UnknownError : TaskError(6000)
|
class UnknownError : TaskError(6000)
|
||||||
|
|
||||||
//Issuer Data Errors
|
//Specific Command Errors
|
||||||
/**
|
/**
|
||||||
* This error is returned when [ReadIssuerDataTask] or [ReadIssuerExtraDataTask] expects a counter
|
* This error is returned when [ReadIssuerDataTask] or [ReadIssuerExtraDataTask] expects a counter
|
||||||
* (when the card's requires it), but the counter is missing.
|
* (when the card's requires it), but the counter is missing.
|
||||||
*/
|
*/
|
||||||
class MissingCounter : TaskError(7001)
|
class MissingCounter : TaskError(7001)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This error is returned when [com.tangem.commands.personalization.PersonalizeCommand] is attempted
|
||||||
|
* without [com.tangem.commands.personalization.entities.Issuer] being set in the [com.tangem.Config].
|
||||||
|
*/
|
||||||
|
class IssuerIsRequired : TaskError(7002)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.tangem.tasks
|
package com.tangem.tasks
|
||||||
|
|
||||||
import com.tangem.commands.Card
|
import com.tangem.commands.Card
|
||||||
import com.tangem.commands.SettingsMask
|
import com.tangem.commands.Settings
|
||||||
import com.tangem.commands.WriteIssuerDataCommand
|
import com.tangem.commands.WriteIssuerDataCommand
|
||||||
import com.tangem.commands.WriteIssuerDataResponse
|
import com.tangem.commands.WriteIssuerDataResponse
|
||||||
import com.tangem.commands.common.IssuerDataToVerify
|
import com.tangem.commands.common.IssuerDataToVerify
|
||||||
|
|
@ -55,7 +55,7 @@ class WriteIssuerDataTask(
|
||||||
if (isCounterRequired()) issuerDataCounter != null else true
|
if (isCounterRequired()) issuerDataCounter != null else true
|
||||||
|
|
||||||
private fun isCounterRequired(): Boolean =
|
private fun isCounterRequired(): Boolean =
|
||||||
card.settingsMask?.contains(SettingsMask.protectIssuerDataAgainstReplay) != false
|
card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false
|
||||||
|
|
||||||
private fun verifySignature(command: WriteIssuerDataCommand, cardId: String): Boolean {
|
private fun verifySignature(command: WriteIssuerDataCommand, cardId: String): Boolean {
|
||||||
return command.verify(
|
return command.verify(
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
package com.tangem.tasks
|
package com.tangem.tasks
|
||||||
|
|
||||||
import com.tangem.commands.Card
|
import com.tangem.commands.*
|
||||||
import com.tangem.commands.SettingsMask
|
|
||||||
import com.tangem.commands.WriteIssuerDataResponse
|
|
||||||
import com.tangem.commands.WriteIssuerExtraDataCommand
|
|
||||||
import com.tangem.commands.common.IssuerDataMode
|
import com.tangem.commands.common.IssuerDataMode
|
||||||
import com.tangem.commands.common.IssuerDataToVerify
|
import com.tangem.commands.common.IssuerDataToVerify
|
||||||
import com.tangem.common.CardEnvironment
|
import com.tangem.common.CardEnvironment
|
||||||
|
|
@ -98,7 +95,7 @@ internal class WriteIssuerExtraDataTask(
|
||||||
if (isCounterRequired()) issuerDataCounter != null else true
|
if (isCounterRequired()) issuerDataCounter != null else true
|
||||||
|
|
||||||
private fun isCounterRequired(): Boolean =
|
private fun isCounterRequired(): Boolean =
|
||||||
card.settingsMask?.contains(SettingsMask.protectIssuerDataAgainstReplay) != false
|
card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false
|
||||||
|
|
||||||
private fun verifySignatures(command: WriteIssuerExtraDataCommand): Boolean {
|
private fun verifySignatures(command: WriteIssuerExtraDataCommand): Boolean {
|
||||||
val publicKey = issuerPublicKey ?: card.issuerPublicKey!!
|
val publicKey = issuerPublicKey ?: card.issuerPublicKey!!
|
||||||
|
|
|
||||||
|
|
@ -68,15 +68,15 @@ class TlvMapperTest {
|
||||||
.isNotNull()
|
.isNotNull()
|
||||||
assertThat(settingsMask.rawValue)
|
assertThat(settingsMask.rawValue)
|
||||||
.isEqualTo(32289)
|
.isEqualTo(32289)
|
||||||
assertThat(settingsMask.contains(SettingsMask.skipSecurityDelayIfValidatedByLinkedTerminal))
|
assertThat(settingsMask.contains(Settings.SkipSecurityDelayIfValidatedByLinkedTerminal))
|
||||||
.isFalse()
|
.isFalse()
|
||||||
assertThat(settingsMask.contains(SettingsMask.isReusable))
|
assertThat(settingsMask.contains(Settings.IsReusable))
|
||||||
.isTrue()
|
.isTrue()
|
||||||
assertThat(settingsMask.contains(SettingsMask.allowSwapPIN2))
|
assertThat(settingsMask.contains(Settings.AllowSwapPIN2))
|
||||||
.isTrue()
|
.isTrue()
|
||||||
assertThat(settingsMask.contains(SettingsMask.useDynamicNdef))
|
assertThat(settingsMask.contains(Settings.UseDynamicNdef))
|
||||||
.isTrue()
|
.isTrue()
|
||||||
assertThat(settingsMask.contains(SettingsMask.forbidPurgeWallet))
|
assertThat(settingsMask.contains(Settings.ForbidPurgeWallet))
|
||||||
.isFalse()
|
.isFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
|
||||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||||
implementation 'androidx.core:core-ktx:1.1.0'
|
implementation 'androidx.core:core-ktx:1.2.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.tangem.CardManager
|
import com.tangem.CardManager
|
||||||
|
import com.tangem.commands.personalization.CardConfig
|
||||||
import com.tangem.tangem_sdk_new.extensions.init
|
import com.tangem.tangem_sdk_new.extensions.init
|
||||||
|
import com.tangem.tangemtest.extensions.init
|
||||||
import com.tangem.tasks.ScanEvent
|
import com.tangem.tasks.ScanEvent
|
||||||
import com.tangem.tasks.TaskError
|
import com.tangem.tasks.TaskError
|
||||||
import com.tangem.tasks.TaskEvent
|
import com.tangem.tasks.TaskEvent
|
||||||
|
|
@ -151,6 +153,30 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
btn_read_write_user_data?.setOnClickListener { startActivity(Intent(this, TestUserDataActivity::class.java)) }
|
btn_read_write_user_data?.setOnClickListener { startActivity(Intent(this, TestUserDataActivity::class.java)) }
|
||||||
|
btn_personalize?.setOnClickListener { _ ->
|
||||||
|
cardManager.personalize(
|
||||||
|
CardConfig.init(application), "BB00000000000395"
|
||||||
|
) {
|
||||||
|
when (it) {
|
||||||
|
is TaskEvent.Completion -> {
|
||||||
|
if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName }
|
||||||
|
}
|
||||||
|
is TaskEvent.Event -> runOnUiThread { tv_card_cid?.text = it.data.cardId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
btn_depersonalize?.setOnClickListener { _ ->
|
||||||
|
cardManager.depersonalize(cardId) {
|
||||||
|
when (it) {
|
||||||
|
is TaskEvent.Completion -> {
|
||||||
|
if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName }
|
||||||
|
}
|
||||||
|
is TaskEvent.Event -> runOnUiThread {
|
||||||
|
tv_card_cid?.text = "Depersonalized: ${it.data.success.toString()}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSampleHashes(): Array<ByteArray> {
|
private fun createSampleHashes(): Array<ByteArray> {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
package com.tangem.tangemtest.extensions
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import com.tangem.commands.*
|
||||||
|
import com.tangem.commands.personalization.CardConfig
|
||||||
|
import com.tangem.commands.personalization.NdefRecord
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
fun CardConfig.Companion.init(application: Application): CardConfig {
|
||||||
|
|
||||||
|
val preferences = application.getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
val signingMethod = SigningMethod.build(
|
||||||
|
signHash = preferences.getBoolean("personalization_SigningMethod_0", false),
|
||||||
|
signRaw = preferences.getBoolean("personalization_SigningMethod_1", false),
|
||||||
|
signHashValidatedByIssuer = preferences.getBoolean("personalization_SigningMethod_2", false),
|
||||||
|
signRawValidatedByIssuer = preferences.getBoolean("personalization_SigningMethod_3", false),
|
||||||
|
signHashValidatedByIssuerAndWriteIssuerData = preferences.getBoolean("personalization_SigningMethod_4", false),
|
||||||
|
signRawValidatedByIssuerAndWriteIssuerData = preferences.getBoolean("personalization_SigningMethod_5", false),
|
||||||
|
signPos = preferences.getBoolean("personalization_SigningMethod_6", false)
|
||||||
|
)
|
||||||
|
|
||||||
|
val isNote = preferences.getBoolean("personalization_ProductMask_IsNote", true)
|
||||||
|
val isTag = preferences.getBoolean("personalization_ProductMask_IsTag", false)
|
||||||
|
val isIdCard = preferences.getBoolean("personalization_ProductMask_IsIDCard", false)
|
||||||
|
|
||||||
|
val productMaskBuilder = ProductMaskBuilder()
|
||||||
|
if (isNote) productMaskBuilder.add(ProductMask.note)
|
||||||
|
if (isTag) productMaskBuilder.add(ProductMask.tag)
|
||||||
|
if (isIdCard) productMaskBuilder.add(ProductMask.idCard)
|
||||||
|
val productMask = productMaskBuilder.build()
|
||||||
|
|
||||||
|
var tokenSymbol: String? = null
|
||||||
|
var tokenContractAddress: String? = null
|
||||||
|
var tokenDecimal: Int? = null
|
||||||
|
if (preferences.getBoolean("personalization_isToken", false)) {
|
||||||
|
tokenSymbol = preferences.getString("personalization_token_symbol", "")
|
||||||
|
tokenContractAddress = preferences.getString("personalization_token_contract_address", "")
|
||||||
|
tokenDecimal = preferences.getString("personalization_token_decimal", "")!!.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
val cardData = CardData(
|
||||||
|
blockchainName = preferences.getString("personalization_Blockchain", "BTC"),
|
||||||
|
batchId = preferences.getString("personalization_card_batch", "FFFF"),
|
||||||
|
productMask = productMask,
|
||||||
|
tokenSymbol = tokenSymbol,
|
||||||
|
tokenContractAddress = tokenContractAddress,
|
||||||
|
tokenDecimal = tokenDecimal,
|
||||||
|
issuerName = null,
|
||||||
|
manufactureDateTime = Calendar.getInstance().time,
|
||||||
|
manufacturerSignature = null)
|
||||||
|
|
||||||
|
|
||||||
|
val ndefAar = preferences.getString("personalization_NDEF_AAR", "Release APP")
|
||||||
|
val ndefUri = preferences.getString("personalization_NDEF_URI", "https://tangem.com")
|
||||||
|
|
||||||
|
val ndefs = mutableListOf<NdefRecord>()
|
||||||
|
if (!ndefUri.isNullOrEmpty()) {
|
||||||
|
ndefs.add(NdefRecord(NdefRecord.Type.URI, ndefUri))
|
||||||
|
}
|
||||||
|
if (ndefAar != "None") {
|
||||||
|
val type = NdefRecord.Type.AAR
|
||||||
|
val value = when (ndefAar) {
|
||||||
|
"Debug APP" -> {
|
||||||
|
"com.tangem.wallet.debug"
|
||||||
|
}
|
||||||
|
"Release APP" -> {
|
||||||
|
"com.tangem.wallet"
|
||||||
|
}
|
||||||
|
"--- CUSTOM ---" -> {
|
||||||
|
preferences.getString("personalization_NDEF_CUSTOM_AAR", "com.tangem.wallet")!!
|
||||||
|
}
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
ndefs.add(NdefRecord(type, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return CardConfig(
|
||||||
|
cardData = cardData,
|
||||||
|
curveID = EllipticCurve.byName(preferences.getString("personalization_CurveId", "secp256k1")!!)
|
||||||
|
?: EllipticCurve.Secp256k1,
|
||||||
|
signingMethod = signingMethod,
|
||||||
|
createWallet = preferences.getBoolean("personalization_CreateWallet", true),
|
||||||
|
maxSignatures = preferences.getString("personalization_MaxSignatures", "1000")!!.toInt(),
|
||||||
|
isReusable = preferences.getBoolean("personalization_SettingsMask_IsReusable", true),
|
||||||
|
protocolAllowUnencrypted = preferences.getBoolean("personalization_SettingsMask_AllowEncryption_None", true),
|
||||||
|
protocolAllowStaticEncryption = preferences.getBoolean("personalization_SettingsMask_AllowEncryption_Fast", true),
|
||||||
|
useActivation = preferences.getBoolean("personalization_SettingsMask_NeedActivation", false),
|
||||||
|
|
||||||
|
useOneCommandAtTime = preferences.getBoolean("personalization_SettingsMask_OneApduAtOnce", false),
|
||||||
|
useCvc = preferences.getBoolean("personalization_SettingsMask_UseCVC", false),
|
||||||
|
useBlock = preferences.getBoolean("personalization_SettingsMask_UseBlock", false),
|
||||||
|
allowSwapPin = preferences.getBoolean("personalization_SettingsMask_AllowSwapPIN", true),
|
||||||
|
allowSwapPin2 = preferences.getBoolean("personalization_SettingsMask_AllowSwapPIN2", true),
|
||||||
|
useNdef = preferences.getBoolean("personalization_SettingsMask_UseNDEF", true),
|
||||||
|
useDynamicNdef = preferences.getBoolean("personalization_SettingsMask_UseDynamicNDEF", true),
|
||||||
|
protectIssuerDataAgainstReplay = preferences.getBoolean("personalization_SettingsMask_ProtectIssuerDataAgainstReplay", true),
|
||||||
|
forbidDefaultPin = preferences.getBoolean("personalization_SettingsMask_ForbidDefaultPIN", false),
|
||||||
|
smartSecurityDelay = preferences.getBoolean("personalization_SettingsMask_SmartSecurityDelay", false),
|
||||||
|
pauseBeforePin2 = preferences.getString("personalization_PauseBeforePIN2", "15")!!.toInt() * 1000,
|
||||||
|
allowSelectBlockchain = preferences.getBoolean("personalization_SettingsMask_AllowSelectBlockchain", false),
|
||||||
|
forbidPurgeWallet = preferences.getBoolean("personalization_SettingsMask_ForbidPurgeWallet", false)
|
||||||
|
?: false,
|
||||||
|
disablePrecomputedNdef = preferences.getBoolean("personalization_SettingsMask_DisablePrecomputedNDEF", false)
|
||||||
|
?: false,
|
||||||
|
skipSecurityDelayIfValidatedByIssuer = preferences.getBoolean("personalization_SettingsMask_SkipSecurityDelayIfValidatedByIssuer", true),
|
||||||
|
skipCheckPIN2andCVCIfValidatedByIssuer = preferences.getBoolean("personalization_SettingsMask_SkipCheckPIN2andCVCIfValidatedByIssuer", true),
|
||||||
|
|
||||||
|
skipSecurityDelayIfValidatedByLinkedTerminal = preferences.getBoolean("personalization_SettingsMask_SkipSecurityDelayIfValidatedByLinkedTerminal", true),
|
||||||
|
restrictOverwriteIssuerDataEx = preferences.getBoolean("personalization_SettingsMask_RestrictOverwriteIssuerDataEx", true),
|
||||||
|
|
||||||
|
requireTerminalTxSignature = preferences.getBoolean("personalization_SettingsMask_RequireTerminalTxSignature", false),
|
||||||
|
requireTerminalCertSignature = preferences.getBoolean("personalization_SettingsMask_RequireTerminalCertSignature", false),
|
||||||
|
checkPin3onCard = preferences.getBoolean("personalization_SettingsMask_CheckPIN3onCard", true),
|
||||||
|
|
||||||
|
cvc = preferences.getString("personalization_cvc", "000") ?: "000",
|
||||||
|
pin = preferences.getString("personalization_pin", "000000") ?: "000000",
|
||||||
|
pin2 = preferences.getString("personalization_pin2", "000") ?: "000",
|
||||||
|
pin3 = preferences.getString("personalization_pin3", "123") ?: "123",
|
||||||
|
hexCrExKey = preferences.getString("personalization_CrEx_Key", "00112233445566778899AABBCCDDEEFFFFEEDDCCBBAA998877665544332211000000111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFF"),
|
||||||
|
|
||||||
|
ndefRecords = ndefs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -111,6 +111,26 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/btn_create_wallet" />
|
app:layout_constraintTop_toBottomOf="@+id/btn_create_wallet" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_personalize"
|
||||||
|
android:layout_width="200dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="Personalize"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/btn_read_write_user_data" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_depersonalize"
|
||||||
|
android:layout_width="200dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="Depersonalize"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/btn_personalize" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
@ -47,11 +47,11 @@ dependencies {
|
||||||
implementation 'com.skyfishjy.ripplebackground:library:1.0.1'
|
implementation 'com.skyfishjy.ripplebackground:library:1.0.1'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.1.0'
|
implementation 'androidx.core:core-ktx:1.2.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"
|
implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
|
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:1.3.61"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
|
||||||
|
|
||||||
implementation 'at.favre.lib:armadillo:0.9.0'
|
implementation 'at.favre.lib:armadillo:0.9.0'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showReadingDialog(activity: FragmentActivity, cardId: String?) {
|
private fun showReadingDialog(activity: FragmentActivity, cardId: String?) {
|
||||||
val dialogView = activity.getLayoutInflater().inflate(R.layout.nfc_bottom_sheet, null)
|
val dialogView = activity.layoutInflater.inflate(R.layout.nfc_bottom_sheet, null)
|
||||||
readingDialog = BottomSheetDialog(activity)
|
readingDialog = BottomSheetDialog(activity)
|
||||||
readingDialog?.setContentView(dialogView)
|
readingDialog?.setContentView(dialogView)
|
||||||
readingDialog?.dismissWithAnimation = true
|
readingDialog?.dismissWithAnimation = true
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue