From 1a7342a03bf6dcafa9dbcdf99d44e77e457b726a Mon Sep 17 00:00:00 2001 From: dankito Date: Wed, 30 Oct 2019 22:10:13 +0100 Subject: [PATCH] Flickercode: - Implemented parsing ASCII - Implemented parsing data elements --- .../android/util/FlickercodeAnimator.kt | 2 +- .../net/dankito/fints/tan/Flickercode.kt | 11 +- .../fints/tan/FlickercodeDatenelement.kt | 15 +++ .../dankito/fints/tan/FlickercodeDecoder.kt | 116 ++++++++++++++---- .../fints/tan/FlickercodeDecoderTest.kt | 36 +++++- 5 files changed, 140 insertions(+), 40 deletions(-) create mode 100644 fints4javaLib/src/main/kotlin/net/dankito/fints/tan/FlickercodeDatenelement.kt diff --git a/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/util/FlickercodeAnimator.kt b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/util/FlickercodeAnimator.kt index b0cc1134..f0777303 100644 --- a/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/util/FlickercodeAnimator.kt +++ b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/util/FlickercodeAnimator.kt @@ -30,7 +30,7 @@ open class FlickercodeAnimator { // TODO: move to fints4javaLib open fun animateFlickercode(flickercode: Flickercode, frequency: Int = DefaultFrequency, showStep: (Array) -> Unit) { currentFrequency = frequency currentStepIndex = 0 - val steps = FlickerCanvas(flickercode.rendered).steps + val steps = FlickerCanvas(flickercode.parsedDataSet).steps stop() // stop may still running previous animation diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/tan/Flickercode.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/tan/Flickercode.kt index 0401eeed..9b1aec90 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/tan/Flickercode.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/tan/Flickercode.kt @@ -2,13 +2,6 @@ package net.dankito.fints.tan open class Flickercode( - val challenge: String, - val challengeLength: Int, - val hasControlByte: Boolean, - val startCodeEncoding: FlickercodeEncoding, - val startCodeLength: Int, - val startCode: String, - val luhnChecksum: Int, - val xorChecksum: String, - val rendered: String + val challengeHHD_UC: String, + val parsedDataSet: String ) \ No newline at end of file diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/tan/FlickercodeDatenelement.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/tan/FlickercodeDatenelement.kt new file mode 100644 index 00000000..85fabf4f --- /dev/null +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/tan/FlickercodeDatenelement.kt @@ -0,0 +1,15 @@ +package net.dankito.fints.tan + + +open class FlickercodeDatenelement( + val lengthInByte: String, + val data: String, + val encoding: FlickercodeEncoding, + val endIndex: Int +) { + + override fun toString(): String { + return data + } + +} \ No newline at end of file diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/tan/FlickercodeDecoder.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/tan/FlickercodeDecoder.kt index f5582327..25f902e1 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/tan/FlickercodeDecoder.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/tan/FlickercodeDecoder.kt @@ -1,37 +1,43 @@ package net.dankito.fints.tan +import java.util.regex.Pattern import kotlin.math.floor open class FlickercodeDecoder { - open fun decodeChallenge(challenge: String): Flickercode { - var code = challenge.toUpperCase().replace ("[^a-fA-F0-9]", "") + companion object { + val ContainsOtherSymbolsThanFiguresPattern: Pattern = Pattern.compile("\\D") + } - val challengeLength = parseIntToHex(challenge.substring(0, 2)) - val startCodeLengthByte = parseIntToHex(challenge.substring(2, 4)) + open fun decodeChallenge(challengeHHD_UC: String): Flickercode { + var code = challengeHHD_UC.toUpperCase().replace ("[^a-fA-F0-9]", "") + + val challengeLength = parseIntToHex(challengeHHD_UC.substring(0, 2)) + + val startCodeLengthByte = parseIntToHex(challengeHHD_UC.substring(2, 4)) val hasControlByte = isBitSet(startCodeLengthByte, 7) - val startCodeEncoding = if (isBitNotSet(startCodeLengthByte, 6)) FlickercodeEncoding.BCD else FlickercodeEncoding.ASCII - val startCodeLength = startCodeLengthByte and 0b00011111 // TODO: is this correct? + val startCodeEncoding = getEncodingFromLengthByte(startCodeLengthByte) + val startCodeLength = getLengthFromLengthByte(startCodeLengthByte) val controlByte = "" // TODO (there can be multiple of them!) val startCodeStartIndex = if (hasControlByte) 6 else 4 val startCodeEndIndex = startCodeStartIndex + startCodeLength - val startCode = code.substring(startCodeStartIndex, startCodeEndIndex) - - val de1 = "" // TODO - val de2 = "" // TODO - val de3 = "" // TODO - - var luhnData = controlByte + startCode + de1 + de2 + de3 - if (luhnData.length % 2 != 0) { - luhnData = luhnData + "F" // for Luhn checksum it's required to have full bytes // TODO: should be incorrect. E.g. controlByte has to be checked / stuffed to full byte + var startCode = challengeHHD_UC.substring(startCodeStartIndex, startCodeEndIndex) + if (startCode.length % 2 != 0) { + startCode += "F" // Im Format BCD ggf. mit „F“ auf Bytegrenze ergänzt } + val de1 = parseDatenelement(challengeHHD_UC, startCodeEndIndex) + val de2 = parseDatenelement(challengeHHD_UC, de1.endIndex) + val de3 = parseDatenelement(challengeHHD_UC, de2.endIndex) + + val luhnData = controlByte + startCode + de1.data + de2.data + de3.data + val luhnSum = luhnData.mapIndexed { index, char -> - val asNumber = char.toString().toInt() + val asNumber = char.toString().toInt(16) if (index % 2 == 1) { val doubled = asNumber * 2 @@ -44,17 +50,22 @@ open class FlickercodeDecoder { val luhnChecksum = 10 - (luhnSum % 10) val countStartCodeBytes = startCodeLength / 2 - val dataWithoutLengthAndChecksum = toHex(countStartCodeBytes, 2) + controlByte + startCode + de1 + de2 + de3 // TODO add length of de1-3 (for controlByte as well?) + // TODO: + // 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 + // 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. + val dataWithoutLengthAndChecksum = toHex(countStartCodeBytes, 2) + controlByte + startCode + de1.lengthInByte + de1.data + de2.lengthInByte + de2.data + de3.lengthInByte + de3.data val dataLength = (dataWithoutLengthAndChecksum.length + 2) / 2 // + 2 for checksum val dataWithoutChecksum = toHex(dataLength, 2) + dataWithoutLengthAndChecksum - val xorByteData = dataWithoutChecksum.map { parseIntToHex(it) } var xorChecksum = 0 + val xorByteData = dataWithoutChecksum.map { parseIntToHex(it) } xorByteData.forEach { xorChecksum = xorChecksum xor it } val xorChecksumString = toHex(xorChecksum, 1) - val rendered = dataWithoutChecksum + luhnChecksum + xorChecksumString + val parsedDataSet = dataWithoutChecksum + luhnChecksum + xorChecksumString /* length check: first byte */ @@ -84,10 +95,69 @@ open class FlickercodeDecoder { code = code.substring(0, code.length - 1) + toHex(xorsum, 1) - return Flickercode(challenge, challengeLength, hasControlByte, startCodeEncoding, startCodeLength, startCode, luhnChecksum, toHex(xorChecksum, 1), rendered) + return Flickercode(challengeHHD_UC, parsedDataSet) } - open fun toHex(number: Int, minLength: Int): String { + protected open fun parseDatenelement(code: String, startIndex: Int): FlickercodeDatenelement { + val lengthByteLength = 2 + val dataElementAndRest = code.substring(startIndex) + + if (dataElementAndRest.isEmpty() || dataElementAndRest.length < lengthByteLength) { // data element not set + return FlickercodeDatenelement("", "", FlickercodeEncoding.BCD, startIndex) + } + + val lengthByteString = dataElementAndRest.substring(0, lengthByteLength) + val lengthByte = lengthByteString.toInt() + + var encoding = getEncodingFromLengthByte(lengthByte) + var dataLength = getLengthFromLengthByte(lengthByte) + + val endIndex = lengthByteLength + dataLength + var data = dataElementAndRest.substring(lengthByteLength, endIndex) + + // 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. + if (ContainsOtherSymbolsThanFiguresPattern.matcher(data).find()) { + encoding = FlickercodeEncoding.ASCII + } + + if (encoding == FlickercodeEncoding.ASCII) { + data = data.map { toHex(it.toInt(), 2) }.joinToString("") + } + + if (encoding == FlickercodeEncoding.BCD && data.length % 2 != 0) { + data += "F" // Im Format BCD ggf. mit „F“ auf Bytegrenze ergänzt + } + + dataLength = data.length + + var lengthInByte = dataLength / 2 + + if (encoding == FlickercodeEncoding.ASCII) { + if (lengthInByte < 16) { + lengthInByte += 16 // set left half byte to '1' for ASCII + } + } + + val lengthInByteString = 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 toHex(number: Int, minLength: Int): String { var result = number.toString (16).toUpperCase() while (result.length < minLength) { @@ -146,8 +216,4 @@ open class FlickercodeDecoder { return num and (1 shl bit) != 0 } - protected open fun isBitNotSet(num: Int, bit: Int): Boolean { - return num and (1 shl bit) == 0 - } - } \ No newline at end of file diff --git a/fints4javaLib/src/test/kotlin/net/dankito/fints/tan/FlickercodeDecoderTest.kt b/fints4javaLib/src/test/kotlin/net/dankito/fints/tan/FlickercodeDecoderTest.kt index 2fcaf342..ff6cf9df 100644 --- a/fints4javaLib/src/test/kotlin/net/dankito/fints/tan/FlickercodeDecoderTest.kt +++ b/fints4javaLib/src/test/kotlin/net/dankito/fints/tan/FlickercodeDecoderTest.kt @@ -10,7 +10,7 @@ class FlickercodeDecoderTest { @Test - fun decodeChallenge_ExampleFromChapter_C_4_1_BCD_without_ControlByte() { + fun decodeChallenge_ExampleFromChapter_C_4_1_BCD_without_ControlByte_and_DataElements() { // given val challenge = "070A2082901998" @@ -21,10 +21,36 @@ class FlickercodeDecoderTest { // then - assertThat(response.startCode).isEqualTo("2082901998") - assertThat(response.luhnChecksum).isEqualTo(1) - assertThat(response.xorChecksum).isEqualTo("A") - assertThat(response.rendered).isEqualTo("070520829019981A") + assertThat(response.parsedDataSet).isEqualTo("070520829019981A") + } + + + @Test + fun decodeChallenge_ExampleFromChapter_C_4_1_without_ControlByte() { + + // given + val challenge = "070A208290199872IE99BOFI" + + + // when + val response = underTest.decodeChallenge(challenge) + + + // then + assertThat(response.parsedDataSet).isEqualTo("100520829019981849453939424F46494B") + } + + @Test + fun decodeChallenge_AmountInASCII() { + + // given + val challenge = "2908881696281098765432100532,00" + + // when + val result = underTest.decodeChallenge(challenge) + + // then + assertThat(result.parsedDataSet).isEqualTo("1204881696280598765432101533322C30303A") } } \ No newline at end of file