forked from raphaelm/python-fints
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathflicker.py
More file actions
277 lines (227 loc) · 8 KB
/
Copy pathflicker.py
File metadata and controls
277 lines (227 loc) · 8 KB
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
# 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 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])
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 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).
"""
# 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)
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)