Exporte hashes de senha no formato PHC

Os detalhes atuais sobre o sistema de armazenamento de senhas do Discourse podem ser encontrados em discourse/docs/SECURITY.md at main · discourse/discourse · GitHub. No momento da escrita deste texto, utilizamos PBKDF2-SHA256 com 600.000 iterações.

Em algumas situações, você pode querer exportar todas as senhas hash do Discourse e importá-las para outro sistema. Por exemplo, você pode estar migrando da autenticação nativa do Discourse para um sistema SSO personalizado. Lembre-se: é impossível extrair as senhas originais do banco de dados, portanto, seu sistema de destino precisa ser capaz de executar o mesmo algoritmo de hash com os mesmos parâmetros.

Os dados de senha são armazenados na tabela user_passwords, que contém as colunas password_hash, password_salt e password_algorithm. A coluna password_algorithm armazena o prefixo completo do algoritmo PHC (por exemplo, $pbkdf2-sha256$i=600000,l=32$), que pode variar por usuário se a contagem de iterações tiver sido aumentada ao longo do tempo.

Você pode usar o Data Explorer para exportar as informações em um formato legível por computador:

SELECT users.id, users.username, up.password_salt, up.password_hash, up.password_algorithm
FROM users
INNER JOIN user_passwords up ON users.id = up.user_id
WHERE users.id > 0

Isso exportará os dados no formato nativo do Discourse. O salt está codificado em hexadecimal e o hash da senha também está codificado em hexadecimal.

Alguns sistemas externos suportam o formato de string PHC, que visa representar a saída de uma função de hash de senha de forma independente do algoritmo. Para o pbkdf2-sha256, essa string contém o tipo de algoritmo, o número de iterações, o salt codificado em base64 e o hash codificado em base64. Felizmente, o Postgres pode lidar com tudo isso para nós em uma única consulta.

Para gerar strings PHC para cada usuário do Discourse, você pode usar uma consulta no Data Explorer como esta:

SELECT users.id, users.username,
  concat(
    up.password_algorithm,
    replace(encode(up.password_salt::bytea, 'base64'), '=', ''),
    '$',
    replace(encode(decode(up.password_hash, 'hex'), 'base64'), '=', '')
  ) as phc
FROM users
INNER JOIN user_passwords up ON users.id = up.user_id
WHERE users.id > 0

Se você usa o Auth0, então use esta consulta em vez da anterior:

SELECT
    user_emails.email,
    users.active as email_verified,
    concat(
        up.password_algorithm,
        replace(encode(up.password_salt::bytea, 'base64'), '=', ''),
        '$',
        replace(encode(decode(up.password_hash, 'hex'), 'base64'), '=', '')
    ) as password_hash
FROM users
INNER JOIN user_passwords up ON users.id = up.user_id
INNER JOIN user_emails 
ON users.id = user_emails.user_id 
AND user_emails.primary IS TRUE
AND users.id > 0
13 curtidas

Apenas uma observação aqui: eu (com alguma ajuda da equipe amigável do Auth0) acabei ajustando a consulta de exemplo para gerar strings PHC válidas para importar senhas de usuários no Auth0.

Também codifiquei o salt como base64 alterando esta linha

salt,

para

replace(encode(users.salt::bytea, 'base64'), '=', ''),

Veja mais aqui (incluindo um passo a passo sobre como importar usuários do Discourse e suas senhas no Auth0).

1 curtida

Obrigado, @angus - isso é interessante porque tivemos um cliente que usou a consulta no OP para importar usuários com sucesso para o Auth0. Estou me perguntando se algo mudou no processo de importação deles - se não me engano, a capacidade de importar strings PHC para o Auth0 era muito nova em novembro :thinking:

3 curtidas

É, eu estava me perguntando sobre isso e pensei o mesmo.

Também não tinha certeza quanto à linguagem na especificação do PHC. Não sei se isso significa que o salt deve ou não ser codificado em B64.

O salt consiste em uma sequência de caracteres em: [a-zA-Z0-9/+.-] (letras minúsculas, letras maiúsculas, dígitos, /, +, . e -). A especificação da função DEVE definir o conjunto de valores válidos para o salt e um comprimento máximo para esse campo. Funções que operam com salts binários arbitrários DEVEM definir esse campo como a codificação B64 de um valor binário cujo comprimento caia em um intervalo definido ou em um conjunto de intervalos.

2 curtidas

Desculpe-me por fugir do assunto, mas alguém já importou hashes de senha do Auth0 para o Discourse? Estou pensando em fazer essa migração, então qualquer ajuda seria muito apreciada. Não sou um cliente pago do Auth0, então só queria saber se isso é viável antes de pagar pela exportação dos hashes de senha.

Obrigado.

A importação de senhas não é suportada pelo núcleo do Discourse, embora possa ser possível usando uma versão adaptada deste plugin de terceiros:

4 curtidas

Obrigado! Encontrei o repositório no GitHub, mas não o tópico aqui no meta.

1 curtida

Olá @angus, estamos organizando as coisas por aqui.

É verdade que o bloco de código do OP deve ler:

SELECT id, username,
  concat(
    '$pbkdf2-sha256$i=64000,l=32$',
    replace(encode(users.salt::bytea, 'base64'), '=', ''),
    '$',
    replace(encode(decode(password_hash, 'hex'), 'base64'), '=', '')
  ) as phc
FROM users

e incluir algo como:

Para mais informações sobre a importação de senhas do Discourse para o Auth0, veja Bulk User Import Custom Password Hash Issue - Auth0 Community.

Para mover dados do Auth0 para o Discourse, isso pode ajudar: Migrated password hashes support.

3 curtidas

Sim, parece bom.

Talvez você queira adicionar algo sobre a necessidade de atenção específica para cada importação, já que os dados do usuário tratados variarão dependendo do caso de uso, ou seja, não basta apenas copiar e colar essas consultas.

Além disso, exportar senhas no PHC não é necessariamente apenas para o Auth0, então talvez isso deva ser referido apenas como um “exemplo”.

A consulta completa que usei para minha exportação foi

SELECT
    user_emails.email,
    users.active as email_verified,
    concat(
        '$pbkdf2-sha256$i=64000,l=32$',
        replace(encode(users.salt::bytea, 'base64'), '=', ''),
        '$',
        replace(encode(decode(users.password_hash, 'hex'), 'base64'), '=', '')
    ) as password_hash
FROM users
INNER JOIN user_emails 
ON users.id = user_emails.user_id 
AND user_emails.primary IS TRUE
AND users.id > 0
2 curtidas