Pular para o conteúdo
ecdsa.com

Chave de acesso vs senha: phishing, vazamento, recuperação

Uma senha é um segredo que o usuário digita em qualquer página que a peça. Uma chave de acesso é um par de chaves vinculado a um único site, e a metade privada nunca sai do dispositivo ou do gerenciador de senhas do usuário.

Chaves de acesso vs senhas compara um segredo compartilhado com a autenticação por chave pública. A senha é conhecida pelo usuário e, na forma de hash, pelo servidor, e funciona em qualquer página que a peça, inclusive em uma falsa. Uma chave de acesso (passkey) é uma credencial WebAuthn: um par de chaves criado para um único site, desbloqueado no dispositivo do usuário com impressão digital, reconhecimento facial ou PIN e usado para assinar um desafio novo, vinculado à identidade do site.

Visão geral

SenhaChave de acesso
O que o usuário provaConhecimento de um segredo compartilhadoPosse de uma chave privada, desbloqueada localmente por biometria ou PIN
O que o servidor guardaSalt e hash lento (Argon2id, scrypt, PBKDF2)ID da credencial e chave pública (91 bytes para P-256 na demonstração abaixo)
Site de phishing com domínio parecidoRecebe a senha se o usuário a digitarNão obtém nada utilizável: a credencial está vinculada ao RP ID e à origem do site
Reutilização entre sitesComum, o que possibilita credential stuffingImpossível: um par de chaves por site e por conta
Vazamento do banco de dadosTentativas de adivinhação offline contra os hashesSó vazam chaves públicas; não há nada para reutilizar
NIST SP 800-63B-4 (julho de 2025)Fator único: pelo menos 15 caracteres, AAL1Criptográfico multifator: AAL2; chaves de hardware vinculadas ao dispositivo podem chegar ao AAL3, chaves de acesso sincronizadas não
Dispositivo perdidoNão é afetadaChaves de acesso sincronizadas são restauradas com a conta do provedor; chaves vinculadas ao dispositivo precisam de uma credencial reserva
Outros dispositivosBasta digitá-la em qualquer lugarSincronizada entre os dispositivos do provedor; QR code e proximidade por Bluetooth (híbrido) para os demais
PadrõesNenhum além de formulários HTML e autenticação HTTPW3C WebAuthn Level 3 (Recomendação desde 25 de agosto de 2026), FIDO CTAP 2.2

O que acontece quando você faz login

No registro, o autenticador (um celular, notebook, gerenciador de senhas ou chave de segurança) cria um par de chaves para o ID da parte confiável (relying party, RP) do site, por exemplo example.com, e o site guarda a chave pública. No login, o site envia um desafio aleatório; o navegador acrescenta a origem da página aos dados do cliente assinados, e o autenticador, depois da verificação do usuário, assina esses dados junto com um hash do RP ID. O servidor confere o desafio, a origem, o hash do RP ID e a assinatura.

O script abaixo (Node.js 26.10, Apple M4 Max) mostra o que cada abordagem deixa no servidor e quais verificações barram um site com domínio parecido.

O que o servidor guarda e por que uma origem parecida falha
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));
Saída
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 }

O caso do site parecido é propositalmente generoso com o atacante: na prática, o navegador se recusa a usar credenciais de example.com em qualquer outro site, então nenhuma assinatura chega a ser produzida. Mesmo uma assinatura obtida retransmitindo a sessão levaria a origem e o hash do RP ID errados, e é por isso que o NIST classifica o WebAuthn como resistente a phishing pela vinculação ao nome do verificador (verifier name binding), enquanto senhas, códigos de uso único e aprovações por notificação push não são. O Argon2id faz cada tentativa de adivinhar uma senha custar dezenas de milissegundos, mas as senhas comuns ou reutilizadas que os usuários escolhem continuam sendo descobertas rapidamente; uma chave pública não dá ao atacante nada para adivinhar.

Onde cada uma ainda falha

As senhas falham pela reutilização (credential stuffing com listas vazadas), por páginas de phishing e proxies de phishing em tempo real que também capturam códigos de uso único, e por escolhas fracas. Por isso, o NIST SP 800-63B-4 exige pelo menos 15 caracteres para senhas de fator único (8 dentro da autenticação multifator), proíbe regras de composição e trocas periódicas, exige verificar senhas novas contra valores vazados e comuns e recomenda aceitar pelo menos 64 caracteres. Um gerenciador de senhas com preenchimento automático vinculado ao domínio resolve a reutilização e parte do phishing, mas nem todo usuário tem um.

As chaves de acesso transferem o risco para dois lugares. O primeiro é a conta do provedor: chaves de acesso sincronizadas são tão seguras quanto a conta da Apple, do Google, da Microsoft ou do gerenciador de senhas que as guarda, e é por isso que o NIST não permite chaves sincronizadas e exportáveis no AAL3. O segundo é a recuperação de conta no seu site: se uma chave de acesso perdida pode ser substituída por um link enviado por e-mail ou por um código por SMS, os atacantes vão usar esse caminho em vez disso. Os usuários também precisam de uma segunda credencial ou de um provedor com sincronização para não ficarem sem acesso à conta.

Quando escolher cada uma

  • Sites e aplicativos para consumidores: chaves de acesso como método principal, criadas logo após o próximo login com senha; senhas opcionais.
  • Login de funcionários: chaves de acesso pelo seu provedor de identidade; chaves de segurança vinculadas ao dispositivo para administradores e outras funções AAL3.
  • Contas de consumidores de alto valor (bancos, e-mail, consoles de nuvem): chaves de acesso mais um processo de recuperação forte, de preferência uma segunda chave de acesso registrada.
  • Dispositivos compartilhados ou quiosques: login híbrido pelo celular do usuário, em vez de guardar credenciais no dispositivo compartilhado.
  • Protocolos sem WebAuthn (IMAP, VPNs legadas, SSH): senhas específicas de aplicativo ou fluxos OAuth que começam em um navegador, onde as chaves de acesso funcionam.

Notas de migração

  1. Adicione chaves de acesso ao lado das senhas: depois de um login com senha bem-sucedido, ofereça a criação de uma chave de acesso, ou use a criação condicional do WebAuthn Level 3 para adicionar uma automaticamente quando o gerenciador de senhas do navegador oferecer suporte a isso.
  2. Use a interface de preenchimento automático: chame navigator.credentials.get com mediation: "conditional" na página de login para que as chaves de acesso apareçam nas sugestões do campo de nome de usuário.
  3. Guarde o que você precisa: ID da credencial, chave pública, contador de assinaturas, transportes, as flags de backup e o AAGUID, que identifica o provedor; a consulta de AAGUID associa esse valor a um produto.
  4. Corrija a recuperação antes de remover as senhas: exija uma chave de acesso existente, uma segunda chave de acesso registrada ou verificação de identidade, e não apenas um link por e-mail.
  5. Mantenha os provedores sincronizados com os métodos de sinalização (signal methods) do Level 3 quando os usuários excluírem credenciais ou mudarem de nome, e declare origens relacionadas se o seu login abrange vários domínios.
  6. Teste no ambiente de testes de chaves de acesso e inspecione respostas reais com o inspetor de WebAuthn antes da implantação.

Referências

Todas as comparações