Привет, Хабр!

Представляю вам свою первую статью о программировании на Rust. Я только начинаю изучать этот язык, и в качестве первого задания выбрал написание генератора паролей.

Процесс разработки консольного приложения будет состоять из нескольких этапов:

  1. Формирование структуры пароля.

  2. Разработка функции генерации пароля.

  3. Создание функций для проверки пароля и их объединение в одну.

  4. Обеспечение возможности пользовательского ввода.

  5. Формирование функции генерации проверенного пароля.

1. Формирование структуры пароля.

Пароль представляет собой комбинацию символов и их количество. В нём могут использоваться следующие символы:

  1. Строчные и заглавные буквы латинского алфавита.(A, a, B, b, C, c, и т.д.).

  2. Цифры.(0, 1, 2, и т.д.).

  3. Специальные символы.(@, !, _, и т.д.).

Создадим модуль, в котором будет хранится структура и поместим ее туда.

pub struct PasswordStructure {
    pub long: usize,
    pub register_up: u8,
    pub register_low: u8,
    pub numbers: u8,
    pub special_char: u8,
}

2. Разработка функции генерации пароля.

Чтобы создать пароль, нам необходимо добавить разрешённые символы в строку. Для этого мы воспользуемся модулем IteratorRandom из библиотеки rand. Также нам понадобится строка, в которую мы будем добавлять символы. Создадим изменяемую переменную password, с заранее выделенным пространством для хранения символов. В этом нам поможет метод with_capacity(). Для создания пароля заданной длинны мы будем использовать цикл while, который будет выполняться, пока длинна пароля не будет равна указанной длине. Пока цикл выполняется мы будем брать случайный символ из заранее подготовленной строки symbolss и добавлять его к строке password.

fn creating_password(long: usize) -> String {
    let mut rng = rand::thread_rng();
    let symbolss = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890!@#$%&*;:?_-";
    let mut password = String::with_capacity(long);

    while password.len() != long {
        password.push(symbolss.chars().choose(&mut rng).unwrap());

    }
    password

}

3. Создание функций для проверки пароля и их объединение в одну.

Мы создаем приложение, которое генерирует пароль по заданным критериям:

  1. Длине.

  2. Количеству строчных и заглавных букв.

  3. Количеству цифр.

  4. Количеству специальных символов.

Для правильного создания нам нужно будет реализовать проверки. Каждая созданная функция будет возвращать true, если проверка пройдена успешно, и false в случае неудачи.

  1. Длинна пароля.

    Функция принимает указанную длину и сгенерированный пароль.

    Чтобы проверить длину пароля, мы сравним количество символов в полученном пароле с требуемым количеством символов. По сути эта проверка не нужна, но реализация дополнительной функции не помешает.

fn checking_length(long: usize, password: &str) -> bool {
    if password.len() == long {
        return true;
    }
    false

}
  1. Строчные и заглавные буквы.

    Функция принимает длину пароля, количество маленьких и больших букв, сгенерированный пароль.

    Мы создадим две переменные, в которые запишем количество строчных и заглавных букв в сгенерированом пароле. С помощью цикла for пройдемся по всему паролю. Воспользовавшись методами is_ascii_uppercase(), is_ascii_lowercase() и as_bytes() мы проверим каждый код каждого символа в ASCII. После сравним созданные переменные с переменными хранящие количество маленьких и больших букв.

fn checking_register(long: usize, register_up: u8, register_low: u8, password: &str) -> bool {
    let mut capital_letters_up = 0;
    let mut capital_letters_low = 0;
    for i in 0..long {
        if password.as_bytes()[i].is_ascii_uppercase() {
            capital_letters_up += 1;
        }
        if password.as_bytes()[i].is_ascii_lowercase(){
            capital_letters_low += 1;
        }
    }
    (capital_letters_up >= register_up) && (capital_letters_low >= register_low)
      
}
  1. Количество цифр.

    Функция принимает длину, количество цифр и сгенерированный пароль.

    Проверка цифр будет аналогична проверке букв. Создадим переменную, считающую цифры. Организуем цикл for по длине переданного пароля. Пройдемся по всем символам и будем прибавлять единицу к нашей созданной переменной, каждый раз, когда код символа совпадет с кодом цифры из ASCII. Помогут искать методы is_ascii_digit() и as_bytes() . Дальше сравним получившееся значение с нужным количеством цифр.

fn checking_numbers(long: usize,numbers: u8, password: &str) -> bool {
    let mut quantity_numbers = 0;
    for i in 0..long{
        if password.as_bytes()[i].is_ascii_digit() {
            quantity_numbers += 1;
        }
    }
    quantity_numbers >= numbers 
  
}
  1. Спец. символы.

    Функция принимает количество специальных символов и сгенерированный пароль.

    Создадим переменную, считающую спец. символы. Организуем цикл for таким образом, чтобы в переменную цикла i записывался номер символа из ASCII. С помощью match будем сравнивать коды разрешенных специальных символов с кодом текущего символа, если хотя бы один символ совпадет, то увеличим значение переменной на один. В конце как обычно сравним найденое количество символов с нужным количеством.

fn checking_special_char(special_char: u8, password: &str) -> bool {
    let mut quantity_special_char = 0;
    for i in password.as_bytes() {
        match i {
            b'!' | b'@' | b'#' | b'$' | b'%' | 
            b'&' | b'*' | b';' | b':' | 
            b'?' | b'_' | b'-' => quantity_special_char += 1,
            _ => (),
        }
    }
    quantity_special_char >= special_char

}

После реализации всех проверок соберем их в одну маленькую функцию возвещающую true или false в зависимости от результатов всех проверок.

fn password_verification(long: usize, register_up: u8, register_low: u8, numbers: u8, special_char: u8, password: &str) -> bool {
    matches!((checking_length(long, password), checking_register(long, register_up, register_low, password), checking_numbers(long, numbers, password), checking_special_char(special_char, password)), (true, true, true, true))

}

4. Обеспечение возможности пользовательского ввода.

Теперь нам нужна функция, которая будет давать критерии для генерации пароля. Для начала воспользовавшись созданной структурой создадим функции, возвращающие структуры с заданными полями. Они будут храниться в том же модуле, что и материнская структура.

pub fn vk() -> PasswordStructure {
    let long: usize = 8;
    let register_up: u8 = 1;
    let register_low: u8 = 1;
    let numbers: u8 = 1;
    let special_char: u8 = 1;
    PasswordStructure {long, register_up, register_low, numbers, special_char}
}

pub fn ok() -> PasswordStructure {
    let long: usize = 12;
    let register_up: u8 = 1;
    let register_low: u8 = 1;
    let numbers: u8 = 1;
    let special_char: u8 = 1;
    PasswordStructure {long, register_up, register_low, numbers, special_char}
}

pub fn default() -> PasswordStructure {
    let long: usize = 15;
    let register_up: u8 = 1;
    let register_low: u8 = 1;
    let numbers: u8 = 1;
    let special_char: u8 = 1;
    PasswordStructure {long, register_up, register_low, numbers, special_char}
}

Настроем пользовательский ввод. Человек, использующий программу должен выбрать какой пароль он хочет сгенерировать. На данный момент представлены пароли для VK, OK и значения по умолчанию.

fn sites_standards() -> Option<(usize, u8, u8, u8, u8)> {

    let mut site_name = String::new();

    println!("\nВыбирете сайт, для которого хотите сгенерировать пароль(введите цифру): \n1. VK \n2. OK \n3. Стандартный пароль\n");

    let _ = io::stdin().read_line( &mut site_name);

        match site_name.as_str().trim() {
            "1" => Some((vk().long, vk().register_up, vk().register_low, vk().numbers, vk().special_char)),
            "2" => Some((ok().long, vk().register_up, ok().register_low, ok().numbers, ok().special_char)),
            "3" => Some((default().long, default().register_up, default().register_low, default().numbers, default().special_char)),
            _ => {
                println!("\n{}Вы ввели неверное значение! Мы создадим пароль по дефольтным значениям.\n", color::Fg(color::Red));
                Some((default().long, default().register_up, default().register_low, default().numbers, default().special_char))
            } 
    }

}

Эта функция вернет значения, которые находятся в выбранной пользователем структуре.

3. Формирование функции генерации проверенного пароля.

После того как мы реализовали все необходимые функции, пришло время объединить их. Мы создадим переменную, принимающую кортеж из функции критериев пароля. Далее с помощью match мы обработаем значения, переданные в переменную. Внутри match мы создадим переменные password и verification. В password запишем созданный пароль, а в verification результат проверки пароля. Потом создадим цикл while, который будет выполняться, пока verification не станет true. До этого момента мы будем перезаписывать переменную password, генерируя новые пароли. Результатом выполнения функции станет правильно сгенерированный пароль.

fn verified_password() -> Option<String> {    
    let site_values = sites_standards();
    match site_values {
        Some((long, register_up, register_low, numbers, special_char)) => {

            let mut password = creating_password(long);
            let mut verification = password_verification(long, register_up, register_low, numbers, special_char, &password.clone());
            if !verification {
                while  !verification {
                    password = creating_password(long);
                    verification = password_verification(long, register_up, register_low, numbers, special_char, &password.clone());        
                }
            }
            Some(password)

        }
        None => None,
        
    }

}

Остается только красиво вывеси пароль в функции main().

fn main() {
    let password: Option<_> =  verified_password();
    match password {
        Some(password) => println!("{}Ваш пароль: {}", color::Fg(color::Green), password),
        None => println!("{}!!!ОШИБКА!!!", color::Fg(color::Red)),
    }

}

Заключение

Мы создали простую консольную программу на Rust, которая быстро и безошибочно генерирует пароль по заданным значениям.

Я оставил много возможностей для дальнейшего развития. Например, можно реализовать возможность ручного ввода критериев для пароля или создать веб-приложение, основанное на логике этого кода. Если кто-нибудь будет делать или уже делал подобное, прошу поделиться в комментариях.

Ссылка на полный код

Комментарии (7)


  1. saege5b
    08.10.2024 02:40
    +1

    Прибивать гвоздями спецсимволы выйдет ровно до первого случая, когда какой-будь сервис откажет по недопустимым символам.

    Да и остальное - вариативно.

    Я когда подобным развлекался, ставил на выбор отдельно строчные/прописные, кириллица/латиница, цифры, спецсимволы, отдельная строка для индивидуальной последовательности спецсимволов.

    А когда-то давно, какой-то сайт не принимал пароли, потому, что там подряд шли строчные, цифры - а им хотелось, что бы подряд одинаковые субтипы не стояли. Типа "ММегаППППароль" - так нельзя, хотят: "МеГ@ПаР0лЬ"


  1. Grey83
    08.10.2024 02:40

    Я 5 лет назад, при создании аналогичного генератора пароля на языке SourcePawn, делал ещё проверку, чтобы не было двух букв в одном регистре и двух цифр подряд.

    static const char
    	SYMBOL[] = "abcdefghijklmnopqrstuvwxyz0123456789'+-=@_ABCDEFGHIJKLMNOPQRSTUVWXYZ",
    
    ...
    
    	int i, j, k;
    	while(i < iSize[client])
    	{
    		while(k == j || (k < 26 && j < 26) || (k > 25 && k < 36 && j > 25 && j < 36) || (k > 41 && j > 41))
    			k = GetRandomInt(0, 67);
    		sPass[1][client][i++] = SYMBOL[k];
    		j = k;
    	}
    	sPass[1][client][i] = 0;
    

    где iSize[client] – целочисленная переменная, хранящая необходимую длину пароля, заданную игроком;
    а sPass[1][client] – строковая переменная, куда будет сохранён сгенерированый пароль (в последней строке предоставленного кода завершается строка, т.к. у нас используются нуль-терминированные строки).


    1. mxr
      08.10.2024 02:40
      +1

      ...проверку, чтобы не было двух букв в одном регистре и двух цифр подряд.

      Разве подобное условие не делает наш пароль более предсказуемым? Условно, если мы подбираем пароль то можем существенно сократить количество вариантов с таким условием, и соответственно его сложность сильно пострадает.


      1. Grey83
        08.10.2024 02:40

        Для подбора давалось от 1 до 10 попыток (зависит от настроек), после чего был кик или бан по IP на срок до получаса. Ну а минимальная длина выставлялась от 5 до 63 символов (пользователь мог выбрать любую длину, но минимальную планку нового пароля задавал сервер). Ну и время на ввод можно было ограничить макисимум до 3 минут (можно меньше или вообще выключить это ограничение), после чего опять таки кик или бан IP.

        Так что криптостойкость была вполне себе достаточной, кмк.


        1. AndrewZhukov
          08.10.2024 02:40

          А можно рассмотреть сценарий, когда сливается список хэшей паролей с сервера, где крутится база данных. Тогда зная такие ограничения количество вариантов сокращается значительно. Именно в таком сетапе проводится анализ криптостойкости


          1. Grey83
            08.10.2024 02:40

            Я в БД пароли в открытом виде хранил, потому что сама БД ничем наружу не торчала (и вообще была SQLite).
            Это было для серваков CS:S v34 (ну и прочих NoSteam), когда развелось спуферов SteamID для пиратки. Т.е. выкачивать БД всё равно ради такого мамкины хакиры не стали бы.


  1. MountainGoat
    08.10.2024 02:40

    for i in 0..long{

    Такое чувство, что код был написан на С, потом превращен в Rust автоматически.

    for c in password.chars() {
      if c.is_ascii_digit() { digits += 1; }
    }

    А ещё лучше

    let digits = password.chars().filter(char::is_ascii_digit).count();

    Ну и вообще не комментирую фееричность рандомного угадывания пароля пока не подойдёт под условия, вместо того, чтобы согласно условиям накидать в буфер символов и потом их перемешать.