From: Raphael Michel Date: Mon, 2 Jan 2017 16:56:08 +0000 (+0100) Subject: Bootstrap communication X-Git-Tag: v0.1.0~10 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f7bad65b50736bdf479ef979d802f728d42d349c;p=thirdparty%2Fpython-fints.git Bootstrap communication --- diff --git a/fints/protocol/fints3/__init__.py b/fints/protocol/fints3/__init__.py index b266940..e69de29 100644 --- a/fints/protocol/fints3/__init__.py +++ b/fints/protocol/fints3/__init__.py @@ -1,61 +0,0 @@ -from .segments.crypto import FinTS3CryptoHeader, FinTS3CryptoBody -from .segments.message import FinTS3Footer, FinTS3Header -from .utils import segments_to_ascii - - -class FinTS3: - version = 300 - - def __init__(self): - self.segments = [] - - # global segment counter; used for "sub-segments" in the crypto body - self.i = 1 - - # these segments are special and have to be controlled by this class - self.header = FinTS3Header(self.version) - self.footer = FinTS3Footer() - - def append(self, segment): - self.segments.append(segment) - - def to_ascii(self): - self.i, ascii = segments_to_ascii(self.segments) - size = len(ascii) - - # footer stuff for correct size calculation - self.footer.set_counter(self.i + 1) - footer = self.footer.to_ascii() + "'" - size += len(footer) - - # prepend header with correct size - self.header.set_counter(1) - self.header.set_size(size + 1) - ascii = self.header.to_ascii() + "'" + ascii - - # append footer - ascii += footer - - return ascii - - -class FinTS3PinTan(FinTS3): - def __init__(self, customer, blz, server=None): - self.server = server - - super().__init__() - - self.crypto_header = FinTS3CryptoHeader(customer, blz) - self.crypto_body = FinTS3CryptoBody() - self.append(self.crypto_header) - self.append(self.crypto_body) - - def append(self, segment): - self.segments.append(segment) - - def to_ascii(self): - # create the crypto body - self.i, ascii = segments_to_ascii(self.segments, self.i) - self.crypto_body.set_data(ascii) - - return super().to_ascii() diff --git a/fints/protocol/fints3/client.py b/fints/protocol/fints3/client.py new file mode 100644 index 0000000..8dd1765 --- /dev/null +++ b/fints/protocol/fints3/client.py @@ -0,0 +1,25 @@ +from .connection import FinTSHTTPSConnection +from .dialog import FinTSDialog + + +class FinTS3Client: + version = 300 + + +class FinTS3PinTanClient(FinTS3Client): + def __init__(self, blz, username, pin, server): + self.username = username + self.blz = blz + self.pin = pin + self.connection = FinTSHTTPSConnection(server) + self.systemid = 0 + super().__init__() + + def _new_dialog(self): + dialog = FinTSDialog(self.blz, self.username, self.pin, self.systemid, self.connection) + return dialog + + def get_sepa_accounts(self): + dialog = self._new_dialog() + dialog.sync() + diff --git a/fints/protocol/fints3/connection.py b/fints/protocol/fints3/connection.py new file mode 100644 index 0000000..3845a68 --- /dev/null +++ b/fints/protocol/fints3/connection.py @@ -0,0 +1,22 @@ +import base64 + +import requests + +from fints.protocol.fints3.message import FinTSMessage + + +class FinTSConnectionError(Exception): + pass + + +class FinTSHTTPSConnection: + def __init__(self, url): + self.url = url + + def send(self, msg: FinTSMessage): + r = requests.post( + self.url, data=base64.b64encode(str(msg).encode('iso-8859-1')), + ) + if r.status_code < 200 or r.status_code > 299: + raise FinTSConnectionError('Bad status code {}'.format(r.status_code)) + return base64.b64decode(r.content.decode('iso-8859-1')).decode('iso-8859-1') diff --git a/fints/protocol/fints3/dialog.py b/fints/protocol/fints3/dialog.py new file mode 100644 index 0000000..bd7cd0b --- /dev/null +++ b/fints/protocol/fints3/dialog.py @@ -0,0 +1,47 @@ +import logging + +from .segments.auth import HKIDN, HKSYN, HKVVB +from .message import FinTSMessage + +logger = logging.getLogger(__name__) + + +class FinTSDialog: + def __init__(self, blz, username, pin, systemid, connection): + self.blz = blz + self.username = username + self.pin = pin + self.systemid = systemid + self.connection = connection + self.msgno = 1 + self.dialogid = 0 + + def sync(self): + logger.info('Initialize SYNC') + + seg_identification = HKIDN(3, self.blz, self.username, 0) + seg_prepare = HKVVB(4) + seg_sync = HKSYN(5) + + msg_sync = FinTSMessage(self.blz, self.username, self.pin, self.systemid, self.dialogid, self.msgno, [ + seg_identification, + seg_prepare, + seg_sync + ]) + + logger.debug('Sending SYNC: {}'.format(msg_sync)) + resp = self.send(msg_sync) + logger.debug('Got SYNC response: {}'.format(resp)) + + def send(self, msg): + logger.info('Sending Message') + msg.msgno = self.msgno + msg.dialogid = self.dialogid + + try: + resp = self.connection.send(msg) + self.msgno += 1 + return resp + except: + # TODO: Error handlign + raise diff --git a/fints/protocol/fints3/message.py b/fints/protocol/fints3/message.py new file mode 100644 index 0000000..65ada9e --- /dev/null +++ b/fints/protocol/fints3/message.py @@ -0,0 +1,51 @@ +import random + +from fints.protocol.fints3.segments.message import HNHBK, HNSHK, HNVSK, HNVSD, HNSHA, HNHBS + + +class FinTSMessage: + def __init__(self, blz, username, pin, systemid, dialogid, msgno, encrypted_segments): + self.blz = blz + self.username = username + self.pin = pin + self.systemid = systemid + self.dialogid = dialogid + self.msgno = msgno + self.segments = [] + self.encrypted_segments = [] + + sig_head = self.build_signature_head() + enc_head = self.build_encryption_head() + self.segments.append(enc_head) + + self.enc_envelop = HNVSD(999, '') + self.segments.append(self.enc_envelop) + + self.append_enc_segment(sig_head) + for s in encrypted_segments: + self.append_enc_segment(s) + + cur_count = len(encrypted_segments) + 3 + + sig_end = HNSHA(cur_count, self.secref, self.pin) + self.append_enc_segment(sig_end) + self.segments.append(HNHBS(cur_count + 1, msgno)) + + def append_enc_segment(self, seg): + self.encrypted_segments.append(seg) + self.enc_envelop.set_data(self.enc_envelop.encoded_data + str(seg)) + + def build_signature_head(self): + rand = random.SystemRandom() + self.secref = rand.randint(1000000, 9999999) + return HNSHK(2, self.secref, self.blz, self.username, self.systemid) + + def build_encryption_head(self): + return HNVSK(998, self.blz, self.username, self.systemid) + + def build_header(self): + l = sum([len(str(s)) for s in self.segments]) + return HNHBK(l, self.dialogid, self.msgno) + + def __str__(self): + return str(self.build_header()) + ''.join([str(s) for s in self.segments]) diff --git a/fints/protocol/fints3/segments/__init__.py b/fints/protocol/fints3/segments/__init__.py index 86081bb..8692601 100644 --- a/fints/protocol/fints3/segments/__init__.py +++ b/fints/protocol/fints3/segments/__init__.py @@ -1,52 +1,14 @@ -from collections import OrderedDict - - class FinTS3Segment: - def __init__(self): - raise NotImplementedError() - - def get_counter(self): - return self.elements['head']['counter'] - - def set_counter(self, counter): - self.elements['head']['counter'] = counter - - # Parse/encode binary dataelements while ignoring the size - def __parse_binary(self, DE): - if DE[0:1] == '@': - bdata = DE.split('@') - return bytes(bdata[2], 'ISO-8859-2') - return DE - - def __encode_binary(self, DE): - if type(DE) is bytes: - return str('@{0}@{1}'.format(len(DE), str(DE, 'ISO-8859-2'))) - return str(DE) - - def from_ascii(self, ascii): - for i, DG in enumerate(ascii.split('+')): - key = list(self.elements.keys())[i] - - if type(self.elements[key]) is OrderedDict: - - for ii, DE in enumerate(DG.split(':')): - kkey = list(self.elements[key].keys())[ii] - self.elements[key][kkey] = self.__parse_binary(DE) - - else: - self.elements[key] = self.__parse_binary(DG) - - def to_ascii(self): - ascii = '' - - for key in self.elements.keys(): - if type(self.elements[key]) is OrderedDict: - for element in self.elements[key]: - ascii += self.__encode_binary(self.elements[key][element]) + ':' - ascii = ascii[:-1] - else: - ascii += self.__encode_binary(self.elements[key]) - ascii += '+' - ascii = ascii[:-1] - - return ascii + type = '???' + country_code = 280 + version = 2 + + def __init__(self, segmentno, data): + self.segmentno = segmentno + self.data = data + + def __str__(self): + res = '{}:{}:{}'.format(self.type, self.segmentno, self.version) + for d in self.data: + res += '+' + str(d) + return res + "'" diff --git a/fints/protocol/fints3/segments/auth.py b/fints/protocol/fints3/segments/auth.py new file mode 100644 index 0000000..6cc366e --- /dev/null +++ b/fints/protocol/fints3/segments/auth.py @@ -0,0 +1,60 @@ +from . import FinTS3Segment + + +class HKIDN(FinTS3Segment): + """ + HKIDN (Identifikation) + Section C.3.1.2 + """ + type = 'HKIDN' + version = 2 + + def __init__(self, segmentno, blz, username, systemid=0, customerid=1): + data = [ + '{}:{}'.format(self.country_code, blz), + username, + systemid, + customerid + ] + super().__init__(segmentno, data) + + +class HKVVB(FinTS3Segment): + """ + HKVVB (Verarbeitungsvorbereitung) + Section C.3.1.3 + """ + type = 'HKVVB' + version = 3 + + LANG_DE = 1 + LANG_EN = 2 + LANG_FR = 3 + + PRODUCT_NAME = 'pyfints' + PRODUCT_VERSION = '0.1' + + def __init__(self, segmentno, lang=LANG_EN): + data = [ + 0, 0, lang, self.PRODUCT_NAME, self.PRODUCT_VERSION + ] + super().__init__(segmentno, data) + + +class HKSYN(FinTS3Segment): + """ + HKSYN (Synchronisation) + Section C.8.1.2 + """ + type = 'HKSYN' + version = 3 + + SYNC_MODE_NEW_CUSTOMER_ID = 0 + SYNC_MODE_LAST_MSG_NUMBER = 1 + SYNC_MODE_SIGNATURE_ID = 2 + + def __init__(self, segmentno, mode=SYNC_MODE_NEW_CUSTOMER_ID): + data = [ + mode + ] + super().__init__(segmentno, data) diff --git a/fints/protocol/fints3/segments/crypto.py b/fints/protocol/fints3/segments/crypto.py deleted file mode 100644 index 85c8f13..0000000 --- a/fints/protocol/fints3/segments/crypto.py +++ /dev/null @@ -1,75 +0,0 @@ -# See FinTS_3.0_Security_Sicherheitsverfahren_HBCI_Rel_20130718_final_version.pdf -import time -from collections import OrderedDict - -from . import FinTS3Segment - - -# See B.5.3 -# Currently PINTAN only -class FinTS3CryptoHeader(FinTS3Segment): - def __init__(self, customer=None, blz=None): - self.elements = OrderedDict([ - ('head', OrderedDict([ - ('identifier', 'HNVSK'), - ('counter', 998), # See B.8 - ('version', 3) - ])), - ('profile', OrderedDict([ - ('mode', 'PIN'), - ('version', 1) - ])), - ('function', 998), # kinda magic, but this is according to the example E.3.1 - ('role', '1'), - ('identification', OrderedDict([ - ('identifier', 1), - ('cid', ''), # according to the documentation we can ignore this - ('part', 0) # according to the documentation we can ignore this - ])), - ('datetime', OrderedDict([ - ('identifier', 1), # Sicherheitszeitstempel - ('date', time.strftime('%Y%m%d')), # YYYYMMDD - ('time', time.strftime('%H%M%S')) # HHMMSS - ])), - # p. 191 - ('encryption', OrderedDict([ - ('usage', 2), # Owner Symmetric (OSY) - ('mode', 2), # Cipher Block Chaining (CBC) - ('algorithm', 13), # 2-Key-Triple-DES - ('algparams', b'NOKEY'), # Do we care? - ('algkeyidentifier', 6), # Must be right, according to aqBanking! - ('algparamidentifier', 1) # Only possible value --> Clear text (IVC) - ])), - # p. 184 - ('keyname', OrderedDict([ - ('foo', 280), - ('bank', blz), # so-called Bankleitzahl - ('customer', customer), # Username, in Germany often the account number - ('keytype', 'V'), # Chiffrierschlüssel - ('keynumber', 1), - ('keyversion', 1) - ])), - ('compression', 0) # No compression - ]) - - -# See B.5.4 -class FinTS3CryptoBody(FinTS3Segment): - def __init__(self, data=None): - self.elements = OrderedDict([ - ('head', OrderedDict([ - ('identifier', 'HNVSD'), - ('counter', 999), # See B.8 - ('version', 1) - ])), - ('data', b'') - ]) - - def set_data(self, data): - self.elements['data'] = bytes(data, 'ISO-8859-2') - - def get_data(self, data): - return self.elements['data'] - -# See B.5.1 -# class FinTS3SignatureHeader(FinTS3Segment): diff --git a/fints/protocol/fints3/segments/message.py b/fints/protocol/fints3/segments/message.py index 1a6fac9..a6f2ad2 100644 --- a/fints/protocol/fints3/segments/message.py +++ b/fints/protocol/fints3/segments/message.py @@ -1,40 +1,141 @@ -# See FinTS_3.0_Security_Sicherheitsverfahren_HBCI_Rel_20130718_final_version.pdf -from collections import OrderedDict - +import time from . import FinTS3Segment -# See B.5.2 -class FinTS3Header(FinTS3Segment): - def __init__(self, version=300): - self.elements = OrderedDict([ - ('head', OrderedDict([ - ('identifier', 'HNHBK'), - ('counter', 0), - ('version', 3) - ])), - ('size', 0), - ('version', version), - ('dialog', 0), - ('msg', 0) - ]) - - def set_size(self, size): - selfSize = len(self.to_ascii()) + 1 - self.elements['size'] = size + selfSize - - def to_ascii(self): - self.elements['size'] = str(self.elements['size']).zfill(12) - return super(FinTS3Header, self).to_ascii() - - -# See B.5.3 -class FinTS3Footer(FinTS3Segment): - def __init__(self): - self.elements = OrderedDict([ - ('head', OrderedDict([ - ('identifier', 'HNHBS'), - ('counter', 0), - ('version', 1) - ])), - ]) +class HNHBK(FinTS3Segment): + """ + HNHBK (Nachrichtenkopf) + Section B.5.2 + """ + type = 'HNHBK' + version = 3 + + HEADER_LENGTH = 29 + + def __init__(self, msglen, dialogid, msgno): + + if len(str(msglen)) != 12: + msglen = str(int(msglen) + self.HEADER_LENGTH + len(str(dialogid)) + len(str(msgno))).zfill(12) + + data = [ + msglen, + 300, + dialogid, + msgno + ] + super().__init__(1, data) + + +class HNSHK(FinTS3Segment): + """ + HNSHK (Signaturkopf) + Section B.5.1 + """ + type = 'HNSHK' + version = 4 + + SECURITY_FUNC = 999 + SECURITY_BOUNDARY = 1 # SHM + SECURITY_SUPPLIER_ROLE = 1 # ISS + PINTAN_VERSION = 1 # 1-step + + def __init__(self, segno, secref, blz, username, systemid): + data = [ + ':'.join(['PIN', str(self.PINTAN_VERSION)]), + self.SECURITY_FUNC, + secref, + self.SECURITY_BOUNDARY, + self.SECURITY_SUPPLIER_ROLE, + ':'.join(['1', '', str(systemid)]), + 1, + ':'.join(['1', time.strftime('%Y%m%d'), time.strftime('%H%M%S')]), + ':'.join(['1', '999', '1']), # Negotiate hash algorithm + ':'.join(['6', '10', '16']), # RSA mode + ':'.join([str(self.country_code), blz, username, 'S', '0', '0']), + ] + super().__init__(segno, data) + + +class HNVSK(FinTS3Segment): + """ + HNVSK (Verschlüsslungskopf) + Section B.5.3 + """ + type = 'HNVSK' + version = 3 + + COMPRESSION_NONE = 0 + SECURITY_SUPPLIER_ROLE = 1 # ISS + PINTAN_VERSION = 1 # 1-step + + def __init__(self, segno, blz, username, systemid): + data = [ + ':'.join(['PIN', str(self.PINTAN_VERSION)]), + 998, + self.SECURITY_SUPPLIER_ROLE, + ':'.join(['1', '', str(systemid)]), + ':'.join(['1', time.strftime('%Y%m%d'), time.strftime('%H%M%S')]), + ':'.join(['2', '2', '13', '@8@00000000', '5', '1']), # Crypto algorithm + ':'.join([str(self.country_code), blz, username, 'S', '0', '0']), + self.COMPRESSION_NONE + ] + super().__init__(segno, data) + + +class HNVSD(FinTS3Segment): + """ + HNVSD (Verschlüsselte Daten) + Section B.5.4 + """ + type = 'HNVSD' + version = 1 + + def __init__(self, segno, encoded_data): + self.encoded_data = encoded_data + data = [ + '@{}@{}'.format(len(encoded_data), encoded_data) + ] + super().__init__(segno, data) + + def set_data(self, encoded_data): + self.encoded_data = encoded_data + self.data = [ + '@{}@{}'.format(len(encoded_data), encoded_data) + ] + + +class HNSHA(FinTS3Segment): + """ + HNSHA (Signaturabschluss) + Section B.5.2 + """ + type = 'HNSHA' + version = 2 + + SECURITY_FUNC = 999 + SECURITY_BOUNDARY = 1 # SHM + SECURITY_SUPPLIER_ROLE = 1 # ISS + PINTAN_VERSION = 1 # 1-step + + def __init__(self, segno, secref, pin): + data = [ + secref, + '', + pin + ] + super().__init__(segno, data) + + +class HNHBS(FinTS3Segment): + """ + HNHBS (Nachrichtenabschluss) + Section B.5.3 + """ + type = 'HNHBS' + version = 1 + + def __init__(self, segno, msgno): + data = [ + str(msgno) + ] + super().__init__(segno, data)