Atualizando hash de senha sem forçar uma nova senha para usuários existentes

32

Você mantém um aplicativo existente com uma base de usuários estabelecida. Com o tempo, é decidido que a técnica atual de hash de senha está desatualizada e precisa ser atualizada. Além disso, por motivos de UX, você não deseja que os usuários existentes sejam forçados a atualizar suas senhas. Toda a atualização do hash da senha precisa ocorrer atrás da tela.

Suponha um modelo de banco de dados 'simplista' para usuários que contenha:

  1. identidade
  2. O email
  3. Senha

Como se pode resolver esse requisito?


Meus pensamentos atuais são:

  • crie um novo método de hash na classe apropriada
  • atualize a tabela do usuário no banco de dados para conter um campo de senha adicional
  • Depois que um usuário efetuar login com êxito usando o hash de senha desatualizado, preencha o segundo campo de senha com o hash atualizado

Isso me deixa com o problema de que não posso diferenciar razoavelmente entre usuários que possuem e aqueles que não atualizaram seu hash de senha e, portanto, serão forçados a verificar os dois. Isso parece terrivelmente falho.

Além disso, isso significa basicamente que a antiga técnica de hash pode ser forçada a permanecer indefinidamente até que cada usuário atualize sua senha. Somente naquele momento eu poderia começar a remover a verificação de hash antiga e remover o campo supérfluo do banco de dados.

Estou procurando principalmente algumas dicas de design aqui, já que minha 'solução' atual está suja, incompleta e não, mas se for necessário um código real para descrever uma possível solução, fique à vontade para usar qualquer idioma.

Willem
fonte
4
Por que você não seria capaz de distinguir entre um conjunto e um hash secundário não configurado? Apenas torne a coluna do banco de dados anulável e verifique se há nulo.
Kilian Foth
6
Vá com um tipo de hash como sua nova coluna, em vez de um campo para o novo hash. Quando você atualiza o hash, altere o campo de tipo. Dessa forma, quando isso acontecer novamente no futuro, você já estará em posição de negociar, e não há chance de você se apegar a um hash antigo (presumivelmente menos seguro).
22813 Michael Kohne
1
Eu acho que você está no caminho certo. Daqui a 6 meses ou um ano, você pode forçar os usuários restantes a alterar sua senha. Um ano depois, ou o que for, você pode se livrar do campo antigo.
GlenPeterson
3
Por que não apenas o hash antigo?
Siyuan Ren
porque você deseja que a senha seja hash (e não o hash antigo), para que a remoção de um hash (ou seja, comparando-a com um hash fornecido pelo usuário) forneça a você ... a senha - e não outro valor que precise ser 'desassociado' sob o método antigo. Possível, mas não mais limpo.
precisa

Respostas:

25

Eu sugeriria adicionar um novo campo, "hash_method", com talvez 1 para indicar o método antigo e 2 para indicar o novo método.

Razoavelmente falando, se você se importa com esse tipo de coisa e seu aplicativo é relativamente longo (o que aparentemente já é), isso provavelmente acontecerá novamente, pois a criptografia e a segurança da informação são um campo tão evolutivo e imprevisível. Houve um tempo em que uma simples execução no MD5 era padrão, se o hash era usado! Então, pode-se pensar que eles devem usar SHA1, e agora há salga, sal global + sal aleatório individual, SHA3, diferentes métodos de geração de números aleatórios prontos para criptografia ... isso não vai apenas 'parar', então você pode conserte isso de maneira extensível e repetível.

Então, digamos que agora você tenha algo como (em pseudo-javascript por simplicidade, espero):

var user = getUserByID(id);
var tryPassword = hashPassword(getInputPassword());


if (user.getPasswordHash() == tryPassword)
{
    // Authenticated!
}

function hashPassword(clearPassword)
{
    // TODO: Learn what "hash" means
    return clearPassword + "H@$I-I";
}

Agora, percebendo que existe um método melhor, você só precisa fazer uma pequena refatoração:

var user = getUserByID(id);
var tryPassword = hashPassword(getInputPassword(), user.getHashingMethod());

if (user.getPasswordHash() == tryPassword)
{
    // Authenticated!
}

function hashPassword(clearPassword, hashMethod)
{
    // Note: Hash doesn't mean what we thought it did. Oops...

    var hash;
    if (hashMethod == 1)
    {
        hash = clearPassword + "H@$I-I";
    }
    else if (hashMethod == 2)
    {
        // Totally gonna get it right this time.
        hash = SuperMethodTheNSASaidWasAwesome(clearPassword);
    }
    return hash;
}

Nenhum agente secreto ou programador foi prejudicado na produção dessa resposta.

BrianH
fonte
+1 Isso parece um resumo bastante sensato, obrigado. Embora eu possa acabar movendo os campos hash e hashmethod para uma tabela separada quando implementar isso.
Willem
1
O problema com esta técnica é que, para as contas não efetuarem login, não é possível atualizar os hashes. Na minha experiência, a maioria dos usuários não faz login por anos, a não ser que você exclua usuários inativos. Sua abstração também não funciona, pois não conta com sais. A abstração padrão está tendo duas funções, uma para verificação e outra para criação.
CodesInChaos
O hash de senha mal evoluiu nos últimos 15 anos. O Bcrypt foi publicado em 1999 e ainda é um dos hashes recomendados. Apenas uma melhoria significativa ocorreu desde então: sequencialmente os hard hashes da memória, pioneiros no scrypt.
CodesInChaos
Isso deixa os hashes antigos vulneráveis ​​(por exemplo, se alguém rouba o banco de dados). Hash cada hash antigo com o novo atenua método mais seguro este em algum grau (você ainda vai precisar do tipo de hash para distinguir entre newHash(oldHash, salt)ounewHash(password, salt)
dbkk
42

Se você se importa o suficiente com a implementação do novo esquema de hash para todos os usuários o mais rápido possível (por exemplo, porque o antigo é realmente inseguro), existe realmente uma maneira de "migração" instantânea de cada senha.

A idéia é basicamente hash do hash . Em vez de aguardar que os usuários forneçam suas senhas existentes ( p) no próximo login, use imediatamente o novo algoritmo de hash ( H2) no hash existente já produzido pelo antigo algoritmo ( H1):

hash = H2(hash)  # where hash was previously equal to H1(p) 

Depois de fazer essa conversão, você ainda poderá executar a verificação de senha. você só precisa calcular em H2(H1(p'))vez do anterior H1(p')sempre que o usuário tentar fazer login com senha p'.

Em teoria, esta técnica poderia ser aplicada a múltiplos migrações ( H3, H4, etc.). Na prática, você desejaria se livrar da antiga função hash, por motivos de desempenho e legibilidade. Felizmente, isso é bastante fácil: no próximo login bem-sucedido, simplesmente calcule o novo hash da senha do usuário e substitua o hash-a-a-hash existente por ele:

hash = H2(p)

Você também precisará de uma coluna adicional para lembrar o hash que está armazenando: aquele da senha ou do hash antigo. No infeliz evento de vazamento do banco de dados, essa coluna não deve facilitar o trabalho do cracker; independentemente do seu valor, o invasor ainda teria que reverter o H2algoritmo seguro em vez do antigo H1.

Xion
fonte
3
Um gigante +1: H2 (H1 (p)) é geralmente tão seguro quanto usar H2 (p) diretamente, mesmo que H1 seja terrível, porque (1) sal e alongamento são tratados por H2 e (2) os problemas com hashes "quebrados", como o MD5, não afetam o hash da senha. Veja também: crypto.stackexchange.com/questions/2945/…
orip
Interessante, eu imaginaria que o hash antigo criaria algum tipo de 'problema' de segurança. Parece que eu estava enganado.
Willem
4
se o H1 for realmente terrível, isso não será seguro. Ser terrível neste contexto é principalmente sobre perder muita entropia da entrada. Até o MD4 e o MD5 são quase perfeitos nesse aspecto, portanto, essa abordagem é insegura para alguns hashes de homebrew ou hashes com saída realmente curta (abaixo de 80 bits).
CodesInChaos
1
Nesse caso, provavelmente sua única opção é admitir que você errou duro e ir em frente e de redefinição de senha para todos os usuários. É menos sobre migração nesse ponto e mais sobre controle de danos.
Xion
8

Sua solução (coluna adicional no banco de dados) é perfeitamente aceitável. O único problema com ele é o que você já mencionou: que o hash antigo ainda é usado por usuários que não se autenticaram desde a alteração.

Para evitar essa situação, você pode esperar que os usuários mais ativos alternem para o novo algoritmo de hash e:

  1. Remova as contas de usuários que não se autenticam por muito tempo. Não há nada errado em remover uma conta de usuário que nunca acessa o site há três anos.

  2. Envie um email para os demais usuários entre aqueles que não se autenticaram por um tempo, informando que podem estar interessados ​​em algo novo. Isso ajudará a reduzir ainda mais o número de contas que usam o hash mais antigo.

    Cuidado: se você tem uma opção sem spam, os usuários podem verificar no seu site, não envie o email aos usuários que o verificaram.

  3. Por fim, algumas semanas após a etapa 2, destrua a senha dos usuários que ainda estão usando a técnica antiga. Quando e se tentarem se autenticar, verão que a senha é inválida; se eles ainda estiverem interessados ​​no seu serviço, poderão redefini-lo.

Arseni Mourzenko
fonte
1

Você provavelmente nunca terá mais do que alguns tipos de hash, para poder resolver isso sem estender seu banco de dados.

Se os métodos tiverem comprimentos diferentes de hash e o comprimento de cada método for constante (por exemplo, a transição de md5 para hmac-sha1), você poderá diferenciar o método do comprimento de hash. Se eles tiverem o mesmo comprimento, você poderá calcular o hash usando o novo método primeiro, depois o método antigo se o primeiro teste falhar. Se você possui um campo (não mostrado) informando quando o usuário foi atualizado / criado pela última vez, você pode usá-lo como um indicador para qual método usar.

Horizonte de eventos
fonte
1

Eu fiz algo assim e existe uma maneira de saber se é o novo hash E torná-lo ainda mais seguro. Acho que mais seguro é uma coisa boa para você, se você estiver investindo um tempo para atualizar seu hash ;-)

Adicione uma nova coluna à sua tabela de banco de dados chamada "salt" e use-a como um sal gerado aleatoriamente por usuário. (praticamente evita ataques à mesa arco-íris)

Dessa forma, se minha senha for "pass123" e o salt aleatório for 3333, minha nova senha será o hash de "pass1233333".

Se o usuário tem sal, você sabe que é o novo hash. Se o sal do usuário for nulo, você sabe que é o hash antigo.

Ben
fonte
0

Não importa quão boa seja sua estratégia de migração, se o banco de dados já tiver sido roubado (e você não puder provar negativamente que isso nunca aconteceu), as senhas armazenadas com funções de hash inseguras já poderão ser comprometidas. A única redução para isso é exigir que os usuários alterem suas senhas.

Este não é um problema que pode ser resolvido (apenas) escrevendo código. A solução é informar usuários e clientes sobre os riscos aos quais foram expostos. Quando você estiver disposto a se abrir, pode fazer a migração simplesmente redefinindo a senha de todos os usuários, porque é a solução mais fácil e segura. Todas as alternativas são realmente sobre esconder o fato de que você errou.

Florian Winter
fonte
1
Um compromisso que eu fiz no passado é para manter as senhas antigas por um período de tempo, remoendo-los como log pessoas, mas depois que o tempo passou-lhe , em seguida, forçar uma redefinição de senha em qualquer um que não faça o login durante naquele tempo. Portanto, você tem um período em que ainda possui as senhas menos seguras, mas o tempo é limitado e você também minimiza as interrupções para os usuários que fazem logon regularmente.
Sean Burton