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) {
|
open fun animateFlickercode(flickercode: Flickercode, frequency: Int = DefaultFrequency, showStep: (Array<Bit>) -> Unit) {
|
||||||
currentFrequency = frequency
|
currentFrequency = frequency
|
||||||
currentStepIndex = 0
|
currentStepIndex = 0
|
||||||
val steps = FlickerCanvas(flickercode.rendered).steps
|
val steps = FlickerCanvas(flickercode.parsedDataSet).steps
|
||||||
|
|
||||||
stop() // stop may still running previous animation
|
stop() // stop may still running previous animation
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,6 @@ package net.dankito.fints.tan
|
||||||
|
|
||||||
|
|
||||||
open class Flickercode(
|
open class Flickercode(
|
||||||
val challenge: String,
|
val challengeHHD_UC: String,
|
||||||
val challengeLength: Int,
|
val parsedDataSet: String
|
||||||
val hasControlByte: Boolean,
|
|
||||||
val startCodeEncoding: FlickercodeEncoding,
|
|
||||||
val startCodeLength: Int,
|
|
||||||
val startCode: String,
|
|
||||||
val luhnChecksum: Int,
|
|
||||||
val xorChecksum: String,
|
|
||||||
val rendered: 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
|
package net.dankito.fints.tan
|
||||||
|
|
||||||
|
import java.util.regex.Pattern
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
|
||||||
|
|
||||||
open class FlickercodeDecoder {
|
open class FlickercodeDecoder {
|
||||||
|
|
||||||
open fun decodeChallenge(challenge: String): Flickercode {
|
companion object {
|
||||||
var code = challenge.toUpperCase().replace ("[^a-fA-F0-9]", "")
|
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 hasControlByte = isBitSet(startCodeLengthByte, 7)
|
||||||
val startCodeEncoding = if (isBitNotSet(startCodeLengthByte, 6)) FlickercodeEncoding.BCD else FlickercodeEncoding.ASCII
|
val startCodeEncoding = getEncodingFromLengthByte(startCodeLengthByte)
|
||||||
val startCodeLength = startCodeLengthByte and 0b00011111 // TODO: is this correct?
|
val startCodeLength = getLengthFromLengthByte(startCodeLengthByte)
|
||||||
|
|
||||||
val controlByte = "" // TODO (there can be multiple of them!)
|
val controlByte = "" // TODO (there can be multiple of them!)
|
||||||
|
|
||||||
val startCodeStartIndex = if (hasControlByte) 6 else 4
|
val startCodeStartIndex = if (hasControlByte) 6 else 4
|
||||||
val startCodeEndIndex = startCodeStartIndex + startCodeLength
|
val startCodeEndIndex = startCodeStartIndex + startCodeLength
|
||||||
val startCode = code.substring(startCodeStartIndex, startCodeEndIndex)
|
var startCode = challengeHHD_UC.substring(startCodeStartIndex, startCodeEndIndex)
|
||||||
|
if (startCode.length % 2 != 0) {
|
||||||
val de1 = "" // TODO
|
startCode += "F" // Im Format BCD ggf. mit „F“ auf Bytegrenze ergänzt
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 luhnSum = luhnData.mapIndexed { index, char ->
|
||||||
val asNumber = char.toString().toInt()
|
val asNumber = char.toString().toInt(16)
|
||||||
|
|
||||||
if (index % 2 == 1) {
|
if (index % 2 == 1) {
|
||||||
val doubled = asNumber * 2
|
val doubled = asNumber * 2
|
||||||
|
@ -44,17 +50,22 @@ open class FlickercodeDecoder {
|
||||||
val luhnChecksum = 10 - (luhnSum % 10)
|
val luhnChecksum = 10 - (luhnSum % 10)
|
||||||
|
|
||||||
val countStartCodeBytes = startCodeLength / 2
|
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 dataLength = (dataWithoutLengthAndChecksum.length + 2) / 2 // + 2 for checksum
|
||||||
val dataWithoutChecksum = toHex(dataLength, 2) + dataWithoutLengthAndChecksum
|
val dataWithoutChecksum = toHex(dataLength, 2) + dataWithoutLengthAndChecksum
|
||||||
val xorByteData = dataWithoutChecksum.map { parseIntToHex(it) }
|
|
||||||
|
|
||||||
var xorChecksum = 0
|
var xorChecksum = 0
|
||||||
|
val xorByteData = dataWithoutChecksum.map { parseIntToHex(it) }
|
||||||
xorByteData.forEach { xorChecksum = xorChecksum xor it }
|
xorByteData.forEach { xorChecksum = xorChecksum xor it }
|
||||||
|
|
||||||
val xorChecksumString = toHex(xorChecksum, 1)
|
val xorChecksumString = toHex(xorChecksum, 1)
|
||||||
|
|
||||||
val rendered = dataWithoutChecksum + luhnChecksum + xorChecksumString
|
val parsedDataSet = dataWithoutChecksum + luhnChecksum + xorChecksumString
|
||||||
|
|
||||||
|
|
||||||
/* length check: first byte */
|
/* length check: first byte */
|
||||||
|
@ -84,10 +95,69 @@ open class FlickercodeDecoder {
|
||||||
|
|
||||||
code = code.substring(0, code.length - 1) + toHex(xorsum, 1)
|
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()
|
var result = number.toString (16).toUpperCase()
|
||||||
|
|
||||||
while (result.length < minLength) {
|
while (result.length < minLength) {
|
||||||
|
@ -146,8 +216,4 @@ open class FlickercodeDecoder {
|
||||||
return num and (1 shl bit) != 0
|
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
|
@Test
|
||||||
fun decodeChallenge_ExampleFromChapter_C_4_1_BCD_without_ControlByte() {
|
fun decodeChallenge_ExampleFromChapter_C_4_1_BCD_without_ControlByte_and_DataElements() {
|
||||||
|
|
||||||
// given
|
// given
|
||||||
val challenge = "070A2082901998"
|
val challenge = "070A2082901998"
|
||||||
|
@ -21,10 +21,36 @@ class FlickercodeDecoderTest {
|
||||||
|
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(response.startCode).isEqualTo("2082901998")
|
assertThat(response.parsedDataSet).isEqualTo("070520829019981A")
|
||||||
assertThat(response.luhnChecksum).isEqualTo(1)
|
}
|
||||||
assertThat(response.xorChecksum).isEqualTo("A")
|
|
||||||
assertThat(response.rendered).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