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

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

Задача

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

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

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

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

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

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

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

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

000DD0BFD801860C09116B9AAD880B125F1:53
00791BB54CC9122C70C1156FD97134EB83E:5
0088D3EFF796511B3833D74664B042D531A:1
008CDEBE10E31BF09C9BD20CBCC2C9CEDA3:3
00BD64FF4BE8674BC4C85CE380856184F9C:1

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

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

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

import hashlib, urllib.request

def 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 0

if __name__ == '__main__':
    import getpass
    print(pwned_count(getpass.getpass('Пароль: ')))

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

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

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

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

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

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

import hashlib, os

def 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) от неё отказались ещё несколько лет назад, а следом за ними то же самое написали в большинстве современных руководств.

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

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

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

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

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

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

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

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

Комментарии (5)


  1. Fedyaration
    26.08.2026 14:10

    Понятно и по делу, спасибо.


  1. censor2005
    26.08.2026 14:10

    Сходу не смог найти информацию по ресурсу api.pwnedpasswords.com, у него есть лимиты? Документация какая-нибудь? Сам домен pwnedpasswords.com недоступен


    1. jbenderov Автор
      26.08.2026 14:10

      Это бесплатный API от сервиса Have I Been Pwned (HIBP), созданный для безопасной проверки, не был ли ваш пароль скомпрометирован в известных утечках данных

      Подробнее в официальной доке:

      https://haveibeenpwned.com/API/v3

      В отличие от других API HIBP, на этот не накладывается ограничений по количеству запросов, но требуется, чтобы обязательно был заголовок User-Agent


      1. censor2005
        26.08.2026 14:10

        В документации именно этот адрес не упоминается, поэтому я заинтересовался, кто же его админит и ведёт реестр


        1. jbenderov Автор
          26.08.2026 14:10