Каждый раз, когда речь заходит про проверку паролей сотрудников по базам утечек, разговор упирается в один и тот же вопрос: вы что, предлагаете отправить наши пароли на чужой сервис?
Вопрос правильный. Ответ на него — нет, отправлять ничего не нужно, и это не обещание на словах, а свойство протокола, которое проверяется самостоятельно за пять минут. Механика там простая и красивая, а знают её почему-то немногие.
Задача
Есть база из миллиардов паролей, засветившихся в утечках. Есть ваш пароль. Надо узнать, есть ли он в базе, не сообщив владельцу базы, что именно вы проверяли.
Наивное решение — отправить хеш вместо пароля — не работает. Хеш от пароля однозначно ему соответствует, и по популярным паролям обратное соответствие давно составлено. Отправив хеш, вы фактически отправили пароль.
Решение, которое применяется на практике, называется 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)

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

jbenderov Автор
26.08.2026 14:10Это бесплатный API от сервиса Have I Been Pwned (HIBP), созданный для безопасной проверки, не был ли ваш пароль скомпрометирован в известных утечках данных
Подробнее в официальной доке:
https://haveibeenpwned.com/API/v3
В отличие от других API HIBP, на этот не накладывается ограничений по количеству запросов, но требуется, чтобы обязательно был заголовок User-Agent

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

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