fints4k/fints4javaLib/src/main/kotlin/net/dankito/fints/tan/FlickercodeDecoder.kt

170 lines
No EOL
6.4 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package net.dankito.fints.tan
import org.slf4j.LoggerFactory
import java.util.regex.Pattern
open class FlickercodeDecoder {
companion object {
val ContainsOtherSymbolsThanFiguresPattern: Pattern = Pattern.compile("\\D")
private val log = LoggerFactory.getLogger(FlickercodeDecoder::class.java)
}
open fun decodeChallenge(challengeHHD_UC: String): Flickercode {
try {
val challengeLength = parseIntToHex(challengeHHD_UC.substring(0, 2))
val startCode = parseStartCode(challengeHHD_UC, 2)
val controlByte = "" // TODO (there can be multiple of them!)
val de1 = parseDatenelement(challengeHHD_UC, startCode.endIndex)
val de2 = parseDatenelement(challengeHHD_UC, de1.endIndex)
val de3 = parseDatenelement(challengeHHD_UC, de2.endIndex)
val luhnChecksum = calculateLuhnChecksum(startCode, controlByte, de1, de2, de3)
// 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 = startCode.lengthInByte + 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 xorChecksumString = calculateXorChecksum(dataWithoutChecksum)
val parsedDataSet = dataWithoutChecksum + luhnChecksum + xorChecksumString
return Flickercode(challengeHHD_UC, parsedDataSet)
} catch (e: Exception) {
log.error("Could not decode challenge $challengeHHD_UC")
return Flickercode(challengeHHD_UC, "", e)
}
}
protected fun parseStartCode(challengeHHD_UC: String, startIndex: Int): FlickercodeDatenelement {
return parseDatenelement(challengeHHD_UC, startIndex) { lengthByteString -> parseIntToHex(lengthByteString) }
}
protected open fun parseDatenelement(code: String, startIndex: Int): FlickercodeDatenelement {
return parseDatenelement(code, startIndex) { lengthByteString -> lengthByteString.toInt() }
}
protected open fun parseDatenelement(code: String, startIndex: Int, lengthParser: (lengthByteString: String) -> 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 = lengthParser(lengthByteString)
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 calculateLuhnChecksum(startCode: FlickercodeDatenelement, controlByte: String,
de1: FlickercodeDatenelement, de2: FlickercodeDatenelement, de3: FlickercodeDatenelement): Int {
val luhnData = controlByte + startCode.data + de1.data + de2.data + de3.data
val luhnSum = luhnData.mapIndexed { index, char ->
val asNumber = char.toString().toInt(16)
if (index % 2 == 1) {
val doubled = asNumber * 2
return@mapIndexed (doubled / 10) + (doubled % 10)
}
asNumber
}.sum()
return 10 - (luhnSum % 10)
}
protected open fun calculateXorChecksum(dataWithoutChecksum: String): String {
var xorChecksum = 0
val xorByteData = dataWithoutChecksum.map { parseIntToHex(it) }
xorByteData.forEach { xorChecksum = xorChecksum xor it }
return toHex(xorChecksum, 1)
}
protected open fun toHex(number: Int, minLength: Int): String {
var result = number.toString (16).toUpperCase()
while (result.length < minLength) {
result = '0' + result
}
return result
}
protected open fun parseIntToHex(char: Char): Int {
return parseIntToHex(char.toString())
}
protected open fun parseIntToHex(string: String): Int {
return Integer.parseInt(string, 16)
}
protected open fun isBitSet(num: Int, bit: Int): Boolean {
return num and (1 shl bit) != 0
}
}