비밀번호 해싱과 암호화

비밀번호를 DB에 저장할 때 암호화해야 한다고 생각하기 쉽다.

하지만 정확하게는 암호화가 아니라 해싱(Hashing) 을 사용한다.

암호화는 원본 데이터를 다시 복호화해야 하는 경우에 사용한다.

Plaintext -> Encryption + Key -> Ciphertext
Ciphertext -> Decryption + Key -> Plaintext

비밀번호는 다시 원래 값으로 가져올 필요가 없다.

로그인에서 필요한 것은 사용자가 입력한 비밀번호 자체가 아니라, DB에 등록된 비밀번호와 같은 비밀번호인지 확인하는 것이기 때문이다.

그래서 비밀번호는 암호화하지 않고 해싱해서 저장한다.

Password -> Hashing -> Password Hash -> DB

DB에 저장되는 것은 password가 아니라 password_hash다.

CREATE TABLE users (
    user_id BIGINT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL
);

로그인할 때도 DB의 해시값을 복호화하는 과정은 없다.

사용자가 입력한 비밀번호를 해시 알고리즘으로 검증하고, 저장된 해시와 일치하는지만 확인한다.

Input Password -> Verify -> Stored Password Hash

여기서 SHA-256 같은 일반적인 해시 함수를 사용하면 되는 것은 아니다.

SHA-256은 계산 속도가 빠르다.

일반적인 데이터 무결성 검증에서는 장점이지만 비밀번호에서는 문제가 된다.

공격자가 DB의 해시값을 가져갔다면 가능한 비밀번호를 계속 해싱하면서 비교할 수 있기 때문이다.

password1 -> SHA-256
password2 -> SHA-256
password3 -> SHA-256
...

그래서 비밀번호에는 일부러 계산 비용을 높인 password hashing 알고리즘을 사용한다.

대표적으로 Argon2id, bcrypt, scrypt 등이 있다.

새로운 시스템이라면 Argon2id를 우선 고려할 수 있다.

Password -> Argon2id -> Password Hash

그런데 여기서 Salt라는 값이 하나 더 등장한다.

같은 비밀번호를 같은 방식으로 해싱하면 항상 같은 결과가 나온다.

hello123 -> Hash A
hello123 -> Hash A

사용자마다 같은 비밀번호를 사용하고 있다는 것을 해시만 보고 알 수 있게 된다.

그래서 해싱할 때 랜덤한 Salt를 함께 사용한다.

Password + Salt -> Argon2id -> Hash

사용자마다 Salt가 다르면 같은 비밀번호를 사용하더라도 결과가 달라진다.

User A
hello123 + Salt A -> Hash A

User B
hello123 + Salt B -> Hash B

Salt는 비밀번호를 복호화하기 위한 값이 아니다.

해싱 결과가 서로 달라지도록 만드는 값이다.

여기서 암호화에서 사용하는 IV와 헷갈릴 수 있다.

둘 다 랜덤한 값을 사용하는 경우가 있지만 역할은 다르다.

Encryption
Plaintext + Key + IV / Nonce -> Ciphertext
Password Hashing
Password + Salt -> Argon2id -> Hash

IV는 암호화 과정에서 사용하고, Salt는 비밀번호 해싱 과정에서 사용한다.

비밀번호 해싱에 IV가 필요한 것은 아니다.

그렇다면 비밀번호를 클라이언트에서 먼저 해싱해서 서버로 보내면 되는 것도 아니다.

일반적인 HTTPS 기반 인증에서는 클라이언트가 입력한 비밀번호를 HTTPS를 통해 서버로 전달하고, 서버에서 password hashing을 수행한다.

Client Password -> HTTPS / TLS -> Server -> Argon2id + Salt -> DB

여기서 HTTPS와 Argon2id는 서로 다른 문제를 해결한다.

HTTPS는 클라이언트와 서버 사이의 전송 과정을 보호한다.

Argon2id는 DB에 저장되는 비밀번호를 보호한다.

HTTPS / TLS -> 전송 보호
Argon2id    -> 저장 보호

Node.js에서는 argon2 패키지를 이용할 수 있다.

npm install argon2

회원가입에서는 비밀번호를 해싱한다.

import argon2 from 'argon2';

const passwordHash = await argon2.hash(password, {
    type: argon2.argon2id
});

await db.query(
    'INSERT INTO users (email, password_hash) VALUES (?, ?)',

[email, passwordHash]

);

로그인에서는 저장된 해시를 복호화하지 않는다.

const valid = await argon2.verify(
    user.password_hash,
    password
);

if (!valid) {
    throw new Error('Invalid password');
}

결국 비밀번호 처리 과정은 간단하다.

회원가입
Client Password -> HTTPS -> Server -> Argon2id + Salt -> DB Hash
로그인
Client Password -> HTTPS -> Server -> Verify -> DB Hash

암호화는 복호화가 필요한 데이터를 보호할 때 사용한다.

비밀번호는 복호화할 필요가 없다.

그래서 DB에는 원본 비밀번호가 아니라 해시값을 저장한다.

비밀번호 해싱의 목적은 비밀번호를 다시 복호화하는 것이 아니라, 원본 비밀번호를 DB에 저장하지 않는 것이다.

그리고 이때 HTTPS는 전송을 보호하고, Argon2id는 저장을 보호한다.

둘은 같은 역할을 하는 것이 아니다.