Я хочу изучить Python, поэтому я портировал свою реализацию Enigma на C #. Он получил UnitTests и работает.
Я ищу обзор, в котором рассказывается, где я не знаю лучших практик, где я нарушаю соглашения об именах, где я выгляжу как программист на C #, пишущий Python 🙂
Спасибо и развлекайся, Гарри
https://github.com/HaraldLeitner/Enigma
BusinessLogic.py
from Enums import Mode
class BusinessLogic:
def __init__(self, rolls):
self._rolls = rolls
self._rolls_reverse = rolls.copy()
self._rolls_reverse.reverse()
def transform_file(self, infile, outfile, mode):
buffer_size = 65536
in_file = open(infile, 'rb', buffer_size)
out_file = open(outfile, 'wb', buffer_size)
buffer = bytearray(in_file.read(buffer_size))
while len(buffer):
self.transform_buffer(buffer, mode)
out_file.write(buffer)
buffer = bytearray(in_file.read(buffer_size))
in_file.close()
out_file.close()
def transform_buffer(self, buffer, mode):
if mode == Mode.ENC:
for i in range(len(buffer)):
for roll in self._rolls:
roll.encrypt(buffer, i)
self.roll_on()
if mode == Mode.DEC:
for i in range(len(buffer)):
for roll in self._rolls_reverse:
roll.decrypt(buffer, i)
self.roll_on()
def roll_on(self):
for roll in self._rolls:
if not roll.roll_on():
break
Enums.py
from enum import Enum
class Mode(Enum):
ENC = 0
DEC = 1
Roll.py
class Roll:
def __init__(self, transitions, turn_over_indices):
self._transitions = transitions
self._turn_over_indices = turn_over_indices
self._re_transitions = bytearray(256)
for x in self._transitions:
self._re_transitions[self._transitions[x]] = x
self._position = 0
def check_input(self, turnover_indices_count):
if len(self._transitions) != 256:
raise ValueError("Wrong Transition length ")
for i in range(256):
found = 0
for j in self._transitions:
if self._transitions[j] == i:
found = 1
continue
if not found:
raise ValueError("Transitions not 1-1 complete")
if len(self._turn_over_indices) != turnover_indices_count:
raise ValueError("Wrong TurnOverIndices length ")
for i in range(len(self._turn_over_indices) - 1):
if self._turn_over_indices[i] == self._turn_over_indices[i + 1]:
raise ValueError("turn_over_indices has doubles")
def encrypt(self, buffer, index):
buffer[index] = self._transitions[(buffer[index] + self._position) & 0xff]
def decrypt(self, buffer, index):
buffer[index] = (self._re_transitions[int(buffer[index])] - self._position) & 0xff
def roll_on(self):
self._position = (self._position + 1) & 0xff
return self._turn_over_indices.count(self._position)
Enigma.ini
[DEFAULT]
TransitionCount = 53
Main.py
from configparser import ConfigParser
import os
import sys
from random import random, randbytes, randint
from time import sleep
from BusinessLogic import BusinessLogic
from Enums import Mode
from Roll import Roll
class Program:
def __init__(self, transition_count=0):
self._mode = None
self._rolls = []
self._inputFilename = None
self._keyFilename = None
self._transitionCount = None
config = ConfigParser()
config.read("enigma.ini")
if transition_count < 1:
self._transitionCount = config.getint("DEFAULT", "TransitionCount")
else:
self._transitionCount = transition_count
def main(self):
if len(sys.argv) != 4:
print("Generate key with 'keygen x key.file' where x > 3 is the number of rolls.")
print("Encrypt a file with 'enc a.txt key.file'")
print("Decrypt a file with 'dec a.txt key.file'")
exit(1)
self.run_main(sys.argv[1], sys.argv[2], sys.argv[3])
def run_main(self, arg1, arg2, arg3):
self._keyFilename = arg3
if arg1 == "keygen":
self.keygen(int(arg2))
return
self._inputFilename = arg2
if arg1.lower() == 'enc':
self._mode = Mode.ENC
elif arg1 == 'dec':
self._mode = Mode.DEC
else:
raise Exception("Undefined Encryption Mode.")
self.create_rolls()
BusinessLogic(self._rolls).transform_file(self._inputFilename, self._inputFilename + '.' + self._mode.name,
self._mode)
def keygen(self, roll_count):
if roll_count < 4:
raise Exception("Not enough rolls.")
if os.path.exists(self._keyFilename):
os.remove(self._keyFilename)
key = bytearray()
for i in range(roll_count):
transform = bytearray(256)
for j in range(256):
transform[j] = j
while not self.is_twisted(transform):
for j in range(256):
rand1 = randint(0, 255)
rand2 = randint(0, 255)
temp = transform[rand1]
transform[rand1] = transform[rand2]
transform[rand2] = temp
key += transform
transitions = bytearray()
while len(transitions) < self._transitionCount:
rand = randint(0, 255)
if not transitions.count(rand):
transitions.append(rand)
key += transitions
file = open(self._keyFilename, 'wb')
file.write(key)
file.close()
print("Keys generated.")
sleep(1)
def is_twisted(self, trans):
for i in range(256):
if trans[i] == i:
return 0
return 1
def create_rolls(self):
roll_key_length = 256 + self._transitionCount
file = open(self._keyFilename, 'rb')
key = file.read()
file.close()
if len(key) % roll_key_length:
raise Exception('Invalid key_size')
roll_count = int(len(key) / roll_key_length)
for rollNumber in range(roll_count):
self._rolls.append(Roll(key[rollNumber * roll_key_length: rollNumber * roll_key_length + 256],
key[
rollNumber * roll_key_length + 256: rollNumber * roll_key_length + 256 + self._transitionCount]))
for roll in self._rolls:
roll.check_input(self._transitionCount)
if __name__ == '__main__':
Program().main()
UnitTest.py
import os
import unittest
from BusinessLogic import BusinessLogic
from Enums import Mode
from Roll import Roll
from main import Program
class MyTestCase(unittest.TestCase):
def setUp(self):
self.trans_linear = bytearray(256) # here every char is mapped to itself
self.trans_linear_invert = bytearray(256) # match the first to the last etc
self.trans_shift_1 = bytearray(256) # 'a' is mapped to 'b' etc
self.trans_shift_2 = bytearray(256) # 'a' is mapped to 'c' etc
self.businesslogic_encode = None
self.businesslogic_decode = None
self.encrypted_message = bytearray()
self.decrypted_message = bytearray()
self.init_test_rolls()
def init_test_rolls(self):
for i in range(256):
self.trans_linear[i] = i
self.trans_linear_invert[i] = 255 - i
self.trans_shift_1[i] = (i + 1) % 256
self.trans_shift_2[i] = (i + 2) % 256
def init_business_logic(self, transitions, turnovers):
rolls_encrypt = []
rolls_decrypt = []
for index in range(len(transitions)):
rolls_encrypt.append(Roll(transitions[index], turnovers[index]))
rolls_decrypt.append(Roll(transitions[index], turnovers[index]))
self.businesslogic_encode = BusinessLogic(rolls_encrypt)
self.businesslogic_decode = BusinessLogic(rolls_decrypt)
def crypt(self, msg):
self.encrypted_message = bytearray(msg)
self.businesslogic_encode.transform_buffer(self.encrypted_message, Mode.ENC)
self.decrypted_message = bytearray(self.encrypted_message)
self.businesslogic_decode.transform_buffer(self.decrypted_message, Mode.DEC)
def test_one_byte_one_roll_linear(self):
for i in range(256):
self.init_business_logic([self.trans_linear], [[0]])
self.crypt([i])
self.assertEqual(i, self.encrypted_message[0])
self.assertEqual(i, self.decrypted_message[0])
def test_one_byte_one_roll_shift_one(self):
for i in range(256):
self.init_business_logic([self.trans_shift_1], [[0]])
self.crypt([i])
self.assertEqual((i + 1) % 256, self.encrypted_message[0])
self.assertEqual(i, self.decrypted_message[0])
def test_one_byte_one_roll_shift_two(self):
for i in range(256):
self.init_business_logic([self.trans_shift_2], [[0]])
self.crypt([i])
self.assertEqual((i + 2) % 256, self.encrypted_message[0])
self.assertEqual(i, self.decrypted_message[0])
def test_two_byte_one_roll_linear(self):
for i in range(256):
self.init_business_logic([self.trans_linear], [[0]])
self.crypt([i, (i + 1) % 256])
self.assertEqual(i, self.encrypted_message[0])
self.assertEqual((i + 2) % 256, self.encrypted_message[1])
self.assertEqual(i, self.decrypted_message[0])
self.assertEqual((i + 1) % 256, self.decrypted_message[1])
def test_two_byte_one_roll_shift1(self):
for i in range(256):
self.init_business_logic([self.trans_shift_1], [[0]])
self.crypt([i, (i + 1) % 256])
self.assertEqual((i + 1) % 256, self.encrypted_message[0])
self.assertEqual((i + 3) % 256, self.encrypted_message[1])
self.assertEqual(i, self.decrypted_message[0])
self.assertEqual((i + 1) % 256, self.decrypted_message[1])
def test_two_byte_one_roll_invert(self):
for i in range(256):
self.init_business_logic([self.trans_linear_invert], [[0]])
self.crypt([i, i])
self.assertEqual(255 - i, self.encrypted_message[0])
self.assertEqual((256 + 255 - i - 1) & 0xff, self.encrypted_message[1])
self.assertEqual(i, self.decrypted_message[0])
self.assertEqual(i, self.decrypted_message[1])
def test_two_byte_two_roll_linear(self):
for i in range(256):
self.init_business_logic([self.trans_linear, self.trans_linear], [[0], [0]])
self.crypt([i, (i + 1) & 0xff])
self.assertEqual(i, self.encrypted_message[0])
self.assertEqual((i + 2) & 0xff, self.encrypted_message[1])
self.assertEqual(i, self.decrypted_message[0])
self.assertEqual((i + 1) & 0xff, self.decrypted_message[1])
def test_two_byte_two_roll_shift1(self):
for i in range(256):
self.init_business_logic([self.trans_shift_1, self.trans_shift_1], [[0], [0]])
self.crypt([i, (i + 1) & 0xff])
self.assertEqual((i + 2) & 0xff, self.encrypted_message[0])
self.assertEqual((i + 4) & 0xff, self.encrypted_message[1])
self.assertEqual(i, self.decrypted_message[0])
self.assertEqual((i + 1) & 0xff, self.decrypted_message[1])
def test_two_byte_two_roll_shift2(self):
self.init_business_logic([self.trans_shift_2, self.trans_shift_2], [[0], [0]])
self.crypt([7, 107])
self.assertEqual(11, self.encrypted_message[0])
self.assertEqual(112, self.encrypted_message[1])
self.assertEqual(7, self.decrypted_message[0])
self.assertEqual(107, self.decrypted_message[1])
def test_two_byte_two_roll_invert(self):
for i in range(256):
self.init_business_logic([self.trans_linear_invert, self.trans_linear_invert], [[0], [0]])
self.crypt([i, (i + 1) & 0xff])
self.assertEqual(i, self.encrypted_message[0])
self.assertEqual((i + 2) & 0xff, self.encrypted_message[1])
self.assertEqual(i, self.decrypted_message[0])
self.assertEqual((i + 1) & 0xff, self.decrypted_message[1])
def test_three_byte_two_roll_turnover(self):
for i in range(256):
self.init_business_logic([self.trans_linear, self.trans_linear], [range(256), range(256)])
self.crypt([i, (i + 1) & 0xff, (i + 2) & 0xff])
self.assertEqual(i, self.encrypted_message[0])
self.assertEqual((i + 3) & 0xff, self.encrypted_message[1])
self.assertEqual((i + 6) & 0xff, self.encrypted_message[2])
self.assertEqual(i, self.decrypted_message[0])
self.assertEqual((i + 1) & 0xff, self.decrypted_message[1])
self.assertEqual((i + 2) & 0xff, self.decrypted_message[2])
def test_three_byte_two_different_roll_turnover(self):
self.init_business_logic([self.trans_linear, self.trans_shift_1], [range(4), range(4)])
self.crypt([7, 107])
self.assertEqual(8, self.encrypted_message[0])
self.assertEqual(110, self.encrypted_message[1])
self.assertEqual(7, self.decrypted_message[0])
self.assertEqual(107, self.decrypted_message[1])
def test_three_byte_two_different_roll_turnover3(self):
self.init_business_logic([self.trans_linear, self.trans_linear_invert], [range(4), range(4)])
self.crypt([7, 107])
self.assertEqual(248, self.encrypted_message[0])
self.assertEqual(146, self.encrypted_message[1])
self.assertEqual(7, self.decrypted_message[0])
self.assertEqual(107, self.decrypted_message[1])
def test_real_live(self):
msg_size = 65536
msg = bytearray(msg_size)
for i in range(msg_size):
msg[i] = i & 0xff
self.init_business_logic([self.trans_linear, self.trans_linear_invert, self.trans_shift_1, self.trans_shift_2],
[[0, 22, 44, 100], [11, 44, 122, 200], [33, 77, 99, 222], [55, 67, 79, 240]])
self.crypt(msg)
for i in range(msg_size):
self.assertEqual(msg[i], self.decrypted_message[i])
def test_integration(self):
key_file_name = "any.key"
msg_file_name = "msg.file"
msg_size = 65536 * 5
msg = bytearray(msg_size)
for i in range(msg_size):
msg[i] = i & 0xff
if os.path.exists(msg_file_name):
os.remove(msg_file_name)
file = open(msg_file_name, 'wb')
file.write(msg)
file.close()
program = Program(55)
program.run_main("keygen", 5, key_file_name)
program.run_main("enc", msg_file_name, key_file_name)
program2 = Program(55)
program2.run_main("dec", msg_file_name + ".enc", key_file_name)
file = open(msg_file_name + ".enc.dec", 'rb')
decypted = file.read()
file.close()
for i in range(msg_size):
self.assertEqual(msg[i], decypted[i])
self.assertEqual(msg_size, len(decypted))
if __name__ == '__main__':
unittest.main()
```
1 ответ
Во-первых, добро пожаловать на язык Python! Ничего из этого не в каком-то определенном порядке.
argparse
Есть действительно хорошая библиотека анализа аргументов, доступная из коробки, которая делает синтаксический анализ аргументов командной строки гораздо менее громоздким. Я бы настроил ваш парсер так:
# Maybe put this into cli.py
from argparse import ArgumentParser
def setup_parser():
parser = ArgumentParser(desc="Enigma written in python")
parser.add_argument("mode", type=str, choices=['enc', 'dec', 'keygen'], help="Encode, Decode, or generate key")
parser.add_argument("-i", "--input-file", dest="input_file", type=str, help="File to be encrypted/decrypted")
parser.add_argument("key_file", type=str, help="Key file to be generated or to be used to encode/decode")
return parser
parser = setup_parser()
Тогда ваши аргументы можно использовать как:
args = parser.parse_args()
args.mode
'enc'
args.key_file
'mykey.rsa'
args.input_file
'to_be_encrypted.txt'
Имена переменных
Чтобы согласиться с предложением argparse, переменным следует дать описательные имена. В run_main
, у вас есть такие имена, как arg<i>
, что затрудняет чтение и отладку кода. Назовите их, как вы собираетесь их использовать:
def main(mode, key_file, input_file=None, num_rolls=0):
...
Имена переменных и функций рекомендуется snake_case
, где занятия рекомендуется проводить PascalCase
. Руководство по стилю (также известное как PEP-8) можно найти здесь. Однако обратите внимание, что это руководство. Ты не имеют следовать ему на каждом этапе, но в большинстве случаев это сделает ваш код лучше.
Перечисления и проверка типов
Перечисления отлично справляются с проверкой типов. Вы можете переопределить свое перечисление следующим образом:
class Mode(Enum):
ENC = 'enc'
DEC = 'dec'
Тогда не только парсер аргументов поймает недопустимую запись перечисления, но и класс:
mode = Mode('enc')
mode is Mode.ENC
True
mode = Mode('dec')
mode is Mode.DEC
True
mode = Mode('bad')
TypeError:
'bad' is not a valid Mode
Создание rolls
Есть несколько способов очистить этот код. Во-первых, я думаю, что определение точек среза вне самой нотации среза на ключе немного проще для чтения:
~snip~
for roll_number in range(roll_count):
# For readability, I would just compute these values here. It
# makes the slices look better
start = roll_number * roll_key_length
middle = roll_number * roll_key_length + 256
end = roll_number * roll_key_length + 256 + transition_count
# now it's easier to see what the key slices are
roll = Roll(key[start:middle], key[middle:end]))
Во-вторых, вы дважды перебираете все свои броски, когда в этом нет необходимости. Просто сделайте проверку во время итерации:
for roll_number in range(roll_count):
~snip~
roll.check_input(transition_count)
rolls.append(roll)
Обработка файлов
Лучше открывать файлы с помощью диспетчера контекста with
, поскольку он автоматически закроет дескриптор файла, когда вы закончите его использовать, независимо от того, что произойдет:
# go from this
fh = open(myfile, 'rb')
content = fh.read()
fh.close()
# to this
with open(myfile, 'rb') as fh:
content = fh.read()
# and to open multiple files:
with open('file1.txt') as fh1, open('file2.txt') as fh2, ..., open('fileN.txt') as fhN:
# do things
В вашем коде также есть место, где вы удаляете файл, выполняете некоторые вычисления, а затем снова создаете его, записывая в него. В w
и wb
режимы обрежут для вас существующий файл. Итак, идите от:
if os.path.exists(self._keyFilename):
os.remove(file)
# skipping code
file = open(self._keyFilename, 'wb')
file.write(key)
file.close()
Вместо этого сделайте это:
# get rid of the `if file exists` check
# skipping code
with open(self.key_file_name, 'wb') as fh:
fh.write(key)
Заполнение bytearray
У вас есть цикл, в котором вы заполняете bytearray
с числами:
for i in range(roll_count):
transform = bytearray(256)
for j in range(256):
transform[j] = j
Вы можете пропустить другой цикл и просто bytearray
потреблять range
:
for i in range(roll_count):
transform = bytearray(range(256))
Обмен переменными
Вы можете использовать назначение в стиле кортежа, чтобы быстро и легко поменять местами значения переменных:
# go from this
temp = transform[rand1]
transform[rand1] = transform[rand2]
transform[rand2] = temp
# to this
transform[rand1], transform[rand2] = transform[rand2], transform[rand1]
Создание списка псевдослучайных чисел (без дубликатов)
Для этого у вас есть следующий код:
transitions = bytearray()
while len(transitions) < self._transitionCount:
rand = randint(0, 255)
if not transitions.count(rand):
transitions.append(rand)
Это можно упростить до
transitions = bytearray(
random.sample(range(256), transition_count)
)
Это намного быстрее. transitions.count(x)
будет O (N), где N растет с каждой итерацией. Не идеально.
is_twisted
Пока 0
и 1
можно интерпретировать как False
и True
соответственно в силу их правдивости я думаю, что лучше было бы здесь действительно вернуть логическое значение. Кроме того, это можно было бы более кратко выразить как any
утверждение:
# go from this
def is_twisted(self, trans):
for i in range(256):
if trans[i] == i:
return 0
return 1
# to this
def is_twisted(self, trans):
return any(item == i for i, item in enumerate(trans))
Где enumerate
будет производить index
, item
пары для каждого элемента в итерабельном / последовательном / генераторе. Наконец, вы на самом деле не используете self
вот так is_twisted
может быть staticmethod
:
@staticmethod
def is_twisted(trans):
return any(item == i for i, item in enumerate(trans))
for
…else
?
В Rolls.py
, у вас есть следующий фрагмент:
for i in range(256):
found = 0
for j in self._transitions:
if self._transitions[j] == i:
found = 1
continue
if not found:
raise ValueError("Transitions not 1-1 complete")
Это можно уменьшить, используя else
заявление по for
петля. Да, есть else
для for
. Вы можете подумать об этом, проверяя, есть ли StopIteration
произошло:
for i in range(10):
if i == 5:
break
else:
print("Didn't exit early!")
for i in range(10:
if i == 11:
break # this won't happen
else:
print("Didn't exit early!")
Didn't exit early!
Таким образом, ваш фрагмент кода может сократиться до:
for i in range(256):
for j in self._transitions:
if self._transitions[j] == i:
break
else:
# is not found
raise ValueError("Transitions not 1-1 complete")
Проверка соответствующих элементов
Каждый раз, когда у вас есть код, сравнивающий соответствующие элементы коллекции, вы, вероятно, можете использовать zip
:
for i in range(len(self._turn_over_indices) - 1):
if self._turn_over_indices[i] == self._turn_over_indices[i + 1]:
raise ValueError("turn_over_indices has doubles")
Может быть превращен в:
for left, right in zip(
self._turn_over_indices[:-1],
self._turn_over_indices[1:]
):
if left == right:
raise ValueError("doubles!")
Это даже можно исправить в ваших модульных тестах:
def init_business_logic(self, transitions, turnovers):
rolls_encrypt = []
rolls_decrypt = []
for index in range(len(transitions)):
rolls_encrypt.append(Roll(transitions[index], turnovers[index]))
rolls_decrypt.append(Roll(transitions[index], turnovers[index]))
# can now be
def init_business_logic(self, transitions, turnovers):
rolls_encrypt, rolls_decrypt = [], []
for transition, turnover in zip(transitions, turnovers):
rolls_encrypt.append(Roll(transition, turnover))
rolls_decrypt.append(Roll(transition, turnover))
transform_buffer
Это можно было бы отредактировать, чтобы сделать его более СУХИМ:
def transform_buffer(self, buffer, mode):
if mode == Mode.ENC:
# almost all of this code is repeated except roll.encrypt/decrypt
for i in range(len(buffer)):
for roll in self._rolls:
roll.encrypt(buffer, i)
self.roll_on()
if mode == Mode.DEC:
for i in range(len(buffer)):
for roll in self._rolls_reverse:
roll.decrypt(buffer, i)
self.roll_on()
Единственная разница roll.encrypt
, roll.decrypt
, и направление, в котором мы перебираем self._rolls
:
def transform_buffer(self, buffer, mode):
if mode is Mode.ENC:
rolls = self._rolls
func_name="encrypt"
else:
rolls = self._rolls[::-1]
func_name="decrypt"
for i in range(len(buffer)):
for roll in rolls:
f = getattr(roll, func_name)
f(buffer, i)
self.roll_on()
Говоря о которых…
Реверсивные итераторы
Вместо копирования объекта и изменения его порядка вы можете просто сделать:
for item in reversed(collection):
print(item)
Однако для transform_buffer
, Я использовал self._rolls[::-1]
потому что после первого цикла reversed
итератор будет использован, тогда как срез можно будет использовать повторно.
Итак, вместо того, чтобы делать:
class Rolls:
def __init__(self, rolls):
self._rolls = rolls
self._rolls_reverse = rolls.copy()
self._rolls_reverse.reverse()
Просто сохраните одну копию self._rolls
вокруг.
class Rolls:
def __init__(self, rolls):
self._rolls = rolls
# no reverse copy
Rolls.roll_on
Я думаю, что этот метод можно было бы назвать лучше, поскольку у вас есть две реализации roll_on
функция для двух разных классов. Однако, Rolls.roll_on
есть другая проблема. Он использует bytearray.count
чтобы эффективно провести тест на членство:
def roll_on(self):
self._position = (self._position + 1) & 0xff
return self._turn_over_indices.count(self._position)
Теперь, обычно с тестом на членство, я бы порекомендовал dict
или set
, но для этого примера я бы просто использовал in
, поскольку вы либо создаете в памяти совершенно новый объект, который необходимо синхронизировать с bytearray
или жертвуя временем, чтобы проверить членство в массиве из 256 элементов. Я бы выбрал второе. Однако, bytearray.count
(или любой collection.count
, если на то пошло) всегда будет проходить через все это. in
короткое замыкание:
def roll_on(self):
self._position = (self._position + 1) & 0xff
return self._position in self._turn_over_indices
Я буду добавлять новые предметы по мере их нахождения, но думаю, этого более чем достаточно для начала.
Вау, сколько советов! Я действительно с нетерпением жду возможности реализовать это после отпуска … Большое спасибо!
— Профессор Зальцман