Implemented decoding control bytes

This commit is contained in:
dankito 2020-05-16 13:40:43 +02:00
parent 827fd7bc92
commit 26c9ef02a3
4 changed files with 111 additions and 42 deletions

View File

@ -702,7 +702,8 @@ open class FinTsClient @JvmOverloads constructor(
return when (tanProcedure.type) { return when (tanProcedure.type) {
TanProcedureType.ChipTanFlickercode, TanProcedureType.ChipTanManuell, TanProcedureType.ChipTanUsb -> TanProcedureType.ChipTanFlickercode, TanProcedureType.ChipTanManuell, TanProcedureType.ChipTanUsb ->
FlickerCodeTanChallenge(FlickerCodeDecoder().decodeChallenge(challenge), messageToShowToUser, challenge, tanProcedure, tanResponse.tanMediaIdentifier) FlickerCodeTanChallenge(FlickerCodeDecoder().decodeChallenge(challenge, tanProcedure.hhdVersion ?: HHDVersion.HHD_1_4), // HHD 1.4 is currently the most used version
messageToShowToUser, challenge, tanProcedure, tanResponse.tanMediaIdentifier)
TanProcedureType.ChipTanQrCode, TanProcedureType.ChipTanPhotoTanMatrixCode, TanProcedureType.ChipTanQrCode, TanProcedureType.ChipTanPhotoTanMatrixCode,
TanProcedureType.QrCode, TanProcedureType.photoTan -> TanProcedureType.QrCode, TanProcedureType.photoTan ->

View File

@ -5,6 +5,7 @@ open class FlickerCodeDatenelement(
val lengthInByte: String, val lengthInByte: String,
val data: String, val data: String,
val encoding: FlickerCodeEncoding, val encoding: FlickerCodeEncoding,
val controlBytes: List<String>, // TODO: only available at start byte
val endIndex: Int val endIndex: Int
) { ) {

View File

@ -1,5 +1,6 @@
package net.dankito.fints.tan package net.dankito.fints.tan
import net.dankito.fints.model.HHDVersion
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.util.regex.Pattern import java.util.regex.Pattern
@ -13,26 +14,27 @@ open class FlickerCodeDecoder {
} }
open fun decodeChallenge(challengeHHD_UC: String): FlickerCode { open fun decodeChallenge(challengeHHD_UC: String, hhdVersion: HHDVersion): FlickerCode {
try { try {
val challengeLength = parseIntToHex(challengeHHD_UC.substring(0, 2)) val challengeLengthFieldLength = if (hhdVersion == HHDVersion.HHD_1_3) 2 else 3
val challengeLength = parseIntToHex(challengeHHD_UC.substring(0, challengeLengthFieldLength))
val startCode = parseStartCode(challengeHHD_UC, 2) val startCode = parseStartCode(challengeHHD_UC, challengeLengthFieldLength, hhdVersion)
val controlByte = "" // TODO (there can be multiple of them!) val controlBytesString = startCode.controlBytes.joinToString("")
val de1 = parseDatenelement(challengeHHD_UC, startCode.endIndex) val de1 = parseDatenelement(challengeHHD_UC, startCode.endIndex, hhdVersion)
val de2 = parseDatenelement(challengeHHD_UC, de1.endIndex) val de2 = parseDatenelement(challengeHHD_UC, de1.endIndex, hhdVersion)
val de3 = parseDatenelement(challengeHHD_UC, de2.endIndex) val de3 = parseDatenelement(challengeHHD_UC, de2.endIndex, hhdVersion)
val luhnChecksum = calculateLuhnChecksum(startCode, controlByte, de1, de2, de3) val luhnChecksum = calculateLuhnChecksum(startCode, controlBytesString, de1, de2, de3)
// TODO: // TODO:
// können im HHDUC-Protokoll Datenelemente ausgelassen werden, indem als Länge LDE1, LDE2 oder LDE3 = 00 angegeben wird. // können im HHDUC-Protokoll Datenelemente ausgelassen werden, indem als Länge LDE1, LDE2 oder LDE3 = 00 angegeben wird.
// Dadurch wird gekennzeichnet, dass das jeweilige, durch den Start-Code definierte Datenelement nicht im HHDUC-Datenstrom // Dadurch wird gekennzeichnet, dass das jeweilige, durch den Start-Code definierte Datenelement nicht im HHDUC-Datenstrom
// enthalten ist. Somit sind für leere Datenelemente die Längenfelder zu übertragen, wenn danach noch nicht-leere // enthalten ist. Somit sind für leere Datenelemente die Längenfelder zu übertragen, wenn danach noch nicht-leere
// Datenelemente folgen. Leere Datenelemente am Ende des Datenstromes können komplett inklusive Längenfeld entfallen. // Datenelemente folgen. Leere Datenelemente am Ende des Datenstromes können komplett inklusive Längenfeld entfallen.
val dataWithoutLengthAndChecksum = startCode.lengthInByte + controlByte + startCode + de1.lengthInByte + de1.data + de2.lengthInByte + de2.data + de3.lengthInByte + de3.data val dataWithoutLengthAndChecksum = startCode.lengthInByte + controlBytesString + startCode + de1.lengthInByte + de1.data + de2.lengthInByte + de2.data + de3.lengthInByte + de3.data
val dataLength = (dataWithoutLengthAndChecksum.length + 2) / 2 // + 2 for checksum val dataLength = (dataWithoutLengthAndChecksum.length + 2) / 2 // + 2 for checksum
val dataWithoutChecksum = toHex(dataLength, 2) + dataWithoutLengthAndChecksum val dataWithoutChecksum = toHex(dataLength, 2) + dataWithoutLengthAndChecksum
@ -48,30 +50,33 @@ open class FlickerCodeDecoder {
} }
} }
protected open fun parseStartCode(challengeHHD_UC: String, startIndex: Int): FlickerCodeDatenelement { protected open fun parseStartCode(challengeHHD_UC: String, startIndex: Int, hhdVersion: HHDVersion): FlickerCodeDatenelement {
return parseDatenelement(challengeHHD_UC, startIndex) { lengthByteString -> parseIntToHex(lengthByteString) } return parseDatenelement(challengeHHD_UC, startIndex, hhdVersion) { lengthByteString -> parseIntToHex(lengthByteString) }
} }
protected open fun parseDatenelement(code: String, startIndex: Int): FlickerCodeDatenelement { protected open fun parseDatenelement(code: String, startIndex: Int, hhdVersion: HHDVersion): FlickerCodeDatenelement {
return parseDatenelement(code, startIndex) { lengthByteString -> lengthByteString.toInt() } return parseDatenelement(code, startIndex, hhdVersion) { lengthByteString -> lengthByteString.toInt() }
} }
protected open fun parseDatenelement(code: String, startIndex: Int, lengthParser: (lengthByteString: String) -> Int): FlickerCodeDatenelement { protected open fun parseDatenelement(code: String, startIndex: Int, hhdVersion: HHDVersion, lengthParser: (lengthByteString: String) -> Int): FlickerCodeDatenelement {
val lengthByteLength = 2 val lengthByteLength = 2
val dataElementAndRest = code.substring(startIndex) val dataElementAndRest = code.substring(startIndex)
if (dataElementAndRest.isEmpty() || dataElementAndRest.length < lengthByteLength) { // data element not set if (dataElementAndRest.isEmpty() || dataElementAndRest.length < lengthByteLength) { // data element not set
return FlickerCodeDatenelement("", "", FlickerCodeEncoding.BCD, startIndex) return FlickerCodeDatenelement("", "", FlickerCodeEncoding.BCD, listOf(), startIndex)
} }
val lengthByteString = dataElementAndRest.substring(0, lengthByteLength) val lengthByteString = dataElementAndRest.substring(0, lengthByteLength)
val lengthByte = lengthParser(lengthByteString) val lengthByte = lengthParser(lengthByteString)
var encoding = getEncodingFromLengthByte(lengthByte)
var dataLength = getLengthFromLengthByte(lengthByte) var dataLength = getLengthFromLengthByte(lengthByte)
var encoding = getEncodingFromLengthByte(lengthByte)
val endIndex = lengthByteLength + dataLength val controlBytes = parseControlBytes(lengthByte, lengthByteLength, dataElementAndRest)
var data = dataElementAndRest.substring(lengthByteLength, endIndex)
val dataStartIndex = lengthByteLength + controlBytes.size * 2
val dataEndIndex = dataStartIndex + dataLength
var data = dataElementAndRest.substring(dataStartIndex, dataEndIndex)
// Sollte ein Datenelement eine Zahl mit Komma-Trennung oder Vorzeichen beinhalten (z. B. Betrag oder Anzahl), // Sollte ein Datenelement eine Zahl mit Komma-Trennung oder Vorzeichen beinhalten (z. B. Betrag oder Anzahl),
// so muss als Format ASCII gewählt werden, da ggf. auch ein Sonderzeichen mit übertragen werden muss. // so muss als Format ASCII gewählt werden, da ggf. auch ein Sonderzeichen mit übertragen werden muss.
@ -89,7 +94,56 @@ open class FlickerCodeDecoder {
dataLength = data.length dataLength = data.length
var lengthInByte = dataLength / 2 val lengthInByteString = calculateLengthInByteString(dataLength, controlBytes, hhdVersion, encoding)
return FlickerCodeDatenelement(
lengthInByteString,
data,
encoding,
controlBytes,
startIndex + dataEndIndex
)
}
protected open fun getLengthFromLengthByte(lengthByte: Int): Int {
return lengthByte and 0b00011111
}
protected open fun getEncodingFromLengthByte(lengthByte: Int): FlickerCodeEncoding {
return if (isBitSet(lengthByte, 6)) FlickerCodeEncoding.ASCII else FlickerCodeEncoding.BCD
}
protected open fun isControlBitSet(lengthByte: Int): Boolean {
return isBitSet(lengthByte, 7)
}
protected open fun parseControlBytes(lengthByte: Int, lengthByteLength: Int, dataElementAndRest: String): MutableList<String> {
val controlBytes = mutableListOf<String>()
var isControlByteSet = isControlBitSet(lengthByte)
while (isControlByteSet) {
val controlByteStartIndex = lengthByteLength + controlBytes.size * 2
val controlByteString = dataElementAndRest.substring(controlByteStartIndex, controlByteStartIndex + 2)
val controlByte = parseIntToHex(controlByteString)
controlBytes.add(controlByteString)
isControlByteSet = isControlBitSet(controlByte)
}
return controlBytes
}
protected open fun calculateLengthInByteString(dataLength: Int, controlBytes: MutableList<String>,
hhdVersion: HHDVersion, encoding: FlickerCodeEncoding): String {
var lengthInByte = dataLength / 2 + controlBytes.size * 128
if (hhdVersion == HHDVersion.HHD_1_4 && encoding == FlickerCodeEncoding.ASCII) {
lengthInByte += 64
}
if (encoding == FlickerCodeEncoding.ASCII) { if (encoding == FlickerCodeEncoding.ASCII) {
if (lengthInByte < 16) { if (lengthInByte < 16) {
@ -97,29 +151,14 @@ open class FlickerCodeDecoder {
} }
} }
val lengthInByteString = toHex(lengthInByte, 2) return toHex(lengthInByte, 2)
return FlickerCodeDatenelement(
lengthInByteString,
data,
encoding,
startIndex + endIndex
)
}
protected open fun getEncodingFromLengthByte(engthByte: Int): FlickerCodeEncoding {
return if (isBitSet(engthByte, 6)) FlickerCodeEncoding.ASCII else FlickerCodeEncoding.BCD
}
protected open fun getLengthFromLengthByte(lengthByte: Int): Int {
return lengthByte and 0b00011111
} }
protected open fun calculateLuhnChecksum(startCode: FlickerCodeDatenelement, controlByte: String, protected open fun calculateLuhnChecksum(startCode: FlickerCodeDatenelement, controlBytes: String,
de1: FlickerCodeDatenelement, de2: FlickerCodeDatenelement, de3: FlickerCodeDatenelement): Int { de1: FlickerCodeDatenelement, de2: FlickerCodeDatenelement, de3: FlickerCodeDatenelement): Int {
val luhnData = controlByte + startCode.data + de1.data + de2.data + de3.data val luhnData = controlBytes + startCode.data + de1.data + de2.data + de3.data
val luhnSum = luhnData.mapIndexed { index, char -> val luhnSum = luhnData.mapIndexed { index, char ->
val asNumber = char.toString().toInt(16) val asNumber = char.toString().toInt(16)

View File

@ -1,5 +1,6 @@
package net.dankito.fints.tan package net.dankito.fints.tan
import net.dankito.fints.model.HHDVersion
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Test import org.junit.Test
@ -17,7 +18,7 @@ class FlickerCodeDecoderTest {
// when // when
val response = underTest.decodeChallenge(challenge) val response = underTest.decodeChallenge(challenge, HHDVersion.HHD_1_3)
// then // then
@ -33,7 +34,7 @@ class FlickerCodeDecoderTest {
// when // when
val response = underTest.decodeChallenge(challenge) val response = underTest.decodeChallenge(challenge, HHDVersion.HHD_1_3)
// then // then
@ -47,7 +48,7 @@ class FlickerCodeDecoderTest {
val challenge = "2908881696281098765432100532,00" val challenge = "2908881696281098765432100532,00"
// when // when
val result = underTest.decodeChallenge(challenge) val result = underTest.decodeChallenge(challenge, HHDVersion.HHD_1_3)
// then // then
assertThat(result.parsedDataSet).isEqualTo("1204881696280598765432101533322C30303A") assertThat(result.parsedDataSet).isEqualTo("1204881696280598765432101533322C30303A")
@ -60,10 +61,37 @@ class FlickerCodeDecoderTest {
val challenge = "100880040243" val challenge = "100880040243"
// when // when
val result = underTest.decodeChallenge(challenge) val result = underTest.decodeChallenge(challenge, HHDVersion.HHD_1_3)
// then // then
assertThat(result.parsedDataSet).isEqualTo("0604800402430B") assertThat(result.parsedDataSet).isEqualTo("0604800402430B")
} }
@Test
fun `decode HHD 1_4 Get transactions`() {
// given
val challenge = "0388A01239230124622DE26123456780987654321"
// when
val result = underTest.decodeChallenge(challenge, HHDVersion.HHD_1_4)
// then
assertThat(result.parsedDataSet).isEqualTo("1F85012392301246564445323631323334353637383039383736353433323175")
}
@Test
fun `decode HHD 1_4 Money Transfer`() {
// given
val challenge = "0438701109374422DE2612345678098765432106100,00"
// when
val result = underTest.decodeChallenge(challenge, HHDVersion.HHD_1_4)
// then
assertThat(result.parsedDataSet).isEqualTo("2584011093744F5644453236313233343536373830393837363534333231463130302C303008")
}
} }