Flickercode: - Implemented parsing ASCII - Implemented parsing data elements
This commit is contained in:
parent
faef5ace27
commit
1a7342a03b
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue