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()
-
\$\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\$Sten Petrov– Sten Petrov2021年05月06日 20:12:56 +00:00Commented May 6, 2021 at 20:12
-
\$\begingroup\$ @StenPetrov it's just for fun/practice :) \$\endgroup\$Have a nice day– Have a nice day2021年05月07日 00:37:36 +00:00Commented May 7, 2021 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\$Sten Petrov– Sten Petrov2021年05月14日 17:24:50 +00:00Commented May 14, 2021 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\$Have a nice day– Have a nice day2021年05月14日 17:33:15 +00:00Commented May 14, 2021 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\$Sten Petrov– Sten Petrov2021年05月17日 21:53:14 +00:00Commented May 17, 2021 at 21:53
1 Answer 1
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 andj
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.
- https://docs.python.org/3/library/random.html
- For secure randomness you could use the secrets module: https://docs.python.org/3/library/secrets.html#module-secrets
-
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 toi
. And for the randomness part, the reason I don't use therandom
module is it isn't secure. Anyway, thanks for the feedback! \$\endgroup\$Have a nice day– Have a nice day2021年05月06日 15:11:16 +00:00Commented May 6, 2021 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– Kendall2021年05月06日 18:49:51 +00:00Commented May 6, 2021 at 18:49
-
\$\begingroup\$ While it is a "toy" program I figured I'd make it as good as possible. \$\endgroup\$Have a nice day– Have a nice day2021年05月07日 00:38:24 +00:00Commented May 7, 2021 at 0:38
Explore related questions
See similar questions with these tags.