Implemented parsing TAN response (HITAN)
This commit is contained in:
parent
3ccef79596
commit
c20bf13c5c
|
@ -17,6 +17,8 @@ enum class InstituteSegmentId(override val id: String) : ISegmentId {
|
||||||
|
|
||||||
TanInfo("HITANS"),
|
TanInfo("HITANS"),
|
||||||
|
|
||||||
|
Tan("HITAN"),
|
||||||
|
|
||||||
Balance("HISAL"),
|
Balance("HISAL"),
|
||||||
|
|
||||||
AccountTransactionsMt940("HIKAZ")
|
AccountTransactionsMt940("HIKAZ")
|
||||||
|
|
|
@ -5,6 +5,7 @@ import net.dankito.fints.messages.segmente.id.ISegmentId
|
||||||
import net.dankito.fints.messages.segmente.id.MessageSegmentId
|
import net.dankito.fints.messages.segmente.id.MessageSegmentId
|
||||||
import net.dankito.fints.response.segments.ReceivedMessageHeader
|
import net.dankito.fints.response.segments.ReceivedMessageHeader
|
||||||
import net.dankito.fints.response.segments.ReceivedSegment
|
import net.dankito.fints.response.segments.ReceivedSegment
|
||||||
|
import net.dankito.fints.response.segments.TanResponse
|
||||||
|
|
||||||
|
|
||||||
open class Response(
|
open class Response(
|
||||||
|
@ -18,6 +19,15 @@ open class Response(
|
||||||
open val successful: Boolean
|
open val successful: Boolean
|
||||||
get() = didReceiveResponse && didResponseContainErrors == false
|
get() = didReceiveResponse && didResponseContainErrors == false
|
||||||
|
|
||||||
|
open val isStrongAuthenticationRequired: Boolean
|
||||||
|
get() {
|
||||||
|
getFirstSegmentById<TanResponse>(InstituteSegmentId.Tan)?.let { tanResponse ->
|
||||||
|
return tanResponse.isStrongAuthenticationRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
open val messageHeader: ReceivedMessageHeader?
|
open val messageHeader: ReceivedMessageHeader?
|
||||||
get() = getFirstSegmentById(MessageSegmentId.MessageHeader)
|
get() = getFirstSegmentById(MessageSegmentId.MessageHeader)
|
||||||
|
|
|
@ -84,7 +84,9 @@ open class ResponseParser @JvmOverloads constructor(
|
||||||
|
|
||||||
InstituteSegmentId.UserParameters.id -> parseUserParameters(segment, dataElementGroups)
|
InstituteSegmentId.UserParameters.id -> parseUserParameters(segment, dataElementGroups)
|
||||||
InstituteSegmentId.AccountInfo.id -> parseAccountInfo(segment, dataElementGroups)
|
InstituteSegmentId.AccountInfo.id -> parseAccountInfo(segment, dataElementGroups)
|
||||||
|
|
||||||
InstituteSegmentId.TanInfo.id -> parseTanInfo(segment, dataElementGroups)
|
InstituteSegmentId.TanInfo.id -> parseTanInfo(segment, dataElementGroups)
|
||||||
|
InstituteSegmentId.Tan.id -> parseTanResponse(segment, dataElementGroups)
|
||||||
|
|
||||||
InstituteSegmentId.Balance.id -> parseBalanceSegment(segment, dataElementGroups)
|
InstituteSegmentId.Balance.id -> parseBalanceSegment(segment, dataElementGroups)
|
||||||
InstituteSegmentId.AccountTransactionsMt940.id -> parseMt940AccountTransactions(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<String>): 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<String>): BalanceSegment {
|
protected open fun parseBalanceSegment(segment: String, dataElementGroups: List<String>): BalanceSegment {
|
||||||
// dataElementGroups[1] is account details
|
// dataElementGroups[1] is account details
|
||||||
|
|
||||||
|
@ -487,13 +506,27 @@ open class ResponseParser @JvmOverloads constructor(
|
||||||
return amount
|
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 {
|
protected open fun parseDate(dateString: String): Date {
|
||||||
return Datum.HbciDateFormat.parse(dateString)
|
return Datum.HbciDateFormat.parse(dateString)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun parseNullableDate(dateString: String): Date? {
|
protected open fun parseNullableDate(dateString: String): Date? {
|
||||||
try {
|
try {
|
||||||
return Datum.HbciDateFormat.parse(dateString)
|
return parseDate(dateString)
|
||||||
} catch (ignored: Exception) { }
|
} catch (ignored: Exception) { }
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
@ -503,6 +536,14 @@ open class ResponseParser @JvmOverloads constructor(
|
||||||
return Uhrzeit.HbciTimeFormat.parse(timeString)
|
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 {
|
protected open fun extractBinaryData(binaryData: String): String {
|
||||||
if (binaryData.startsWith('@')) {
|
if (binaryData.startsWith('@')) {
|
||||||
val headerEndIndex = binaryData.indexOf('@', 2)
|
val headerEndIndex = binaryData.indexOf('@', 2)
|
||||||
|
|
|
@ -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.
|
||||||
|
*
|
||||||
|
* <br> Zeilenumbruch
|
||||||
|
* <p> Neuer Absatz
|
||||||
|
* <b> ... </b> Fettdruck
|
||||||
|
* <i> ... </i> Kursivdruck
|
||||||
|
* <u> ... </u> Unterstreichen
|
||||||
|
* <ul> ... </ul> Beginn / Ende Aufzählung
|
||||||
|
* <ol> ... </ol> Beginn / Ende Nummerierte Liste
|
||||||
|
* <li> ... </li> 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
|
||||||
|
|
||||||
|
}
|
|
@ -52,6 +52,10 @@ abstract class FinTsTestBase {
|
||||||
return Datum.HbciDateFormat.format(date)
|
return Datum.HbciDateFormat.format(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected open fun unmaskString(string: String): String {
|
||||||
|
return string.replace("?'", "'").replace("?+", "+").replace("?:", ":")
|
||||||
|
}
|
||||||
|
|
||||||
protected open fun normalizeBinaryData(message: String): String {
|
protected open fun normalizeBinaryData(message: String): String {
|
||||||
return message.replace(0.toChar(), ' ')
|
return message.replace(0.toChar(), ' ')
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.HbciVersion
|
||||||
import net.dankito.fints.messages.datenelemente.implementierte.signatur.Sicherheitsverfahren
|
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.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.datenelementgruppen.implementierte.signatur.Sicherheitsprofil
|
||||||
import net.dankito.fints.messages.segmente.id.ISegmentId
|
import net.dankito.fints.messages.segmente.id.ISegmentId
|
||||||
import net.dankito.fints.messages.segmente.id.MessageSegmentId
|
import net.dankito.fints.messages.segmente.id.MessageSegmentId
|
||||||
|
@ -214,6 +215,84 @@ class ResponseParserTest : FinTsTestBase() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun parseTanInfo() {
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.parse("HITANS:171:6:4+1+1+1+J:N:0:910:2:HHD1.3.0:::chipTAN manuell:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:911:2:HHD1.3.2OPT:HHDOPT1:1.3.2:chipTAN optisch:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:912:2:HHD1.3.2USB:HHDUSB1:1.3.2:chipTAN-USB:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:913:2:Q1S:Secoder_UC:1.2.0:chipTAN-QR:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:920:2:smsTAN:::smsTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:2:N:5:921:2:pushTAN:::pushTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:2:N:2:900:2:iTAN:::iTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:0'")
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertSuccessfullyParsedSegment(result, InstituteSegmentId.TanInfo, 171, 6, 4)
|
||||||
|
|
||||||
|
result.getFirstSegmentById<TanInfo>(InstituteSegmentId.TanInfo)?.let { segment ->
|
||||||
|
assertThat(segment.maxCountJobs).isEqualTo(1)
|
||||||
|
assertThat(segment.minimumCountSignatures).isEqualTo(1)
|
||||||
|
assertThat(segment.securityClass).isEqualTo("1")
|
||||||
|
assertThat(segment.tanProcedureParameters.oneStepProcedureAllowed).isTrue()
|
||||||
|
assertThat(segment.tanProcedureParameters.moreThanOneTanDependentJobPerMessageAllowed).isFalse()
|
||||||
|
assertThat(segment.tanProcedureParameters.jobHashValue).isEqualTo("0")
|
||||||
|
|
||||||
|
assertThat(segment.tanProcedureParameters.procedureParameters).hasSize(7)
|
||||||
|
assertThat(segment.tanProcedureParameters.procedureParameters).extracting("procedureName")
|
||||||
|
.containsExactlyInAnyOrder("chipTAN manuell", "chipTAN optisch", "chipTAN-USB", "chipTAN-QR",
|
||||||
|
"smsTAN", "pushTAN", "iTAN")
|
||||||
|
}
|
||||||
|
?: 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<TanResponse>(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<TanResponse>(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
|
@Test
|
||||||
fun parseBalance() {
|
fun parseBalance() {
|
||||||
|
|
||||||
|
@ -242,32 +321,6 @@ class ResponseParserTest : FinTsTestBase() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun parseTanInfo() {
|
|
||||||
|
|
||||||
// when
|
|
||||||
val result = underTest.parse("HITANS:171:6:4+1+1+1+J:N:0:910:2:HHD1.3.0:::chipTAN manuell:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:911:2:HHD1.3.2OPT:HHDOPT1:1.3.2:chipTAN optisch:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:912:2:HHD1.3.2USB:HHDUSB1:1.3.2:chipTAN-USB:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:913:2:Q1S:Secoder_UC:1.2.0:chipTAN-QR:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:1:920:2:smsTAN:::smsTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:2:N:5:921:2:pushTAN:::pushTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:2:N:2:900:2:iTAN:::iTAN:6:1:TAN-Nummer:3:J:2:N:0:0:N:N:00:0:N:0'")
|
|
||||||
|
|
||||||
// then
|
|
||||||
assertSuccessfullyParsedSegment(result, InstituteSegmentId.TanInfo, 171, 6, 4)
|
|
||||||
|
|
||||||
result.getFirstSegmentById<TanInfo>(InstituteSegmentId.TanInfo)?.let { segment ->
|
|
||||||
assertThat(segment.maxCountJobs).isEqualTo(1)
|
|
||||||
assertThat(segment.minimumCountSignatures).isEqualTo(1)
|
|
||||||
assertThat(segment.securityClass).isEqualTo("1")
|
|
||||||
assertThat(segment.tanProcedureParameters.oneStepProcedureAllowed).isTrue()
|
|
||||||
assertThat(segment.tanProcedureParameters.moreThanOneTanDependentJobPerMessageAllowed).isFalse()
|
|
||||||
assertThat(segment.tanProcedureParameters.jobHashValue).isEqualTo("0")
|
|
||||||
|
|
||||||
assertThat(segment.tanProcedureParameters.procedureParameters).hasSize(7)
|
|
||||||
assertThat(segment.tanProcedureParameters.procedureParameters).extracting("procedureName")
|
|
||||||
.containsExactlyInAnyOrder("chipTAN manuell", "chipTAN optisch", "chipTAN-USB", "chipTAN-QR",
|
|
||||||
"smsTAN", "pushTAN", "iTAN")
|
|
||||||
}
|
|
||||||
?: run { Assert.fail("No segment of type TanInfo found in ${result.receivedSegments}") }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun assertSuccessfullyParsedSegment(result: Response, segmentId: ISegmentId, segmentNumber: Int,
|
private fun assertSuccessfullyParsedSegment(result: Response, segmentId: ISegmentId, segmentNumber: Int,
|
||||||
segmentVersion: Int, referenceSegmentNumber: Int? = null) {
|
segmentVersion: Int, referenceSegmentNumber: Int? = null) {
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue