Всем привет, я новый пользователь Хабра. Нахожу тут много статей разного уровня сложности. И пришла в голову мысль опубликовать свою. Немного о себе, меня зовут Сергей Новицкий. Я .NET разработчик, у меня 3 года коммерческого опыта работы. Последнее время я изучаю язык программирования Rust. И хотел бы поделиться своими первыми утилитами.

Введение

Один из вопросов моей обыденности использования ПК, это пароли. Их сложно придумывать, можно генерировать. Есть конечно утилиты по типу pwgen, генераторы от браузеров.

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

Если говорить о pwgen, его я раньше и использовал. Он не плохой, использует собственные алгоритмы генерации пароля. Я бы и использовал его дальше, если бы не...

Прочитал однажды статью А. В. Столяров: Введение в операционные системы, я обнаружил стандартные устройства /dev/random и /dev/urandom. Которые предоставляют поток случайных байт генерируемых ядром Linux.

Изучив тему на форуме разработчиков GPG я узнал, что /dev/random используется в генерации ключей, и на мой взгляд такому источнику энтропии можно доверять.

Эти знания вдохновили меня к использованию этих устройств в качестве генератора пароля.

Немного о том что такое /dev/random

/dev/random предоставляет случайный поток байт, генерируемый ядром при помощи энтропии созданной пользователем, при вводе с клавиатуры и использования мыши. Это конечный источник энтропии, то есть данные в потоке могу закончиться, если он закончился, нужно просто начать вводить с клавиатуры данные и двигать мышью.
В свою очередь /dev/urandom - бесконечный источник псевдослучайных байт, так как использует CSPRNG на базе /dev/random.

Размышления на этот счет

Я подумал, что было бы не плохо иметь генератор паролей, который:

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

Во-вторых, работает быстрее, по той же причине, отсутствие алгоритма генерации.

И сама идея использования встроенных решений Linux нравится мне тем, что она следует принципам Do One Thing And Do It Well, DOTADIW - Делай одну вещь и делай её хорошо. Если в Linux уже есть источник энтропии, и он сделан хорошо. Для чего пытаться создать то, что уже готово.

Проблема

/dev/urandom выдает случайный поток байт.
Это означает, что каждый взятый байт из потока будет иметь значение от 0 до 255.
В контексте паролей, это нам не сильно подходит, так как ASCII кодировка содержит менее 255 символов, а подходящих нам и того меньше.

Если разобраться, то нам подходят цифры от 0 до 9, заглавные и строчные буквы от A до Z, и некоторые специальные символы.

В среднем около 90 символов нам подходят, это означает, что остальные нам нужно игнорировать.

Далее я покажу с чего я начал

Pipe команда

Первые опыты выглядели следующим образом:

tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16; echo #Команда
BEWUSUwCJQWVlIUF #Результат

Немного объясню, что из себя представляет скрипт:

tr - утилита фильтрации/замены символов
-d - удаляет символы
-c - инверсия: оставляет ТОЛЬКО указанные
'A-Za-z0-9' - Диапазон: A-Z, a-z, 0-9

head - берёт первые N байт
head -c 16 - первые 16 байт (символов)

echo - добавляет перенос строки

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

Bash-скрипт

Тогда мне пришла в голову идея написать Bash скрипт, с передаваемыми ему параметрами.
Исходный код доступен на моем GitHub
Следующая попытка выглядит так:

Развернуть длинный скрипт
#!/bin/bash
# pgdr.sh — CLI password generator

length=16
lowercase_only=false
uppercase_only=false
with_nums=false
with_symbols=false
use_random=false 

while getopts "l:LUnShR" opt; do
    case $opt in
        l) length="$OPTARG" ;;
        L) lowercase_only=true ;;
        U) uppercase_only=true ;;
        n) with_nums=true ;;
        S) with_symbols=true ;;
        R) use_random=true ;; 
        h) 
            cat << EOF
passgen.sh 1.0 — Password generator from /dev/urandom or /dev/random

USAGE:
    $0 -l <length> [-L] [-U] [-n] [-S] [-R]

OPTIONS:
    -l <length>    Password length (REQUIRED)
    -L             Only lowercase letters a-z
    -U             Only uppercase letters A-Z  
    -n             Include numbers 0-9
    -S             Include symbols !@#$%^&*()_+-=
    -R             Use /dev/random (blocks on low entropy)
    -h             Show this help

EXAMPLES:
    $0 -l 16 -L                     # /dev/urandom + lowercase
    $0 -l 32 -L -U -n -S            # Full charset (/dev/urandom)
    $0 -l 16 -L -R                  # /dev/random + lowercase

NOTES:
    -R: Blocks if low entropy (safer but slower)
    -L + -U = both cases (a-zA-Z)
    Default: /dev/urandom (fast, production-ready)
EOF
            exit 0
            ;;
        \?) echo "Usage: $0 -l <length> [-L] [-U] [-n] [-S] [-R] [-h]" >&2; exit 1 ;;
    esac
done

if [ "$lowercase_only" = false ] && [ "$uppercase_only" = false ] && [ "$with_nums" = false ] && [ "$with_symbols" = false ]; then
    echo "Error: specify at least one character type: -L/-U/-n/-S" >&2
    echo "Usage: $0 -l <length> [-L] [-U] [-n] [-S] [-R]" >&2
    exit 1
fi

charset=""
if [ "$lowercase_only" = true ] && [ "$uppercase_only" = false ]; then
    charset="a-z"
elif [ "$uppercase_only" = true ] && [ "$lowercase_only" = false ]; then
    charset="A-Z"
elif [ "$lowercase_only" = true ] && [ "$uppercase_only" = true ]; then
    charset="a-zA-Z"
fi

if [ "$with_nums" = true ]; then
    charset="${charset}0-9"
fi

if [ "$with_symbols" = true ]; then
    charset="${charset}!@#$%^&*()_+-="
fi

if [ "$use_random" = true ]; then
    entropy_source="/dev/random"
    echo "Using /dev/random (may block on low entropy)" >&2
else
    entropy_source="/dev/urandom"
fi

timeout=10  # seconds
if timeout "$timeout" tr -dc "$charset" < "$entropy_source" | head -c "$length" | tr -d '\0' | grep -q .; then
    tr -dc "$charset" < "$entropy_source" | head -c "$length" | tr -d '\0'; echo
else
    echo "Error: /dev/random blocked (low entropy). Use -R only for small passwords or wait." >&2
    echo "Tip: Use default /dev/urandom for production (no blocking)." >&2
    exit 1
fi

Использование выглядит так:

./pgdr.sh -L -U -n #Команда
6TvuMzNJj9JKQX1T   #Результат

И справка выглядит так:

./pgdr.sh -h #Команда
#Результат
passgen.sh 1.0 — Password generator from /dev/urandom or /dev/random

USAGE:
    ./pgdr.sh -l <length> [-L] [-U] [-n] [-S] [-R]

OPTIONS:
    -l <length>    Password length (REQUIRED)
    -L             Only lowercase letters a-z
    -U             Only uppercase letters A-Z  
    -n             Include numbers 0-9
    -S             Include symbols !@#$%^&*()_+-=
    -R             Use /dev/random (blocks on low entropy)
    -h             Show this help

EXAMPLES:
    ./pgdr.sh -l 16 -L                     # /dev/urandom + lowercase
    ./pgdr.sh -l 32 -L -U -n -S            # Full charset (/dev/urandom)
    ./pgdr.sh -l 16 -L -R                  # /dev/random + lowercase

Это поинтереснее, работать сильно удобнее, можно указать параметры, источник энтропии, шаблон пароля, длину.

Rust утилита

В принципе такой результат меня устраивал. Но тут я вспомнил, что изучаю Rust. И было бы не плохо написать такую утилиту на Rust. Что я и сделал.

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

Развернуть исходный код
fn main() -> io::Result<()> {
    let cli = Cli::parse();
    if !cli.lowercase && !cli.uppercase && !cli.numbers && !cli.symbols {
        Cli::command()
            .error(ClapErrorKind::MissingRequiredArgument, "Specify at least one: -L, -U, -n, -s")
            .exit();
    }
    let mut charset: Vec<u8> = Vec::new();
    if cli.lowercase {
        charset.extend(b'a'..=b'z');
    }
    if cli.uppercase {
        charset.extend(b'A'..=b'Z');
    }
    if cli.numbers {
        charset.extend(b'0'..=b'9');
    }
    if cli.symbols {
        charset.extend_from_slice(b"!@#$%^&*()_+-=");
    }
    let charset_len = charset.len() as u32;
    let path = if cli.random_source { "/dev/random" } else { "/dev/urandom" };
    let mut entropy_source = File::open(path)?;
    let mut stdout = io::BufWriter::with_capacity(cli.buffer_size, io::stdout().lock());
    let mut read_buf = vec![0u8; cli.buffer_size];
    let mut written_total = 0;
    while written_total < cli.length {
        let n = entropy_source.read(&mut read_buf)?;
        if n == 0 {
            return Err(io::Error::new(
                io::ErrorKind::UnexpectedEof,
                format!("Insufficient entropy available in {}. Try using /dev/urandom (remove -R) or wait for more system interrupts.", path)
            ));
        }
        for &byte in &read_buf[..] {
            if written_total >= cli.length as usize { break; }
            let idx = (byte as u32) % charset_len;
            stdout.write_all(&[charset[idx as usize]])?;
            written_total += 1;
        }
    }
    stdout.write_all(b"\n")?;
    stdout.flush()?;
    Ok(())
}

А ниже покажу как использовать утилиту pgdr -h:

pgdr -h     
 
High-performance cryptographically secure password generator

Usage: pgdr [OPTIONS]

Options:
  -l, --length <INT>         Total number of characters to generate [default: 16]
  -L, --lowercase            Include lowercase letters (a-z)
  -U, --uppercase            Include uppercase letters (A-Z)
  -N, --numbers              Include digits (0-9)
  -S, --symbols              Include special symbols (!@#$%^&*()_+-=)
  -b, --buffer-size <BYTES>  IO buffer size in bytes for reading and writing [default: 8192]
  -r, --random-source        Use /dev/random (blocking) instead of /dev/urandom (non-blocking)
  -h, --help                 Print help
  -V, --version              Print version

И пример использования

pgdr -LUN
oWUvYstE8piYyLR1

Как видно из инструкции, стандартная длина пароля установлена 16 символов, стандартный источник энтропии /dev/urandom, и стандартный размер буфера 8192 байта.

Выводить можно и без буфера, но отсутствие буфера увеличивает количество системных вызовов для чтения из /dev/urandom и вывода в stdout. Буфер позволяет снизить количество системных вызовов, тем самым увеличив производительность.

Инструкция по установке

  1. Rust приложение доступно в репозитории AUR

yay -S pgdr
  1. Установка с помощью Cargo

cargo install --git ssh://git@github.com/sergik776/pgdr.git
  1. Или просто скачать Git репозиторий

git clone git@github.com:sergik776/pgdr.git

Комментарий

Надеюсь эта статья поможет вам немного больше узнать о тонкостях работы с Linux, терминалом и командами.

Буду рад критике, советам, благодарности, и pull request'ам.

Ссылки

А. В. Столяров: Введение в операционные системы
Тема на форуме разработчиков GPG
SH скрипт
Rust исходник

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


  1. shenmue
    03.01.2026 19:23

    Не помню как точно но идея знающим будет понятна и они меня поправят
    echo "цитата" | base64 | md5sum


    1. Sergik776 Автор
      03.01.2026 19:23

      Идея понятна.

      Взять текст, конвертировать в base64 строку и посчитать хеш такой строки.

      Это будет означать, что хеш всегда одинаковый.

      То есть, что бы "пароль" был разный, изначальная "цитата" тоже должна быть разная. Что как бы намекает на то, что нужно придумать цитату... Можно конечно взять текущее время вместо цитаты...


      1. Fenex
        03.01.2026 19:23

        "цитата" может быть получена из того же /dev/urandom

        Я генерирую пароли этой командой:

        base64 < /dev/random | head -c 10
        


        1. Sergik776 Автор
          03.01.2026 19:23

          Так и есть, и смысл теряется, по крайней мере для меня.
          Вместо того, что бы сделать минимально нагрузки, берется цитата, конвертируется в base64 (нагрузка), потом еще хешируется (нагрузка), и только потом выдается.
          Хотя в действительности достаточно убрать лишние символы.

          Немного заморачиваюсь с нагрузкой, приложение на расте выдавало 10 млн симполов за 0.2 секунды, я его оптимизировал, теперь выдает 10 млн за 0.06 секунды. (Еще не отправлял на гит изменения).
          Это конечно не имеет большого значения, ибо там у всех 8-10 ядер, много ОЗУ, в принципе люди могут позволить себе писать плохой код...

          Но все же мне кажется, что лучше экономить ресурсы... У каждого своя параноя ))))


          1. thethee
            03.01.2026 19:23

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

            Я заметил в примерах слишком мало цифр и много букв. Было бы интересно сравнить ваш подход (rand) в сравнении с другими, которые показали выше (rand -> b64 например) и посмотреть на распределение символов таким образом.

            Правда в base64 не понятно как символы получать...


            1. Sergik776 Автор
              03.01.2026 19:23

              Я не говорю, что это кому то нужно. Я говорил, что у меня своя параноя.

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

              В моих примерах действительно мало цифр, так как это связано на прямую с /dev/random

              Именно из этого источника беруться данные. Плюс, если вы читали статью, из 255 байт около около 150 игнорируються фильтром. Отфильтрированный набор символов из энтропии ядра... Не я генерирую данные, а ядро.

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

              Я привел три примера, с одной pipe командой, скриптом и утилитой.

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

              Остальное по желанию, просто немного развил pipe команду.


  1. Fedorkov
    03.01.2026 19:23

    openssl rand -base64 12


  1. Cheater
    03.01.2026 19:23

    Странная логика подсчёта сколько байт прочитано из random/urandom.
    Ваш код молча предполагает что буфер read_buf заполнится за 1 вызов read, по крайней мере тело while-цикла зовёт read() и далее итерируется по всему read_buf. На деле, реализация трейта Read::read() не даёт никаких гарантий на то, сколько именно байт она запишет за вызов. Может быть 0, 1, ..., размер буфера. Если по каким-то причинам ваша read() прочитает 1 байт, вся логика сломается - цикл будет читать нули из read_buf. Ну и write_all по одному байту это такое себе.


    1. Sergik776 Автор
      03.01.2026 19:23

          while written_total < cli.length {
              let n = entropy_source.read(&mut read_buf)?;
              if n == 0 {
                  return Err(io::Error::new(
                      io::ErrorKind::UnexpectedEof,
                      format!("Insufficient entropy available in {}. Try using /dev/urandom (remove -R) or wait for more system interrupts.", path)
                  ));
              }
              for &byte in &read_buf[..] {
                  if written_total >= cli.length as usize { break; }
                  let idx = (byte as u32) % charset_len;
                  stdout.write_all(&[charset[idx as usize]])?;
                  written_total += 1;
              }
          }

      По поводу странной логики.
      В данном листинге, на строке 1, цикл while ни как не связан с ни с буфером ввода ни с буфером вывода, он остановится, когда written_total будет >= cli.length.
      Далее, я передаю read_buf для заполениня в файловый дискриптор
      let n = entropy_source.read(&mut read_buf).
      Метод read возвращает реальное количество прочитаннх байт и это n.
      Далее это n проверяется, равен ли он 0.
      0 я выбрал потому, что если там есть хоть какие то данные, они выведутся на экран, если же данные в /dev/random закончились, будет выведено соответствующее сообщение.
      Так как там в проверке есть return.
      Что же касается шанса такого развития событий, выше я писал, что :
      "В свою очередь /dev/urandom - бесконечный источник псевдослучайных байт, так как использует CSPRNG на базе /dev/random" - это означает, что используя /dev/urandom, буфер будет заполнен всегда, и только при использовании /dev/random есть вероятность, что буфер не будет заполнен. В таком случае при исчерпании данных - выдаст ошибку.

      Поправьте меня, если я не прав.


      1. Cheater
        03.01.2026 19:23

        Метод read возвращает реальное количество прочитаннх байт и это n.

        Нет.

        "It is not an error if the returned value n is smaller than the buffer size, even when the reader is not at the end of the stream yet".

        он остановится, когда written_total будет >= cli.length

        Тоже ничего хорошего. Код знает заранее, сколько именно итераций сделать, и при этом делает if внутри каждой итерации.


        1. Sergik776 Автор
          03.01.2026 19:23

          Все, до меня дошло. Вы правы.
          Что же, стоит исправить read_buf[..] на read_buf[..n].

          А разве объявление
          let mut stdout = io::BufWriter::with_capacity(cli.buffer_size, io::stdout().lock());

          не указывает на то, что при вызове
          stdout.write_all(&[charset[idx as usize]])?;
          данные будут заполнять внутренний буфер, и только при его переполнении выведутся в терминал ?

          Имею в виду, какая разница, создать свой буфер, записать в него данные и вызвать write_all с буфером.
          Или инициализировать io::BufWriter::with_capacity с внутренним буфером. И он будет делать то же самое ?


          1. Cheater
            03.01.2026 19:23

            а, не заметил что это BufWriter. Да, всё так, множественный write здесь допусти́м


  1. kt97679
    03.01.2026 19:23

    Я для генерации паролей использую вот такой скрипт:

    #!/bin/bash
    
    password_length=${1:-20}
    
    punct_symbols="~,./@#%&?=+_-"
    digit_symbols="0-9"
    upper_symbols="A-Z"
    lower_symbols="a-z"
    all_symbols=$digit_symbols$upper_symbols$lower_symbols$punct_symbols
    
    tr -dc $all_symbols </dev/urandom \
        | fold -w $password_length \
        | sed "/[$lower_symbols]/!d; /[$upper_symbols]/!d; /[$digit_symbols]/!d; /[$punct_symbols]/!d;" \
        | sed "20q;"
    

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


    1. Sergik776 Автор
      03.01.2026 19:23

      Очень круто, Вы тоже используете /dev/urandom )))
      Я как только об этом узнал, не смог сдержать восторга, сразу решил поделиться с миром вокруг )))


  1. 321785
    03.01.2026 19:23

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


    1. Sergik776 Автор
      03.01.2026 19:23

      Эту проблему я даже не рассматривал. Хотя конечно можно... Использовать слова и их корни, для генерации... У меня пароли хранятся в хранилище, потому, я их в принципе не запоминаю. Разве что мастер пароль помню )
      Но... Идея не плохая, как вы думаете ? Если брать слова или корни слов и комбинировать их с цифрами, символами... Будет ли это... Полезно ?
      Или можно создавать несуществующие слова, главное что бы чередовались гласные и согласные, для удобства произношения и запоминания...


      1. vabka
        03.01.2026 19:23

        Ну например в keepassxc есть возможность сгенерировать пароль по словарю слов.

        bip39 также использует идею кодирования каких-то байтов в виде мнемоники.

        Можно взять взять произвольный словарь и генерировать так пароли.

        Такое и запомнить не трудно (например у меня используется в качестве мастер-пароля фраза с 200+ бит энтропией)


    1. cebka
      03.01.2026 19:23

      Я недавно заморачивался заменой старого-доброго pwgen, и тоже наведосипедил свое на расте - режим с «читабельным» паролем на основании цепей Маркова и режим passphrase (в том числе с искажениями) мне очень нравится по выводу: https://github.com/vstakhov/pwgen-x


      1. vabka
        03.01.2026 19:23

        Идейно круто, особенно с цепями Маркова. Только на счёт "no external dependencies" немножко обман, так как растовые бинари по-умолчанию от glibc зависят.

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

        Сам код попробую глянуть, может закину какой-нибудь issue или pr.


        1. cebka
          03.01.2026 19:23

          Как раз не хотелось иметь внешний словарь, а хотелось иметь один бинарь, который легко собрать и установить на любой системе (через ansible, например), а потом использовать для простого создания паролей для того же htpasswd. Я понимаю, что задача достаточно специфическая, и в теории достаточно `strings -a -5 -- /dev/urandom`, но раз уж можно поудобнее, то сделал так :)


  1. sdramare
    03.01.2026 19:23

    getrandom() правильней использовать, он блокирует вызов до момента инициализации источника энтропии.


    1. Sergik776 Автор
      03.01.2026 19:23

      Да, я видел эту библиотеку. Особо не увидел разницы в производительности и решил оставить Reader вместо библиотеки.
      Но как я уже сказал, я новичок в Rust. И если мои выводы были ориентированны лишь на скорость. То... Действительно стоило бы дождаться инициализации источника энтропии.
      Может мне выкладывать все свои проекты ? Лучший code-review )))


      1. sdramare
        03.01.2026 19:23

        getrandom это не библиотека, это функция ядра https://man7.org/linux/man-pages/man2/getrandom.2.html


        1. Sergik776 Автор
          03.01.2026 19:23

          Я думал Вы об этом:
          https://docs.rs/getrandom/latest/getrandom/


  1. wibsea
    03.01.2026 19:23

    Чем pwgen не устроил?


    1. Sergik776 Автор
      03.01.2026 19:23

      Тем, что его нужно устанавливать,
      tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16 устанавливать не нужно.
      Тем, что у него свои алгоритмы генерации. Хотя на линукс это избыточно.
      Так как можно прийти к такому же результату, стандартными инструментами ядра.


      1. wibsea
        03.01.2026 19:23

        Времени не жалко?


        1. Sergik776 Автор
          03.01.2026 19:23

          Жалко, на установку pwgen ))


          1. wibsea
            03.01.2026 19:23

            :)


  1. mc2
    03.01.2026 19:23

    perl -le 'print map { ("a".."z", "A".."Z", 0..9, split //, "!@#$%^&*()-_=+") [rand 74] } 1..shift' 20

    Одной строкой


  1. Egres
    03.01.2026 19:23

    pwgen

    хорош тем, что пароли у него "произносимые", т.е. запоминаемые.