PHC 형식으로 비밀번호 해시 내보내기

Discourse의 비밀번호 저장 시스템에 대한 최신 세부 사항은 https://github.com/discourse/discourse/blob/main/docs/SECURITY.md에서 확인할 수 있습니다. 작성 시점 기준으로 우리는 600,000회 반복을 사용하는 PBKDF2-SHA256 알고리즘을 사용하고 있습니다.

일부 상황에서는 Discourse에서 해시된 모든 비밀번호를 내보내고 다른 시스템에 가져와야 할 수 있습니다. 예를 들어, Discourse의 내장 인증에서 사용자 지정 SSO 시스템으로 마이그레이션하는 경우입니다. 데이터베이스에서 원래 비밀번호를 추출하는 것은 불가능하므로, 대상 시스템은 동일한 매개변수로 동일한 해싱 알고리즘을 실행할 수 있어야 합니다.

비밀번호 데이터는 user_passwords 테이블에 저장되며, 여기에는 password_hash, password_salt, password_algorithm 열이 포함되어 있습니다. password_algorithm 열에는 전체 PHC 알고리즘 접두사(예: $pbkdf2-sha256$i=600000,l=32$)가 저장되며, 시간이 지남에 따라 반복 횟수가 증가한 경우 사용자별로 달라질 수 있습니다.

데이터 탐색기(data explorer)를 사용하여 정보를 컴퓨터가 읽을 수 있는 형식으로 내보낼 수 있습니다:

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

이렇게 하면 데이터를 Discourse의 네이티브 형식으로 내보냅니다. 솔트(salt)는 16진수(hex)로 인코딩되고, 비밀번호 해시도 16진수(hex)로 인코딩됩니다.

일부 외부 시스템은 비밀번호 해싱 함수의 출력을 알고리즘 간에 표현하기 위한 목적으로 설계된 PHC 문자열 형식을 지원합니다. pbkdf2-sha256의 경우, 이 문자열에는 알고리즘 유형, 반복 횟수, base64로 인코딩된 솔트 및 base64로 인코딩된 해시가 포함됩니다. 다행히도 Postgres는 단일 쿼리로 이 모든 것을 처리해 줄 수 있습니다.

각 Discourse 사용자를 위한 PHC 문자열을 생성하려면 다음과 같은 데이터 탐색기 쿼리를 사용할 수 있습니다:

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

Auth0을 사용하는 경우 대신 다음을 사용해야 합니다:

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개의 좋아요

여기 한 가지 참고할 점이 있습니다. (친절한 Auth0 팀의 도움으로) 예제 쿼리를 수정하여 Auth0에 사용자 비밀번호를 가져올 때 유효한 PHC 문자열을 생성하도록 했습니다.

또한 이 줄을 변경하여 소금을 base64로 인코딩했습니다.

salt,

다음과 같이 변경했습니다.

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

자세한 내용은 여기(Auth0로 Discourse 사용자 및 비밀번호를 가져오는 단계별 가이드 포함)를 참고하세요.

https://community.auth0.com/t/bulk-user-import-custom-password-hash-issue/47408

1개의 좋아요

@angus 감사합니다. 이 내용은 흥미롭습니다. OP의 쿼리를 사용해 고객이 Auth0에 사용자를 성공적으로 가져온 적이 있었거든요. 그들의 가져오기 프로세스에 어떤 변화가 있었는지 궁금합니다. 기억이 정확하다면, Auth0에 PHC 문자열을 가져오는 기능은 11월 당시 매우 새로운 것이었거든요 :thinking:

3개의 좋아요

네, 저도 그 부분에 대해 궁금해하고 있었으며 같은 생각을 했었습니다.

또한 PHC 명세에 사용된 표현이 정확히 무엇을 의미하는지 확신이 서지 않았습니다. 솔트가 B64 인코딩되어야 하는지, 아니면 그렇지 않은지 불분명합니다.

솔트는 [a-zA-Z0-9/+.-] 범위의 문자(소문자, 대문자, 숫자, /, +, . 및 -)로 구성된 문자열입니다. 함수 명세는 유효한 솔트 값의 집합과 이 필드의 최대 길이를 정의해야 합니다(MUST). 임의의 바이너리 솔트로 작동하는 함수는 해당 필드가 정의된 범위 또는 범위 집합 내의 길이를 가진 바이너리 값의 B64 인코딩임을 정의해야 합니다(SHOULD).

2개의 좋아요

주제에서 약간 벗어난 질문이지만, Auth0에서 Discourse로 비밀번호 해시를 가져온 경험이 있는 분이 계신가요? 이 마이그레이션을 진행하려고 하는데, 도움이 되면 좋겠습니다. 저는 Auth0 유료 고객이 아니어서, 비밀번호 해시 내보내기를 위해 비용을 지불하기 전에 이 작업이 가능한지 먼저 알고 싶었습니다.

감사합니다.

Discourse 코어에서는 비밀번호 가져오기를 지원하지 않지만, 이 서드파티 플러그인의 수정된 버전을 사용하면 가능할 수 있습니다:

4개의 좋아요

감사합니다! GitHub에서 저장소를 찾았지만, 여기 메타에서는 해당 주제를 찾지 못했습니다.

1개의 좋아요

Hey @angus, 여기서 정리 작업을 하고 있어요.

OP 코드 블록이 다음과 같이 읽혀야 한다는 게 사실인가요?

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

그리고 다음과 같은 내용을 포함해야 하나요:

Auth0로 Discourse 비밀번호를 가져오는 방법에 대한 자세한 정보는 https://community.auth0.com/t/bulk-user-import-custom-password-hash-issue/47408 을 참고하세요.

Auth0에서 Discourse로 데이터를 이동하는 경우, 다음 링크가 도움이 될 수 있습니다: Migrated password hashes support.

3개의 좋아요

네, 괜찮아 보입니다.

각 가져오기(import)에 대해 특별한 주의가 필요하다는 내용을 추가하는 것이 좋을 수 있습니다. 사용 사례에 따라 처리되는 사용자 데이터가 달라지기 때문에, 이 쿼리를 단순히 복사/붙여넣기만 하면 안 됩니다.

또한 PHC에서 비밀번호를 내보내는 것은 반드시 Auth0에만 국한되지 않으므로, 해당 부분을 단순히 "예시"라고 언급하는 것이 더 적절할 수 있습니다.

저가 내보내기에 사용한 전체 쿼리는 다음과 같습니다.

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개의 좋아요