- Шифрованный файл .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>
128 lines
3.9 KiB
Rust
128 lines
3.9 KiB
Rust
//! Генератор паролей и оценка их стойкости.
|
||
|
||
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);
|
||
}
|
||
}
|