Skip to content

Commit

Permalink
AND-9038 Added Sign transactions with different sizes of hashes
Browse files Browse the repository at this point in the history
  • Loading branch information
iMaks99 committed Nov 8, 2024
1 parent 776a942 commit fe61796
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ fun TangemSdkError.localizedDescriptionRes(): TangemSdkErrorDescription {
is TangemSdkError.UnsupportedWalletConfig,
is TangemSdkError.WalletIsNotCreated,
is TangemSdkError.WalletIsPurged,
is TangemSdkError.HashSizeMustBeEqual,
is TangemSdkError.SignHashesNotAvailable,
is TangemSdkError.CardVerificationFailed,
is TangemSdkError.NonHardenedDerivationNotSupported,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,6 @@ sealed class TangemSdkError(code: Int) : TangemError(code) {
*/
class EmptyHashes : TangemSdkError(code = 40902)

/**
* This error is returned when a [com.tangem.operations.SignCommand]
* receives hashes of different lengths for signature.
*/
class HashSizeMustBeEqual : TangemSdkError(code = 40903)
class WalletIsNotCreated : TangemSdkError(code = 40904)
class SignHashesNotAvailable : TangemSdkError(code = 40905)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.tangem.operations.sign

object ChunkHashesUtils {
// The max answer is 1152 bytes (unencrypted) and 1120 (encrypted).
// The worst case is 8 hashes * 64 bytes for ed + 512 bytes of signatures + cardId, SignedHashes + TLV + SW is ok.
private const val PACKAGE_SIZE = 512
// Card limitation
private const val MAX_CHUNK_SIZE = 10

fun chunkHashes(hashesRaw: Array<ByteArray>): List<Chunk> {
val hashes = hashesRaw.mapIndexed { index, hash -> Hash(index = index, data = hash) }
val hashesBySize = hashes.groupBy { it.data.size }

return hashesBySize.flatMap { hashesGroup ->
val hashSize = hashesGroup.key
val chunkSize = getChunkSize(hashSize)

hashesGroup.value
.chunked(chunkSize)
.map { Chunk(hashSize, it) }
}
}

private fun getChunkSize(hashSize: Int) = (PACKAGE_SIZE / hashSize).coerceIn(1, MAX_CHUNK_SIZE)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.tangem.operations.sign

class ChunkedHashesContainer(
hashes: Array<ByteArray>,
) {
val isEmpty: Boolean = hashes.isEmpty()
var currentChunkIndex: Int = 0
private set

val chunks = ChunkHashesUtils.chunkHashes(hashes)

private var signedChunks: MutableList<SignedChunk> = mutableListOf()

fun getCurrentChunk(): Chunk {
return chunks[currentChunkIndex]
}

fun addSignedChunk(signedChunk: SignedChunk) {
signedChunks.add(signedChunk)
currentChunkIndex++
}

fun getSignatures(): List<ByteArray> {
return signedChunks
.flatMap { it.signedHashes }
.sortedBy { it.index }
.map { it.signature }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,44 @@ import com.tangem.common.core.SessionEnvironment
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toByteArray
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.CryptoUtils
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.sign
import com.tangem.operations.Command
import com.tangem.operations.CommandResponse
import kotlin.math.max
import kotlin.math.min

/**
* @property cardId CID, Unique Tangem card ID number
* @property signatures Signed hashes (array of resulting signatures)
* @property totalSignedHashes Total number of signed hashes returned by the wallet since its creation. COS: 1.16+
*/
@JsonClass(generateAdapter = true)
class SignResponse(
val cardId: String,
open class SignResponse(
open val cardId: String,
val signatures: List<ByteArray>,
val totalSignedHashes: Int?,
) : CommandResponse
open val totalSignedHashes: Int?,
) : CommandResponse {

/**
* Model to store chunked sign response.
* @property cardId CID, Unique Tangem card ID number
* @property signedChunk Signed hashes of given chunk
* @property totalSignedHashes Total number of signed hashes returned by the wallet since its creation. COS: 1.16+
*/
@JsonClass(generateAdapter = true)
data class PartialSignResponse(
override val cardId: String,
val signedChunk: SignedChunk,
override val totalSignedHashes: Int?,
) : SignResponse(
cardId = cardId,
signatures = emptyList(),
totalSignedHashes = totalSignedHashes,
)
}

/**
* Signs transaction hashes using a wallet private key, stored on the card.
Expand All @@ -56,20 +72,9 @@ internal class SignCommand(

private var terminalKeys: KeyPair? = null

private val hashSizes = if (hashes.isNotEmpty()) hashes.first().size else 0
private val chunkSize: Int = if (hashSizes > 0) {
val estimatedChunkSize = PACKAGE_SIZE / hashSizes
max(1, min(estimatedChunkSize, MAX_CHUNK_SIZE))
} else {
MAX_CHUNK_SIZE
}
private val hashesChunked = hashes.asList().chunked(chunkSize)
private val chunkHashesHelper: ChunkedHashesContainer = ChunkedHashesContainer(hashes)
private var environment: SessionEnvironment? = null

private val signatures = mutableListOf<ByteArray>()
private val currentChunkNumber: Int
get() = signatures.size / chunkSize

override fun requiresPasscode(): Boolean = true

override fun performPreCheck(card: Card): TangemSdkError? {
Expand Down Expand Up @@ -104,28 +109,27 @@ internal class SignCommand(
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
if (hashes.isEmpty()) {
if (chunkHashesHelper.isEmpty) {
callback(CompletionResult.Failure(TangemSdkError.EmptyHashes()))
return
}
if (hashes.any { it.size != hashSizes }) {
callback(CompletionResult.Failure(TangemSdkError.HashSizeMustBeEqual()))
return
}
terminalKeys = retrieveTerminalKeys(card, session.environment)

sign(session, callback)
}

private fun sign(session: CardSession, callback: CompletionCallback<SignResponse>) {
environment = session.environment
if (hashesChunked.size > 1) setSignedChunksMessage(session)
if (chunkHashesHelper.chunks.size > 1) setSignedChunksMessage(session)

transceive(session) { result ->
when (result) {
is CompletionResult.Success -> {
signatures.addAll(result.data.signatures)
if (signatures.size == hashes.size) {
val response = result.data as SignResponse.PartialSignResponse
chunkHashesHelper.addSignedChunk(response.signedChunk)

if (chunkHashesHelper.currentChunkIndex >= chunkHashesHelper.chunks.size) {
val signatures = processSignatures(session.environment)
session.environment.card?.wallet(walletPublicKey)?.let {
val wallet = it.copy(
totalSignedHashes = result.data.totalSignedHashes,
Expand All @@ -135,15 +139,16 @@ internal class SignCommand(
}

val finalResponse = SignResponse(
result.data.cardId,
processSignatures(session.environment, signatures.toList()),
result.data.totalSignedHashes,
cardId = result.data.cardId,
signatures = signatures,
totalSignedHashes = result.data.totalSignedHashes,
)
callback(CompletionResult.Success(finalResponse))
} else {
sign(session, callback)
}
}

is CompletionResult.Failure -> callback(result)
}
}
Expand All @@ -153,7 +158,7 @@ internal class SignCommand(
val message = LocatorMessage(
headerSource = LocatorMessage.Source(
id = StringsLocator.ID.SIGN_MULTIPLE_CHUNKS_PART,
formatArgs = arrayOf(currentChunkNumber + 1, hashesChunked.size),
formatArgs = arrayOf(chunkHashesHelper.currentChunkIndex + 1, chunkHashesHelper.chunks.size),
),
bodySource = null,
)
Expand All @@ -171,21 +176,23 @@ internal class SignCommand(
override fun serialize(environment: SessionEnvironment): CommandApdu {
val walletIndex = environment.card?.wallet(walletPublicKey)?.index ?: throw TangemSdkError.WalletNotFound()

val dataToSign = hashesChunked[currentChunkNumber].reduce { arr1, arr2 -> arr1 + arr2 }
val hashSize = if (hashSizes > 255) hashSizes.toByteArray(2) else hashSizes.toByteArray(1)
val chunk = chunkHashesHelper.getCurrentChunk()
val hashSize = chunk.hashSize
val hashSizeData = if (hashSize > 255) hashSize.toByteArray(2) else hashSize.toByteArray(1)
val flattedHashes = chunk.hashes.map { it.data }.reduce { arr1, arr2 -> arr1 + arr2 }

val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.accessCode.value)
tlvBuilder.append(TlvTag.Pin2, environment.passcode.value)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.TransactionOutHashSize, hashSize)
tlvBuilder.append(TlvTag.TransactionOutHash, dataToSign)
tlvBuilder.append(TlvTag.TransactionOutHashSize, hashSizeData)
tlvBuilder.append(TlvTag.TransactionOutHash, flattedHashes)
tlvBuilder.append(TlvTag.Cvc, environment.cvc)
// Wallet index works only on COS v. 4.0 and higher. For previous version index will be ignored
tlvBuilder.append(TlvTag.WalletIndex, walletIndex)

terminalKeys?.let {
val signedData = dataToSign.sign(it.privateKey)
val signedData = flattedHashes.sign(it.privateKey)
tlvBuilder.append(TlvTag.TerminalTransactionSignature, signedData)
tlvBuilder.append(TlvTag.TerminalPublicKey, it.publicKey)
}
Expand All @@ -194,35 +201,43 @@ internal class SignCommand(
return CommandApdu(Instruction.Sign, tlvBuilder.serialize())
}

override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): SignResponse {
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): SignResponse.PartialSignResponse {
val tlvData = deserializeApdu(environment, apdu)

val decoder = TlvDecoder(tlvData)
val signature: ByteArray = decoder.decode(TlvTag.WalletSignature)
val splittedSignatures = splitSignedSignature(signature, getChunk().count())

return SignResponse(
decoder.decode(TlvTag.CardId),
splittedSignatures,
decoder.decodeOptional(TlvTag.WalletSignedHashes),
val chunk = chunkHashesHelper.getCurrentChunk()
val signatureBLOB: ByteArray = decoder.decode(TlvTag.WalletSignature)
val signatures = splitSignatureBLOB(signatureBLOB, chunk.hashes.size)

val signedHashes = chunk.hashes.zip(signatures).map { (hash, signature) ->
SignedHash(
index = hash.index,
data = hash.data,
signature = signature,
)
}

val signedChunk = SignedChunk(signedHashes)

return SignResponse.PartialSignResponse(
cardId = decoder.decode(TlvTag.CardId),
signedChunk = signedChunk,
totalSignedHashes = decoder.decode(TlvTag.WalletSignedHashes),
)
}

private fun processSignatures(environment: SessionEnvironment, signatures: List<ByteArray>): List<ByteArray> {
private fun processSignatures(environment: SessionEnvironment): List<ByteArray> {
val signatures = chunkHashesHelper.getSignatures()

if (!environment.config.canonizeSecp256k1Signatures) return signatures
val wallet = environment.card?.wallet(walletPublicKey) ?: return signatures
if (wallet.curve != EllipticCurve.Secp256k1) return signatures

return signatures.map { CryptoUtils.normalize(it) }
}

private fun getChunk(): IntRange {
val from = currentChunkNumber * chunkSize
val to = min(from + chunkSize, hashes.size)
return from until to
}

private fun splitSignedSignature(signature: ByteArray, numberOfSignatures: Int): List<ByteArray> {
private fun splitSignatureBLOB(signature: ByteArray, numberOfSignatures: Int): List<ByteArray> {
val signatures = mutableListOf<ByteArray>()
val signatureSize = signature.size / numberOfSignatures
for (index in 0 until numberOfSignatures) {
Expand All @@ -242,12 +257,4 @@ internal class SignCommand(

return environment.terminalKeys
}

private companion object {
// The max answer is 1152 bytes (unencrypted) and 1120 (encrypted).
// The worst case is 8 hashes * 64 bytes for ed + 512 bytes of signatures + cardId, SignedHashes + TLV + SW is ok.
const val PACKAGE_SIZE = 512
// Card limitation
const val MAX_CHUNK_SIZE = 10
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.tangem.operations.sign

data class Hash(
val index: Int,
val data: ByteArray,
)

data class SignedHash(
val index: Int,
val data: ByteArray,
val signature: ByteArray,
)

data class Chunk(
val hashSize: Int,
val hashes: List<Hash>,
)

data class SignedChunk(
val signedHashes: List<SignedHash>,
)
Loading

0 comments on commit fe61796

Please sign in to comment.