Перевірка складності пароля без знущання над користувачем

Перевірка складності пароля без знущання над користувачем

"Мінімум 8 символів, велика літера, цифра, спецсимвол, не менше 8 символів" — знайомо? Наче в ЖЕК прийшов. Пароль aaaaaA1! формально проходить, а "LoremIpsum LoremIpsum LoremLorem IpsumIpsum" - ні. То ж я роблю перевірку через підрахунок ентропії: оцінюється реальна складність саме підбору, а не чекбокси для секʼюріті. Довгий простий пароль може бути надійнішим за короткий "складний". Бонус - жартівливий текст помилки, щоб підбадьорити користувача йти далі.

Як рахувати ентропію?

Та як, як, потрібно просто порахувати скільки спроб потрібно зробити, щоб підібрати пароль! Тобто кількість можливих символів (базу) в степені довжини (експонента). Все. Але не зовсім. Чому? Бо якщо пароль "aaaaaa", то він формально має 6 символів, але насправді це лише 1 унікальний символ, який повторюється. Тобто потрібно враховувати не тільки довжину, а й різноманітність символів. І зараз це все зробимо.

Крок 1: Визначимо базу

Підрахуємо, які класи символів є в паролі:

КласДодає до бази
Малі літери (a-z)+26
Великі літери (A-Z)+26
Цифри (0-9)+10
Інші символи (!@# тощо)+26

Використав і малі, і великі, і цифри — база = 62. Тільки цифри — база = 10.

Крок 2: Експонента з урахуванням повторів

Кожен унікальний символ дає вагу 1.0, кожен повторний — 0.3. Нащо? Щоб понизити вагу однакових символів в паролі, але все ще їх рахувати, як-ніяк вони ускладнюють пароль.

Експонента = кількість_унікальних × 1.0 + кількість_повторів × 0.3

Крок 3: Поріг

Визначаємо поріг - баланс між зручністю користувача і безпекою. Я вибрав 10¹², це приблизно рівень, де брутфорс стає непрактичним для онлайн-атак. Якщо результат (база в степені експонента) менше порога — пароль слабкий, і користувача змушую придумати інший.

Приклади мінімально підходящих паролів (на 1 символ менше вже не підходять)

Пароль База Унік.Повт.Експонента Результат
V1@dDra 88 7 0 7 88^7 ≈ 40e12
V1@dV1@dV1@d 88 4 8 6.4 88^6.4 ≈ 3e12
VladVladVladVl 52 4 10 7 52^7 ≈ 1e12
vladvladvladvladvla264 15 8.5 26^8.5 ≈ 1e12
123456789012345671010 7 12.1 10^12.1 ≈ 1e12
1111111111111111111111111111111111111111013812.410^12.4 ≈ 2e12
horsejump26 9 0 9 26^9 ≈ 5e12

Причому, як ви можете побачити, найперший пароль з 7 символів найскладніший. А другий пароль 12 символів - уже пройде всі перевірки складності на різноманітних сайтах з запасом, а в мене ледь-ледь укладується в поріг.

Ну і жартівлива помилка, щоб змусити користувача таки ввести складний пароль

Error: this password uses Epstein, choose another one

Приклад коду

var funnyNames = []string{
	"Alice", "Bob", "Gizmo", "Noodle", "Pickle", "Wombat", "Bubbles", "Sprocket", "Muffin",
	"Einstein", "Tesla", "Curie", "Newton", "Turing", "Lovelace", "Hawking",
	"Gandalf", "Yoda", "Sherlock", "Pikachu", "Groot", "Dobby", "Epstein",
}

func checkPasswordStrength(password string) error {
	const minComplexity = 1e12
	var hasLower, hasUpper, hasDigit, hasOther bool
	unique := make(map[rune]struct{})
	for _, r := range password {
		switch {
		case unicode.IsLower(r):
			hasLower = true
		case unicode.IsUpper(r):
			hasUpper = true
		case unicode.IsDigit(r):
			hasDigit = true
		default:
			hasOther = true
		}
		unique[r] = struct{}{}
	}

	baseComplexity := 0
	if hasLower {
		baseComplexity += 26
	}
	if hasUpper {
		baseComplexity += 26
	}
	if hasDigit {
		baseComplexity += 10
	}
	if hasOther {
		baseComplexity += 26
	}

	total := utf8.RuneCountInString(password)
	exponent := float64(len(unique)) + float64(total-len(unique))*0.3
	result := math.Pow(float64(baseComplexity), exponent)
	if result < minComplexity {
		name := funnyNames[rand.Intn(len(funnyNames))]
		return fmt.Errorf("error, this password uses %s, choose another one", name)
	}
	return nil
}

Спробуй сам на цьому інтерактивному демо

Введені паролі не відправляються нікуди, чесно-чесно.

P.S. Використовуйте менеджери паролів. Потрібно памʼятати лише один пароль. А інші - довгі, складні та завжди унікальні.

Коментарі