From: Raphael Michel Date: Thu, 26 Jul 2018 09:24:14 +0000 (+0200) Subject: Add FlickerCode implementation X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c38f2e66980006c2499002c6112004a859f18f4d;p=thirdparty%2Fpython-fints.git Add FlickerCode implementation --- diff --git a/fints/hhd/__init__.py b/fints/hhd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fints/hhd/flicker.py b/fints/hhd/flicker.py new file mode 100644 index 0000000..a72f176 --- /dev/null +++ b/fints/hhd/flicker.py @@ -0,0 +1,263 @@ +# Inspired by: +# https://github.com/willuhn/hbci4java/blob/master/src/org/kapott/hbci/manager/FlickerCode.java +# https://6xq.net/flickercodes/ +# https://wiki.ccc-ffm.de/projekte:tangenerator:start#flickercode_uebertragung +import math +import re + +import time + +HHD_VERSION_13 = 13 +HHD_VERSION_14 = 14 +LC_LENGTH_HHD14 = 3 +LC_LENGTH_HHD13 = 2 +LDE_LENGTH_DEFAULT = 2 +LDE_LENGTH_SPARDA = 3 +BIT_ENCODING = 6 # Position of encoding bit +BIT_CONTROLBYTE = 7 # Position of bit that tells if there are a control byte +ENCODING_ASC = 1 +ENCODING_BCD = 2 + + +def parse(code): + code = clean(code) + try: + return FlickerCode(code, HHD_VERSION_14) + except: + try: + return FlickerCode(code, HHD_VERSION_14, LDE_LENGTH_SPARDA) + except: + return FlickerCode(code, HHD_VERSION_13) + + +def clean(code): + code = code.replace(" ", "").strip() + if "CHLGUC" in code and "CHLGTEXT" in code: + # Sometimes, HHD 1.3 codes are not transfered in the challenge field but in the free text, + # contained in CHLGUCXXXXCHLGTEXT + code = "0" + code[code.index("CHLGUC") + 11:code.index("CHLGTEXT")] + return code + + +def bit_sum(num, bits): + s = 0 + for i in range(bits): + s += num & (1 << i) + return s + + +def digitsum(n): + q = 0 + while n != 0: + q += n % 10 + n = math.floor(n / 10) + return q + + +def h(num, l): + return hex(num).upper()[2:].zfill(l) + + +def asciicode(s): + return ''.join(h(ord(c), 2) for c in s) + + +def swap_bytes(s): + b = "" + for i in range(0, len(s), 2): + b += s[i + 1] + b += s[i] + return b + + +class FlickerCode: + def __init__(self, code, version, lde_len=LDE_LENGTH_DEFAULT): + self.version = version + self.lc = None + self.startcode = Startcode() + self.de1 = DE(lde_len) + self.de2 = DE(lde_len) + self.de3 = DE(lde_len) + self.rest = None + self.parse(code) + + def parse(self, code): + length = LC_LENGTH_HHD14 if self.version == HHD_VERSION_14 else LC_LENGTH_HHD13 + self.lc = int(code[0:length]) + code = code[length:] + code = self.startcode.parse(code) + self.version = self.startcode.version + code = self.de1.parse(code, self.version) + code = self.de2.parse(code, self.version) + code = self.de3.parse(code, self.version) + self.rest = code or None + + def render(self): + s = self.create_payload() + luhn = self.create_luhn_checksum() + xor = self.create_xor_checksum(s) + return s + luhn + xor + + def create_payload(self): + s = str(self.startcode.render_length()) + for b in self.startcode.control_bytes: + s += h(b, 2) + s += self.startcode.render_data() + for de in (self.de1, self.de2, self.de3): + s += de.render_length() + s += de.render_data() + + l = (len(s) + 2) // 2 # data + checksum / chars per byte + lc = h(l, 2) + return lc + s + + def create_xor_checksum(self, payload): + xorsum = 0 + for c in payload: + xorsum ^= int(c, 16) + return h(xorsum, 1) + + def create_luhn_checksum(self): + s = "" + for b in self.startcode.control_bytes: + s += h(b, 2) + s += self.startcode.render_data() + if self.de1.data is not None: + s += self.de1.render_data() + if self.de2.data is not None: + s += self.de2.render_data() + if self.de3.data is not None: + s += self.de3.render_data() + + luhnsum = 0 + for i in range(0, len(s), 2): + luhnsum += 1 * int(s[i], 16) + digitsum(2 * int(s[i + 1], 16)) + + m = luhnsum % 10 + if m == 0: + return "0" + r = 10 - m + ss = luhnsum + r + luhn = ss - luhnsum + return h(luhn, 1) + + +class DE: + def __init__(self, lde_len): + self.length = 0 + self.lde = 0 + self.lde_length = lde_len + self.encoding = None + self.data = None + + def parse(self, data, version): + self.version = version + if not data: + return data + self.lde = int(data[0:self.lde_length]) + data = data[self.lde_length:] + + self.length = bit_sum(self.lde, 5) + self.data = data[0:self.length] + return data[self.length:] + + def set_encoding(self): + if self.data is None: + self.encoding = ENCODING_BCD + elif self.encoding is not None: + pass + elif re.match("^[0-9]{1,}$", self.data): + # BCD only if the value is fully numeric, no IBAN etc. + self.encoding = ENCODING_BCD + else: + self.encoding = ENCODING_ASC + + def render_length(self): + self.set_encoding() + if self.data is None: + return "" + l = len(self.render_data()) // 2 + if self.encoding == ENCODING_BCD: + return h(l, 2) + + if self.version == HHD_VERSION_14: + l = l + (1 << BIT_ENCODING) + return h(l, 2) + + return "1" + h(l, 1) + + def render_data(self): + self.set_encoding() + if self.data is None: + return "" + + if self.encoding == ENCODING_ASC: + return asciicode(self.data) + + if len(self.data) % 2 == 1: + return self.data + "F" + + return self.data + + +class Startcode(DE): + def __init__(self): + super().__init__(LDE_LENGTH_DEFAULT) + self.control_bytes = [] + + def parse(self, data): + self.lde = int(data[:2], 16) + data = data[2:] + + self.length = bit_sum(self.lde, 5) + + self.version = HHD_VERSION_13 + if self.lde & (1 << BIT_CONTROLBYTE) != 0: + self.version = HHD_VERSION_14 + for i in range(10): + cbyte = int(data[:2], 16) + self.control_bytes.append(cbyte) + data = data[2:] + if cbyte & (1 << BIT_CONTROLBYTE) == 0: + break + + self.data = data[:self.length] + return data[self.length:] + + def render_length(self): + s = super().render_length() + if self.version == HHD_VERSION_13 or not self.control_bytes: + return s + l = int(s, 16) + (1 << BIT_CONTROLBYTE) + return h(l, 2) + + +def terminal_flicker_unix(code, field_width=3, space_width=3, height=1, clear=False, wait=0.05): + # Inspired by Andreas Schiermeier + # https://git.ccc-ffm.de/?p=smartkram.git;a=blob_plain;f=chiptan/flicker/flicker.sh;h + # =7066293b4e790c2c4c1f6cbdab703ed9976ffe1f;hb=refs/heads/master + data = swap_bytes(code) + high = '\033[48;05;15m' + low = '\033[48;05;0m' + std = '\033[0m' + stream = ['10000', '00000', '11111', '01111', '11111', '01111', '11111'] + for c in data: + v = int(c, 16) + stream.append('1' + str(v & 1) + str((v & 2) >> 1) + str((v & 4) >> 2) + str((v & 8) >> 3)) + stream.append('0' + str(v & 1) + str((v & 2) >> 1) + str((v & 4) >> 2) + str((v & 8) >> 3)) + + while True: + for frame in stream: + if clear: + print('\033c', end='') + + for i in range(height): + for c in frame: + print(low + ' ' * space_width, end='') + if c == '1': + print(high + ' ' * field_width, end='') + else: + print(low+ ' ' * field_width, end='') + print(low + ' ' * space_width + std) + + time.sleep(wait)