AspansQulyp/src/generator.rs
deni.reyton 45de8f9f51 Qulyp: локальный менеджер паролей на Rust + egui
- Шифрованный файл .qulyp: Argon2id + XChaCha20-Poly1305, атомарная запись и .bak
- Записи с TOTP, папками, избранным и историей паролей; генератор паролей
- Синхронизация через облачную папку с послезаписным слиянием и ручное слияние файлов
- Автоблокировка, автоочистка буфера обмена, тёмная/светлая тема
- Сборка под Windows и Linux, CI для Gitea Actions
- Заметки проекта в notes/ с лимитами строк (tests/notes.rs)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:32:11 +05:00

128 lines
3.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Генератор паролей и оценка их стойкости.
use rand::Rng;
use rand::rngs::OsRng;
use rand::seq::SliceRandom;
use serde::{Deserialize, Serialize};
const LOWER: &str = "abcdefghijklmnopqrstuvwxyz";
const UPPER: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const DIGITS: &str = "0123456789";
const SYMBOLS: &str = "!@#$%^&*()-_=+[]{};:,.?/~";
const AMBIGUOUS: &str = "Il1O0o|";
#[derive(Serialize, Deserialize, Clone, PartialEq)]
#[serde(default)]
pub struct GenOptions {
pub length: usize,
pub lower: bool,
pub upper: bool,
pub digits: bool,
pub symbols: bool,
pub avoid_ambiguous: bool,
}
impl Default for GenOptions {
fn default() -> Self {
Self { length: 20, lower: true, upper: true, digits: true, symbols: true, avoid_ambiguous: true }
}
}
pub fn generate(o: &GenOptions) -> String {
let pick =
|set: &str| -> Vec<char> { set.chars().filter(|c| !o.avoid_ambiguous || !AMBIGUOUS.contains(*c)).collect() };
let mut sets: Vec<Vec<char>> = [(o.lower, LOWER), (o.upper, UPPER), (o.digits, DIGITS), (o.symbols, SYMBOLS)]
.into_iter()
.filter(|(on, _)| *on)
.map(|(_, s)| pick(s))
.collect();
if sets.is_empty() {
sets.push(pick(LOWER));
}
let all: Vec<char> = sets.concat();
let len = o.length.clamp(4, 128);
let mut rng = OsRng;
// По одному символу из каждого включённого набора, остальное — из общего.
let mut out: Vec<char> = sets.iter().map(|s| s[rng.gen_range(0..s.len())]).collect();
while out.len() < len {
out.push(all[rng.gen_range(0..all.len())]);
}
out.truncate(len);
out.shuffle(&mut rng);
out.into_iter().collect()
}
#[derive(Clone, Copy)]
pub struct Strength {
/// 0 — очень слабый … 4 — отличный.
pub score: u8,
pub bits: f64,
}
impl Strength {
pub fn label(&self) -> &'static str {
["Очень слабый", "Слабый", "Средний", "Хороший", "Отличный"][self.score as usize]
}
}
/// Грубая оценка энтропии: размер алфавита × длина, со штрафом за повторы.
pub fn strength(pw: &str) -> Strength {
if pw.is_empty() {
return Strength { score: 0, bits: 0.0 };
}
let mut pool = 0u32;
if pw.chars().any(|c| c.is_ascii_lowercase()) {
pool += 26;
}
if pw.chars().any(|c| c.is_ascii_uppercase()) {
pool += 26;
}
if pw.chars().any(|c| c.is_ascii_digit()) {
pool += 10;
}
if pw.chars().any(|c| c.is_ascii_punctuation() || c == ' ') {
pool += 33;
}
if !pw.is_ascii() {
pool += 66; // кириллица и прочее
}
let mut unique: Vec<char> = pw.chars().collect();
unique.sort_unstable();
unique.dedup();
let len = pw.chars().count() as f64;
// Повторяющиеся символы почти не добавляют стойкости.
let effective = (unique.len() as f64).max(len * 0.5).min(len);
let bits = effective * (pool.max(1) as f64).log2();
let score = match bits {
b if b < 28.0 => 0,
b if b < 40.0 => 1,
b if b < 60.0 => 2,
b if b < 80.0 => 3,
_ => 4,
};
Strength { score, bits }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn respects_options() {
let o = GenOptions { length: 32, symbols: false, ..Default::default() };
for _ in 0..50 {
let p = generate(&o);
assert_eq!(p.chars().count(), 32);
assert!(p.chars().all(|c| c.is_ascii_alphanumeric()));
assert!(p.chars().any(|c| c.is_ascii_digit()));
assert!(!p.chars().any(|c| AMBIGUOUS.contains(c)));
}
}
#[test]
fn strength_ordering() {
assert!(strength("123456").score < strength("Tr0ub4dor&3xK!9qZ").score);
}
}