From c20bf13c5c17f1801e7fea335567ccc2e7b5ec81 Mon Sep 17 00:00:00 2001 From: dankl Date: Sun, 13 Oct 2019 02:43:28 +0200 Subject: [PATCH] Implemented parsing TAN response (HITAN) --- .../fints/response/InstituteSegmentId.kt | 2 + .../net/dankito/fints/response/Response.kt | 10 ++ .../dankito/fints/response/ResponseParser.kt | 43 ++++++- .../fints/response/segments/TanResponse.kt | 49 ++++++++ .../kotlin/net/dankito/fints/FinTsTestBase.kt | 4 + .../fints/response/ResponseParserTest.kt | 109 +++++++++++++----- 6 files changed, 188 insertions(+), 29 deletions(-) create mode 100644 fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/TanResponse.kt diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/InstituteSegmentId.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/InstituteSegmentId.kt index 341d0ddc..e1df0d1d 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/InstituteSegmentId.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/InstituteSegmentId.kt @@ -17,6 +17,8 @@ enum class InstituteSegmentId(override val id: String) : ISegmentId { TanInfo("HITANS"), + Tan("HITAN"), + Balance("HISAL"), AccountTransactionsMt940("HIKAZ") diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/Response.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/Response.kt index 1cb2c828..bc862962 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/Response.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/Response.kt @@ -5,6 +5,7 @@ import net.dankito.fints.messages.segmente.id.ISegmentId import net.dankito.fints.messages.segmente.id.MessageSegmentId import net.dankito.fints.response.segments.ReceivedMessageHeader import net.dankito.fints.response.segments.ReceivedSegment +import net.dankito.fints.response.segments.TanResponse open class Response( @@ -18,6 +19,15 @@ open class Response( open val successful: Boolean get() = didReceiveResponse && didResponseContainErrors == false + open val isStrongAuthenticationRequired: Boolean + get() { + getFirstSegmentById(InstituteSegmentId.Tan)?.let { tanResponse -> + return tanResponse.isStrongAuthenticationRequired + } + + return false + } + open val messageHeader: ReceivedMessageHeader? get() = getFirstSegmentById(MessageSegmentId.MessageHeader) diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/ResponseParser.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/ResponseParser.kt index 2562e78f..9c1b187b 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/ResponseParser.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/ResponseParser.kt @@ -84,7 +84,9 @@ open class ResponseParser @JvmOverloads constructor( InstituteSegmentId.UserParameters.id -> parseUserParameters(segment, dataElementGroups) InstituteSegmentId.AccountInfo.id -> parseAccountInfo(segment, dataElementGroups) + InstituteSegmentId.TanInfo.id -> parseTanInfo(segment, dataElementGroups) + InstituteSegmentId.Tan.id -> parseTanResponse(segment, dataElementGroups) InstituteSegmentId.Balance.id -> parseBalanceSegment(segment, dataElementGroups) InstituteSegmentId.AccountTransactionsMt940.id -> parseMt940AccountTransactions(segment, dataElementGroups) @@ -249,6 +251,23 @@ open class ResponseParser @JvmOverloads constructor( } + protected open fun parseTanResponse(segment: String, dataElementGroups: List): TanResponse { + val binaryJobHashValue = if (dataElementGroups.size > 2) parseStringToNullIfEmpty(dataElementGroups[2]) else null + val binaryChallengeHHD_UC = if (dataElementGroups.size > 5) parseStringToNullIfEmpty(dataElementGroups[5]) else null + + return TanResponse( + parseCodeEnum(dataElementGroups[1], TanProcess.values()), + binaryJobHashValue?.let { extractBinaryData(it) }, + if (dataElementGroups.size > 3) parseStringToNullIfEmpty(dataElementGroups[3]) else null, + if (dataElementGroups.size > 4) parseStringToNullIfEmpty(dataElementGroups[4]) else null, + binaryChallengeHHD_UC?.let { extractBinaryData(it) }, + if (dataElementGroups.size > 6) parseNullableDateTime(dataElementGroups[6]) else null, + if (dataElementGroups.size > 7) parseStringToNullIfEmpty(dataElementGroups[7]) else null, + segment + ) + } + + protected open fun parseBalanceSegment(segment: String, dataElementGroups: List): BalanceSegment { // dataElementGroups[1] is account details @@ -487,13 +506,27 @@ open class ResponseParser @JvmOverloads constructor( return amount } + protected open fun parseNullableDateTime(dataElementGroup: String): Date? { + val dataElements = getDataElements(dataElementGroup) + + if (dataElements.size >= 2) { + parseNullableDate(dataElements[0])?.let { date -> + parseNullableTime(dataElements[1])?.let { time -> + return Date(date.time + time.time) + } + } + } + + return null + } + protected open fun parseDate(dateString: String): Date { return Datum.HbciDateFormat.parse(dateString) } protected open fun parseNullableDate(dateString: String): Date? { try { - return Datum.HbciDateFormat.parse(dateString) + return parseDate(dateString) } catch (ignored: Exception) { } return null @@ -503,6 +536,14 @@ open class ResponseParser @JvmOverloads constructor( return Uhrzeit.HbciTimeFormat.parse(timeString) } + protected open fun parseNullableTime(timeString: String): Date? { + try { + return parseTime(timeString) + } catch (ignored: Exception) { } + + return null + } + protected open fun extractBinaryData(binaryData: String): String { if (binaryData.startsWith('@')) { val headerEndIndex = binaryData.indexOf('@', 2) diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/TanResponse.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/TanResponse.kt new file mode 100644 index 00000000..e72c1d58 --- /dev/null +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/TanResponse.kt @@ -0,0 +1,49 @@ +package net.dankito.fints.response.segments + +import net.dankito.fints.messages.datenelemente.implementierte.tan.TanProcess +import java.util.* + + +open class TanResponse( + val tanProcess: TanProcess, + val jobHashValue: String?, // M: bei Auftrags-Hashwertverfahren<>0 und TAN-Prozess=1. N: sonst + val jobReference: String?, // M: bei TAN-Prozess=2, 3, 4. O: bei TAN-Prozess=1 + + /** + * Dieses Datenelement enthält im Falle des Zwei-Schritt-TAN-Verfahrens die Challenge zu einem + * eingereichten Auftrag. Aus der Challenge wird vom Kunden die eigentliche TAN ermittelt. + * Die Challenge wird unabhängig vom Prozessvariante 1 oder 2 in der Kreditinstitutsantwort im + * Segment HITAN übermittelt. + * + * Ist der BPD-Parameter „Challenge strukturiert“ mit „J“ belegt, so können im Text folgende + * Formatsteuerzeichen enthalten sein, die kundenseitig entsprechend zu interpretieren sind. + * Eine Kaskadierung von Steuerzeichen ist nicht erlaubt. + * + *
Zeilenumbruch + *

Neuer Absatz + * ... Fettdruck + * ... Kursivdruck + * ... Unterstreichen + *

    ...
Beginn / Ende Aufzählung + *
    ...
Beginn / Ende Nummerierte Liste + *
  • ...
  • Listenelement einer Aufzählung / Nummerierten Liste + */ + val challenge: String?, // M: bei TAN-Prozess=1, 3, 4. O: bei TAN-Prozess=2 + + val challengeHHD_UC: String?, + val validityDateTimeForChallenge: Date?, + val tanMediaIdentifier: String? = null, // M: bei TAN-Prozess=1, 3, 4 und „Anzahl unterstützter aktiver TAN-Medien“ nicht vorhanden. O: sonst + + segmentString: String +) : + ReceivedSegment(segmentString) { + + companion object { + const val NoChallengeResponse = "nochallenge" + const val NoJobReferenceResponse = "noref" + } + + open val isStrongAuthenticationRequired: Boolean + get() = challenge != NoChallengeResponse + +} \ No newline at end of file diff --git a/fints4javaLib/src/test/kotlin/net/dankito/fints/FinTsTestBase.kt b/fints4javaLib/src/test/kotlin/net/dankito/fints/FinTsTestBase.kt index 79d9347d..dcfbc2fd 100644 --- a/fints4javaLib/src/test/kotlin/net/dankito/fints/FinTsTestBase.kt +++ b/fints4javaLib/src/test/kotlin/net/dankito/fints/FinTsTestBase.kt @@ -52,6 +52,10 @@ abstract class FinTsTestBase { return Datum.HbciDateFormat.format(date) } + protected open fun unmaskString(string: String): String { + return string.replace("?'", "'").replace("?+", "+").replace("?:", ":") + } + protected open fun normalizeBinaryData(message: String): String { return message.replace(0.toChar(), ' ') } diff --git a/fints4javaLib/src/test/kotlin/net/dankito/fints/response/ResponseParserTest.kt b/fints4javaLib/src/test/kotlin/net/dankito/fints/response/ResponseParserTest.kt index 0b62d918..17d5000a 100644 --- a/fints4javaLib/src/test/kotlin/net/dankito/fints/response/ResponseParserTest.kt +++ b/fints4javaLib/src/test/kotlin/net/dankito/fints/response/ResponseParserTest.kt @@ -5,6 +5,7 @@ import net.dankito.fints.messages.datenelemente.implementierte.Dialogsprache import net.dankito.fints.messages.datenelemente.implementierte.HbciVersion import net.dankito.fints.messages.datenelemente.implementierte.signatur.Sicherheitsverfahren import net.dankito.fints.messages.datenelemente.implementierte.signatur.VersionDesSicherheitsverfahrens +import net.dankito.fints.messages.datenelemente.implementierte.tan.TanProcess import net.dankito.fints.messages.datenelementgruppen.implementierte.signatur.Sicherheitsprofil import net.dankito.fints.messages.segmente.id.ISegmentId import net.dankito.fints.messages.segmente.id.MessageSegmentId @@ -214,34 +215,6 @@ class ResponseParserTest : FinTsTestBase() { } - @Test - fun parseBalance() { - - // given - val balance = 1234.56.toBigDecimal() - val date = LocalDate.of(1988, 3, 27).asUtilDate() - val bankCode = "12345678" - val accountId = "0987654321" - val accountProductName = "Sichteinlagen" - - // when - val result = underTest.parse("HISAL:8:5:3+$accountId::280:$bankCode+$accountProductName+EUR+" + - "C:${convertAmount(balance)}:EUR:${convertDate(date)}+C:0,:EUR:20191006++${convertAmount(balance)}:EUR") - - // then - assertSuccessfullyParsedSegment(result, InstituteSegmentId.Balance, 8, 5, 3) - - result.getFirstSegmentById(InstituteSegmentId.Balance)?.let { segment -> - assertThat(segment.balance).isEqualTo(balance) - assertThat(segment.currency).isEqualTo("EUR") - assertThat(segment.date).isEqualTo(date) - assertThat(segment.accountProductName).isEqualTo(accountProductName) - assertThat(segment.balanceOfPreBookedTransactions).isNull() - } - ?: run { Assert.fail("No segment of type Balance found in ${result.receivedSegments}") } - } - - @Test fun parseTanInfo() { @@ -267,6 +240,86 @@ class ResponseParserTest : FinTsTestBase() { ?: run { Assert.fail("No segment of type TanInfo found in ${result.receivedSegments}") } } + @Test + fun parseTanResponse_NoStrongAuthenticationRequired() { + + // when + val result = underTest.parse("HITAN:6:6:5+4++noref+nochallenge") + + // then + assertSuccessfullyParsedSegment(result, InstituteSegmentId.Tan, 6, 6, 5) + + assertThat(result.isStrongAuthenticationRequired).isFalse() + + result.getFirstSegmentById(InstituteSegmentId.Tan)?.let { segment -> + assertThat(segment.tanProcess).isEqualTo(TanProcess.TanProcess4) + assertThat(segment.jobHashValue).isNull() + assertThat(segment.jobReference).isEqualTo(TanResponse.NoJobReferenceResponse) + assertThat(segment.challenge).isEqualTo(TanResponse.NoChallengeResponse) + assertThat(segment.challengeHHD_UC).isNull() + assertThat(segment.validityDateTimeForChallenge).isNull() + assertThat(segment.tanMediaIdentifier).isNull() + } + ?: run { Assert.fail("No segment of type TanResponse found in ${result.receivedSegments}") } + } + + @Test + fun parseTanResponse_StrongAuthenticationRequired() { + + // given + val jobReference = "4937-10-13-02.30.03.700259" + val challenge = "Sie möchten eine \"Umsatzabfrage\" freigeben?: Bitte bestätigen Sie den \"Startcode 80085335\" mit der Taste \"OK\"." + val challengeHHD_UC = "100880085335" + val tanMediaIdentifier = "Kartennummer ******0892" + + // when + val result = underTest.parse("'HITAN:5:6:4+4++$jobReference+$challenge+@12@$challengeHHD_UC++$tanMediaIdentifier'") + + // then + assertSuccessfullyParsedSegment(result, InstituteSegmentId.Tan, 5, 6, 4) + + assertThat(result.isStrongAuthenticationRequired).isTrue() + + result.getFirstSegmentById(InstituteSegmentId.Tan)?.let { segment -> + assertThat(segment.tanProcess).isEqualTo(TanProcess.TanProcess4) + assertThat(segment.jobHashValue).isNull() + assertThat(segment.jobReference).isEqualTo(jobReference) + assertThat(segment.challenge).isEqualTo(unmaskString(challenge)) + assertThat(segment.challengeHHD_UC).isEqualTo(challengeHHD_UC) + assertThat(segment.validityDateTimeForChallenge).isNull() + assertThat(segment.tanMediaIdentifier).isEqualTo(tanMediaIdentifier) + } + ?: run { Assert.fail("No segment of type TanResponse found in ${result.receivedSegments}") } + } + + + @Test + fun parseBalance() { + + // given + val balance = 1234.56.toBigDecimal() + val date = LocalDate.of(1988, 3, 27).asUtilDate() + val bankCode = "12345678" + val accountId = "0987654321" + val accountProductName = "Sichteinlagen" + + // when + val result = underTest.parse("HISAL:8:5:3+$accountId::280:$bankCode+$accountProductName+EUR+" + + "C:${convertAmount(balance)}:EUR:${convertDate(date)}+C:0,:EUR:20191006++${convertAmount(balance)}:EUR") + + // then + assertSuccessfullyParsedSegment(result, InstituteSegmentId.Balance, 8, 5, 3) + + result.getFirstSegmentById(InstituteSegmentId.Balance)?.let { segment -> + assertThat(segment.balance).isEqualTo(balance) + assertThat(segment.currency).isEqualTo("EUR") + assertThat(segment.date).isEqualTo(date) + assertThat(segment.accountProductName).isEqualTo(accountProductName) + assertThat(segment.balanceOfPreBookedTransactions).isNull() + } + ?: run { Assert.fail("No segment of type Balance found in ${result.receivedSegments}") } + } + private fun assertSuccessfullyParsedSegment(result: Response, segmentId: ISegmentId, segmentNumber: Int, segmentVersion: Int, referenceSegmentNumber: Int? = null) {