Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #155 -- Implement HKTAN7 and decoupled TAN process #162

Merged
merged 2 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1004,11 +1004,13 @@
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 @@ -1096,6 +1098,7 @@
3: HKTAN3,
5: HKTAN5,
6: HKTAN6,
7: HKTAN7,
}


Expand Down Expand Up @@ -1205,10 +1208,10 @@
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'):

Check warning on line 1211 in fints/client.py

View check run for this annotation

Codecov / codecov/patch

fints/client.py#L1211

Added line #L1211 was not covered by tests
seg.task_reference = tan_seg.task_reference

if tan_process in ('1', '2'):
if tan_process in ('1', '2', 'S'):

Check warning on line 1214 in fints/client.py

View check run for this annotation

Codecov / codecov/patch

fints/client.py#L1214

Added line #L1214 was not covered by tests
seg.further_tan_follows = False

return seg
Expand All @@ -1235,8 +1238,14 @@
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(),

Check warning on line 1246 in fints/client.py

View check run for this annotation

Codecov / codecov/patch

fints/client.py#L1246

Added line #L1246 was not covered by tests
resp.code == '3955',
)
if resp.code.startswith('9'):
raise Exception("Error response: {!r}".format(response))
else:
Expand All @@ -1254,17 +1263,41 @@
"""
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':

Check warning on line 1292 in fints/client.py

View check run for this annotation

Codecov / codecov/patch

fints/client.py#L1292

Added line #L1292 was not covered by tests
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
Loading