File: flicker.py

package info (click to toggle)
python-fints 4.0.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 700 kB
  • sloc: python: 5,021; makefile: 196
file content (282 lines) | stat: -rw-r--r-- 8,344 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# 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):
    if code.startswith('@'):
        code = code[res.challenge_hhd_uc.index('@', 2) + 1:]
    code = code.replace(" ", "").strip()
    if "CHLGUC" in code and "CHLGTEXT" in code:
        # Sometimes, HHD 1.3 codes are not transferred 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])
        if len(code) < length+self.lc:
            raise ValueError("lc too large: {} + {} > {}".format(self.lc, length, len(code)))
        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 code_to_bitstream(code):
    """Convert a flicker code into a bitstream in strings."""
    # 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
    code = parse(code).render()
    data = swap_bytes(code)
    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))
    return stream

def terminal_flicker_unix(code, field_width=3, space_width=3, height=1, clear=False, wait=0.05):
    """
    Re-encodes a flicker code and prints it on a unix terminal.

    :param code: Challenge value
    :param field_width: Width of fields in characters (default: 3).
    :param space_width: Width of spaces in characters (default: 3).
    :param height: Height of fields in characters (default: 1).
    :param clear: Clear terminal after every line (default: ``False``).
    :param wait: Waiting interval between lines (default: 0.05).
    """
    stream = code_to_bitstream(code)

    high = '\033[48;05;15m'
    low = '\033[48;05;0m'
    std = '\033[0m'

    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)