구현하게 된 이유

현재 API서버는 Django로 만들어진 서버를 이용해 사용자의 아이디와 패드워드를 저장하고 있었다. 하지만 Django API서버의 기능 전부를 Node.js로 전환을 하고 있었다. 그러기 위해서는 Node.js의 사용자 패스워드 저장방식과 패스워드 검증 방식이  Django와 같은 방식으로 구현해야했다. 만약 다른 방식으로 패스워드 저장과 검증을 하게된다면 DB에 저장되어 있는 기존의 사용자의 패스워드를 사용할 수 없기 때문이다. 오늘은 Django 패스워드 저장방식(PBKDF2)과 검증방식을 Node.js로 구현하고자 한다.

 

Django가 패스워드를 생성하는 방법

공식문서에 따르면 Django의 패스워드는 기본적으로 PBKDF2 알고리즘을 사용하여 아래와 같은 형태로 저장이 된다.

<algorithm>$<iterations>$<salt>$<hash>

네 가지의 값이 "$" 기준으로 나뉘어져 저장되어 있다. 실제 DB에 저장되어 있는 패스워드는 아래와 같았다.

pbkdf2_sha256$216000$<mysalt>$<hash>

즉 pbkdf2_sha256 알고리즘을 사용했고 216,000번 iteration 을 해서 패스워드를 생성한 것을 확인할 수 있다.
(별 다른 설정 없이 NIST의 권장사항을 잘 준수하여 프레임워크가 제공하는 것을 보니 너무 좋아보인다.) 

Django 코드

PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
    'django.contrib.auth.hashers.Argon2PasswordHasher',
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
    'django.contrib.auth.hashers.BCryptPasswordHasher',
]

알고리즘을 생성하는 PASSWORD_HASHERS 를 확인하면 PBKDF2PasswordHasher 를 사용한다. 참고로 Argon2PasswordHasher 는 2015 패스워드 해싱 대회에서 우승한 알고리즘이라고 한다.
그래서 지금 사용중인 Django API서버의 PBDKF2PasswordHasher 의 코드를 보면

class PBKDF2PasswordHasher(BasePasswordHasher):
    """
    Secure password hashing using the PBKDF2 algorithm (recommended)

    Configured to use PBKDF2 + HMAC + SHA256.
    The result is a 64 byte binary string.  Iterations may be changed
    safely but you must rename the algorithm if you change SHA256.
    """
    algorithm = "pbkdf2_sha256"
    iterations = 216000
    digest = hashlib.sha256

    def encode(self, password, salt, iterations=None):
        assert password is not None
        assert salt and '$' not in salt
        iterations = iterations or self.iterations
        hash = pbkdf2(password, salt, iterations, digest=self.digest)
        hash = base64.b64encode(hash).decode('ascii').strip()
        return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)

algorithm 은 pbkdf2_sha256을 사용하고 iterations 의 값은 216,000 번이다.
그리고 salt 는 PBKDF2PasswordHasher 에 정의되어 있지 않아서 부모 클래스인 BasePasswordHasher 를 확인하면

class BasePasswordHasher:
    """
    Abstract base class for password hashers

    When creating your own hasher, you need to override algorithm,
    verify(), encode() and safe_summary().

    PasswordHasher objects are immutable.
    """
    def salt(self):
        """
        Generate a cryptographically secure nonce salt in ASCII.
        """
        # 12 returns a 71-bit value, log_2((26+26+10)^12) =~ 71 bits
        return get_random_string(12)

salt 값은 random string 임을 확인할 수 있다.
즉, Django의 패스워드는 pbkdf2_sha256 알고리즘으로 랜덤으로 생성된 salt 값으로 216,000번 iterations 하여 hash 값을 생성하여 "pbkdf2_sha256$216000$<salt>$<hash>" 형태로 저장을 한다.

구현

위에서 분석한 내용으로 Node.js crypto 를 이용하여 구현하면 아래와 같다.

const crypto = require('crypto');

encodePBKDF2 = (password, algorithm = 'pbkdf2_sha256', salt = crypto.randomBytes(6).toString('hex'), iterations = 216000) => {
    const hash = crypto.pbkdf2Sync(password, salt, iterations, 32, 'sha256');
    return `${algorithm}$${iterations}$${salt}$${hash.toString('base64')}`;
}

console.log(encodePBKDF2('abcd')); 
//pbkdf2_sha256$216000$23272be7e460$u6MiO2RvIYzK1DmjbU+EYNVpnBu9l2xJGPpZej5j9XI=

Django와 같은 PBKDF2 알고리즘을 사용하여 같은 방식으로 출력하였다. 해당 값은 salt가 매번 바뀌기 때문에 출력 결과는 다르게 나올 것이다. 다음은 사용자가 입력한 비밀번호를 encodePBKDF2 하여 DB에 저장되어 있는 패스워드를 비교하는 로직이다.

const crypto = require("crypto");

encodePBKDF2 = (password, setting = { algorithm: 'pbkdf2_sha256', salt: crypto.randomBytes(6).toString('hex'), iterations: 216000 }) => {
    const hash = crypto.pbkdf2Sync(password, setting.salt, setting.iterations, 32, 'sha256');
    return `${setting.algorithm}$${setting.iterations}$${setting.salt}$${hash.toString('base64')}`;
}

decodePBKDF2 = (encoded) => {
    const [algorithm, iterations, salt, hash] = encoded.split('$'); //$ 단위 split
    return {
        algorithm,
        hash,
        iterations: parseInt(iterations, 10),
        salt,
    };
};

authPassword = (password, encoded) => {
    const decoded = decodePBKDF2(encoded);
    const encodedPassword = encodePBKDF2(password, decoded);
    return encoded === encodedPassword;
};

const encode = encodePBKDF2('abcd');
console.log(encode);
//pbkdf2_sha256$216000$576e4cc78a56$8KLWCYRfddcl3PpyiHQr+zWk+Zl6HlxP6dRWX59kY5I=

console.log(authPassword('abcd', encode)); //true
console.log(authPassword('abcde', encode)); //false

다음 글은 Django JWT를 Node.js에서 구현해보려고 한다.

'Node.js > crypto' 카테고리의 다른 글

[node.js] crypto RSA 공개키 알고리즘 구현 예제  (0) 2021.06.23