# 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
[docs]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)