Skip to content

Commit

Permalink
Fix #155 -- Implement HKTAN7 and decoupled TAN process (#162)
Browse files Browse the repository at this point in the history
* Implement HKTAN7

* Implement decoupled TAN process
  • Loading branch information
raphaelm committed Mar 28, 2024
1 parent 4682e55 commit 18fa496
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 22 deletions.
7 changes: 5 additions & 2 deletions docs/debits.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Expand Down
7 changes: 5 additions & 2 deletions docs/transfers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Expand Down
20 changes: 12 additions & 8 deletions docs/trouble.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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]
Expand All @@ -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 = [
Expand Down Expand Up @@ -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)
51 changes: 42 additions & 9 deletions fints/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -1111,6 +1113,7 @@ def _parse_tan_challenge(self):
3: HKTAN3,
5: HKTAN5,
6: HKTAN6,
7: HKTAN7,
}


Expand Down Expand Up @@ -1220,10 +1223,10 @@ def _get_tan_segment(self, orig_seg, tan_process, tan_seg=None):
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
Expand All @@ -1250,8 +1253,14 @@ def _send_with_possible_retry(self, dialog, command_seg, resume_func):
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:
Expand All @@ -1269,17 +1278,41 @@ def send_tan(self, challenge: NeedTANResponse, tan: str):
"""
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)

Expand Down
30 changes: 30 additions & 0 deletions fints/formals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down
37 changes: 36 additions & 1 deletion fints/segments/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
ParameterTwostepTAN2, ParameterTwostepTAN3, ParameterTwostepTAN4,
ParameterTwostepTAN5, ParameterTwostepTAN6, ResponseHHDUC,
SystemIDStatus, TANMedia4, TANMedia5, TANMediaClass3,
TANMediaClass4, TANMediaType2, TANUsageOption,
TANMediaClass4, TANMediaType2, TANUsageOption, ParameterTwostepTAN7,
)

from .base import FinTS3Segment, ParameterSegment
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 18fa496

Please sign in to comment.