Обновить

Пользователям Linux посвящается. Генератор паролей из /dev/random: от one-liner'а к Rust CLI

Уровень сложностиПростой
Время на прочтение6 мин
Охват и читатели11K
Всего голосов 3: ↑3 и ↓0+4
Комментарии31

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

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

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

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

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

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

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

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

base64 < /dev/random | head -c 10

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

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

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

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

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

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

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

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

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

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

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

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

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

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

openssl rand -base64 12

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

    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 есть вероятность, что буфер не будет заполнен. В таком случае при исчерпании данных - выдаст ошибку.

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

Метод 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 внутри каждой итерации.

Все, до меня дошло. Вы правы.
Что же, стоит исправить 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 с внутренним буфером. И он будет делать то же самое ?

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

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

#!/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;"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

:)

pwgen

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации