Друкарня від WE.UA

Створюємо надійний генератор паролів та визначаємо ентропію отриманого пароля

Зміст

Всіх вітаю! У сьогоднішній статті ми реалізуємо метод для генерації випадкових паролів та метод, який буде визначати ентропію пароля. Крім того, у цій статті ми зробимо генератор паролів реально криптографічно безпечним, бо будемо використовувати модуль 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() для визначення ентропії пароля. Крім цього, ми торкнулися й теоретичного означення ентропії. Всім дякую за увагу!

Статті про вітчизняний бізнес та цікавих людей:

Поділись своїми ідеями в новій публікації.
Ми чекаємо саме на твій довгочит!
Mystique Lord
Mystique Lord@nocturnal_reader we.ua/nocturnal_reader

Нічний читач

44Довгочити
492Прочитання
14Підписники
Підтримати
На Друкарні з 14 липня

Більше від автора

  • Генерація правдивих випадковостей за допомогою secrects та SystemRandom

    Всіх вітаю! У цій статті ми вивчимо яким чином можна генерувати правдиві випадкові числа за допомогою вбудованого модуля secrects та класу SystemRandom() у модулі random розберемося із функціоналом

    Теми цього довгочиту:

    Програмування
  • Метод replace() у Python

    Всіх вітаю! У даній статті ми розберемо вбудований метод для роботи з рядками — метод replace(). У рамках матеріалу ми наведемо три приклади роботи з даним методом та визначимо для себе суть самого метода.

    Теми цього довгочиту:

    Програмування
  • Перетворення стилю графіків у науковий формат

    Всіх вітаю! У даній статті ми розглянемо бібліотеку SciencePlots, яка надає можливість перетворювати графіки на науковий формат. Саме у такому форматі графіки відображаються у різного роду наукових статтях і т. д.

    Теми цього довгочиту:

    Програмування

Це також може зацікавити:

  • 🕵️ Автентифікація без пароля?

    У світі, де кібербезпека стає все більш важливою, традиційні методи автентифікації, такі як паролі, вже не відповідають сучасним вимогам безпеки. На допомогу приходить безпарольна автентифікація, зокрема стандарти FIDO2 та WebAuthn.

    Теми цього довгочиту:

    Security
  • Види (дистрибутиви) Linux та який обрати

    Linux дистрибутиви бувають різні і у кожної своя задача. Деякі з них є спеціалізованими, інші розраховані на звичайного користувача. Проблема в тому що їх багато і обрати щось серед них складно. Ми спробуємо розповісти про основні типи дистрибутивів Linux та який краще обрати.

    Теми цього довгочиту:

    Linux

Коментарі (0)

Підтримайте автора першим.
Напишіть коментар!

Це також може зацікавити:

  • 🕵️ Автентифікація без пароля?

    У світі, де кібербезпека стає все більш важливою, традиційні методи автентифікації, такі як паролі, вже не відповідають сучасним вимогам безпеки. На допомогу приходить безпарольна автентифікація, зокрема стандарти FIDO2 та WebAuthn.

    Теми цього довгочиту:

    Security
  • Види (дистрибутиви) Linux та який обрати

    Linux дистрибутиви бувають різні і у кожної своя задача. Деякі з них є спеціалізованими, інші розраховані на звичайного користувача. Проблема в тому що їх багато і обрати щось серед них складно. Ми спробуємо розповісти про основні типи дистрибутивів Linux та який краще обрати.

    Теми цього довгочиту:

    Linux