2
\$\begingroup\$

This is a follow-up question to this one.

I have tried to implement all the recommended things in the answers (except commenting, and not being OS specific). Again, if you see anything that needs improvement, or would make it faster, please tell me!

Here's the code. Only 222 lines this time :).

#!/usr/bin/env python 
# not sure if I did this right

import base64
import random
import os


def add_padding(plain_text, block_size=128):
    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 decimal_to_binary(padded_text)


def xor_string(key, secret):
    xored_secret = ''

    for i in range(len(secret) // len(key)):
        if i > 0:
            key = get_round_key(key)

        xored_secret += decimal_to_binary([bin_to_decimal(key, len(key))[0] ^ bin_to_decimal(secret[i * len(key):len(key) + (i * len(key))], len(key))[0]], len(key))

    return xored_secret


def generate_key(key):
    if len(key) >= 128:
        key = decimal_to_binary(key.encode())
        return key[:1024]
    elif len(key) < 128:

        key = key.encode()

        for i in range(128 - len(key)):
            b = decimal_to_binary([key[i]])
            b = xor_string(decimal_to_binary([sum(key) // len(key)]), b[::-1])

            key += bytes([int(b, 2)])

        new_key = ''.join(str(i) for i in key)

        half1 = new_key[:len(new_key) // 2]
        half2 = new_key[len(new_key) // 2:]
        new_key = decimal_to_binary([int(half1 + half2)])
        new_key = new_key[:1024]
        return new_key


def bin_to_base64(binary):
    return base64.b64encode(bytes([int(binary[i * 8:8 + i * 8], 2) for i in range(len(binary) // 8)])).decode()


def bin_to_decimal(binary, length=8):
    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, length=8):
    return ''.join(str(bin(num)[2:].zfill(length)) for num in decimal)


def base64_to_bin(base):
    decoded = ''
    for letter in base64.b64decode(base):
        decoded += bin(letter)[2:].zfill(8)

    return decoded


def matrix_to_str(m):
    return ''.join(str(m[i][j]) for i in range(32) for j in range(32))


def obfuscate(binary, key, encrypting, loops):
    shuffled_binary = ''
    round_key = key

    for i in range(len(binary) // 1024):
        if i > 0:
            round_key = get_round_key(round_key)

        if encrypting:
            m = [list(binary[j * 32 + i * 1024:j * 32 + i * 1024 + 32]) for j in range(32)]
            m = shuffle(m, bin_to_decimal(round_key, 1024)[0], loops)
            shuffled_binary += xor_string(round_key, matrix_to_str(m))
        else:
            xor = xor_string(round_key, binary[i * 1024:i * 1024 + 1024])
            m = [list(xor[j * 32:j * 32 + 32]) for j in range(32)]
            m = reverse_shuffle(m, bin_to_decimal(round_key, 1024)[0], loops)
            shuffled_binary += matrix_to_str(m)

    return xor_string(key, shuffled_binary)


def shuffle(m, key, loops):
    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] * 32 for _ in range(32)]

        for idx, sidx in enumerate(test(key)):
            shuffled_m[idx // 32][idx % 32] = m[sidx // 32][sidx % 32]

        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, key, loops):
    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] * 32 for _ in range(32)]

        for idx, sidx in enumerate(test(key)):
            shuffled_m[sidx // 32][sidx % 32] = m[idx // 32][idx % 32]

        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 test(seed):
    random.seed(seed)
    lst = list(range(1024))
    random.shuffle(lst)

    return lst


def get_round_key(key):
    key = [[key[(j * 32 + n)] for n in range(32)] for j in range(32)]
    # get the last column
    col = [i[-1] for i in key]
    # interweave
    col = [x for i in range(len(col) // 2) for x in (col[-i - 1], col[i])]
    new_key = ''
    for i in range(32):
        cols = ''
        for row in key:
            cols += row[i]

        cols = cols[16:] + cols[:16]
        new_key += xor_string(''.join(str(ele) for ele in col), cols)

    return new_key


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


def encrypt(password, secret, loops):
    key = generate_key(password)
    secret = add_padding(secret)
    secret = xor_string(key, secret)
    secret = obfuscate(secret, key, True, loops)
    secret = bin_to_base64(secret)
    return secret


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


if __name__ == '__main__':
    while True:
        os.system('cls')
        com = input('1)Encrypt Text \n2)Decrypt Text\n3)Exit\n')

        if com == '1':
            os.system('cls')
            secret = input('Enter the text you wish to encrypt: ')
            os.system('cls')
            key = input('Enter your key: ')
            os.system('cls')
            print(f'Encrypted text: {encrypt(key, secret, 1)}')
            input()
        elif com == '2':
            os.system('cls')
            b64 = input('Enter the text you wish to decrypt: ')
            os.system('cls')
            key = input('Enter your key: ')
            os.system('cls')
            print(f'Decrypted text: {decrypt(key, b64, 1)}')
            input()
        elif com == '3':
            break
\$\endgroup\$
2
\$\begingroup\$
  • Consider adding PEP484 type hints. I needed to go through this to make some sense of the values you're passing around.

  • not being OS specific - indeed. Your call to cls has dubious security value, and if you deem it to have such value, it's better to call into a cross-platform library that will accomplish the same thing. Currently you're pegged to Windows and that is bad. You're so close to having a cross-compatible application; it would be a shame to let this remain your only obstacle. For now in the example I have simply deleted your cls calls. If they were only for aesthetic purposes, you should keep it that way.

  • Of much (much) higher security value is getpass instead of input, to prevent an over-the-shoulder of passwords.

  • obfuscate is not a particularly good name for a symmetric crypto function; it only "obfuscates" if encrypting=True. Names are hard; maybe call this process_crypto or somesuch.

  • A string of 0 and 1 characters - or possibly worse, a list of strings of length 1, each a 0 or 1 character - is a very inefficient and impractical internal representation of binary data. It's more work than I'm willing to do, but for an application that whatsoever exceeds superficial, beginner-level instructional code, it's of critical importance that you refactor to use bytes arrays (in the case of immutable data) or bytearray() (in the case of mutable data)

  • Related to the above - probably not a great idea to carry around an arbitrary-length integer of over 300 digits (!!). Again bytes is a better representation.

  • Avoid incremental concatenation of strings in a loop, O(n^2) in time.

  • You need to relax with the one-liners. This:

      xored_secret += decimal_to_binary([bin_to_decimal(key, len(key))[0] ^ bin_to_decimal(secret[i * len(key):len(key) + (i * len(key))], len(key))[0]], len(key))
    

is illegible and unmaintainable, and I see at least three different expressions in there that should receive their own separate, temporary variable on a separate line.

  • Reassigning key to a value of a different type - str to bytes - is not adviseable; make a different variable name.
  • Having a __main__ guard is insufficient to create scope. To put your main variables into function scope you need an actual function.
  • Calling into random is deleterious to the security of your crypto, and is one of the mistakes that probably every newcomer to crypto commits. Call into secrets instead.

Covering some (certainly not all) of the above:

#!/usr/bin/env python
# not sure if I did this right

import base64
import random
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 decimal_to_binary(padded_text)


def xor_string(key: str, secret: str) -> str:
    xored_secret = ''

    for i in range(len(secret) // len(key)):
        if i > 0:
            key = get_round_key(key)

        some_decimals = bin_to_decimal(secret[i * len(key):len(key) + (i * len(key))], len(key))

        some_values = [
            bin_to_decimal(key, len(key))[0] ^ some_decimals[0]
        ]

        xored_secret += decimal_to_binary(some_values, len(key))

    return xored_secret


def generate_key(key: str) -> str:
    if len(key) >= 128:
        key = decimal_to_binary(key.encode())
        return key[:1024]
    elif len(key) < 128:

        key = key.encode()

        for i in range(128 - len(key)):
            b = decimal_to_binary([key[i]])
            b = xor_string(decimal_to_binary([sum(key) // len(key)]), b[::-1])

            key += bytes([int(b, 2)])

        new_key = ''.join(str(i) for i in key)

        half1 = new_key[:len(new_key) // 2]
        half2 = new_key[len(new_key) // 2:]
        new_key = decimal_to_binary([int(half1 + half2)])
        new_key = new_key[:1024]
        return new_key


def bin_to_base64(binary: str) -> str:
    ints = [
        int(binary[i * 8:8 + i * 8], 2)
        for i in range(len(binary) // 8)
    ]
    return base64.b64encode(bytes(ints)).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_bin(base: str) -> str:
    decoded = ''
    for letter in base64.b64decode(base):
        decoded += bin(letter)[2:].zfill(8)

    return decoded


def matrix_to_str(m: List[List[str]]) -> str:
    return ''.join(
        str(m[i][j])
        for i in range(32) for j in range(32)
    )


def obfuscate(binary: str, key: str, encrypting: bool, loops: int) -> str:
    shuffled_binary = ''
    round_key = key

    for i in range(len(binary) // 1024):
        if i > 0:
            round_key = get_round_key(round_key)

        if encrypting:
            m = [list(binary[j * 32 + i * 1024:j * 32 + i * 1024 + 32]) for j in range(32)]
            m = shuffle(m, bin_to_decimal(round_key, 1024)[0], loops)
            shuffled_binary += xor_string(round_key, matrix_to_str(m))
        else:
            xor = xor_string(round_key, binary[i * 1024:i * 1024 + 1024])
            m = [list(xor[j * 32:j * 32 + 32]) for j in range(32)]
            m = reverse_shuffle(m, bin_to_decimal(round_key, 1024)[0], loops)
            shuffled_binary += matrix_to_str(m)

    return xor_string(key, shuffled_binary)


def shuffle(m: List[List[str]], key: int, loops: int) -> List[List[str]]:
    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] * 32 for _ in range(32)]

        for idx, sidx in enumerate(test(key)):
            shuffled_m[idx // 32][idx % 32] = m[sidx // 32][sidx % 32]

        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] * 32 for _ in range(32)]

        for idx, sidx in enumerate(test(key)):
            shuffled_m[sidx // 32][sidx % 32] = m[idx // 32][idx % 32]

        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 test(seed: int) -> List[int]:
    random.seed(seed)
    lst = list(range(1024))
    random.shuffle(lst)

    return lst


def get_round_key(key):
    key = [[key[(j * 32 + n)] for n in range(32)] for j in range(32)]
    # get the last column
    col = [i[-1] for i in key]
    # interweave
    col = [x for i in range(len(col) // 2) for x in (col[-i - 1], col[i])]
    new_key = ''
    for i in range(32):
        cols = ''
        for row in key:
            cols += row[i]

        cols = cols[16:] + cols[:16]
        new_key += xor_string(''.join(str(ele) for ele in col), cols)

    return new_key


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 = bin_to_base64(secret)
    return secret


def decrypt(password: str, base: str, loops: int = 1) -> str:
    key = generate_key(password)
    binary = base64_to_bin(base)
    binary = xor_string(key, binary)
    binary = obfuscate(binary, key, False, loops)
    binary = bin_to_bytes(binary)
    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: ')

        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__':
    main()

Speaking more generally, for educational and recreational purposes writing this kind of code is fun. However, cryptographic implementations are viciously difficult to get correct, and sometimes even more difficult to prove that they're correct. In the real, production world, please do not use this; just call into a library.

\$\endgroup\$
12
  • \$\begingroup\$ Concerning the arbitrary length int, how would I replace that with a bytes array? \$\endgroup\$ – Have a nice day Apr 27 at 22:38
  • \$\begingroup\$ So far as I see, you're only using the key int to seed random. Don't use random. You need to call into a secure PRNG that accepts a byte array as seed state; secrets doesn't support this. \$\endgroup\$ – Reinderien Apr 27 at 22:58
  • \$\begingroup\$ I was planning on switching to os.urandom(). Would that work? \$\endgroup\$ – Have a nice day Apr 27 at 23:00
  • \$\begingroup\$ no, because it cannot be seeded \$\endgroup\$ – Reinderien Apr 27 at 23:07
  • \$\begingroup\$ And with the lists, how would I refactor to bytes? \$\endgroup\$ – Have a nice day Apr 27 at 23:10

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.