Updated on 2026-08-14
This commit is contained in:
parent
fcf04d9602
commit
ec51c84a78
27 changed files with 811 additions and 73 deletions
|
|
@ -12,9 +12,9 @@ dependencies {
|
|||
implementation "com.madgag.spongycastle:core:1.58.0.0"
|
||||
implementation "com.madgag.spongycastle:prov:1.58.0.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 "com.google.truth:truth:1.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
|
||||
}
|
||||
|
||||
sourceCompatibility = "8"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package com.tangem
|
||||
|
||||
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.TerminalKeysService
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
|
|
@ -176,16 +180,16 @@ class CardManager(
|
|||
* User_ProtectedCounter and User_ProtectedData additionaly need PIN2 to confirmation.
|
||||
*/
|
||||
fun writeUserData(
|
||||
cardId: String,
|
||||
userData: ByteArray? = null,
|
||||
userProtectedData: ByteArray? = null,
|
||||
userCounter: Int? = null,
|
||||
userProtectedCounter: Int? = null,
|
||||
callback: (result: TaskEvent<WriteUserDataResponse>) -> Unit
|
||||
cardId: String,
|
||||
userData: ByteArray? = null,
|
||||
userProtectedData: ByteArray? = null,
|
||||
userCounter: Int? = null,
|
||||
userProtectedCounter: Int? = null,
|
||||
callback: (result: TaskEvent<WriteUserDataResponse>) -> Unit
|
||||
) {
|
||||
val writeUserDataCommand = WriteUserDataCommand(userData, userProtectedData, userCounter, userProtectedCounter)
|
||||
val task = SingleCommandTask(writeUserDataCommand)
|
||||
runTask(task, cardId, callback)
|
||||
val writeUserDataCommand = WriteUserDataCommand(userData, userProtectedData, userCounter, userProtectedCounter)
|
||||
val task = SingleCommandTask(writeUserDataCommand)
|
||||
runTask(task, cardId, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -199,8 +203,8 @@ class CardManager(
|
|||
* For example, this fields may contain blockchain nonce value.
|
||||
*/
|
||||
fun readUserData(cardId: String, callback: (result: TaskEvent<ReadUserDataResponse>) -> Unit) {
|
||||
val task = SingleCommandTask(ReadUserDataCommand())
|
||||
runTask(task, cardId, callback)
|
||||
val task = SingleCommandTask(ReadUserDataCommand())
|
||||
runTask(task, cardId, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -234,6 +238,39 @@ class CardManager(
|
|||
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) {
|
||||
val personalizationCommand = PersonalizeCommand(config, cardId)
|
||||
val task = SingleCommandTask(personalizationCommand)
|
||||
task.performPreflightRead = false
|
||||
runTask(task, callback = callback)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -14,7 +14,12 @@ class OpenSessionResponse(
|
|||
val uid: ByteArray
|
||||
) : 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>() {
|
||||
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
|
||||
val tlvBuilder = TlvBuilder()
|
||||
|
|
|
|||
|
|
@ -32,6 +32,49 @@ data class SigningMethod(val rawValue: Int) {
|
|||
const val signHashValidatedByIssuerAndWriteIssuerData = 4
|
||||
const val signRawValidatedByIssuerAndWriteIssuerData = 5
|
||||
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.
|
||||
*
|
||||
* @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) {
|
||||
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 {
|
||||
const val isReusable = 0x0001
|
||||
const val useActivation = 0x0002
|
||||
const val forbidPurgeWallet = 0x0004
|
||||
const val useBlock = 0x0008
|
||||
AllowSwapPIN(0x0010),
|
||||
AllowSwapPIN2(0x0020),
|
||||
UseCVC(0x0040),
|
||||
ForbidDefaultPIN(0x0080),
|
||||
|
||||
const val allowSwapPIN = 0x0010
|
||||
const val allowSwapPIN2 = 0x0020
|
||||
const val useCVC = 0x0040
|
||||
const val forbidDefaultPIN = 0x0080
|
||||
UseOneCommandAtTime(0x0100),
|
||||
UseNdef(0x0200),
|
||||
UseDynamicNdef(0x0400),
|
||||
SmartSecurityDelay(0x0800),
|
||||
|
||||
const val useOneCommandAtTime = 0x0100
|
||||
const val useNdef = 0x0200
|
||||
const val useDynamicNdef = 0x0400
|
||||
const val smartSecurityDelay = 0x0800
|
||||
ProtocolAllowUnencrypted(0x1000),
|
||||
ProtocolAllowStaticEncryption(0x2000),
|
||||
|
||||
const val protocolAllowUnencrypted = 0x1000
|
||||
const val protocolAllowStaticEncryption = 0x2000
|
||||
ProtectIssuerDataAgainstReplay(0x4000),
|
||||
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 SHA256-hashed [cardId] concatenated with [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([cardId] | [issuerData] | [issuerDataCounter]).
|
||||
*/
|
||||
|
|
@ -36,7 +36,7 @@ class ReadIssuerDataResponse(
|
|||
|
||||
/**
|
||||
* 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].
|
||||
*/
|
||||
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 SHA256-hashed [cardId] concatenated with [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([cardId] | [issuerData] | [issuerDataCounter]).
|
||||
*/
|
||||
|
|
@ -41,7 +41,7 @@ class ReadIssuerExtraDataResponse(
|
|||
|
||||
/**
|
||||
* 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].
|
||||
*/
|
||||
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,155 @@
|
|||
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.Acquirer
|
||||
import com.tangem.commands.personalization.entities.Issuer
|
||||
import com.tangem.commands.personalization.entities.Manufacturer
|
||||
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 {
|
||||
return CommandApdu(
|
||||
Instruction.Personalize,
|
||||
serializePersonalizationData(cardId, config),
|
||||
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): 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.dataPublicKey)
|
||||
tlvBuilder.append(TlvTag.IssuerTransactionPublicKey, Issuer.transactionPublicKey)
|
||||
|
||||
tlvBuilder.append(TlvTag.AcquirerPublicKey, Acquirer.publicKey)
|
||||
|
||||
tlvBuilder.append(TlvTag.CardData, serializeCardData(cardId, config.cardData))
|
||||
|
||||
return tlvBuilder.serialize()
|
||||
}
|
||||
|
||||
private fun serializeCardData(cardId: String, cardData: CardData): 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(Manufacturer.devPrivateKey))
|
||||
|
||||
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,9 @@
|
|||
package com.tangem.commands.personalization.entities
|
||||
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
|
||||
object Acquirer {
|
||||
val privateKey = "21222324252627284771ED81F2BACF57479E4735EB1405083927372D40DA9E92".hexToBytes()
|
||||
val publicKey: ByteArray by lazy { CryptoUtils.generatePublicKey(privateKey) }
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.commands.personalization.entities
|
||||
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
|
||||
object Issuer {
|
||||
|
||||
val name = "TANGEM SDK"
|
||||
val id = name + "\u0000"
|
||||
val dataPrivateKey = "11121314151617184771ED81F2BACF57479E4735EB1405083927372D40DA9E92".hexToBytes()
|
||||
|
||||
val dataPublicKey: ByteArray by lazy { CryptoUtils.generatePublicKey(dataPrivateKey) }
|
||||
|
||||
val transactionPrivateKey =
|
||||
"11121314151617184771ED81F2BACF57479E4735EB1405081918171615141312".hexToBytes()
|
||||
|
||||
val transactionPublicKey: ByteArray by lazy { CryptoUtils.generatePublicKey(transactionPrivateKey) }
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.commands.personalization.entities
|
||||
|
||||
object Manufacturer {
|
||||
val devPrivateKey = byteArrayOf(
|
||||
0x1B.toByte(), 0x48.toByte(), 0xCF.toByte(), 0xD2.toByte(), 0x4B.toByte(), 0xBB.toByte(), 0x5B.toByte(), 0x39.toByte(),
|
||||
0x47.toByte(), 0x71.toByte(), 0xED.toByte(), 0x81.toByte(), 0xF2.toByte(), 0xBA.toByte(), 0xCF.toByte(), 0x57.toByte(),
|
||||
0x47.toByte(), 0x9E.toByte(), 0x47.toByte(), 0x35.toByte(), 0xEB.toByte(), 0x14.toByte(), 0x05.toByte(), 0x08.toByte(),
|
||||
0x39.toByte(), 0x27.toByte(), 0x37.toByte(), 0x2D.toByte(), 0x40.toByte(), 0xDA.toByte(), 0x9E.toByte(), 0x92.toByte()
|
||||
)
|
||||
|
||||
val devPublicKey = byteArrayOf(0x04,
|
||||
0xBA.toByte(), 0xB8.toByte(), 0x6D.toByte(), 0x56.toByte(), 0x29.toByte(), 0x8C.toByte(), 0x99.toByte(), 0x6F.toByte(),
|
||||
0x56.toByte(), 0x4A.toByte(), 0x84.toByte(), 0xFC.toByte(), 0x88.toByte(), 0xE2.toByte(), 0x8A.toByte(), 0xED.toByte(),
|
||||
0x38.toByte(), 0x18.toByte(), 0x4B.toByte(), 0x12.toByte(), 0xF0.toByte(), 0x7E.toByte(), 0x51.toByte(), 0x91.toByte(),
|
||||
0x13.toByte(), 0xBE.toByte(), 0xF4.toByte(), 0x8C.toByte(), 0x76.toByte(), 0xF3.toByte(), 0xDF.toByte(), 0x3A.toByte(),
|
||||
0xDC.toByte(), 0x30.toByte(), 0x35.toByte(), 0x99.toByte(), 0xB0.toByte(), 0x8A.toByte(), 0xC0.toByte(), 0x5B.toByte(),
|
||||
0x55.toByte(), 0xEC.toByte(), 0x3D.toByte(), 0xF9.toByte(), 0x8D.toByte(), 0x93.toByte(), 0x38.toByte(), 0x57.toByte(),
|
||||
0x3A.toByte(), 0x62.toByte(), 0x42.toByte(), 0xF7.toByte(), 0x6F.toByte(), 0x5D.toByte(), 0x28.toByte(), 0xF4.toByte(),
|
||||
0xF0.toByte(), 0xF3.toByte(), 0x64.toByte(), 0xE8.toByte(), 0x7E.toByte(), 0x8F.toByte(), 0xCA.toByte(), 0x2F.toByte()
|
||||
)
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ package com.tangem.common.apdu
|
|||
*/
|
||||
enum class Instruction(var code: Int) {
|
||||
Unknown(0x00),
|
||||
Personalize(0xF1),
|
||||
Read(0xF2),
|
||||
VerifyCard(0xF3),
|
||||
ValidateCard(0xF4),
|
||||
|
|
@ -20,7 +21,8 @@ enum class Instruction(var code: Int) {
|
|||
Activate(0xFE),
|
||||
OpenSession(0xFF),
|
||||
WriteUserData(0xE0),
|
||||
ReadUserData(0xE1);
|
||||
ReadUserData(0xE1),
|
||||
Depersonalize(0xE3);
|
||||
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.commands.common.IssuerDataMode
|
|||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toByteArray
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.tasks.TaskError
|
||||
import java.util.*
|
||||
|
||||
|
|
@ -32,11 +33,11 @@ class TlvEncoder {
|
|||
return when (tag.valueType()) {
|
||||
TlvValueType.HexString -> {
|
||||
typeCheck<T, String>(tag)
|
||||
return if (tag == TlvTag.Pin || tag == TlvTag.Pin2) {
|
||||
(value as String).calculateSha256()
|
||||
} else {
|
||||
(value as String).hexToBytes()
|
||||
}
|
||||
(value as String).hexToBytes()
|
||||
}
|
||||
TlvValueType.HexStringToHash -> {
|
||||
typeCheck<T, String>(tag)
|
||||
(value as String).calculateSha256()
|
||||
}
|
||||
TlvValueType.Utf8String -> {
|
||||
typeCheck<T, String>(tag)
|
||||
|
|
@ -52,8 +53,8 @@ class TlvEncoder {
|
|||
}
|
||||
TlvValueType.BoolValue -> {
|
||||
typeCheck<T, Boolean>(tag)
|
||||
Log.e(this::class.simpleName!!, "Unsupported operation: Boolean to ByteArray for tag $tag")
|
||||
throw TaskError.ConvertError()
|
||||
val booleanValue = value as Boolean
|
||||
if (booleanValue) byteArrayOf(1) else byteArrayOf(0)
|
||||
}
|
||||
TlvValueType.ByteArray -> {
|
||||
typeCheck<T, ByteArray>(tag)
|
||||
|
|
@ -61,7 +62,7 @@ class TlvEncoder {
|
|||
}
|
||||
TlvValueType.EllipticCurve -> {
|
||||
typeCheck<T, EllipticCurve>(tag)
|
||||
(value as EllipticCurve).curve.plus("\\0").toByteArray()
|
||||
(value as EllipticCurve).curve.toByteArray()
|
||||
}
|
||||
TlvValueType.DateTime -> {
|
||||
typeCheck<T, Date>(tag)
|
||||
|
|
@ -69,7 +70,7 @@ class TlvEncoder {
|
|||
val year = calendar.get(Calendar.YEAR)
|
||||
val month = calendar.get(Calendar.MONTH) + 1
|
||||
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 -> {
|
||||
typeCheck<T, ProductMask>(tag)
|
||||
|
|
@ -79,7 +80,8 @@ class TlvEncoder {
|
|||
}
|
||||
TlvValueType.SettingsMask -> {
|
||||
typeCheck<T, SettingsMask>(tag)
|
||||
(value as SettingsMask).rawValue.toByteArray(2)
|
||||
val rawValue = (value as SettingsMask).rawValue
|
||||
rawValue.toByteArray(determineByteArraySize(rawValue))
|
||||
}
|
||||
TlvValueType.CardStatus -> {
|
||||
typeCheck<T, CardStatus>(tag)
|
||||
|
|
@ -87,7 +89,7 @@ class TlvEncoder {
|
|||
}
|
||||
TlvValueType.SigningMethod -> {
|
||||
typeCheck<T, SigningMethod>(tag)
|
||||
(value as SigningMethod).rawValue.toByteArray()
|
||||
byteArrayOf((value as SigningMethod).rawValue.toByte())
|
||||
}
|
||||
TlvValueType.IssuerDataMode -> {
|
||||
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) {
|
||||
if (T::class != ExpectedT::class) {
|
||||
Log.e(this::class.simpleName!!,
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class TlvMapper(val tlvList: List<Tlv>) {
|
|||
}
|
||||
|
||||
return when (tag.valueType()) {
|
||||
TlvValueType.HexString -> {
|
||||
TlvValueType.HexString, TlvValueType.HexStringToHash -> {
|
||||
typeCheck<T, String>(tag)
|
||||
tlvValue.toHexString() as T
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package com.tangem.common.tlv
|
|||
*/
|
||||
enum class TlvValueType {
|
||||
HexString,
|
||||
HexStringToHash,
|
||||
Utf8String,
|
||||
Uint16,
|
||||
Uint32,
|
||||
|
|
@ -36,6 +37,7 @@ enum class TlvTag(val code: Int) {
|
|||
SettingsMask(0x0A),
|
||||
CardData(0x0C),
|
||||
NdefData(0x0D),
|
||||
CreateWalletAtPersonalize(0x0E),
|
||||
Health(0x0F),
|
||||
|
||||
Pin(0x10),
|
||||
|
|
@ -51,8 +53,11 @@ enum class TlvTag(val code: Int) {
|
|||
|
||||
SessionKeyA(0x1A),
|
||||
SessionKeyB(0x1B),
|
||||
Uid(0x0B),
|
||||
Pause(0x1C),
|
||||
NewPin3(0x1E),
|
||||
CrExKey(0x1F),
|
||||
|
||||
Uid(0x0B),
|
||||
|
||||
ManufactureId(0x20),
|
||||
ManufacturerSignature(0x86),
|
||||
|
|
@ -63,6 +68,8 @@ enum class TlvTag(val code: Int) {
|
|||
IssuerDataSignature(0x33),
|
||||
IssuerTransactionSignature(0x34),
|
||||
IssuerDataCounter(0x35),
|
||||
AcquirerPublicKey(0x37),
|
||||
|
||||
Size(0x25),
|
||||
Mode(0x23),
|
||||
Offset(0x24),
|
||||
|
|
@ -117,15 +124,15 @@ enum class TlvTag(val code: Int) {
|
|||
*/
|
||||
fun valueType(): TlvValueType {
|
||||
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 ->
|
||||
TlvValueType.Utf8String
|
||||
CurveId -> TlvValueType.EllipticCurve
|
||||
MaxSignatures, PauseBeforePin2, RemainingSignatures,
|
||||
SignedHashes, Health, TokenDecimal,
|
||||
PauseBeforePin2, RemainingSignatures, SignedHashes, Health, TokenDecimal,
|
||||
Offset, Size -> TlvValueType.Uint16
|
||||
UserCounter, UserProtectedCounter, IssuerDataCounter -> TlvValueType.Uint32
|
||||
IsActivated, TerminalIsLinked -> TlvValueType.BoolValue
|
||||
MaxSignatures, UserCounter, UserProtectedCounter, IssuerDataCounter -> TlvValueType.Uint32
|
||||
IsActivated, TerminalIsLinked, CreateWalletAtPersonalize -> TlvValueType.BoolValue
|
||||
ManufactureDateTime -> TlvValueType.DateTime
|
||||
ProductMask -> TlvValueType.ProductMask
|
||||
SettingsMask -> TlvValueType.SettingsMask
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.crypto
|
|||
|
||||
import com.tangem.commands.EllipticCurve
|
||||
import net.i2p.crypto.eddsa.EdDSASecurityProvider
|
||||
import org.spongycastle.jce.provider.BouncyCastleProvider
|
||||
import java.security.PublicKey
|
||||
import java.security.SecureRandom
|
||||
import java.security.Security
|
||||
|
|
@ -13,7 +14,7 @@ import javax.crypto.spec.SecretKeySpec
|
|||
object CryptoUtils {
|
||||
|
||||
fun initCrypto() {
|
||||
Security.insertProviderAt(org.spongycastle.jce.provider.BouncyCastleProvider(), 1)
|
||||
Security.insertProviderAt(BouncyCastleProvider(), 1)
|
||||
Security.addProvider(EdDSASecurityProvider())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.tasks
|
||||
|
||||
import com.tangem.commands.Card
|
||||
import com.tangem.commands.SettingsMask
|
||||
import com.tangem.commands.Settings
|
||||
import com.tangem.commands.WriteIssuerDataCommand
|
||||
import com.tangem.commands.WriteIssuerDataResponse
|
||||
import com.tangem.commands.common.IssuerDataToVerify
|
||||
|
|
@ -55,7 +55,7 @@ class WriteIssuerDataTask(
|
|||
if (isCounterRequired()) issuerDataCounter != null else true
|
||||
|
||||
private fun isCounterRequired(): Boolean =
|
||||
card.settingsMask?.contains(SettingsMask.protectIssuerDataAgainstReplay) != false
|
||||
card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false
|
||||
|
||||
private fun verifySignature(command: WriteIssuerDataCommand, cardId: String): Boolean {
|
||||
return command.verify(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
package com.tangem.tasks
|
||||
|
||||
import com.tangem.commands.Card
|
||||
import com.tangem.commands.SettingsMask
|
||||
import com.tangem.commands.WriteIssuerDataResponse
|
||||
import com.tangem.commands.WriteIssuerExtraDataCommand
|
||||
import com.tangem.commands.*
|
||||
import com.tangem.commands.common.IssuerDataMode
|
||||
import com.tangem.commands.common.IssuerDataToVerify
|
||||
import com.tangem.common.CardEnvironment
|
||||
|
|
@ -98,7 +95,7 @@ internal class WriteIssuerExtraDataTask(
|
|||
if (isCounterRequired()) issuerDataCounter != null else true
|
||||
|
||||
private fun isCounterRequired(): Boolean =
|
||||
card.settingsMask?.contains(SettingsMask.protectIssuerDataAgainstReplay) != false
|
||||
card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false
|
||||
|
||||
private fun verifySignatures(command: WriteIssuerExtraDataCommand): Boolean {
|
||||
val publicKey = issuerPublicKey ?: card.issuerPublicKey!!
|
||||
|
|
|
|||
|
|
@ -68,15 +68,15 @@ class TlvMapperTest {
|
|||
.isNotNull()
|
||||
assertThat(settingsMask.rawValue)
|
||||
.isEqualTo(32289)
|
||||
assertThat(settingsMask.contains(SettingsMask.skipSecurityDelayIfValidatedByLinkedTerminal))
|
||||
assertThat(settingsMask.contains(Settings.SkipSecurityDelayIfValidatedByLinkedTerminal))
|
||||
.isFalse()
|
||||
assertThat(settingsMask.contains(SettingsMask.isReusable))
|
||||
assertThat(settingsMask.contains(Settings.IsReusable))
|
||||
.isTrue()
|
||||
assertThat(settingsMask.contains(SettingsMask.allowSwapPIN2))
|
||||
assertThat(settingsMask.contains(Settings.AllowSwapPIN2))
|
||||
.isTrue()
|
||||
assertThat(settingsMask.contains(SettingsMask.useDynamicNdef))
|
||||
assertThat(settingsMask.contains(Settings.UseDynamicNdef))
|
||||
.isTrue()
|
||||
assertThat(settingsMask.contains(SettingsMask.forbidPurgeWallet))
|
||||
assertThat(settingsMask.contains(Settings.ForbidPurgeWallet))
|
||||
.isFalse()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ dependencies {
|
|||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
|
||||
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'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import android.content.Intent
|
|||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.tangem.CardManager
|
||||
import com.tangem.commands.personalization.CardConfig
|
||||
import com.tangem.tangem_sdk_new.extensions.init
|
||||
import com.tangem.tangemtest.extensions.init
|
||||
import com.tangem.tasks.ScanEvent
|
||||
import com.tangem.tasks.TaskError
|
||||
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_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> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
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 com.tangem.commands.personalization.entities.Issuer
|
||||
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 = Issuer.id,
|
||||
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"),
|
||||
|
||||
issuerName = Issuer.id,
|
||||
acquirerName = "TANGEM SDK",
|
||||
|
||||
ndefRecords = ndefs
|
||||
)
|
||||
}
|
||||
|
|
@ -111,6 +111,26 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
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>
|
||||
|
||||
</ScrollView>
|
||||
|
|
@ -47,11 +47,11 @@ dependencies {
|
|||
implementation 'com.skyfishjy.ripplebackground:library:1.0.1'
|
||||
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-runtime: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'
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class DefaultCardManagerDelegate(private val reader: NfcReader) : CardManagerDel
|
|||
}
|
||||
|
||||
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?.setContentView(dialogView)
|
||||
readingDialog?.dismissWithAnimation = true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue