]> git.ipfire.org Git - thirdparty/python-fints.git/commitdiff
Add FlickerCode implementation
authorRaphael Michel <mail@raphaelmichel.de>
Thu, 26 Jul 2018 09:24:14 +0000 (11:24 +0200)
committerRaphael Michel <mail@raphaelmichel.de>
Thu, 26 Jul 2018 20:27:05 +0000 (22:27 +0200)
fints/hhd/__init__.py [new file with mode: 0644]
fints/hhd/flicker.py [new file with mode: 0644]

diff --git a/fints/hhd/__init__.py b/fints/hhd/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/fints/hhd/flicker.py b/fints/hhd/flicker.py
new file mode 100644 (file)
index 0000000..a72f176
--- /dev/null
@@ -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 CHLGUCXXXX<code>CHLGTEXT
+        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)