]> git.ipfire.org Git - thirdparty/python-fints.git/commitdiff
Fix #155 -- Implement HKTAN7 and decoupled TAN process (#162)
authorRaphael Michel <michel@rami.io>
Thu, 28 Mar 2024 11:43:38 +0000 (12:43 +0100)
committerGitHub <noreply@github.com>
Thu, 28 Mar 2024 11:43:38 +0000 (12:43 +0100)
* Implement HKTAN7

* Implement decoupled TAN process

docs/debits.rst
docs/transfers.rst
docs/trouble.rst
fints/client.py
fints/formals.py
fints/segments/auth.py

index 7829cea0ca42edef75bb6f1772bc638fd107f195..f22f9104f4e414cbd638fce6d5458705b856381f 100644 (file)
@@ -67,7 +67,7 @@ You can easily generate XML using the ``sepaxml`` python library:
             pain_descriptor='urn:iso:std:iso:20022:tech:xsd:pain.008.002.02'
         )
 
-        if isinstance(res, NeedTANResponse):
+        while isinstance(res, NeedTANResponse):
             print("A TAN is required", res.challenge)
 
             if getattr(res, 'challenge_hhduc', None):
@@ -76,7 +76,10 @@ You can easily generate XML using the ``sepaxml`` python library:
                 except KeyboardInterrupt:
                     pass
 
-            tan = input('Please enter TAN:')
+            if result.decoupled:
+                tan = input('Please press enter after confirming the transaction in your app:')
+            else:
+                tan = input('Please enter TAN:')
             res = client.send_tan(res, tan)
 
         print(res.status)
index 28d6a0a683e7539e0a53ab0ebdbb07c242749ea4..787a9cac4f5ef829152c1062271fe81eddcfe9da 100644 (file)
@@ -55,7 +55,7 @@ Full example
             endtoend_id='NOTPROVIDED',
         )
 
-        if isinstance(res, NeedTANResponse):
+        while isinstance(res, NeedTANResponse):
             print("A TAN is required", res.challenge)
 
             if getattr(res, 'challenge_hhduc', None):
@@ -64,7 +64,10 @@ Full example
                 except KeyboardInterrupt:
                     pass
 
-            tan = input('Please enter TAN:')
+            if result.decoupled:
+                tan = input('Please press enter after confirming the transaction in your app:')
+            else:
+                tan = input('Please enter TAN:')
             res = client.send_tan(res, tan)
 
         print(res.status)
index 35a2247cc9a8467362e7ecaafecd163c21fa087c..f2ee0102726bae0ab85e10902919ba0861479fc5 100644 (file)
@@ -44,8 +44,9 @@ the problem.
         getpass.getpass('PIN: '),
         'REPLACEME'  # ENDPOINT
     )
+    product_id = 'REPLACEME'
 
-    f = FinTS3PinTanClient(*client_args)
+    f = FinTS3PinTanClient(*client_args, product_id=product_id)
     minimal_interactive_cli_bootstrap(f)
 
 
@@ -57,19 +58,22 @@ the problem.
                 terminal_flicker_unix(response.challenge_hhduc)
             except KeyboardInterrupt:
                 pass
-        tan = input('Please enter TAN:')
+        if response.decoupled:
+            tan = input('Please press enter after confirming the transaction in your app:')
+        else:
+            tan = input('Please enter TAN:')
         return f.send_tan(response, tan)
 
 
     # Open the actual dialog
     with f:
         # Since PSD2, a TAN might be needed for dialog initialization. Let's check if there is one required
-        if f.init_tan_response:
-            ask_for_tan(f.init_tan_response)
+        while isinstance(f.init_tan_response, NeedTANResponse):
+            f.init_tan_response = ask_for_tan(f.init_tan_response)
 
         # Fetch accounts
         accounts = f.get_sepa_accounts()
-        if isinstance(accounts, NeedTANResponse):
+        while isinstance(accounts, NeedTANResponse):
             accounts = ask_for_tan(accounts)
         if len(accounts) == 1:
             account = accounts[0]
@@ -85,7 +89,7 @@ the problem.
 
     client_data = f.deconstruct(including_private=True)
 
-    f = FinTS3PinTanClient(*client_args, from_data=client_data)
+    f = FinTS3PinTanClient(*client_args, product_id=product_id, from_data=client_data)
     with f.resume_dialog(dialog_data):
         while True:
             operations = [
@@ -167,7 +171,7 @@ the problem.
                         endtoend_id='NOTPROVIDED',
                     )
 
-                    if isinstance(res, NeedTANResponse):
-                        ask_for_tan(res)
+                    while isinstance(res, NeedTANResponse):
+                        res = ask_for_tan(res)
             except FinTSUnsupportedOperation as e:
                 print("This operation is not supported by this bank:", e)
\ No newline at end of file
index fde0a167733adc7a0789b871814272681f9d2ebc..0931e549bc58c4dbb869fa393a6e9a1c97d2b7b0 100644 (file)
@@ -26,7 +26,7 @@ from .security import (
     PinTanTwoStepAuthenticationMechanism,
 )
 from .segments.accounts import HISPA1, HKSPA1
-from .segments.auth import HIPINS1, HKTAB4, HKTAB5, HKTAN2, HKTAN3, HKTAN5, HKTAN6
+from .segments.auth import HIPINS1, HKTAB4, HKTAB5, HKTAN2, HKTAN3, HKTAN5, HKTAN6, HKTAN7
 from .segments.bank import HIBPA3, HIUPA4, HKKOM4
 from .segments.debit import (
     HKDBS1, HKDBS2, HKDMB1, HKDMC1, HKDME1, HKDME2,
@@ -1005,11 +1005,13 @@ class NeedTANResponse(NeedRetryResponse):
     challenge_html = None  #: HTML-safe challenge text, possibly with formatting
     challenge_hhduc = None  #: HHD_UC challenge to be transmitted to the TAN generator
     challenge_matrix = None  #: Matrix code challenge: tuple(mime_type, data)
+    decoupled = None  #: Use decoupled process
 
-    def __init__(self, command_seg, tan_request, resume_method=None, tan_request_structured=False):
+    def __init__(self, command_seg, tan_request, resume_method=None, tan_request_structured=False, decoupled=False):
         self.command_seg = command_seg
         self.tan_request = tan_request
         self.tan_request_structured = tan_request_structured
+        self.decoupled = decoupled
         if hasattr(resume_method, '__func__'):
             self.resume_method = resume_method.__func__.__name__
         else:
@@ -1111,6 +1113,7 @@ IMPLEMENTED_HKTAN_VERSIONS = {
     3: HKTAN3,
     5: HKTAN5,
     6: HKTAN6,
+    7: HKTAN7,
 }
 
 
@@ -1220,10 +1223,10 @@ class FinTS3PinTanClient(FinTS3Client):
         if tan_process == '4' and tan_mechanism.VERSION >= 6:
             seg.segment_type = orig_seg.header.type
 
-        if tan_process in ('2', '3'):
+        if tan_process in ('2', '3', 'S'):
             seg.task_reference = tan_seg.task_reference
 
-        if tan_process in ('1', '2'):
+        if tan_process in ('1', '2', 'S'):
             seg.further_tan_follows = False
 
         return seg
@@ -1250,8 +1253,14 @@ class FinTS3PinTanClient(FinTS3Client):
                 response = dialog.send(command_seg, tan_seg)
 
                 for resp in response.responses(tan_seg):
-                    if resp.code == '0030':
-                        return NeedTANResponse(command_seg, response.find_segment_first('HITAN'), resume_func, self.is_challenge_structured())
+                    if resp.code in ('0030', '3955'):
+                        return NeedTANResponse(
+                            command_seg,
+                            response.find_segment_first('HITAN'),
+                            resume_func,
+                            self.is_challenge_structured(),
+                            resp.code == '3955',
+                        )
                     if resp.code.startswith('9'):
                         raise Exception("Error response: {!r}".format(response))
             else:
@@ -1269,17 +1278,41 @@ class FinTS3PinTanClient(FinTS3Client):
         """
         Sends a TAN to confirm a pending operation.
 
+        If ``NeedTANResponse.decoupled`` is ``True``, the ``tan`` parameter is ignored and can be kept empty.
+        If the operation was not yet confirmed using the decoupled app, this method will again return a
+        ``NeedTANResponse``.
+
         :param challenge: NeedTANResponse to respond to
         :param tan: TAN value
-        :return: Currently no response
+        :return: New response after sending TAN
         """
 
         with self._get_dialog() as dialog:
-            tan_seg = self._get_tan_segment(challenge.command_seg, '2', challenge.tan_request)
-            self._pending_tan = tan
+            if challenge.decoupled:
+                tan_seg = self._get_tan_segment(challenge.command_seg, 'S', challenge.tan_request)
+            else:
+                tan_seg = self._get_tan_segment(challenge.command_seg, '2', challenge.tan_request)
+                self._pending_tan = tan
 
             response = dialog.send(tan_seg)
 
+            if challenge.decoupled:
+                # TAN process = S
+                status_segment = response.find_segment_first('HITAN')
+                if not status_segment:
+                    raise FinTSClientError(
+                        "No TAN status received."
+                    )
+                for resp in response.responses(tan_seg):
+                    if resp.code == '3956':
+                        return NeedTANResponse(
+                            challenge.command_seg,
+                            challenge.tan_request,
+                            challenge.resume_method,
+                            challenge.tan_request_structured,
+                            challenge.decoupled,
+                        )
+
             resume_func = getattr(self, challenge.resume_method)
             return resume_func(challenge.command_seg, response)
 
index 315329c089777bf5c12d61cd28ba2e567425c8f2..5dab04f74b2a86137d3153606f52744481cf6ba0 100644 (file)
@@ -397,6 +397,32 @@ class TwoStepParameters6(TwoStepParametersCommon):
     supported_media_number = DataElementField(type='num', length=1, required=False, _d="Anzahl unterstützter aktiver TAN-Medien")
 
 
+class TwoStepParameters7(TwoStepParametersCommon):
+    zka_id = DataElementField(type='an', max_length=32, _d="DK TAN-Verfahren")
+    zka_version = DataElementField(type='an', max_length=10, _d="Version DK TAN-Verfahren")
+    name = DataElementField(type='an', max_length=30, _d="Name des Zwei-Schritt-Verfahrens")
+    max_length_input = DataElementField(type='num', max_length=2, _d="Maximale Länge des Eingabewertes im Zwei-Schritt-Verfahren")
+    allowed_format = CodeField(enum=AllowedFormat, length=1, _d="Erlaubtes Format im Zwei-Schritt-Verfahren")
+    text_return_value = DataElementField(type='an', max_length=30, _d="Text zur Belegung des Rückgabewertes im Zwei-Schritt-Verfahren")
+    max_length_return_value = DataElementField(type='num', max_length=4, _d="Maximale Länge des Rückgabewertes im Zwei-Schritt-Verfahren")
+    multiple_tans_allowed = DataElementField(type='jn', _d="Mehrfach-TAN erlaubt")
+    tan_time_dialog_association = CodeField(enum=TANTimeDialogAssociation, length=1, _d="TAN Zeit- und Dialogbezug")
+    cancel_allowed = DataElementField(type='jn', _d="Auftragsstorno erlaubt")
+    sms_charge_account_required = CodeField(enum=SMSChargeAccountRequired, length=1, _d="SMS-Abbuchungskonto erforderlich")
+    principal_account_required = CodeField(enum=PrincipalAccountRequired, length=1, _d="Auftraggeberkonto erforderlich")
+    challenge_class_required = DataElementField(type='jn', _d="Challenge-Klasse erforderlich")
+    challenge_structured = DataElementField(type='jn', _d="Challenge strukturiert")
+    initialization_mode = CodeField(enum=InitializationMode, _d="Initialisierungsmodus")
+    description_required = CodeField(enum=DescriptionRequired, length=1, _d="Bezeichnung des TAN-Medium erforderlich")
+    response_hhd_uc_required = DataElementField(type='jn', _d="Antwort HHD_UC erforderlich")
+    supported_media_number = DataElementField(type='num', length=1, required=False, _d="Anzahl unterstützter aktiver TAN-Medien")
+    decoupled_max_poll_number = DataElementField(type='num', max_length=3, required=False, _d="Maximale Anzahl Statusabfragen Decoupled")
+    wait_before_first_poll = DataElementField(type='num', max_length=3, required=False, _d="Wartezeit vor erster Statusabfrage")
+    wait_before_next_poll = DataElementField(type='num', max_length=3, required=False, _d="Wartezeit vor nächster Statusabfrage")
+    manual_confirmation_allowed = DataElementField(type='jn', required=False, _d="Manuelle Bestätigung möglich")
+    automated_polling_allowed = DataElementField(type='jn', required=False, _d="Automatische Statusabfragen erlaubt")
+
+
 class ParameterTwostepCommon(DataElementGroup):
     onestep_method_allowed = DataElementField(type='jn')
     multiple_tasks_allowed = DataElementField(type='jn')
@@ -428,6 +454,10 @@ class ParameterTwostepTAN6(ParameterTwostepCommon):
     twostep_parameters = DataElementGroupField(type=TwoStepParameters6, min_count=1, max_count=98)
 
 
+class ParameterTwostepTAN7(ParameterTwostepCommon):
+    twostep_parameters = DataElementGroupField(type=TwoStepParameters7, min_count=1, max_count=98)
+
+
 class TransactionTanRequired(DataElementGroup):
     transaction = DataElementField(type='an', max_length=6)
     tan_required = DataElementField(type='jn')
index e23d9cdd7df3a01c02c066fb207b44aa45c7f78b..57f665f2181868a8784f30d4fbb102ef72e53f25 100644 (file)
@@ -5,7 +5,7 @@ from fints.formals import (
     ParameterTwostepTAN2, ParameterTwostepTAN3, ParameterTwostepTAN4,
     ParameterTwostepTAN5, ParameterTwostepTAN6, ResponseHHDUC,
     SystemIDStatus, TANMedia4, TANMedia5, TANMediaClass3,
-    TANMediaClass4, TANMediaType2, TANUsageOption,
+    TANMediaClass4, TANMediaType2, TANUsageOption, ParameterTwostepTAN7,
 )
 
 from .base import FinTS3Segment, ParameterSegment
@@ -97,6 +97,24 @@ class HKTAN6(FinTS3Segment):
     response_hhd_uc = DataElementGroupField(type=ResponseHHDUC, required=False, _d="Antwort HHD_UC")
 
 
+class HKTAN7(FinTS3Segment):
+    """Zwei-Schritt-TAN-Einreichung, version 7
+
+    Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
+    tan_process = DataElementField(type='code', length=1, _d="TAN-Prozess")
+    segment_type = DataElementField(type='an', max_length=6, required=False, _d="Segmentkennung")
+    account = DataElementGroupField(type=KTI1, required=False, _d="Kontoverbindung international Auftraggeber")
+    task_hash_value = DataElementField(type='bin', max_length=256, required=False, _d="Auftrags-Hashwert")
+    task_reference = DataElementField(type='an', max_length=35, required=False, _d="Auftragsreferenz")
+    further_tan_follows = DataElementField(type='jn', length=1, required=False, _d="Weitere TAN folgt")
+    cancel_task = DataElementField(type='jn', length=1, required=False, _d="Auftrag stornieren")
+    sms_charge_account = DataElementGroupField(type=KTI1, required=False, _d="SMS-Abbuchungskonto")
+    challenge_class = DataElementField(type='num', max_length=2, required=False, _d="Challenge-Klasse")
+    parameter_challenge_class = DataElementGroupField(type=ParameterChallengeClass, required=False, _d="Parameter Challenge-Klasse")
+    tan_medium_name = DataElementField(type='an', max_length=32, required=False, _d="Bezeichnung des TAN-Mediums")
+    response_hhd_uc = DataElementGroupField(type=ResponseHHDUC, required=False, _d="Antwort HHD_UC")
+
+
 class HITAN2(FinTS3Segment):
     """Zwei-Schritt-TAN-Einreichung Rückmeldung, version 2
 
@@ -152,6 +170,19 @@ class HITAN6(FinTS3Segment):
     tan_medium_name = DataElementField(type='an', max_length=32, required=False, _d="Bezeichnung des TAN-Mediums")
 
 
+class HITAN7(FinTS3Segment):
+    """Zwei-Schritt-TAN-Einreichung Rückmeldung, version 7
+
+    Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN"""
+    tan_process = DataElementField(type='code', length=1, _d="TAN-Prozess")
+    task_hash_value = DataElementField(type='bin', max_length=256, required=False, _d="Auftrags-Hashwert")
+    task_reference = DataElementField(type='an', max_length=35, required=False, _d="Auftragsreferenz")
+    challenge = DataElementField(type='an', max_length=2048, required=False, _d="Challenge")
+    challenge_hhduc = DataElementField(type='bin', required=False, _d="Challenge HHD_UC")
+    challenge_valid_until = DataElementGroupField(type=ChallengeValidUntil, required=False, _d="Gültigkeitsdatum und -uhrzeit für Challenge")
+    tan_medium_name = DataElementField(type='an', max_length=32, required=False, _d="Bezeichnung des TAN-Mediums")
+
+
 class HKTAB4(FinTS3Segment):
     """TAN-Generator/Liste anzeigen Bestand, version 4
 
@@ -216,6 +247,10 @@ class HITANS6(HITANSBase):
     parameter = DataElementGroupField(type=ParameterTwostepTAN6)
 
 
+class HITANS7(HITANSBase):
+    parameter = DataElementGroupField(type=ParameterTwostepTAN7)
+
+
 class HIPINS1(ParameterSegment):
     """PIN/TAN-spezifische Informationen, version 1