Всіх вітаю! У сьогоднішній статті ми реалізуємо метод для генерації випадкових паролів та метод, який буде визначати ентропію пароля. Крім того, у цій статті ми зробимо генератор паролів реально криптографічно безпечним, бо будемо використовувати модуль secrets та клас SystemRandom(). Також у цій статті буде присутня й теоретична частина, у якій ми зможемо дізнатися про суть ентропії.
В якомусь сенсі минула стаття про модуль secrects та клас SystemRandom() пов’язана з тією статтею. Думаю можна вважати цю статтю деяким продовженням попередньої.
Створення метода generate() для генерації пароля
Отже, спочатку створимо код, який би генерував паролі з різних символів. Для цього пропоную створити клас Password(), у якого будуть два методи. Метод generate() буде приймати довжину пароля та тип символів, який згенерований пароль повинен вміщати.
Спершу імпортуємо потрібні модулі:
import random
import re
import math
import secretsДалі створюємо клас Password() та напишемо у конструкторі __init__() потрібні нам набори символів для паролів.
class Password:
def __init__(self):
self.original_password = ""
self.password_list = []
self.all_symbols = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890!@#$%^&*()_+-/:;<>?.\\|"
self.all_symbols_list = list(self.all_symbols)
self.numbers = "1234567890"
self.numbers_list = list(self.numbers)
self.special = "!@#$%^&*()_+-/:;<>?.\\|"
self.special_list = list(self.special)
self.lower_alphabet = "qwertyuiopasdfghjklzxcvbnm"
self.lower_alphabet_list = list(self.lower_alphabet)
self.lower_alphabet_numbers = "qwertyuiopasdfghjklzxcvbnm1234567890"
self.lower_alphabet_numbers_list = list(self.lower_alphabet_numbers)
self.lower_alphabet_special = "qwertyuiopasdfghjklzxcvbnm!@#$%^&*()_+-/:;<>?.{}[]\\|"
self.lower_alphabet_special_list = list(self.lower_alphabet_special)
self.lower_alphabet_numbers_special = "qwertyuiopasdfghjklzxcvbnm1234567890!@#$%^&*()_+-/:;<>?.{}[]\\|"
self.lower_alphabet_numbers_special_list = list(self.lower_alphabet_numbers_special)
self.alphabet = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm"
self.alphabet_list = list(self.alphabet)
self.alphabet_numbers = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890"
self.alphabet_numbers_list = list(self.alphabet_numbers)
self.alphabet_special = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm!@#$%^&*()_+-/:;<>?.{}[]\\|"
self.alphabet_special_list = list(self.alphabet_special)
self.numbers_special = "!@#$%^&*()_+-/:;<>?.{}[]\\|0123456789"
self.numbers_special_list = list(self.numbers_special)Тепер створюємо метод generate(), у якому ми будемо відслідковувати значення password_content та генерувати потрібний за довжиною та типом символів пароль.
Ось готовий метод generate():
def generate(self, password_length, password_content):
"""
This function generate the password based on user preferences
:param password_length: password length
:param password_content: password content
:return: generated password
"""
secure_shuffle = random.SystemRandom()
if password_content == "all":
while len(self.password_list) < password_length:
secure_shuffle.shuffle(self.all_symbols_list)
element = secrets.choice(self.all_symbols_list)
self.password_list.append(element)
self.original_password.join(self.password_list)
if len(self.password_list) == password_length:
return self.original_password.join(self.password_list)
elif password_content == "numbers":
while len(self.password_list) < password_length:
secure_shuffle.shuffle(self.numbers_list)
element = secrets.choice(self.numbers_list)
self.password_list.append(element)
self.original_password.join(self.password_list)
if len(self.password_list) == password_length:
return self.original_password.join(self.password_list)
elif password_content == "lower_alphabet":
while len(self.password_list) < password_length:
secure_shuffle.shuffle(self.lower_alphabet_list)
element = secrets.choice(self.lower_alphabet_list)
self.password_list.append(element)
self.original_password.join(self.password_list)
if len(self.password_list) == password_length:
return self.original_password.join(self.password_list)
elif password_content == "lower_alphabet+numbers":
while len(self.password_list) < password_length:
secure_shuffle.shuffle(self.lower_alphabet_numbers_list)
element = secrets.choice(self.lower_alphabet_numbers_list)
self.password_list.append(element)
self.original_password.join(self.password_list)
if len(self.password_list) == password_length:
return self.original_password.join(self.password_list)
elif password_content == "lower_alphabet+special":
while len(self.password_list) < password_length:
secure_shuffle.shuffle(self.lower_alphabet_special_list)
element = secrets.choice(self.lower_alphabet_special_list)
self.password_list.append(element)
self.original_password.join(self.password_list)
if len(self.password_list) == password_length:
return self.original_password.join(self.password_list)
elif password_content == "lower_alphabet+numbers+special":
while len(self.password_list) < password_length:
secure_shuffle.shuffle(self.lower_alphabet_numbers_special_list)
element = secrets.choice(self.lower_alphabet_numbers_special_list)
self.password_list.append(element)
self.original_password.join(self.password_list)
if len(self.password_list) == password_length:
return self.original_password.join(self.password_list)
elif password_content == "alphabet":
while len(self.password_list) < password_length:
secure_shuffle.shuffle(self.alphabet_list)
element = secrets.choice(self.alphabet_list)
self.password_list.append(element)
self.original_password.join(self.password_list)
if len(self.password_list) == password_length:
return self.original_password.join(self.password_list)
elif password_content == "special":
while len(self.password_list) < password_length:
secure_shuffle.shuffle(self.special_list)
element = secrets.choice(self.special_list)
self.password_list.append(element)
self.original_password.join(self.password_list)
if len(self.password_list) == password_length:
return self.original_password.join(self.password_list)
elif password_content == "alphabet+numbers":
while len(self.password_list) < password_length:
secure_shuffle.shuffle(self.alphabet_numbers_list)
element = secrets.choice(self.alphabet_numbers_list)
self.password_list.append(element)
self.original_password.join(self.password_list)
if len(self.password_list) == password_length:
return self.original_password.join(self.password_list)
elif password_content == "alphabet+special":
while len(self.password_list) < password_length:
secure_shuffle.shuffle(self.alphabet_special_list)
element = secrets.choice(self.alphabet_special_list)
self.password_list.append(element)
self.original_password.join(self.password_list)
if len(self.password_list) == password_length:
return self.original_password.join(self.password_list)
elif password_content == "numbers+special":
while len(self.password_list) < password_length:
secure_shuffle.shuffle(self.numbers_special_list)
element = secrets.choice(self.numbers_special_list)
self.password_list.append(element)
self.original_password.join(self.password_list)
if len(self.password_list) == password_length:
return self.original_password.join(self.password_list)
else:
return f"[Error] Option '{password_content}' is not exist."Також слід одразу звернути увагу на те, що тут ми використовуємо не просто модуль random, а його клас SystemRandom():
secure_shuffle = random.SystemRandom()Далі генеруємо пароль з усіх можливих символів, якщо користувач зазначив, що пароль має вміщати усе (all):
if password_content == "all":
while len(self.password_list) < password_length:
secure_shuffle.shuffle(self.all_symbols_list)
element = secrets.choice(self.all_symbols_list)
self.password_list.append(element)
self.original_password.join(self.password_list)
if len(self.password_list) == password_length:
return self.original_password.join(self.password_list)Тепер давайте розглянемо код детальніше. Спочатку ми перемішуємо елементи у списку self.all_symbols_list, а потім беремо одним символ з нього:
secure_shuffle.shuffle(self.all_symbols_list)
element = secrets.choice(self.all_symbols_list)Тут ми застосовуємо функціонал модуля secrets та SystemRandom(). За допомогою цих інструментів ми надаємо надійність нашому генератору паролів, так як тепер наш код генерує по-справжньому випадкові паролі.
Далі додаємо цей символ у список password_list та приєднуємо цей список до рядка original_password:
self.password_list.append(element)
self.original_password.join(self.password_list)І так наш цикл while буде поступово генерувати пароль, а потім поверне його, коли він буде відповідати параметру password_length:
if len(self.password_list) == password_length:
return self.original_password.join(self.password_list)Так само й для чисел, тільки беремо елементи зі списку numbers_list:
elif password_content == "numbers":
while len(self.password_list) < password_length:
secure_shuffle.shuffle(self.numbers_list)
element = secrets.choice(self.numbers_list)
self.password_list.append(element)
self.original_password.join(self.password_list)
if len(self.password_list) == password_length:
return self.original_password.join(self.password_list)За такою самою логікою працюють й інші опції параметра password_content. Просто змінюються списки із символами, які ми використовуємо для генерації пароля.
Результат коду
В результаті ми отримуємо такий пароль:
manager = Password()
print(f"Generated password: {manager.generate(15, 'all')}")Generated password: ?LnGuK3|.gaW3&'Створення метода entropy() для визначення ентропії паролю
Що таке ентропія?
Ентропія — це такий параметр надійності пароля, який визначає скільки часу (спроб) потрібно буде умовному хакеру для взлому вашого пароля. Ентропія визначається у бітах і обчислюється за такою формулою:

Де L — довжина пароля, R — це кількість можливих символів, які ми застосовуємо для генерації пароля.
Реалізація ентропії у коді
Тепер пропоную створити метод entropy(), який буде приймати пароль у якості параметра, надалі автоматично визначаючи його довжину та вміст символів.
Ось реалзіація методу entropy() у коді:
def entropy(self, password):
password_length = len(password)
all_symbols_escape = re.escape(self.all_symbols)
special_symbols_escape = re.escape(self.special)
if re.fullmatch("[a-z]+", password):
symbols_range = 26
entropy = password_length * math.log2(symbols_range)
return f"{entropy} bits"
elif re.fullmatch("[A-Z]+", password):
symbols_range = 26
entropy = password_length * math.log2(symbols_range)
return f"{entropy} bits"
elif re.fullmatch("[a-zA-Z]+", password):
symbols_range = 52
entropy = password_length * math.log2(symbols_range)
return f"{entropy} bits"
elif re.fullmatch("[0-9]+", password):
symbols_range = 10
entropy = password_length * math.log2(symbols_range)
return f"{entropy} bits"
elif re.fullmatch(f"[a-z0-9{special_symbols_escape}]+", password):
symbols_range = 68
entropy = password_length * math.log2(symbols_range)
return f"{entropy} bits"
elif re.fullmatch("[a-z0-9]+", password):
symbols_range = 36
entropy = password_length * math.log2(symbols_range)
return f"{entropy} bits"
elif re.fullmatch("[A-Z0-9]+", password):
symbols_range = 36
entropy = password_length * math.log2(symbols_range)
return f"{entropy} bits"
elif re.fullmatch(f"[A-Z0-9{special_symbols_escape}]+", password):
symbols_range = 36
entropy = password_length * math.log2(symbols_range)
return f"{entropy} bits"
elif re.fullmatch("[a-zA-Z0-9]+", password):
symbols_range = 62
entropy = password_length * math.log2(symbols_range)
return f"{entropy} bits"
elif re.fullmatch(f"[{special_symbols_escape}]+", password):
symbols_range = 32
entropy = password_length * math.log2(symbols_range)
return f"{entropy} bits"
elif re.fullmatch(f"[a-z{special_symbols_escape}]+", password):
symbols_range = 58
entropy = password_length * math.log2(symbols_range)
return f"{entropy} bits"
elif re.fullmatch(f"[A-Z{special_symbols_escape}]+", password):
symbols_range = 58
entropy = password_length * math.log2(symbols_range)
return f"{entropy} bits"
elif re.fullmatch(f"[a-zA-Z{special_symbols_escape}]+", password):
symbols_range = 84
entropy = password_length * math.log2(symbols_range)
return f"{entropy} bits"
elif re.fullmatch(f"[0-9{special_symbols_escape}]+", password):
symbols_range = 42
entropy = password_length * math.log2(symbols_range)
return f"{entropy} bits"
elif re.fullmatch(f"[{all_symbols_escape}]+", password):
symbols_range = 95
entropy = password_length * math.log2(symbols_range)
return f"{entropy} bits"
else:
return f"[Error] Password '{password}' consist something strange."Отже, тепер розберемо код даного метода. Спершу ми визначаємо довжину пароля, адже це перша складова формули для визначення ентропії:
password_length = len(password)Далі ми визначаємо символи у рядках self.all_symbols та self.special як такі, що є саме звичайними символами, а не командами для модуля регулярних виразів re. Тобто функція escape() виключає варіант, за яким деякі з символів у рядках модуль re може сприйняти за свої команди.
all_symbols_escape = re.escape(self.all_symbols)
special_symbols_escape = re.escape(self.special)Далі ми постійно перевіряємо вміст пароля за допомогою регулярних виразів. Ось частина, де ми перевіряємо пароль на вміст лише малих літер:
if re.fullmatch("[a-z]+", password):
symbols_range = 26
entropy = password_length * math.log2(symbols_range)
return f"{entropy} bits"В умові ми застосовуємо функцію fullmatch(), яка приймає регулярний вираз та сам пароль, до якого буде застосовуватися сам вираз. Особливістю fullmatch() є те, що вона перевіряє весь рядок на відповідність виразу повністю, у той час як інші подібні функції працюють лише до першого знайденого символа.
Наступним кроком ми визначаємо змінну symbols_range — це та сама змінна R у формулі ентропії. У даному випадку, ми надаємо їх значення 26, бо існує всього 26 малих літер. Себто, кількість можливих символів, які ми можемо застосувати до пароля рівно 26. За такою ж логікою визначаємо цю змінну й для інших символів.
Тепер обчислюємо ентропію за формулою:
entropy = password_length * math.log2(symbols_range)І у кінці-кінців повертаємо результат.
Пояснення регулярних виразів
Регулярні вирази — це особливий тип виразів, які мають свій власний стиль написання та використовуються для просунутої перевірки вмісту рядків.
Також окремо треба пояснити, що саме означають ті регулярні вирази, які ми прописали у кожній з умов. Взагалі достатньо розуміти, що у квадратних дужках ми зазначаємо, які символи будемо шукати у рядку, а плюс означає, що ті символи повинні зустрічатися один або більше разів.
Тобто, наприклад, вираз [a-z]+ означає, що ми шукаємо у рядку всі малі літери ([a-z] — бквально від a до z включно), причому траплятися вони там повинні один або більше разів (за це відповідає знак +). Даний вираз також може піддаватися змінам, іноді шукаючи не тільки малі літери, як, наприклад, вираз [a-zA-Z]+, який шукає малі та великі літери алфавіту.
Варто також зазначити, що замість запису шуканих символів через дефіс, ми також можемо просто вказати у квадратних дужках змінну з усіма потрібними символами. Ми зробили так, наприклад, коли шукали спец. символи, де просто записали вираз як [{special_symbols_escape}]+.
Таким чином можна достатньо швидко й просто зрозуміти суть даних виразів. Єдине, що ми змінюємо у цих виразах — це символи, які шукаємо у рядку.
Резульат метода entropy()
Ось як працює метод entropy():
password = "?LnGuK3|.gaW3&'"
manager = Password()
print(f"Entropy of password {password} is {manager.entropy(password)}")Entropy of password ?LnGuK3|.gaW3&' is 98.54783412496421 bitsВисновок
У даній статті ми ознайомилися із резулярними виразами у модулі re, використанням модуля secrets та класу SystemRandom() у модулі random для генерації правдивих випадковостей та криптографічної надійності паролів. Також ми побудували невеличкий модуль, який містить методи generate() для генерації паролів та entropy() для визначення ентропії пароля. Крім цього, ми торкнулися й теоретичного означення ентропії. Всім дякую за увагу!