May-09-2021, 07:03 PM
I am a hobbyist programmer with an occasional fascination with cryptography. I'm in no way a cryptographer, and my algorithm hasn't been vetted or peer-reviewed in any way but I do hope somebody will find it interesting.
It's a symmetric algorithm that uses a key with 2**n number of bytes. On the surface, it seems secure with a 2**(n*8) key-space, but I can't be entirely sure what the period of the key hashing algorithm is. As far as I know, it is only vulnerable to frequency analysis in the case of very large messages, but I could be mistaken. Maybe somebody will find a way to defeat this encryption tomorrow.
The code:
It's a symmetric algorithm that uses a key with 2**n number of bytes. On the surface, it seems secure with a 2**(n*8) key-space, but I can't be entirely sure what the period of the key hashing algorithm is. As far as I know, it is only vulnerable to frequency analysis in the case of very large messages, but I could be mistaken. Maybe somebody will find a way to defeat this encryption tomorrow.
The code:
import secrets # Some textual user interface constants to avoid littering my main function with # magic numbers. TUI_KEYGEN: str = "k" TUI_ENCRYPT: str = "e" TUI_DECRYPT: str = "d" TUI_QUIT: str = "q" TUI_HELP: str = "h" TUI_PROMPT: str = ":::: " TUI_MODE: str = "m" TUI_MENU: str = f"Please select your operation.\n\ Enter {TUI_KEYGEN} to generate a key.\n\ Enter {TUI_ENCRYPT} to encrypt a plaintext string.\n\ Enter {TUI_DECRYPT} to decrypt a cyphertext string.\n\ Enter {TUI_HELP} to view this menu again.\n\ Enter {TUI_MODE} to toggle between file and console input.\n\ Enter {TUI_QUIT} to quit the application.\n" # A little code reused from a previous project. It's a fixed-width integer # whose bits can be indexed individually or in slices. class Register: def __init__ (self, bitwidth: int, initValue: int = 0) -> None: self.bitwidth: int = bitwidth self.mask: int = 2 ** bitwidth - 1 self._value: int = initValue & self.mask def __getitem__ (self, idx: int) -> int: if idx >= self.bitwidth: raise IndexError(f"The index {idx} extends beyond the width of this {self.bitwidth}-bit register.") return (self._value & (1 << idx)) >> idx def __setitem__ (self, idx: int, v: int) -> None: if v & 1 != v: raise ValueError(f"A single bit cannot be set to {v}. It is too large.") if idx >= self.bitwidth: raise IndexError(f"The index {idx} extends beyond the width of this {self.bitwidth}-bit register.") self._value = self._value & ~(1 << idx) # Zero The bit to be changed. self._value = self._value | v << idx # Move the value to the correct bit position and put it in self.value @property def value (self): return self._value @value.setter def value (self, v: int): self._value = v & self.mask # This is a temporary workaround. I can't remember what dunder methods # are used to implement slicing so Imma just hack in the functionality # I need and refactor later. def getMultiple (self, start: int, stop: int) -> int: if start > self.bitwidth or start < 0: raise IndexError(f"The index {start} extends beyond the width of this {self.bitwidth}-bit register.") if stop > self.bitwidth or stop < 0: raise IndexError(f"The index {stop} extends beyond the width of this {self.bitwidth}-bit register.") bitMask: int = ((2 ** (stop - start)) - 1) * (2**start) return (self._value & bitMask) // 2 ** start def __repr__ (self) -> str: return f"Register({self.bitwidth}, {hex(self._value)})" def getByte (self, idx: int) -> int: lsb: int = idx * 8 msb: int = lsb + 8 bitMask: int = ((2 ** (msb - lsb)) - 1) * (2**lsb) return (self._value & bitMask) // 2 ** lsb def setByte (self, idx: int, v: int) -> None: lsb: int = idx * 8 msb: int = lsb + 8 bitMask: int = ((2 ** (msb - lsb)) - 1) * (2**lsb) self._value &= ~bitMask self._value |= v * lsb ##### Globals ##### globalKey: Register = None isConsoleMode: bool = True def advanceKey (key: Register): key.value += 1 byteLen: int = key.bitwidth // 8 idxSize: int = byteLen - 1 idx1: int = key.getMultiple(0, idxSize) idx2: int = key.getMultiple(byteLen, byteLen + idxSize) swp1: int = key.getByte(idx1) swp2: int = key.getByte(idx2) key.setByte(idx2, swp1) key.setByte(idx1, swp2) def encryptStr (plaintext: str, key: Register) -> str: keyLen: int = key.bitwidth // 8 ciphertext: str = "" for i in plaintext: b = ord(i) for j in range(0, keyLen): displacement: int = key.getByte(j) b -= displacement ^ j b %= 256 # I want zero padding to 1 byte in the hex string and I don't # want the 0x, so I don't use the standard hex function. ciphertext += "0123456789ABCDEF"[(b & 0xF0) >> 4] ciphertext += "0123456789ABCDEF"[b & 0x0F] advanceKey(key) return ciphertext def decryptStr (ciphertext: str, key: Register): keyLen: int = key.bitwidth // 8 plaintext: str = "" for i in range(0, len(ciphertext), 2): b = eval("0x" + ciphertext[i: i + 2]) for j in range(0, keyLen): displacement: int = key.getByte(j) b += displacement ^ j b %= 256 plaintext += chr(b) advanceKey(key) return plaintext def doUsrDrivenKeygen () -> bool: global globalKey try: SkeySize: str = input(f"Please enter the size of the key in bytes.\n\ (Must be a power of 2; i.e. 1, 2, 4, 8...)\n{TUI_PROMPT}") keySize: int = int(SkeySize) # Check whether the number of bytes is a power of 2. clone: int = keySize mask:int = 1 if clone == 0: print("The key size must be non-zero!") return False while clone != 0: if mask & clone and ~mask & clone: print("The key size specified is not 2**n bytes!") return False clone &= ~mask mask <<= 1 except ValueError: print(f"Input {SkeySize} not convertable to an integer.") return False dtaSrc: str = input(f"Please enter a random number or 'p' for a pseudorandom number.\n{TUI_PROMPT}").lower() key = Register(keySize * 8) if dtaSrc == 'p': key.value = secrets.randbits(keySize * 8) else: try: key.value = eval(dtaSrc) except SyntaxError: print(f"Input {dtaSrc} not convertable to an integer.") return False globalKey = key print(f"Using key {hex(globalKey.value)} for all cryptographic operations until key is reassigned.") return False def doUsrDrivenEncrypt () -> bool: # Next TODO global globalKey if globalKey is None: doUsrDrivenKeygen() plaintext: str = "" if isConsoleMode: plaintext = input(f"Please enter the text you want to encrypt.\n{TUI_PROMPT}") else: filePath: str = input(f"Please enter the path to the file you want to encrypt.\n{TUI_PROMPT}") with open(filePath, "r") as f: plaintext = f.read() ciphertext: str = encryptStr(plaintext, globalKey) if isConsoleMode: print(ciphertext) else: filePath: str = input(f"Please enter the path of the file to which you want to write the ciphertext.\n{TUI_PROMPT}") with open(filePath, "w") as f: f.write(ciphertext) return False def doUsrDrivenDecrypt () -> bool: global globalKey if globalKey is None: doUsrDrivenKeygen() ciphertext: str = "" if isConsoleMode: ciphertext = input(f"Please enter the text you want to decrypt.\n{TUI_PROMPT}") else: filePath: str = input(f"Please enter the path to the file you want to decrypt.\n{TUI_PROMPT}") with open(filePath, "r") as f: ciphertext = f.read() plaintext: str = decryptStr(ciphertext, globalKey) if isConsoleMode: print(plaintext) else: filePath: str = input(f"Please enter the path of the file to which you want to write the ciphertext.\n{TUI_PROMPT}") with open(filePath, "w") as f: f.write(plaintext) return False def confirmQuit () -> bool: confirmation: bool = input(f"Are you sure you want to quit? (y/n)\n{TUI_PROMPT}").lower() != "n" return confirmation def NOP () -> bool: return False def toggleMode () -> bool: global isConsoleMode isConsoleMode = not isConsoleMode if isConsoleMode: print("Console Mode. User will be prompted to enter plaintext/ciphertext to the console.") else: print("File Mode. User will be prompted for paths to plaintext and ciphertext files.") def main () -> None: print(TUI_MENU) usrCommand: str = "NOP" while True: usrWantsToQuit: bool = False # Synthetic switch statement try: usrWantsToQuit = { TUI_KEYGEN: doUsrDrivenKeygen, TUI_ENCRYPT: doUsrDrivenEncrypt, TUI_DECRYPT: doUsrDrivenDecrypt, TUI_HELP: lambda: print(TUI_MENU), TUI_QUIT: confirmQuit, TUI_MODE: toggleMode, "NOP": NOP } [usrCommand] () except KeyError: print(f"The input {usrCommand} is not a valid command.") if usrWantsToQuit: break usrCommand = input(TUI_PROMPT) if __name__ == '__main__': main()I hope my code isn't too messy. I look forward to seeing what you all think of it...