python - hash password/비밀번호 해시화/hashlib/bcrypt

권수민·2023년 8월 23일
2

👉 비밀번호 해시화가 무엇인가?

비밀번호 해싱은 보안상의 이유로 비밀번호를 원래의 값에서 변경하여 저장하는 기법입니다. 비밀번호 해싱의 주요 목적은 실제 비밀번호를 노출 없이 안전하게 저장하는 것입니다.

*해싱(hashing):

원본 문자열을 알아볼 수 없는 난해한 문자열로 정의하는 방법으로, 해시값을 조사하여 데이터 변조 여부를 확인하는 것이 주된 목적

한마디로 디코딩화 시킬 수 없는 단방향 암호화를 만드는 것이라고 간단하게 설명할 수 있다. 이 해시값으로는 원본을 추출할 수 없지만 동일한 입력값에 대해서는 항상 같은 해시 값을 출력하고, 아주 작은 변화에도 완전히 다른 해시 값을 출력하는 특성이 있다.

👉 특징

  1. 원래 데이터 복원 불가:
    해시 함수는 일방향 함수입니다.
    즉, 해시 값을 가지고 원래의 입력값 (여기서는 비밀번호)을 찾아내거나 복원하는 것이 매우 어렵습니다.

  2. 유일성:
    동일한 비밀번호는 항상 같은 해시 값을 생성하지만,
    약간의 변경 (예를 들어 하나의 문자 변경)도 완전히 다른 해시 값을 생성합니다.

  3. 속도:
    해시 함수는 빠르게 실행되어야 하지만, 비밀번호 해싱에 사용되는 해시 함수는 공격자가 대량의 비밀번호를 빠르게 시도하는 것을 어렵게 하기 위해 의도적으로 느리게 설계될 수 있습니다.
    이를 위해 여러 번 해싱하는 방법 등이 사용됩니다.

  4. 솔트 추가:
    '솔트'라는 임의의 데이터를 비밀번호에 추가하여 해싱하는 것은, 동일한 비밀번호에 대해서도 다양한 해시 값을 생성하게 합니다.
    이는 무차별 대입 공격 (brute force attack)이나 레인보우 테이블 (rainbow table) 공격 등을 방지하는데 도움을 줍니다.

👉 솔트 기능

  1. 동일한 비밀번호에 대한 다양한 해시 생성: 솔트 없이 동일한 비밀번호는 항상 동일한 해시 값을 생성합니다. 그러나 솔트를 추가하면 같은 비밀번호라도 각 사용자마다 다른 해시 값을 가질 수 있습니다.

  2. 레인보우 테이블 대응: 해커들은 레인보우 테이블이라는 사전 계산된 해시 값을 테이블로 저장하여 사용합니다. 이 테이블을 사용하면 해시된 비밀번호를 빠르게 원래 비밀번호로 변환(역해싱)할 수 있습니다. 솔트를 사용하면 이러한 레인보우 테이블 공격을 매우 비효율적으로 만듭니다.

  3. 대량의 비밀번호 시도 방지: 솔트는 해커가 데이터베이스에서 획득한 해시된 비밀번호들에 대해 일괄적인 공격을 어렵게 만듭니다.

👉 그렇다면 비번확인할때 램덤하게 받은 솔트값은 어떻게 확인되나?

사용자가 처음 회원 가입 시 비밀번호를 설정하면:

랜덤한 솔트 값을 생성합니다.
이 솔트와 비밀번호를 함께 결합하여 해싱합니다.
해싱된 비밀번호와 솔트 값을 데이터베이스에 함께 저장합니다.

사용자가 로그인 시 비밀번호를 입력하면:

데이터베이스에서 해당 사용자의 솔트 값을 가져옵니다.
사용자가 입력한 비밀번호와 저장된 솔트 값을 함께 결합하여 해싱합니다.
위의 결과로 나온 해싱된 비밀번호가 데이터베이스에 저장된 해싱된 비밀번호와 일치하는지 확인합니다.

이런 절차를 따르기 위해 주 클래스에 솔트 값을 인스턴스 변수로 저장해야 합니다.

class Member:

    def __init__(self, name, username, password):
        self.name = name
        self.username = username
        self.salt = os.urandom(16)  # 랜덤 솔트 생성
        self.password = self.hash_password(password, self.salt)
        self.display()

👉 사용법 예시

1번째 방법

비번을 받고 해시화시켜 추후에 비번을 확인하는 코드

import hashlib
import os

class PasswordManager:

    def __init__(self):
        self.salt = os.urandom(16)
        self.hashed_password = None

    def hash_password(self, password):
        """Hash a password."""
        password_salt_combi = password.encode('utf-8') + self.salt
        return hashlib.sha256(password_salt_combi).hexdigest()

    def store_password(self, password):
        """Store a hashed version of the password."""
        self.hashed_password = self.hash_password(password)
        print("Password has been stored!")

    def verify_password(self, input_password):
        """Verify the input password against the stored hashed version."""
        input_hashed = self.hash_password(input_password)
        return input_hashed == self.hashed_password

# Example usage:
pm = PasswordManager()

# 1. Get a password from user and store its hashed version
password = input("Please enter a password to store: ")
pm.store_password(password)

# 2. Ask the user for the password again and verify it
check_password = input("Please enter your password again for verification: ")

if pm.verify_password(check_password):
    print("Password is correct!")
else:
    print("Password is incorrect!")

👉 이 함수에 전달하는 문자열은 바이트 문자열이어야 하므로 .encode('utf-8')을 이용하여 유니코드 문자열을 UTF-8 형식의 바이트 문자열로 변환한다.
이 것은 <바이트 문자열 (Byte String): 바이트 데이터를 저장합니다. 바이너리 데이터 처리, 파일 입출력, 네트워크 통신 등에서 주로 사용됩니다.> 과 같다. 다만, 차이점은 UTF-8인코딩 방식의 바이트 문자열로 변환한다는것뿐

b = b"Hello, world!"
print(type(b))  # <class 'bytes'>

s = "Hello, world!"
byte_version = s.encode('utf-8')

👉문자열을 해싱한 다음에는 digest() 또는 hexdigest() 함수를 사용하여 해싱한 문자열을 얻을 수 있다. digest()는 해싱한 바이트 문자열을 반환하고 hexgigest()는 바이트 문자열을 16진수로 변환한 문자열을 반환한다

 m.digest()
 
b"\x9d\x05'^\xcaK\xf8\xf2\x02w!\xce?\xd7\xe6\xf0\xaa\x06\xdc\xc3\x81 N\xd8G[\xe3B\\,S\x84"


m.hexdigest()

'9d05275eca4bf8f2027721ce3fd7e6f0aa06dcc381204ed8475be3425c2c5384'

2번쨰 방법

import hashlib
import os


def check_passwd():
    if os.path.exists('passwd.txt'):
        before_passwd = input('기존 비밀번호를 입력하세요:')
        m = hashlib.sha256()
        m.update(before_passwd.encode('utf-8'))
        with open('passwd.txt', 'r') as f:
            return m.hexdigest() == f.read()  => true 
    else:
        return True


if check_passwd(): #true : 새롭게 패스워드 생성
    passwd = input('새로운 비밀번호를 입력하세요:')
    with open('passwd.txt', 'w') as f:
        m = hashlib.sha256()
        m.update(passwd.encode('utf-8'))
        f.write(m.hexdigest())
else:
    print("비밀번호가 일치하지 않습니다.")

👉check_passwd() 함수는 작성한 비밀번호 파일이 없거나 기존 비밀번호와 일치할 때 True를 반환한다. 이 함수가 True를 반환할 때만 새로운 비밀번호를 생성하여 파일로 저장한다.

이미 저장한 비밀번호 파일이 있을 때는 사용자가 입력한 비밀번호와 기존 비밀번호가 일치하는지 비교하고자 사용자가 입력한 값을 해싱하여 저장한 해시값과 비교했다.

여기서 주의 깊게 봐야 할 부분은비밀번호 일치 여부를 검증하고자 사용자로부터 입력받은 이전 비밀번호를 마찬가지 방법으로 해싱하고 나서 파일에 저장한 값과 비교했다는 점이다. 해싱한 문자열은 복구할 수 없으므로 항상 이런 방법으로 검증해야 한다는 점을 꼭 기억하자.

자세히 풀이를 하자면:

먼저 update()함수 관련하여 알아보겠다.

👉 update(바이트문자열)

update() 함수는 파이썬의 hashlib 모듈 내에서 해시 객체에 데이터를 추가하는 역할을 합니다. 이 함수는 여러 번 호출될 수 있으며, 이렇게 해서 주어진 모든 데이터 조각을 하나의 데이터 스트림으로 취급하게 됩니다.

update() 함수의 주요 사용 사례는 큰 데이터 덩어리나 파일을 처리할 때입니다. 예를 들어, 대용량의 파일을 한 번에 로드하여 메모리에 저장하는 대신, 작은 조각으로 나누어 순차적으로 읽어 들이며 해시를 계산할 때 update()를 사용합니다. (주로 비밀번호보단 양의 많은 텍스트를 처리할때 많이 사용)

간단한 예제로 확인해보겠습니다:

import hashlib

#데이터를 두 부분으로 나누어서 해시 계산
m = hashlib.sha256()
m.update(b"Hello, ")
m.update(b"world!")
print(m.hexdigest())  # 두 데이터 조각을 합쳐서 해시한 결과

#전체 데이터로 한번에 해시 계산
m2 = hashlib.sha256()
m2.update(b"Hello, world!")
print(m2.hexdigest())  # 전체 데이터를 한번에 해시한 결과

위의 두 해시 값은 동일하다.
즉, update() 함수는 주어진 데이터의 순서대로 누적하여 해시를 계산합니다.

코드풀이
👉 hashlib.sha256()로 생성한 객체 m에 해싱할 문자열을 인수로 update() 함수를 호출하면 문자열이 해싱된다. 이 함수에 전달하는 문자열은 바이트 문자열이어야 하므로 .encode('utf-8')을 이용하여 유니코드 문자열을 UTF-8 형식의 바이트 문자열로 변환한다. 해싱할 문자열을 추가하고 싶으면 추가할 문자열과 함께 update() 함수를 추가로 호출한다.
문자열을 해싱한 다음에는 digest() 또는 hexdigest() 함수를 사용하여 해싱한 문자열을 얻을 수 있다. digest()는 해싱한 바이트 문자열을 반환하고 hexgigest()는 바이트 문자열을 16진수로 변환한 문자열을 반환한다

👉 hashlib

hashlib은 MD5, SHA256 등의 해싱 알고리즘으로 문자열을 해싱(hashing)할 때 사용하는 모듈이다. => 라이브러리로도 칭한다. import hashlib 해주면 된다.

SHA-256은 Secure Hash Algorithm (SHA) 패밀리의 일부로, 해시값의 길이가 256 비트인 암호화 해시 함수입니다. SHA-256은 데이터의 무결성 및 원본 인증에 널리 사용되며, 특히 비트코인 블록체인과 같은 애플리케이션에서 중요한 역할을 한다.

hashlib 모듈은 SHA-256 외에도 다양한 해싱 알고리즘, 예를 들면 SHA-1, SHA-512, MD5 등을 지원합니다.

👉 bcrypt

bcrypt란 브루스 슈나이어가 설계한 키(key) 방식의 대칭형 블록 암호에 기반을 둔 암호화 해시 함수다.
pip install bycrpt 해주고 import 시켜줘야한다.

profile
초보개발자

0개의 댓글