Implemented decoding HHD v1.3 codes (at least BCD encoded ones, no support for ASCII yet)
This commit is contained in:
parent
3c7d897d52
commit
826d3cefa2
|
@ -0,0 +1,14 @@
|
||||||
|
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
|
||||||
|
)
|
|
@ -5,9 +5,58 @@ import kotlin.math.floor
|
||||||
|
|
||||||
open class FlickercodeDecoder {
|
open class FlickercodeDecoder {
|
||||||
|
|
||||||
open fun decodeChallenge(challenge: String): String {
|
open fun decodeChallenge(challenge: String): Flickercode {
|
||||||
var code = challenge.toUpperCase().replace ("[^a-fA-F0-9]", "")
|
var code = challenge.toUpperCase().replace ("[^a-fA-F0-9]", "")
|
||||||
|
|
||||||
|
val challengeLength = parseIntToHex(challenge.substring(0, 2))
|
||||||
|
|
||||||
|
val startCodeLengthByte = parseIntToHex(challenge.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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
val luhnSum = luhnData.mapIndexed { index, char ->
|
||||||
|
val asNumber = char.toString().toInt()
|
||||||
|
|
||||||
|
if (index % 2 == 1) {
|
||||||
|
val doubled = asNumber * 2
|
||||||
|
return@mapIndexed (doubled / 10) + (doubled % 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
asNumber
|
||||||
|
}.sum()
|
||||||
|
|
||||||
|
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?)
|
||||||
|
val dataLength = (dataWithoutLengthAndChecksum.length + 2) / 2 // + 2 for checksum
|
||||||
|
val dataWithoutChecksum = toHex(dataLength, 2) + dataWithoutLengthAndChecksum
|
||||||
|
val xorByteData = dataWithoutChecksum.map { parseIntToHex(it) }
|
||||||
|
|
||||||
|
var xorChecksum = 0
|
||||||
|
xorByteData.forEach { xorChecksum = xorChecksum xor it }
|
||||||
|
|
||||||
|
val xorChecksumString = toHex(xorChecksum, 1)
|
||||||
|
|
||||||
|
val rendered = dataWithoutChecksum + luhnChecksum + xorChecksumString
|
||||||
|
|
||||||
|
|
||||||
/* length check: first byte */
|
/* length check: first byte */
|
||||||
val len = code.length / 2 - 1
|
val len = code.length / 2 - 1
|
||||||
code = toHex(len, 2) + code.substring(2)
|
code = toHex(len, 2) + code.substring(2)
|
||||||
|
@ -35,7 +84,7 @@ open class FlickercodeDecoder {
|
||||||
|
|
||||||
code = code.substring(0, code.length - 1) + toHex(xorsum, 1)
|
code = code.substring(0, code.length - 1) + toHex(xorsum, 1)
|
||||||
|
|
||||||
return code
|
return Flickercode(challenge, challengeLength, hasControlByte, startCodeEncoding, startCodeLength, startCode, luhnChecksum, toHex(xorChecksum, 1), rendered)
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun toHex(number: Int, minLength: Int): String {
|
open fun toHex(number: Int, minLength: Int): String {
|
||||||
|
@ -93,4 +142,12 @@ open class FlickercodeDecoder {
|
||||||
return Integer.parseInt(string, 16)
|
return Integer.parseInt(string, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected open fun isBitSet(num: Int, bit: Int): Boolean {
|
||||||
|
return num and (1 shl bit) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun isBitNotSet(num: Int, bit: Int): Boolean {
|
||||||
|
return num and (1 shl bit) == 0
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package net.dankito.fints.tan
|
||||||
|
|
||||||
|
|
||||||
|
enum class FlickercodeEncoding {
|
||||||
|
|
||||||
|
BCD,
|
||||||
|
|
||||||
|
ASCII
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package net.dankito.fints.tan
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
||||||
|
class FlickercodeDecoderTest {
|
||||||
|
|
||||||
|
private val underTest = FlickercodeDecoder()
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun decodeChallenge_ExampleFromChapter_C_4_1_BCD_without_ControlByte() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val challenge = "070A2082901998"
|
||||||
|
|
||||||
|
|
||||||
|
// when
|
||||||
|
val response = underTest.decodeChallenge(challenge)
|
||||||
|
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(response.startCode).isEqualTo("2082901998")
|
||||||
|
assertThat(response.luhnChecksum).isEqualTo(1)
|
||||||
|
assertThat(response.xorChecksum).isEqualTo("A")
|
||||||
|
assertThat(response.rendered).isEqualTo("070520829019981A")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue