Всем привет, я новый пользователь Хабра. Нахожу тут много статей разного уровня сложности. И пришла в голову мысль опубликовать свою. Немного о себе, меня зовут Сергей Новицкий. Я .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. Буфер позволяет снизить количество системных вызовов, тем самым увеличив производительность.
Инструкция по установке
Rust приложение доступно в репозитории AUR
yay -S pgdr
Установка с помощью Cargo
cargo install --git ssh://git@github.com/sergik776/pgdr.git
Или просто скачать Git репозиторий
git clone git@github.com:sergik776/pgdr.git
Комментарий
Надеюсь эта статья поможет вам немного больше узнать о тонкостях работы с Linux, терминалом и командами.
Буду рад критике, советам, благодарности, и pull request'ам.
Ссылки
А. В. Столяров: Введение в операционные системы
Тема на форуме разработчиков GPG
SH скрипт
Rust исходник
Комментарии (31)

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 по одному байту это такое себе.
Sergik776 Автор
03.01.2026 19:23while 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 есть вероятность, что буфер не будет заполнен. В таком случае при исчерпании данных - выдаст ошибку.
Поправьте меня, если я не прав.
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 внутри каждой итерации.

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с внутренним буфером. И он будет делать то же самое ?
Cheater
03.01.2026 19:23а, не заметил что это BufWriter. Да, всё так, множественный write здесь допусти́м

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;"символы пунктуации выбраны так, чтобы можно было выделить пароль в терминале дабл кликом.

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

321785
03.01.2026 19:23Проблема всех этих генераторов в сложности запоминания получившегося результата.
Т.е. пароль мнемонически неудобен, не знаю как ещё точнее сказать.
Вот если бы было хотя бы согласование окончаний, то у пользователя было бы меньше соблазнов написать его на бумажке.
Sergik776 Автор
03.01.2026 19:23Эту проблему я даже не рассматривал. Хотя конечно можно... Использовать слова и их корни, для генерации... У меня пароли хранятся в хранилище, потому, я их в принципе не запоминаю. Разве что мастер пароль помню )
Но... Идея не плохая, как вы думаете ? Если брать слова или корни слов и комбинировать их с цифрами, символами... Будет ли это... Полезно ?
Или можно создавать несуществующие слова, главное что бы чередовались гласные и согласные, для удобства произношения и запоминания...
vabka
03.01.2026 19:23Ну например в keepassxc есть возможность сгенерировать пароль по словарю слов.
bip39 также использует идею кодирования каких-то байтов в виде мнемоники.
Можно взять взять произвольный словарь и генерировать так пароли.
Такое и запомнить не трудно (например у меня используется в качестве мастер-пароля фраза с 200+ бит энтропией)

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

vabka
03.01.2026 19:23Идейно круто, особенно с цепями Маркова. Только на счёт "no external dependencies" немножко обман, так как растовые бинари по-умолчанию от glibc зависят.
Ещё смущает встраивание словаря в код, так как хороший default - это хорошо, а возможность его переопределить - ещё лучше.
Сам код попробую глянуть, может закину какой-нибудь issue или pr.

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

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

Sergik776 Автор
03.01.2026 19:23Да, я видел эту библиотеку. Особо не увидел разницы в производительности и решил оставить Reader вместо библиотеки.
Но как я уже сказал, я новичок в Rust. И если мои выводы были ориентированны лишь на скорость. То... Действительно стоило бы дождаться инициализации источника энтропии.
Может мне выкладывать все свои проекты ? Лучший code-review )))
sdramare
03.01.2026 19:23getrandom это не библиотека, это функция ядра https://man7.org/linux/man-pages/man2/getrandom.2.html

wibsea
03.01.2026 19:23Чем pwgen не устроил?

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

mc2
03.01.2026 19:23perl -le 'print map { ("a".."z", "A".."Z", 0..9, split //, "!@#$%^&*()-_=+") [rand 74] } 1..shift' 20Одной строкой
shenmue
Не помню как точно но идея знающим будет понятна и они меня поправят
echo "цитата" | base64 | md5sum
Sergik776 Автор
Идея понятна.
Взять текст, конвертировать в base64 строку и посчитать хеш такой строки.
Это будет означать, что хеш всегда одинаковый.
То есть, что бы "пароль" был разный, изначальная "цитата" тоже должна быть разная. Что как бы намекает на то, что нужно придумать цитату... Можно конечно взять текущее время вместо цитаты...
Fenex
"цитата" может быть получена из того же /dev/urandom
Я генерирую пароли этой командой:
Sergik776 Автор
Так и есть, и смысл теряется, по крайней мере для меня.
Вместо того, что бы сделать минимально нагрузки, берется цитата, конвертируется в base64 (нагрузка), потом еще хешируется (нагрузка), и только потом выдается.
Хотя в действительности достаточно убрать лишние символы.
Немного заморачиваюсь с нагрузкой, приложение на расте выдавало 10 млн симполов за 0.2 секунды, я его оптимизировал, теперь выдает 10 млн за 0.06 секунды. (Еще не отправлял на гит изменения).
Это конечно не имеет большого значения, ибо там у всех 8-10 ядер, много ОЗУ, в принципе люди могут позволить себе писать плохой код...
Но все же мне кажется, что лучше экономить ресурсы... У каждого своя параноя ))))
thethee
Кому может понадобиться 10 млн паролей за секунду? Даже в утилитах подбора пароля вычисление хеша или ответ от системы, проверяющей пароль, является узким горлышком, но никак не генерация самого пароля.
Я заметил в примерах слишком мало цифр и много букв. Было бы интересно сравнить ваш подход (rand) в сравнении с другими, которые показали выше (rand -> b64 например) и посмотреть на распределение символов таким образом.
Правда в base64 не понятно как символы получать...
Sergik776 Автор
Я не говорю, что это кому то нужно. Я говорил, что у меня своя параноя.
Просто увидеть эфективность кода на 10-20 символах тяжело. Потому сделал по больше поток данных, что бы было видно результат.
В моих примерах действительно мало цифр, так как это связано на прямую с /dev/random
Именно из этого источника беруться данные. Плюс, если вы читали статью, из 255 байт около около 150 игнорируються фильтром. Отфильтрированный набор символов из энтропии ядра... Не я генерирую данные, а ядро.
В этом суть поста, что бы не придумывать велосипед, а брать уже готовые данные из ядра. Не писать генераторы, не писать каке то балансиры соотношения символов, цифр, букв..
Я привел три примера, с одной pipe командой, скриптом и утилитой.
Возможно большинству читатилей будет достаточно первой команды... Так как это быстро, эфективно и вообще не требует установки пакетов. Так сказать генератор пароля из коробки на голом ядре.
Остальное по желанию, просто немного развил pipe команду.