Проверка паролей по базам утечек: как это устроено и почему пароль при этом не уходит наружу

от автора

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

Вопрос правильный. Ответ на него — нет, отправлять ничего не нужно, и это не обещание на словах, а свойство протокола, которое проверяется самостоятельно за пять минут. Механика там простая и красивая, а знают её почему-то немногие.

Задача

Есть база из миллиардов паролей, засветившихся в утечках. Есть ваш пароль. Надо узнать, есть ли он в базе, не сообщив владельцу базы, что именно вы проверяли.

Наивное решение — отправить хеш вместо пароля — не работает. Хеш от пароля однозначно ему соответствует, и по популярным паролям обратное соответствие давно составлено. Отправив хеш, вы фактически отправили пароль.

Решение, которое применяется на практике, называется k-анонимностью и устроено так: вы отправляете не весь хеш, а его начало, а сервер присылает всё, что под это начало подходит. Дальше вы сравниваете у себя.

Как это выглядит по шагам

Считаем SHA-1 от пароля. Берём первые пять символов шестнадцатеричной записи — их называют префиксом. Остальные тридцать пять символов никуда не уходят и остаются у вас.

Отправляем на сервис только префикс:

curl -s https://api.pwnedpasswords.com/range/CBFDA | head -5

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

000DD0BFD801860C09116B9AAD880B125F1:5300791BB54CC9122C70C1156FD97134EB83E:50088D3EFF796511B3833D74664B042D531A:1008CDEBE10E31BF09C9BD20CBCC2C9CEDA3:300BD64FF4BE8674BC4C85CE380856184F9C:1

Дальше вся работа делается на вашей стороне: ищем в этом списке свои тридцать пять символов. Нашли — пароль в утечках, и рядом написано, сколько раз. Не нашли — нет.

Что при этом узнал сервер: пять шестнадцатеричных символов. Под каждый такой префикс подходят тысячи хешей: в ответе на запрос выше пришла 1971 строка, и это типичная величина. Сервер не знает, какая из них ваша, и не знает даже, была ли ваша строка вообще в ответе.

Целиком проверка — двадцать строк:

import hashlib, urllib.requestdef pwned_count(password: str) -> int:    digest = hashlib.sha1(password.encode('utf-8')).hexdigest().upper()    prefix, suffix = digest[:5], digest[5:]    req = urllib.request.Request(        f'https://api.pwnedpasswords.com/range/{prefix}',        headers={'User-Agent': 'password-check/1.0', 'Add-Padding': 'true'},    )    with urllib.request.urlopen(req, timeout=15) as resp:        body = resp.read().decode()    for line in body.splitlines():        tail, _, count = line.partition(':')        if tail == suffix:            return int(count)    return 0if __name__ == '__main__':    import getpass    print(pwned_count(getpass.getpass('Пароль: ')))

Обратите внимание на заголовок Add-Padding. Он просит сервис дополнить ответ случайным числом фиктивных строк. Без него длина ответа для конкретного префикса всегда одна и та же, и наблюдатель, видящий лишь размер зашифрованного трафика, теоретически может сузить круг до одного префикса. С дополнением этот канал закрывается. Мелочь, но раз уж мы обсуждаем протокол, который специально сделан не разглашающим, — пусть будет.

И getpass вместо input — чтобы пароль не оставался в истории терминала и не попал в журналы.

Если наружу нельзя вообще

Бывает, что политика запрещает любые обращения к внешним сервисам, независимо от протокола. Это нормальная позиция, и вариант для неё есть: те же данные выкладываются файлом для локального использования. Полная выгрузка (haveibeenpwned.com/Passwords) занимает порядка сорока гигабайт в текстовом виде — строки формата «хеш:количество», отсортированные.

Дальше два способа искать.

Простой: положить файл на диск и искать двоичным поиском по отсортированному файлу. Поиск в отсортированном файле такого размера — это порядка тридцати обращений к диску, то есть миллисекунды. Никакой базы данных не нужно.

import hashlib, osdef lookup(path: str, password: str) -> int:    target = hashlib.sha1(password.encode()).hexdigest().upper().encode()    lo, hi = 0, os.path.getsize(path)    with open(path, 'rb') as f:        while lo < hi:            mid = (lo + hi) // 2            f.seek(mid)            if mid:                f.readline()              # отбрасываем обрезанную строку            start = f.tell()            if start >= hi:                break                     # блок сузился, хвост дочитаем линейно            line = f.readline()            h, _, count = line.strip().partition(b':')            if h == target:                return int(count)            if h < target:                lo = f.tell()            else:                hi = start        f.seek(lo)        while f.tell() < hi:              # тут остаётся пара строк, не больше            line = f.readline()            if not line:                break            h, _, count = line.strip().partition(b':')            if h == target:                return int(count)    return 0

Способ посложнее и побыстрее: держать хеши в структуре, которая экономит память ценой редких ложных ответов, — фильтре Блума. Для сорока гигабайт исходных данных получается структура на несколько гигабайт, помещающаяся в память. Ложные ответы там односторонние: «нет в базе» всегда правда, «есть в базе» иногда ошибка. Для нашей задачи это приемлемо — в худшем случае вы попросите человека сменить нормальный пароль.

Что с этим делать в организации

Точечная проверка своего пароля — это хорошо, но польза появляется, когда проверка встроена в процесс.

Проверять надо в момент установки пароля, а не постфактум. Пользователь придумывает пароль, система молча сверяет его со списком известных и, если совпадение есть, просит придумать другой. Человек в этот момент уже настроен на придумывание, и лишний круг его почти не раздражает. А письмо «ваш пароль найден в утечке, смените его за три дня» вызывает совсем другую реакцию: часть людей допишет к старому паролю единицу, и на этом всё закончится.

В Active Directory это делается фильтром паролей — библиотекой, которую контроллер домена вызывает при смене пароля. Готовые реализации есть, писать свою не нужно. У Microsoft есть и облачный вариант с собственным списком запрещённых паролей плюс возможностью добавить свой.

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

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

Про смену паролей каждые 90 дней

Раз уж речь зашла про политику. Требование регулярно менять пароль без всякого повода — устаревшая практика, и это не моё частное мнение: в рекомендациях NIST по цифровой идентификации (SP 800-63B) от неё отказались ещё несколько лет назад, а следом за ними то же самое написали в большинстве современных руководств.

Логика отказа простая. Принудительная смена не повышает стойкость, потому что человек не придумывает каждые три месяца новый хороший пароль — он берёт старый и меняет в нём цифру. Зато ухудшает остальное: пароли становятся короче и проще, потому что их надо запоминать заново, и чаще оказываются на бумажках.

Что рекомендуют вместо: длинный пароль без обязательных «спецсимвол, цифра, заглавная», проверка по списку известных при установке, и смена по событию — при подозрении на компрометацию, при увольнении, при появлении в свежей утечке.

Оговорка: если применимость требований у вас определяется внешним регламентом с конкретным сроком, никакие рекомендации не помогут — надо выполнять написанное. Но там, где выбор ваш, менять политику стоит.

Короткий чек-лист

Проверьте, что при смене пароля в вашей организации хоть какая-то проверка по списку известных вообще происходит. Чаще всего оказывается, что нет.

Заведите собственный список запрещённых слов: название компании, продуктов, города, отрасли.

Уберите принудительную смену по расписанию, если её не требует внешний регламент, и замените сменой по событию.

И проверьте свой личный пароль тем самым скриптом на двадцать строк. Ничего никуда не уходит, а результат иногда бывает неожиданным.

ссылка на оригинал статьи https://habr.com/ru/articles/1074840/