#!/usr/bin/env python3 # -*- coding: UTF-8 -*- from RuneText import Rune, RuneText from utils import affine_decrypt ######################################### # RuneSolver : Generic parent class handles interrupts and text highlight ######################################### class RuneSolver(object): def __init__(self): self.reset() def reset(self): self.INTERRUPT = 'ᚠ' self.INTERRUPT_POS = [] # '1' for first occurrence of INTERRUPT def highlight_interrupt(self): return self.highlight_rune(self.INTERRUPT, self.INTERRUPT_POS) def substitute_supports_keylen(self): return False def substitute_get(self, pos, keylen, search_term, found_term, all_data): return found_term.zip_sub(search_term).description(count=True) def enum_data(self, data): irp_i = 0 r_pos = -1 for i, obj in enumerate(data): skip = obj.index == 29 if not skip: r_pos += 1 is_interrupt = obj.rune == self.INTERRUPT if is_interrupt: irp_i += 1 skip = is_interrupt and irp_i in self.INTERRUPT_POS yield obj, i, r_pos, skip def run(self, data): raise NotImplementedError('must subclass') # return RuneText(), [(start-highlight, end-highlight), ...] def __str__(self): return f'interrupt: {self.INTERRUPT}, jumps: {self.INTERRUPT_POS}' ######################################### # SequenceSolver : Decrypt runes with sequential function ######################################### class SequenceSolver(RuneSolver): def __init__(self): super().__init__() self.reset() def reset(self): super().reset() self.FN = None def run(self, data): assert(self.FN) seq_i = 0 ret = [] for rune, i, ri, skip in self.enum_data(data): if not skip: rune = self.FN(seq_i, rune) seq_i += 1 ret.append(rune) return RuneText(ret), [] def __str__(self): return super().__str__() + f'\nf(x): {self.FN}' ######################################### # RunningKeySolver : Decrypt runes with key; handles shift, rotation, etc. ######################################### class RunningKeySolver(RuneSolver): def __init__(self): super().__init__() self.reset() def reset(self): super().reset() self.KEY_DATA = [] # the key material self.KEY_SHIFT = 0 # ABCD -> DABC self.KEY_ROTATE = 0 # ABCD -> ZABC self.KEY_OFFSET = 0 # ABCD -> __ABCD self.KEY_POST_PAD = 0 # ABCD -> ABCD__ def run(self, data): k_len = len(self.KEY_DATA) if k_len <= 0: return data, [] k_full_len = self.KEY_OFFSET + k_len + self.KEY_POST_PAD k_current_pos = 0 ret = [] highlight = [[0, 0]] for rune, i, ri, skip in self.enum_data(data): if not skip: u = k_current_pos - self.KEY_OFFSET if u < 0 or u >= k_len or self.KEY_DATA[u] == 29: self.unmodified_callback(rune) else: key_i = (u + self.KEY_SHIFT) % k_len decrypted = self.decrypt(rune.index, key_i) rune = Rune(i=(decrypted - self.KEY_ROTATE) % 29) if i == highlight[-1][1]: highlight[-1][1] = i + 1 else: highlight.append([i, i + 1]) # rotate_key if k_full_len > 0: # e.g., for key invert without a key k_current_pos = (k_current_pos + 1) % k_full_len ret.append(rune) if highlight[0][1] == 0: highlight = highlight[1:] return RuneText(ret), highlight def decrypt(self, rune_index, key_index): raise NotImplementedError('must subclass') def unmodified_callback(self, rune_index): pass # subclass if needed def key__str__(self): # you should override this return RuneText(self.KEY_DATA).description(indexWhitespace=True) def __str__(self): txt = super().__str__() txt += f'\nkey: {self.key__str__()}' txt += f'\nkey offset: {self.KEY_OFFSET} runes' txt += f'\nkey post pad: {self.KEY_POST_PAD} runes' txt += f'\nkey shift: {self.KEY_SHIFT} indices' txt += f'\nkey rotate: {self.KEY_ROTATE} indices' return txt ######################################### # VigenereSolver : Decrypt runes with an array of indices ######################################### class VigenereSolver(RunningKeySolver): def decrypt(self, rune_index, key_index): return rune_index - self.KEY_DATA[key_index] def substitute_supports_keylen(self): return True def substitute_get(self, pos, keylen, search_term, found_term, all_data): ret = [Rune(r='⁚')] * keylen for i, r in enumerate(found_term.zip_sub(search_term)): ret[(pos + i) % keylen] = r return RuneText(ret).description(count=True, index=False) ######################################### # AffineSolver : Decrypt runes with an array of (s, t) affine keys ######################################### class AffineSolver(RunningKeySolver): def decrypt(self, rune_index, key_index): return affine_decrypt(rune_index, self.KEY_DATA[key_index]) def key__str__(self): return self.KEY_DATA ######################################### # AutokeySolver : Decrypts runes by using previously decrypted ones as input ######################################### class AutokeySolver(RunningKeySolver): def run(self, data): key = self.KEY_DATA[self.KEY_SHIFT:] + self.KEY_DATA[:self.KEY_SHIFT] key = [29] * self.KEY_OFFSET + key + [29] * self.KEY_POST_PAD self.running_key = key return super().run(data) def decrypt(self, rune_index, key_index): rune_index = (rune_index - self.running_key.pop(0)) % 29 self.running_key.append(rune_index) return rune_index def unmodified_callback(self, rune_index): self.running_key.pop(0) self.running_key.append(rune_index) def substitute_supports_keylen(self): return True def substitute_get(self, pos, keylen, search_term, found_term, all_data): data = all_data.index_no_white ret = [Rune(r='⁚')] * keylen for o in range(len(search_term)): plain = search_term[o].index i = pos + o while i >= 0: plain = (data[i] - plain) % 29 i -= keylen ret[i + keylen] = Rune(i=plain) return RuneText(ret).description(count=True, index=False) if __name__ == '__main__': slvr = VigenereSolver() slvr.KEY_DATA = [1] print(slvr) txt = RuneText('hi there') sol = slvr.run(txt) print(sol[0].text) sol, mark = slvr.run(txt) print(sol.text) slvr.KEY_DATA = [-1] print(slvr.run(sol)[0].text) print(mark)