Flickercode: - Implemented parsing ASCII - Implemented parsing data elements

This commit is contained in:
dankito 2019-10-30 22:10:13 +01:00 committed by dankito
parent faef5ace27
commit 1a7342a03b
5 changed files with 140 additions and 40 deletions

View File

@ -30,7 +30,7 @@ open class FlickercodeAnimator { // TODO: move to fints4javaLib
open fun animateFlickercode(flickercode: Flickercode, frequency: Int = DefaultFrequency, showStep: (Array<Bit>) -> Unit) {
currentFrequency = frequency
currentStepIndex = 0
val steps = FlickerCanvas(flickercode.rendered).steps
val steps = FlickerCanvas(flickercode.parsedDataSet).steps
stop() // stop may still running previous animation

View File

@ -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
)

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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")
}
}