Перейти к содержимому
ecdsa.com

Ключи доступа или пароли: фишинг, утечки и восстановление

Пароль — это секрет, который пользователь вводит на любой странице, где его попросят. Ключ доступа — пара ключей, привязанная к одному сайту, и её закрытая часть никогда не покидает устройство пользователя или его менеджер паролей.
Что сравниваетсяКлюч доступа (passkey)Пароль

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

Коротко о главном

ПарольКлюч доступа
Что доказывает пользовательЗнание общего секретаВладение закрытым ключом, который разблокируется локально биометрией или PIN-кодом
Что хранит серверСоль и медленный хеш (Argon2id, scrypt, PBKDF2)Идентификатор учётных данных и открытый ключ (91 байт для P-256 в примере ниже)
Фишинговый сайт-двойникПолучает пароль, если пользователь его введётНе получает ничего, чем можно воспользоваться: учётные данные привязаны к RP ID и источнику (origin) сайта
Повторное использование на разных сайтахОбычное дело, что и делает возможной подстановку учётных данных (credential stuffing)Невозможно: одна пара ключей на сайт и аккаунт
Утечка базы данныхОфлайн-подбор по хешамУтекают только открытые ключи; повторно предъявить нечего
NIST SP 800-63B-4 (июль 2025 г.)Однофакторный: не менее 15 символов, AAL1Многофакторный криптографический: AAL2; аппаратные ключи, привязанные к устройству, могут достигать AAL3, синхронизируемые ключи доступа — нет
Потеря устройстваНе влияетСинхронизируемые ключи доступа восстанавливаются вместе с аккаунтом у поставщика; для ключей, привязанных к устройству, нужны резервные учётные данные
Другие устройстваВводится где угодноСинхронизируются между устройствами поставщика; для остальных — QR-код и проверка близости по Bluetooth (гибридный транспорт)
СтандартыТолько HTML-формы и HTTP-аутентификацияW3C WebAuthn Level 3 (рекомендация с 25 авг. 2026 г.), FIDO CTAP 2.2

Что происходит при входе

При регистрации аутентификатор (телефон, ноутбук, менеджер паролей или ключ безопасности) создаёт пару ключей для идентификатора доверяющей стороны (relying party ID, RP ID) сайта, например example.com, а сайт сохраняет открытый ключ. При входе сайт отправляет случайный challenge; браузер добавляет источник (origin) страницы в подписываемые клиентские данные, а аутентификатор после проверки пользователя подписывает их вместе с хешем RP ID. Сервер проверяет challenge, источник, хеш RP ID и подпись.

Скрипт ниже (Node.js 26.10, Apple M4 Max) показывает, что каждый из подходов оставляет на сервере и какие проверки останавливают сайт-двойник.

Что хранит сервер и почему источник-двойник не проходит проверку
import { argon2Sync, createHash, generateKeyPairSync, randomBytes, sign, verify } from "node:crypto";

const sha256 = (b) => createHash("sha256").update(b).digest();

// What a password database stores: an Argon2id hash (OWASP minimum: 19 MiB, t=2, p=1)
const params = { nonce: randomBytes(16), memory: 19456, passes: 2, parallelism: 1, tagLength: 32 };
const t0 = performance.now();
const tag = argon2Sync("argon2id", { message: "correct horse battery staple", ...params });
const ms = performance.now() - t0;
console.log(`password: stores salt + ${tag.length}-byte Argon2id hash; one guess costs ${ms.toFixed(0)} ms`);

// What a passkey database stores: the credential's public key
const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
const spki = publicKey.export({ type: "spki", format: "der" });
console.log(`passkey:  stores a ${spki.length}-byte public key; a leak lets nobody sign in`);

// Authenticator: signs authenticatorData || SHA-256(clientDataJSON)
function getAssertion(rpId, origin, challenge) {
  const clientDataJSON = Buffer.from(JSON.stringify({ type: "webauthn.get", challenge, origin }));
  const authData = Buffer.concat([sha256(rpId), Buffer.from([0x05]), Buffer.alloc(4)]);
  const signature = sign("sha256", Buffer.concat([authData, sha256(clientDataJSON)]), privateKey);
  return { clientDataJSON, authData, signature };
}

// Relying party: the checks from WebAuthn §7.2
function check(a, challenge) {
  const c = JSON.parse(a.clientDataJSON);
  return {
    challenge: c.challenge === challenge,
    origin: c.origin === "https://example.com",
    rpIdHash: a.authData.subarray(0, 32).equals(sha256("example.com")),
    signature: verify("sha256", Buffer.concat([a.authData, sha256(a.clientDataJSON)]), publicKey, a.signature),
  };
}

const challenge = randomBytes(32).toString("base64url");
console.log("real site:", check(getAssertion("example.com", "https://example.com", challenge), challenge));
console.log("look-alike:", check(getAssertion("examp1e.com", "https://examp1e.com", challenge), challenge));
Результат
password: stores salt + 32-byte Argon2id hash; one guess costs 25 ms
passkey:  stores a 91-byte public key; a leak lets nobody sign in
real site: { challenge: true, origin: true, rpIdHash: true, signature: true }
look-alike: { challenge: true, origin: false, rpIdHash: false, signature: true }

Случай с двойником намеренно даёт злоумышленнику фору: на практике браузер вообще не станет использовать учётные данные example.com на другом сайте, так что подпись даже не будет создана. Но и подпись, полученная ретрансляцией сеанса, содержала бы чужой источник и хеш RP ID. Поэтому NIST относит WebAuthn к устойчивым к фишингу методам благодаря привязке к имени проверяющей стороны (verifier name binding), а пароли, одноразовые коды и подтверждения через push-уведомления к ним не относит. С Argon2id каждая попытка угадать пароль обходится в десятки миллисекунд, но если пользователь выбрал распространённый или повторно используемый пароль, его всё равно быстро подберут; открытый ключ не даёт злоумышленнику ничего, что можно угадать.

Где у каждого способа слабые места

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

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

Что когда выбирать

  • Потребительские сайты и приложения: ключи доступа как основной способ, их создают сразу после очередного входа по паролю; пароли — по желанию.
  • Вход сотрудников: ключи доступа через вашего поставщика удостоверений; привязанные к устройству ключи безопасности для администраторов и других ролей уровня AAL3.
  • Ценные потребительские аккаунты (банки, почта, облачные консоли): ключи доступа плюс надёжная процедура восстановления, в идеале — второй зарегистрированный ключ доступа.
  • Общие устройства и киоски: гибридный вход с телефона пользователя, а не хранение учётных данных на общем устройстве.
  • Протоколы без WebAuthn (IMAP, устаревшие VPN, SSH): пароли приложений или потоки OAuth, которые начинаются в браузере, где ключи доступа работают.

Заметки о миграции

  1. Добавьте ключи доступа рядом с паролями: после успешного входа по паролю предложите создать ключ доступа или используйте условное создание (conditional creation) из WebAuthn Level 3, чтобы ключ добавлялся автоматически, если менеджер паролей браузера это поддерживает.
  2. Используйте интерфейс автозаполнения: на странице входа вызывайте navigator.credentials.get с mediation: "conditional", чтобы ключи доступа появлялись в подсказках поля имени пользователя.
  3. Храните то, что понадобится: идентификатор учётных данных, открытый ключ, счётчик подписей, транспорты, флаги резервного копирования и AAGUID, который указывает на поставщика; поиск по AAGUID сопоставляет его с конкретным продуктом.
  4. Наладьте восстановление, прежде чем убирать пароли: требуйте уже существующий ключ доступа, второй зарегистрированный ключ доступа или подтверждение личности, а не просто ссылку из письма.
  5. Поддерживайте синхронизацию с поставщиками с помощью сигнальных методов (signal methods) из Level 3, когда пользователи удаляют учётные данные или меняют имена, и объявите связанные источники (related origins), если вход у вас работает на нескольких доменах.
  6. Перед развёртыванием протестируйте всё в песочнице ключей доступа и разберите реальные ответы в инспекторе WebAuthn.

Источники

Все сравнения