Implemented parsing TAN info

This commit is contained in:
dankl 2019-10-12 23:51:12 +02:00 committed by dankito
parent e9469e131a
commit 8cf57d1c35
14 changed files with 349 additions and 6 deletions

View File

@ -0,0 +1,15 @@
package net.dankito.fints.messages.datenelemente.implementierte.tan
import net.dankito.fints.messages.datenelemente.implementierte.ICodeEnum
/**
* Angabe des erwarteten Formates der TAN im konkreten Zwei-Schritt-Verfahren.
*/
enum class AllowedTanFormat(override val code: String) : ICodeEnum {
Numeric("1"),
Alphanumeric("2")
}

View File

@ -0,0 +1,19 @@
package net.dankito.fints.messages.datenelemente.implementierte.tan
import net.dankito.fints.messages.datenelemente.implementierte.ICodeEnum
/**
* Parameter, der angibt, ob eine Zahlungsverkehrskontoverbindung explizit angegeben werden muss, wenn diese
* im Geschäftsvorfall enthalten ist.
*
* Diese Funktion ermöglicht das Sicherstellen einer gültigen Kontoverbindung z. B. für die Abrechnung von
* SMS-Kosten bereits vor Erzeugen und Versenden einer (ggf. kostenpflichtigen!) TAN.
*/
enum class AuftraggeberkontoErforderlich(override val code: String) : ICodeEnum {
AuftraggeberkontoDarfNichtAngegebenWerden("0"),
AuftraggeberkontoMussAngegebenWerdenWennImGeschaeftsvorfallEnthalten("2")
}

View File

@ -0,0 +1,18 @@
package net.dankito.fints.messages.datenelemente.implementierte.tan
import net.dankito.fints.messages.datenelemente.implementierte.ICodeEnum
/**
* Abhängig vom Kreditinstitut und der Anzahl unterstützter TAN-Medien ist die Angabe der Bezeichnung des TAN-
* Mediums erforderlich, damit der Kunde dem Institut mitteilen kann, welches der TAN-Medien er verwenden möchte.
*/
enum class BezeichnungDesTanMediumsErforderlich(override val code: String) : ICodeEnum {
BezeichnungDesTanMediumsDarfNichtAngegebenWerden("0"),
BezeichnungDesTanMediumsKannAngegebenWerden("1"),
BezeichnungDesTanMediumsMussAngegebenWerden("2")
}

View File

@ -0,0 +1,23 @@
package net.dankito.fints.messages.datenelemente.implementierte.tan
import net.dankito.fints.messages.datenelemente.implementierte.ICodeEnum
/**
* Bezeichnet das Verfahren, welches bei Verwendung von PIN/TAN während der Dialoginitialisierung verwendet wird und
* bezieht sich dabei auf die in der Spezifikation des HandHeldDevice [HHD] bzw. den Belegungsrichtlinien [HHD-Belegung]
* definierten Schablonen 01 und 02.
*
* Die Schablonen werden in [HHD] zwar begrifflich auch als Challengeklassen bezeichnet, sind jedoch Bestandteil des
* dort definierten Start-Code, der in Ausgaberichtung im FinTS Datenelement Challenge übertragen wird und daher nicht
* zu verwechseln mit der Challengeklasse im Sinne einer Geschäftsvorfallsklasse bei HKTAN in der Prozessvariante 1.
*/
enum class Initialisierungsmodus(override val code: String) : ICodeEnum {
InitialisierungsverfahrenMitKlartextPinOhneTan("00"),
VerwendungAnalogDerInHhdBeschriebenenSchablone01_VerschluesseltePinOhneTan("01"),
VerwendungAnalogDerInHhdBeschriebenenSchablone02_ReserviertBeiFinTsDerzeitNichtVerwendet("02")
}

View File

@ -0,0 +1,22 @@
package net.dankito.fints.messages.datenelemente.implementierte.tan
import net.dankito.fints.messages.datenelemente.implementierte.ICodeEnum
/**
* Parameter, der angibt, ob eine Zahlungsverkehrskontoverbindung für die Abbuchung von SMS-Kosten angegeben werden
* kann oder muss. Die Belastung von SMS-Kosten durch das Institut wird unabhängig von dem Vorhandensein einer
* Kontoverbindung z. B. kundenindividuell geregelt.
*
* Das Element in der Version #2 ermöglicht eine detailliertere Steuerung der Belegung. Es wird z. B. in HKTAN
* ab Segmentversion #5 eingesetzt.
*/
enum class SmsAbbuchungskontoErforderlich(override val code: String) : ICodeEnum {
SmsAbbuchungskontoDarfNichtAngegebenWerden("0"),
SmsAbbuchungskontoKannAngegebenWerden("1"),
SmsAbbuchungskontoMussAngegebenWerden("2")
}

View File

@ -3,14 +3,51 @@ package net.dankito.fints.messages.datenelemente.implementierte.tan
import net.dankito.fints.messages.datenelemente.implementierte.ICodeEnum
/**
* Beim Zwei-Schritt-Verfahren werden die notwendigen Prozess-Schritte mittels des Geschäftsvorfalls HKTAN durchgeführt.
* Dieser unterstützt flexibel vier unterschiedliche Ausprägungen für die beiden Prozessvarianten für
* Zwei-Schritt-Verfahren, wobei die TAN-Prozesse 3 und 4 nicht isoliert und nur in Verbindung mit TAN-Prozess=2 auftreten können.
*/
enum class TanProcess(override val code: String) : ICodeEnum {
/**
* Im ersten Schritt wird der Auftrags-Hashwert über den Geschäftsvorfall HKTAN mitgeteilt, im zweiten Schritt
* erfolgt nach Ermittlung der TAN aus der zurückgemeldeten Challenge die Einreichung des eigentlichen Auftrags
* inklusive der TAN über das normale Auftragssegment. Abfolge der Segmente am Beispiel HKCCS:
* 1. Schritt: HKTAN <-> HITAN
* 2. Schritt: HKCCS <-> HIRMS zu HKCCS
*/
TanProcess1("1"),
/**
* Prozessvariante 2:
*
* Im ersten Schritt wird der Auftrag komplett über das normale Auftragssegment eingereicht, jedoch ohne
* Übermittlung der TAN. Im zweiten Schritt erfolgt nach Ermittlung der TAN aus der zurückgemeldeten Challenge die
* Einreichung der TAN über den Geschäftsvorfall HKTAN.
* Abfolge der Segmente am Beispiel HKCCS:
* Schritt 1: HKCCS und HKTAN <-> HITAN
* Schritt 2: HKTAN <-> HITAN und HIRMS zu HICCS
*/
/**
* kann nur im zweiten Schritt auftreten. Er dient zur Übermittlung der TAN mittels HKTAN, nachdem der Auftrag
* selbst zuvor bereits mit TAN-Prozess=3 oder 4 eingereicht wurde.
* Dieser Geschäftsvorfall wird mit HITAN, TAN-Prozess=2 beantwortet.
*/
TanProcess2("2"),
/**
* kann nur im ersten Schritt bei Mehrfach-TANs für die zweite und ggf. dritte TAN auftreten. Hierdurch wird die
* Einreichung eingeleitet, wenn zeitversetzte Einreichung von Mehrfach-TANs erlaubt ist.
*/
TanProcess3("3"),
/**
* kann nur im ersten Schritt auftreten. Hiermit wird das Zwei-Schritt-Verfahren nach Prozessvariante 2 für die
* erste TAN eingeleitet. HKTAN wird zusammen mit dem Auftragssegment übertragen und durch HITAN mit TAN-Prozess=4
* beantwortet. TAN-Prozess=4 wird auch beim Geschäftsvorfall Prüfen / Verbrennen von TANs eingesetzt.
*/
TanProcess4("4"),
}

View File

@ -0,0 +1,26 @@
package net.dankito.fints.messages.datenelemente.implementierte.tan
import net.dankito.fints.messages.datenelemente.implementierte.ICodeEnum
/**
* Beschreibung der protokolltechnischen Möglichkeiten, die dem Kunden im Zusammenhang mit Mehrfach-TANs zur
* Verfügung stehen. Es wird festgelegt, ob die Eingabe der einzelnen TANs zu einem Auftrag durch die unterschiedlichen
* Benutzer synchron in einem Dialog erfolgen muss oder zeitversetzt in mehreren Dialogen erfolgen kann.
* Es wird auch festgelegt, ob ein Institut nur eines dieser Verfahren oder beide parallel anbietet. Voraussetzung ist,
* dass grundsätzlich die Verwendung von Mehrfach-TANs beim Zwei-Schritt-Verfahren erlaubt ist (vgl. Parameter
* Mehrfach-TAN erlaubt). Bei Prozessvariante 1 ist der Parameter immer mit nicht zutreffend zu belegen, da hier
* generell keine zeitversetzte Verarbeitung möglich ist. Dieser Parameter erweitert den Parameter
* TAN zeitversetzt / dialogübergreifend erlaubt.
*/
enum class TanZeitUndDialogbezug(override val code: String) : ICodeEnum {
TanNichtZeitversetztDialoguebergreifendErlaubt("1"),
TanZeitversetztDialoguebergreifendErlaubt("2"),
BothAllowed("3"),
NotSupported("4")
}

View File

@ -0,0 +1,14 @@
package net.dankito.fints.messages.datenelemente.implementierte.tan
enum class ZkaTanProcedure {
HHD,
HHDUC,
HHDOPT1,
mobileTAN
}

View File

@ -15,6 +15,8 @@ enum class InstituteSegmentId(override val id: String) : ISegmentId {
AccountInfo("HIUPD"),
TanInfo("HITANS"),
AccountTransactionsMt940("HIKAZ")
}

View File

@ -4,8 +4,10 @@ import net.dankito.fints.messages.Separators
import net.dankito.fints.messages.datenelemente.implementierte.Dialogsprache
import net.dankito.fints.messages.datenelemente.implementierte.HbciVersion
import net.dankito.fints.messages.datenelemente.implementierte.ICodeEnum
import net.dankito.fints.messages.datenelemente.implementierte.signatur.Sicherheitsfunktion
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.*
import net.dankito.fints.messages.datenelementgruppen.implementierte.Kreditinstitutskennung
import net.dankito.fints.messages.datenelementgruppen.implementierte.signatur.Sicherheitsprofil
import net.dankito.fints.messages.segmente.id.MessageSegmentId
@ -78,6 +80,7 @@ open class ResponseParser @JvmOverloads constructor(
InstituteSegmentId.UserParameters.id -> parseUserParameters(segment, dataElementGroups)
InstituteSegmentId.AccountInfo.id -> parseAccountInfo(segment, dataElementGroups)
InstituteSegmentId.TanInfo.id -> parseTanInfo(segment, dataElementGroups)
InstituteSegmentId.AccountTransactionsMt940.id -> parseMt940AccountTransactions(segment, dataElementGroups)
@ -165,6 +168,82 @@ open class ResponseParser @JvmOverloads constructor(
}
protected open fun parseTanInfo(segment: String, dataElementGroups: List<String>): TanInfo {
val maxCountJobs = parseInt(dataElementGroups[1])
val minimumCountSignatures = parseInt(dataElementGroups[2])
val securityClass = parseString(dataElementGroups[3])
return TanInfo(maxCountJobs, minimumCountSignatures, securityClass,
parseTwoStepTanProcedureParameters(dataElementGroups[4]), segment)
}
protected open fun parseTwoStepTanProcedureParameters(tanProcedures: String): TwoStepTanProcedureParameters {
val dataElements = getDataElements(tanProcedures)
val oneStepProcedureAllowed = parseBoolean(dataElements[0])
val moreThanOneTanDependentJobPerMessageAllowed = parseBoolean(dataElements[1])
val jobHashValue = dataElements[2]
val proceduresDataElements = dataElements.subList(3, dataElements.size)
return TwoStepTanProcedureParameters(oneStepProcedureAllowed, moreThanOneTanDependentJobPerMessageAllowed,
jobHashValue, mapToTanProcedureParameters(proceduresDataElements))
}
protected open fun mapToTanProcedureParameters(proceduresDataElements: List<String>): List<TanProcedureParameters> {
val parsedProceduresParameters = mutableListOf<TanProcedureParameters>()
var remainingDataElements = proceduresDataElements
while (remainingDataElements.size >= 20) { // parameters have at least 20 data elements, the last element is optional
val dataElementForNextProcedure = if (remainingDataElements.size >= 21) remainingDataElements.subList(0, 21)
else remainingDataElements.subList(0, 20)
val procedureParameters = mapToSingleTanProcedureParameters(dataElementForNextProcedure)
parsedProceduresParameters.add(procedureParameters)
if (procedureParameters.countSupportedActiveTanMedia != null) remainingDataElements = remainingDataElements.subList(21, remainingDataElements.size)
else remainingDataElements = remainingDataElements.subList(20, remainingDataElements.size)
}
return parsedProceduresParameters
}
protected open fun mapToSingleTanProcedureParameters(procedureDataElements: List<String>): TanProcedureParameters {
return TanProcedureParameters(
parseCodeEnum(procedureDataElements[0], Sicherheitsfunktion.values()),
parseCodeEnum(procedureDataElements[1], TanProcess.values()),
parseString(procedureDataElements[2]),
tryToParseZkaTanProcedure(procedureDataElements[3]),
parseStringToNullIfEmpty(procedureDataElements[4]),
parseString(procedureDataElements[5]),
parseInt(procedureDataElements[6]),
parseCodeEnum(procedureDataElements[7], AllowedTanFormat.values()),
parseString(procedureDataElements[8]),
parseInt(procedureDataElements[9]),
parseBoolean(procedureDataElements[10]),
parseCodeEnum(procedureDataElements[11], TanZeitUndDialogbezug.values()),
parseBoolean(procedureDataElements[12]),
parseCodeEnum(procedureDataElements[13], SmsAbbuchungskontoErforderlich.values()),
parseCodeEnum(procedureDataElements[14], AuftraggeberkontoErforderlich.values()),
parseBoolean(procedureDataElements[15]),
parseBoolean(procedureDataElements[16]),
parseCodeEnum(procedureDataElements[17], Initialisierungsmodus.values()),
parseCodeEnum(procedureDataElements[18], BezeichnungDesTanMediumsErforderlich.values()),
parseBoolean(procedureDataElements[19]),
if (procedureDataElements.size >= 20) parseNullableInt(procedureDataElements[20]) else null
)
}
protected open fun tryToParseZkaTanProcedure(mayZkaTanProcedure: String): ZkaTanProcedure? {
try {
return ZkaTanProcedure.valueOf(mayZkaTanProcedure)
} catch (ignored: Exception) { }
return null
}
protected open fun parseMt940AccountTransactions(segment: String, dataElementGroups: List<String>): ReceivedAccountTransactions {
val bookedTransactionsString = extractBinaryData(dataElementGroups[1])
@ -323,6 +402,14 @@ open class ResponseParser @JvmOverloads constructor(
return parseString(string).toInt()
}
protected open fun parseNullableInt(mayInt: String): Int? {
try {
return parseInt(mayInt)
} catch (ignored: Exception) { }
return null
}
protected open fun parseStringToNullIfEmpty(string: String): String? {
val parsedString = parseString(string)

View File

@ -0,0 +1,12 @@
package net.dankito.fints.response.segments
open class TanInfo(
val maxCountJobs: Int,
val minimumCountSignatures: Int,
val securityClass: String, // not used for PIN/TAN
val tanProcedureParameters: TwoStepTanProcedureParameters,
segmentString: String
)
: ReceivedSegment(segmentString)

View File

@ -0,0 +1,29 @@
package net.dankito.fints.response.segments
import net.dankito.fints.messages.datenelemente.implementierte.signatur.Sicherheitsfunktion
import net.dankito.fints.messages.datenelemente.implementierte.tan.*
open class TanProcedureParameters(
val securityFunction: Sicherheitsfunktion,
val tanProcess: TanProcess,
val technicalTanProcedureIdentification: String,
val zkaTanProcedure: ZkaTanProcedure?,
val versionZkaTanProcedure: String?,
val procedureName: String,
val maxTanInputLength: Int,
val allowedTanFormat: AllowedTanFormat,
val descriptionToShowToUser: String,
val maxReturnValueLength: Int,
val multipleTansAllowed: Boolean,
val timeAndDialogRelation: TanZeitUndDialogbezug,
val cancellationAllowed: Boolean,
val smsDebitAccountRequired: SmsAbbuchungskontoErforderlich,
val initiatorAccountRequired: AuftraggeberkontoErforderlich,
val challengeClassRequired: Boolean,
val signatureStructured: Boolean,
val initializingMode: Initialisierungsmodus,
val nameOfTanMediaRequired: BezeichnungDesTanMediumsErforderlich,
val hhdUcResponseRequired: Boolean,
val countSupportedActiveTanMedia: Int?
)

View File

@ -0,0 +1,9 @@
package net.dankito.fints.response.segments
open class TwoStepTanProcedureParameters(
val oneStepProcedureAllowed: Boolean,
val moreThanOneTanDependentJobPerMessageAllowed: Boolean,
val jobHashValue: String, // not evaluated for PIN/TAN
val procedureParameters: List<TanProcedureParameters>
)

View File

@ -211,19 +211,49 @@ class ResponseParserTest {
}
@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,
segmentVersion: Int, referenceSegmentNumber: Int? = null) {
assertThat(result.successful).isTrue()
assertThat(result.error).isNull()
assertThat(result.receivedResponse).isNotNull()
assertThat(result.receivedSegments).hasSize(1)
val segment = result.receivedSegments.first()
val segment = result.getFirstSegmentById<ReceivedSegment>(segmentId)
assertThat(segment).isNotNull()
segment?.let {
assertThat(segment.segmentId).isEqualTo(segmentId.id)
assertThat(segment.segmentNumber).isEqualTo(segmentNumber)
assertThat(segment.segmentVersion).isEqualTo(segmentVersion)
assertThat(segment.referenceSegmentNumber).isEqualTo(referenceSegmentNumber)
}
}
}