Menu
CoddyTech

Verificando uma senha

Parte da seção Autenticação e segurança do Journey de Node.js da Coddy. Lição 6 de 24.

Um login deriva uma chave a partir da senha tentada, usando o salt salvo e o comprimento original da chave. Um salt novo produziria uma chave diferente, mesmo para a senha correta. A senha original nunca é decodificada a partir do armazenamento.

// Um código de convite foi armazenado como salt.keyHex
const [salt, keyHex] = savedInvite.split('.');
const expected = Buffer.from(keyHex, 'hex');
const attempted = await scryptAsync(inviteCode, salt, 32);

Compare as sequências de bytes resultantes usando timingSafeEqual, depois de confirmar que elas têm o mesmo comprimento. Isso evita uma saída antecipada dependente do conteúdo, que poderia revelar informações por meio do tempo de execução. === em Buffer verifica a identidade, e uma comparação normal de strings não oferece garantia de tempo constante. Sua função de senha usa o delimitador de dois-pontos do registro de senha e uma chave de 64 bytes. A rota POST /login que você adiciona usa o resultado para retornar 200 ou 401.

challenge icon

Desafio

Fácil

Esta etapa abre o projeto de API de anotações privadas preparado pelo curso, em vez dos arquivos do seu projeto de Never Store Passwords (incluindo passwords.js). Seu trabalho anterior continua na própria lição. Leia os arquivos fornecidos antes de editar passwords.js. Esta cópia contém funções auxiliares para hash de senhas e as rotas de depuração fornecidas.

Agora, o cadastro salva hashes com salt. Adicione um login que confira uma senha com o que foi salvo no cadastro.

  1. Em passwords.js, adicione e exporte uma função async verifyPassword(password, stored). stored é um valor salvo pelo cadastro, como f2bb...f065:c3f7...dbd2. Retorne true quando password for a senha que o gerou e false caso contrário:
    • Obtenha o salt que vem antes dos dois-pontos.
    • Gere um hash de password com esse salt para obter uma chave de 64 bytes, da mesma forma que hashPassword faz.
    • Decodifique em hexadecimal a chave salva que vem depois dos dois-pontos. Se as duas chaves tiverem comprimentos diferentes em bytes, retorne false sem chamar timingSafeEqual, como em Comparando bytes de chave. Caso contrário, retorne o resultado da comparação.
  2. Em server.js, importe verifyPassword junto com hashPassword. Adicione POST /login. Ele recebe o mesmo corpo que o cadastro, por exemplo, {"username": "ada", "password": "correct horse"}.
    • Encontre o usuário com esse nome de usuário. verifyPassword é lenta e retorna uma promise, então aguarde sua conclusão.
    • Quando o usuário existir e a senha estiver correta, responda com o status 200 e {"username": "ada"}.
    • Caso contrário, responda com o status 401 e {"error": "Wrong username or password"}. 401 Unauthorized significa que a solicitação não comprovou quem está fazendo o pedido.
    • Envie exatamente a mesma resposta 401 para uma senha incorreta e para um nome de usuário que ninguém cadastrou. Assim, o corpo da resposta nunca informa a ninguém quais nomes de usuário existem.

Experimente: Pressione Executar. Envie POST /signup com {"username": "ada", "password": "correct horse"}: status 201. Envie POST /login com o mesmo corpo: status 200 e {"username": "ada"}. Envie POST /login com {"username": "ada", "password": "wrong horse"}: status 401 e {"error": "Wrong username or password"}. Envie POST /login com {"username": "nobody", "password": "correct horse"}: o mesmo status 401.

Experimente você mesmo

import { randomBytes, scrypt } from 'node:crypto';
import { promisify } from 'node:util';

const scryptAsync = promisify(scrypt);

// Transforma uma senha no texto que signup armazena.
export async function hashPassword(password) {
    const salt = randomBytes(16).toString('hex');
    const key = await scryptAsync(password, salt, 64);
    return `${salt}:${key.toString('hex')}`;
}

Todas as lições de Autenticação e segurança

Pratique por conta própria: Playground de Express