Переписываем генератор паролей
- суббота, 1 февраля 2020 г. в 00:28:00
У меня сложилось ощущение, что я уже раз пять писал функцию для генерации паролей. И каждый раз делал это по-разному. А причина тому — различные требования к паролю для разных проектов и инструментов. Здесь не будет сложного кода, просто краткое изложение простого нового решения, которое пришло ко мне вчера.
Начнем с простых требований к паролю:
import string
import random
from typing import List
def generate_password(length: int) -> str:
"""
Generate a password of a given `length`.
"""
result: List[str] = []
choices = string.printable # заглавые и строчные буквы, цифры и знаки препинания
while len(result) < length:
symbol = random.choice(string.printable)
result.append(symbol)
return "".join(result)
Пробуем:
>>> generate_password(8)
... "1{k]/2)h"
>>> generate_password(13)
... "9ar|&:a+U]Il$"
Отлично, задача выполнена, можем смотреть картинки с котиками до конца рабочего дня.
Но тут нам говорят, что все не так просто, и пароль для нашей базы данных MyDB должен отвечать некоторым требованиям безопасности. А именно, пароль:
!&?
и прочие)Наш код выше для этого не годится, так что придется дорабатывать. И для решения этого я видел целый ворох разных подходов:
Окей, это уже сложно, так что начнем с функции generate_random_string
, которая будет просто генерировать случайные строки из того, что дали.
import string
import random
from typing import List
def generate_random_string(length: int, *choices: str) -> str:
"""
Generate a string of a given `length`.
The result has at least one symbol from each of `choices` if `length` allows.
Arguments:
length -- Result string length.
choices -- Strings with available symbols.
"""
if not choices:
# будем использовать только буквы если нам все равно, из каких символов пароль
choices = (string.ascii_letters, )
# создадим строку со всеми доступными символами
all_choices = "".join(choices)
result: List[str] = []
choice_index = 0
while len(result) < length:
# получим по символу из каждого списка, чтобы
# каждый список был использован хотя бы один раз
if choice_index < len(choices):
symbol = random.choice(choices[choice_index])
result.append(symbol)
choice_index += 1
continue
# а после этого добавляем символы из любого списка
symbol = random.choice(all_choices)
result.append(symbol)
# перемешаем наш результат чтобы распределить начальные символы
random.shuffle(result)
return "".join(result)
Так, попробуем:
>>> # генерируем строку из цифр
>>> generate_random_string(8, string.digits)
... "59197550"
>>> # а тут обязательно должен быть восклицательный знак
>>> generate_random_string(8, string.ascii_letters, "!")
... "vIOWXN!o"
Отлично, пришло время собственно сгенерировать пароль, отвечающий всем нашим требованиям.
def generate_mydb_password(length: int) -> str:
"""
Generate a random password for MyDB of a given `length`.
The result has at least:
- one uppercase letter
- one lowercase letter
- one digit
- one special character
Raises:
ValueError -- If `length` is lesser than 8.
"""
if length < 8:
raise ValueError("Password length should be at least 8")
return generate_random_string(
length,
string.ascii_uppercase, # в пароле должны быть заглавные буквы
string.ascii_lowercase, # и строчные
string.digits, # и цифры
"!&?", # и спец-символы, добавьте нужных по вкусу
)
Осталось только проверить:
>>> generate_mydb_password(8)
... "P?P1&7zL"
>>> generate_mydb_password(13)
... "tR!QslK!Sl7EO"
>>> generate_mydb_password(2)
... ValueError: Password length should be at least 8
Мы написали простой в понимании и при этом достаточно случайный генератор паролей, а до конца рабочего дня еще куча времени. Если нет доверия библиотеке random
, то можете ее заменить на ту, которая нравится.
Спасибо за внимание!