0
\$\begingroup\$

This is a follow up question to Encryption/Decryption algorithm #2 and Encryption/Decryption algorithm.

In my last question, in @Reinderien's answer, he added this line from typing import List, and stuff like this m: List[List[int]]. Is that just for type hints?

Would it be faster to use numpy arrays instead of python lists of lists?

Aside from those follow up questions, if you see anything needing to be changed, let me now.

import base64
import numpy as np
from randomgen import ChaCha
from getpass import getpass
from typing import List


def add_padding(plain_text: str, block_size: int = 128) -> str:
    plain_text = plain_text.encode()
    padding = -(len(plain_text) + 1) % block_size  # Amount of padding needed to fill block
    
    padded_text = plain_text + b'=' * padding + bytes([padding + 1])
    
    return padded_text


def xor_string(key: bytes, secret: bytes) -> bytes:
    xored_secret = b''
    
    for i in range(len(secret) // len(key)):
        if i > 0:
            key = get_round_key(key)
        
        some_decimals = secret[i * len(key):len(key) + (i * len(key))]
        xored_secret = xored_secret + b''.join(bytes([key[i] ^ some_decimals[i]]) for i in range(len(key)))
    
    return xored_secret


def get_round_key(key: bytes) -> bytes:
    last_col = key[15::16]
    
    # interweave
    last_col = b''.join(x for i in range(len(last_col) // 2) for x in (bytes([last_col[-i - 1]]), bytes([last_col[i]])))
    
    new_key = b''
    for current_col in [key[i::16] for i in range(16)]:
        current_col = xor_string(last_col, current_col)
        new_key = new_key + current_col[len(current_col) // 2:] + current_col[:len(current_col) // 2]
    
    return new_key


def generate_key(key: str) -> bytes:
    if len(key) >= 128:
        key = key.encode()
        return key[:128]
    elif len(key) < 128:
        
        key = key.encode()
        
        for i in range(128 - len(key)):
            key = key + bytes([(sum(key) // len(key)) ^ sum(1 << (8 - 1 - j) for j in range(8) if key[i] >> j & 1)])
        
        decimal = ''.join(str(i) for i in key)
        
        binary = f'{bin(int(decimal[len(decimal) // 2:] + decimal[:len(decimal) // 2]))[2:]:<01024s}'
        
        key = bin_to_bytes(binary[:1024])
        half1 = key[:len(key) // 2]
        half2 = key[len(key) // 2:]
        key = half2 + half1
        return key[:128]


def bytes_to_base64(binary: bytes) -> str:
    # ints = [int(binary[i * 8:8 + i * 8], 2) for i in range(len(binary) // 8)]
    return base64.b64encode(binary).decode()


def bin_to_decimal(binary: str, length: int = 8) -> List[int]:
    b = [
        binary[i * length:length + (i * length)]
        for i in range(len(binary) // length)
    ]
    
    decimal = [int(i, 2) for i in b]
    
    return decimal


def decimal_to_binary(decimal: List[int], length: int = 8) -> str:
    return ''.join(str(bin(num)[2:].zfill(length)) for num in decimal)


def base64_to_bytes(base: str) -> bytes:
    return base64.b64decode(base)


def matrix_to_bytes(m: List[List[int]]) -> bytes:
    return b''.join(bytes([m[i][j]]) for i in range(16) for j in range(8))


def obfuscate(secret: bytes, key: bytes, encrypting: bool, loops: int) -> bytes:
    shuffled_data = b''
    round_key = key
    
    for i in range(len(secret) // 128):
        if i > 0:
            round_key = get_round_key(round_key)
        
        if encrypting:
            m = [
                list(secret[j * 8 + i * 128:j * 8 + i * 128 + 8])
                for j in range(16)
            ]
            m = shuffle(m, round_key, loops)
            m = matrix_to_bytes(m)
            m = shift_bits(round_key, m)
            shuffled_data += xor_string(round_key, m)
        else:
            xor = xor_string(round_key, secret[i * 128:i * 128 + 128])
            xor = unshift_bits(round_key, xor)
            m = [list(xor[j * 8:j * 8 + 8]) for j in range(16)]
            m = reverse_shuffle(m, round_key, loops)
            shuffled_data += matrix_to_bytes(m)
    
    return xor_string(key, shuffled_data)


def shuffle(m: List[List[int]], key: int, loops: int) -> List[List[int]]:
    for j in range(loops):
        # move columns to the right
        m = [row[-1:] + row[:-1] for row in m]
        
        # move rows down
        m = m[-1:] + m[:-1]
        
        shuffled_m = [[0] * 8 for _ in range(16)]
        
        for idx, sidx in enumerate(test(key)):
            shuffled_m[idx // 8][idx % 8] = m[sidx // 8][sidx % 8]
        
        m = shuffled_m
        
        # cut in half and flip halves
        m = m[len(m) // 2:] + m[:len(m) // 2]
        
        # test
        # m = list(map(list, zip(*m)))
    
    return m


def reverse_shuffle(m: List[List[str]], key: int, loops: int) -> List[List[str]]:
    for j in range(loops):
        # test
        # m = list(map(list, zip(*m)))
        
        # cut in half and flip halves
        m = m[len(m) // 2:] + m[:len(m) // 2]
        
        shuffled_m = [[0] * 8 for _ in range(16)]
        
        for idx, sidx in enumerate(test(key)):
            shuffled_m[sidx // 8][sidx % 8] = m[idx // 8][idx % 8]
        
        m = shuffled_m
        
        # move rows up
        m = m[1:] + m[:1]
        
        # move columns to the left
        m = [row[1:] + row[:1] for row in m]
    
    return m


def shift_bits(key: bytes, secret: bytes) -> bytes:
    """As you can see I have no idea what I'm doing :)"""
    shifted = b''
    for idx, byte in enumerate(secret):
        byte = byte ^ 255
        
        byte = sum(1 << (8 - 1 - j) for j in range(8) if byte >> j & 1)
        
        byte = byte ^ (key[idx] ^ 255)
        
        shifted = shifted + bytes([byte])
    
    return shifted


def unshift_bits(key: bytes, secret: bytes) -> bytes:
    shifted = b''
    for idx, byte in enumerate(secret):
        byte = byte ^ (key[idx] ^ 255)
        
        byte = sum(1 << (8 - 1 - j) for j in range(8) if byte >> j & 1)
        
        byte = byte ^ 255
        
        shifted = shifted + bytes([byte])
    
    return shifted


def test(seed: bytes) -> List[int]:
    rg = np.random.Generator(ChaCha(seed=int.from_bytes(seed, byteorder='big'), rounds=8))
    lst = np.arange(128)
    rg.shuffle(lst)
    
    return lst


def bin_to_bytes(binary: str) -> bytes:
    return int(binary, 2).to_bytes(len(binary) // 8, byteorder='big')


def encrypt(password: str, secret: str, loops: int = 1) -> str:
    key = generate_key(password)
    secret = add_padding(secret)
    secret = xor_string(key, secret)
    secret = obfuscate(secret, key, True, loops)
    secret = bytes_to_base64(secret)
    return secret


def decrypt(password: str, base: str, loops: int = 1) -> str:
    key = generate_key(password)
    binary = base64_to_bytes(base)
    binary = xor_string(key, binary)
    binary = obfuscate(binary, key, False, loops)
    pad = binary[-1]
    binary = binary[:-pad]
    return binary.decode()


def main():
    while True:
        com = input('1) Encrypt Text\n' '2) Decrypt Text\n' '3) Exit\n')
        
        input_text = input('Enter the text: ')
        # key = getpass('Enter your key: ')
        key = input('Enter your key: ')  # getpass doesn't work in pycharm, just for testing
        
        if com == '1':
            print(f'Encrypted text: {encrypt(key, input_text)}')
        
        elif com == '2':
            print(f'Decrypted text: {decrypt(key, input_text)}')
        
        elif com == '3':
            break
        
        print()


if __name__ == '__main__':
    # from datetime import datetime
    #
    # start = datetime.now()
    # encrypt('password', 'hello this is a test')
    # print(datetime.now() - start)
    
    main()
\$\endgroup\$
6
  • \$\begingroup\$ The best thing to do with encryption is to not do it yourself. Use a popular package from a reputable source. If you don't trust them - encrypt twice with two different algorithms from two different packages from well-known sources. \$\endgroup\$ May 6 at 20:12
  • \$\begingroup\$ @StenPetrov it's just for fun/practice :) \$\endgroup\$ May 7 at 0:37
  • \$\begingroup\$ It's all fun and games but... toy code becomes proof-of-concept code, which becomes prod code - and nobody is looking into it anymore... and then your company ends up in the news with a massive private data leak \$\endgroup\$ May 14 at 17:24
  • \$\begingroup\$ @StenPetrov Before (if ever) it becomes production code, I would have it be analyzed by cryptologists who know what they are doing. Only then, if it passes, would I use it. \$\endgroup\$ May 14 at 17:33
  • \$\begingroup\$ .... and there lies the problem. "Cryptologists" is an imaginary title and people who explicitly deal with cryptography only exist in universities and very large companies. In companies smaller than the likes of Microsoft, Google, Apple, Facebook etc, you get a "local expert" who guessingly waves a thumbs up. And even then - cryptography experts can get it wrong too. SHA1 is named "Secure", yet it's already considered broken and there were holes found in many popular algorithms and protocols. \$\endgroup\$ May 17 at 21:53
3
+50
\$\begingroup\$

Is that just for type hints?

Correct, everything in the typing module is used for type hints. These are useful when using something like mypy, but the python interpreter will just ignore all of these.

Would it be faster to use numpy arrays instead of python lists of lists?

Theoretically, yes but it depends on the size of the data. numpy is really meant for processing large data sets, and I imagine most things you'd use this for is pretty small where the overhead would wipe away any speedup.

Aside from those follow up questions, if you see anything needing to be changed, let me now.

def generate_key(key: str) -> bytes:
    if len(key) >= 128:
        key = key.encode()
        return key[:128]
    elif len(key) < 128:
        
        key = key.encode()
        
        for i in range(128 - len(key)):
            key = key + bytes([(sum(key) // len(key)) ^ sum(1 << (8 - 1 - j) for j in range(8) if key[i] >> j & 1)])
        
        decimal = ''.join(str(i) for i in key)
        
        binary = f'{bin(int(decimal[len(decimal) // 2:] + decimal[:len(decimal) // 2]))[2:]:<01024s}'
        
        key = bin_to_bytes(binary[:1024])
        half1 = key[:len(key) // 2]
        half2 = key[len(key) // 2:]
        key = half2 + half1
        return key[:128]

The elif here is unnecessary. You could remove some of the indentation by just omitting the elif statement here.

def generate_key(key: str) -> bytes:
    if len(key) >= 128:
        key = key.encode()
        return key[:128]
        
    key = key.encode()
    
    for i in range(128 - len(key)):
        key = key + bytes([(sum(key) // len(key)) ^ sum(1 << (8 - 1 - j) for j in range(8) if key[i] >> j & 1)])
    
    decimal = ''.join(str(i) for i in key)
    
    binary = f'{bin(int(decimal[len(decimal) // 2:] + decimal[:len(decimal) // 2]))[2:]:<01024s}'
    
    key = bin_to_bytes(binary[:1024])
    half1 = key[:len(key) // 2]
    half2 = key[len(key) // 2:]
    key = half2 + half1
    return key[:128]
  • It's kinda weird that you use i for some loops and j for others when there are no name collisions. Weird, but not horrible.
  • I would probably toss 128 into a constant called block size and put that at the top of this file. BLOCK_SIZE = 128. Then replace all instances of 128 with that constant That way if the block size changes you only need to update it in one place.
  • You could probably wrap this in a class and it would be easier to handle namespacing across files if you end up using this in another python file. Going this route you could just expose encrypt/decrypt functions and make all of the others "private" by prefixing them with _.
    • If you go this way blocksize could be set on the object and doesn't need to be a constant
  • Right now you are only using numpy for the randomness functions. I would suggest using pythons built in random functions for that.
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Some of the j's were probably left overs from when there was a collision, but then the code was changed and I never switched to i. And for the randomness part, the reason I don't use the random module is it isn't secure. Anyway, thanks for the feedback! \$\endgroup\$ May 6 at 15:11
  • \$\begingroup\$ You could use docs.python.org/3/library/secrets.html#module-secrets if you wanted to use a crypto safer random function. I assumed this was a toy program so the random module should be fine. \$\endgroup\$
    – Kendall
    May 6 at 18:49
  • \$\begingroup\$ While it is a "toy" program I figured I'd make it as good as possible. \$\endgroup\$ May 7 at 0:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.