현업에서 개발 경력이 2년이나 되어가는데 지금까지 비밀번호 암호화 기법을 제대로 공부해보지도 않고 사용해 왔다.
지금까지 입사한 회사들에서는 이미 비밀번호 암호화가 잘 구현되어 있어 다른 업무들을 처리하느라 평소 더욱 관심을 가지지 못한 것 같다.
이번에 입사한 회사에서는 외주로 제작된 프로젝트를 다시 내제화하는 작업을 진행 중이라 처음부터 끝까지 개발을 진행하고 있는데
이참에 비밀번호 암호화 방식을 제대로 알아보고 사용하자는 생각이 들어 공부 후 이렇게 기록을 남긴다.
단방향 암호화 방식
비밀번호를 암호화하기에는 어떤 암호화 방식이 적합할까?
학부 때 배운 암호화 방식을 생각해 보면 종류는 크게 아래 2가지가 있다.
- 양방향 암호화 방식
- 단방향 암호화 방식
위 2개에 대해 간단히 설명하면
양방향 암호화 방식은 암호화된 다이제스트(digest)가 있을 때 클라이언트와 서버 모두 이 다이제스트를 복호화할 수 있는 방법을 가지고 있는 방식이고
단방향 암호화 방식은 한 번 암호화된 다이제스트를 다시 원본으로 복호화하는 것이 불가능한 방식.
즉, 원본을 가지고 암호화만 가능하고 다시 복호화는 하기는 힘든 방식이다.
따라서 해커가 우리의 DB에서 암호화된 비밀번호 row들을 탈취해 갔다 가정했을 때
원본으로 복호화가 가능하다면 서비스 운영이 힘들어질 만큼 매우 심각한 이슈가 되기 때문에
암호화된 다이제스트를 다시 원본으로 변환하기 힘든 단방향 암호화 방식을 사용하는 것이 유리하다.
그렇다면 그냥 단방향 암호화 방식을 사용하면 보안적으로 안전할까??
모든 것을 막는 무적의 방패는 없듯이 그렇지 않다.
단방향 암호화 알고리즘은 대표적으로 해시 알고리즘을 통해 구현되는데
해시 알고리즘은 수학적인 연산을 통해 데이터를 최종 사용자가 원문을 추정하기 힘든 더 작고,
뒤섞인 조각으로 나누어 최종적으로 암호화된 다이제스트를 생성한다.
이는 암호화된 다이제스트로 원본을 알아내기 어렵다는 특징이 있지만
반대로 원본을 알고 있다면 이를 다시 암호화된 다이제스트로 만들어 낼 수 있다는 특징도 있다.
해커들은 이런 점을 이용해서 레인보우 공격을 한다.
레인보우 공격이란 전처리된 다이제스트를 최대한 많이 확보해 저장해 두고 (이를 레인보우 테이블이라 부른다)
전처리된 다이제스트를 해싱한 것과 비교해 원본을 찾아내는 공격 방법이다.
해시 알고리즘은 본래 암호화에 목적을 둔 알고리즘이 아니라 짧은 시간 내에 데이터를 검색하기 위해 설계된 알고리즘이기 때문에
해커는 해시 알고리즘의 매우 빠른 처리 속도를 이용해 일반적인 장비를 가지고도 1초당 평균 56억 개의 다이제스트를 대입해 비교할 수 있다. 이는 레인보우 테이블에 다이제스트가 많으면 많을수록 우리의 암호화된 비밀번호의 원본을 알아낼 가능성이 커진다.
따라서 실제 현업에서는 단방향 알고리즘에다가 여러 추가적인 기법을 적용해서 비밀번호 암호화를 구현한다.
솔팅 (Salting)
salt를 번역하면 소금인데 요리를 할 때 간이 부족할 때 소금을 첨가하듯
단방향 암호화에도 보안적으로 부족한 부분을 보완하기 위해 salt를 첨가한다고 생각하면 쉽다.
( 실제로 어원이 위에서 나온 건지는 모르겠지만 이해하기 쉽다 )
솔팅 방법은 비밀번호를 해시 함수에 넣기 전에 salt라는 임의의 문자열을 붙여서 암호화된 다이제스트를 생성한다.
그러면 원본 비밀번호가 쉽게 만들어졌더라도 예측할 수 없는 문자열과 함께 해싱되기 때문에
레인보우 테이블에 아무리 많은 다이제스트가 있더라도 어느 정도 예측하기 어렵게 만들 수 있다.
원본 비밀번호와 암호화된 비밀번호를 검증하기 위해선 암호화된 비밀번호와 salt 값을 매핑해 두었다가
검증이 필요한 시점에 원본에 salt를 다시 적용해서 암호화된 비밀번호와 일치하는지 비교한다.
키 스트레칭 (Key Stretching)
키 스트레칭이란 이미 암호화된 다이제스트를 또 해싱 함수에 적용하여 또 다른 암호화된 다이제스트를 만들어내는 방법이다.
이는 정확히 해싱 함수를 적용했는지 알아야만 원본 비밀번호와 암호화된 비밀번호를 검증할 수 있다.
또한 키 스트레칭을 많이 적용하여 다이제스트를 생성하는 시간을 늘리면 무차별 대입 검색 공격(Brute Force Attach)을 어느 정도 방지할 수 있다는 효과도 있다.
하지만 너무 과도한 해싱 함수의 반복은 성능 저하를 초래할 수 있으니 주의해야 한다.
그럼 우린 솔팅 기법도 알고 키 스트레칭 기법도 알았으니 이를 적용해 비밀번호 암호화를 수행할 수 있다.
하지만 실제로는 검증된 비밀번호 암호화 알고리즘을 사용하는 것이 훨씬 보안에 유리하다.
검증된 비밀번호 암호화 알고리즘들은 오늘날까지의 모든 보안 이슈들을 감당해 왔기 때문에 더욱 견고하며 안전하다.
뛰어난 보안전문가가 아니라면 비밀번호 암호화 알고리즘을 직접 구현하는 건 권장되지 않는다.
그럼 우리가 사용할 수 있는 검증된 비밀번호 알고리즘들을 알아보자.
1. PBKDF2
pbkdf2는 "Password-Based Key Derivation Function2"의 약자로 비밀번호를 기반으로 한 키 파생 함수이다.
이는 안전한 비밀번호 저장과 키 생성에 주로 사용되며, 주요 특징 중 하나는 반복적인 해시 작업을 수행하여 공격 기법에 대응한다는 점이다.
Python에서 이를 사용하는 예시를 간단히 들면 아래와 같다.
import hashlib
from Crypto.Protocol.KDF import PBKDF2
password = "user_password"
salt = os.urandom(16) # 무작위 솔트 생성
iterations = 100000 # 반복 횟수
dk_len = 32 # 생성되는 키의 길이
# PBKDF2 함수 호출
key = PBKDF2(password, salt, dkLen=dk_len, count=iterations, prf=lambda p, s: hmac.new(p, s, hashlib.sha256).digest())
PBKDF2는 단순하면서도 매우 강력한 비밀번호 암호화 알고리즘으로 아직까지도 많이 사용되는 알고리즘이다.
Third-Party 라이브러리를 사용하는 것을 원치 않는다면 pbkdf2는 훌륭한 선택지이다.
2. Bcrypt
Bcrypt는 "Blowfish Crypt)의 약자로 PBKDF2보다 미래에 더 경쟁력 있다고 평가되며 현재까지 사용되는 가장 강력한 해시 메커니즘 중 하나이다.
bcrypt에서는 cost factor라는 인자를 통해 해싱 작업을 조절하여 비밀번호 암호화의 복잡도를 늘리거나 줄일 수 있다.
이 cost factor 인자를 조정하는 것만으로 시스템 보안성을 높일 수 있지만 비밀번호 입력 값을 72 bytees character로 제한하고 있다는 제약이 있다.
아래는 Python에서 bcrypt를 사용하는 간단한 예시이다.
import bcrypt
password = "user_password"
# 솔트 생성
salt = bcrypt.gensalt()
# 비밀번호와 솔트를 이용한 해시 생성
hashed_password = bcrypt.hashpw(password.encode('utf-8'), salt)
3. Scrypt
scrypt는 위에서 소개한 알고리즘들보다 상대적으로 최근에 공개되었으며 다이제스트를 생성할 때 메모리 오버헤드를 갖도록 설계되었다.
따라서 무차별 대입 공격에 병렬 처리를 이용하기 힘들어 효과적으로 방어할 수 있다는 장점이 있다.
Scrypt는 PBKDF2보다 더 안전하다고 평가되며 bcrypt보다 더 경쟁력 있다고도 평가된다.
아래는 Python에서 Scrypt를 사용하는 예시이다.
import hashlib
import scrypt
password = "user_password"
salt = os.urandom(16) # 무작위 솔트 생성
N = 2**14 # cost parameter, 높을수록 연산이 늘어남
r = 8
p = 1
# scrypt 함수 호출
hash_result = scrypt.hash(password, salt, N=N, r=r, p=p, buflen=64)
참고자료
https://d2.naver.com/helloworld/318732
https://www.appsealing.com/kr/%ED%95%B4%EC%8B%B1-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98/
'개발공부' 카테고리의 다른 글
동시성 이슈가 발생하는 이유 (0) | 2024.06.26 |
---|---|
Python List 내부 뜯어보기 (0) | 2024.02.18 |
Python에서 N+1을 해결하는 방법 (SQLAlchemy) (0) | 2024.01.28 |
Debezium이란 무엇인가? (0) | 2023.12.21 |
Kafka란 무엇인가? (1) | 2023.12.21 |