-
비밀번호를 암호화 하자!프로젝트/게시판 프로젝트 2022. 5. 19. 19:04728x90
기존에는 그저 memberRequestDTO로 받아온 값을 그대로 저장했습니다.
memberService.join(memberRequestDTO.toEntity());
위의 방법은 유저의 비밀번호를 DB에 그대로 저장하게 됩니다.
이렇게 되면 DB를 관리하는 사람들이 유저의 비밀번호를 그대로 볼 수 있기 때문에 보안상으로 취약해집니다.
또한 법적으로도 개인정보에 대해 법률로 정하고 있고, 최소 기준도 제시하고 있습니다.
암호화란?
재화적 가치가 있는 데이터를 알아볼 수 없는 문자로 바꿈으로써 공격의 목적을 파괴하는 보안 솔루션입니다.
간단하게 어떤 정보를 누구도 알아볼 수 없도록 만들어 버립니다.
예를 들어 비밀번호 123456을 암호화해서 저장한다면 DB에는 fs32a3xzz0이 저장됩니다.
암호화는 어떻게 해야 할까?
여기서는 암호화라고 부르지만 사실 명확하게 부르자면 해싱입니다.
암호화는 양방향성을 가지는데 암호화를 하면 역으로 복호화도 가능합니다.
즉, 복호화하는 방법이 노출된다면 암호화의 의미가 퇴색됩니다.
해싱은 단방향성을 가지며 복호화가 불가능합니다.
만약 HashMap, HashSet을 써보셨다면 조금 더 이해하기 쉬울 텐데 해싱은 어떤 값을 넣으면 항상 똑같은 값으로 변환되는 것이 특징입니다.
즉, 비밀번호의 암호화는 해싱 함수를 사용하여 진행해야 합니다.
단방향 해시 함수
사실 위에서 봤던 그림이 바로 단방향 해시 함수를 잘 설명해줍니다.
123456이라는 비밀번호를 해시 함수에 넣으면 fs32a3xzz0이라는 값이 나오게 됩니다.
fs32a3xzz0는 복호화가 불가능하기 때문에 DB가 노출되더라도 문제가 적어집니다.
그러면 해시 함수를 사용하면 안전한가?
사실 해시 함수를 사용해도 문제가 있을 수 있습니다.
하나의 평문에는 언제나 같은 해쉬 값을 가지기 때문에 이점이 약점으로 작용할 수 있습니다.
모든 평문에 대해 해쉬값을 기록한 것을 레인보우 테이블이라 하며 이러한 레인보우 테이블을 이용하여 무차별적으로 로그인 시도를 하게 되면 비밀번호가 노출될 수도 있습니다.
예를 들어 MD5라는 해시 함수는 속도가 너무 빠른 탓에 1초에 56억 개의 해쉬 함수를 생성하고 대입할 수 있습니다.
즉, 현대 컴퓨터로 레인보우 테이블을 구성할 수 있게 되고 보안에 취약할 수 있습니다.
해결방법(SALT)
레인보우 테이블 문제를 해결하기 위해서는 SALT라는 개념이 활용됩니다.
해시 함수를 만들기 전에 평문 값에 SALT라는 랜덤 값을 추가로 넣어서 해쉬 함수를 만들어버립니다.
즉, 어떤 Salt를 사용했는지 공격하는 측에선 알 수 없기 때문에 레인보우 테이블을 만드는데 오래 걸리게 됩니다.
이러한 방법을 사용한 비밀번호 암호화 방법이 바로 BCrypt입니다.
코드로 예시를 들면 다음과 같습니다.
password = '1234' hashed_password = bcrypt.hashpw(password, bcrypt.gensalt()) print(hashed_password) # output :'$2a$12$6FOiv9dZY05vhTR2a9x4zO6IMFsFhWLG085AxYZSExuYHGMsAEHJe'
또한 여기서 더 강화하자면 암호화된 결괏값에 다시 한번 동일한 Salt를 적용하고 한번 더 결과 해시를 도출하는 방법을 반복할 수 있습니다.
다양한 암호화 종류
- scrypt
많은 메모리와 CPU를 사용합니다.
다른 암호화에 비해 많은 양의 메모리를 사용하도록 설계되었습니다.
보안에 아주 민감한 사용자들을 위한 백업 솔루션을 제공하는 Tarsnap에서 사용하고 있습니다.
- bcrypt
현재까지 사용 중인 가장 강력한 암호화입니다.
반복 횟수를 늘려 연산속도를 늦출 수 있기 때문에 연산 능력이 증가하더라도 완전 탐색 공격에 대비할 수 있습니다.
보안에 집착하기로 유명한 OpenBSD에서 기본 암호 인증 메커니즘으로 사용되고 있습니다.
90년대 후반부터 존재해 왔으며 안정적이고 안전한 것으로 입증되었습니다.
- PBKDF2
SHA와 같이 검증된 해시 함수를 사용하여 Salt를 적용한 후 해시 함수의 반복 횟수를 선택할 수 있습니다.
미국 정부 시스템에서도 사용합니다.
왜 Bcrypt가 많이 추천되는가?
Bcrypt가 가장 가성비가 좋기 때문이라고 생각합니다.
PBKDF2 같은 경우는 SHA 알고리즘을 이용하기 때문에 연산 속도가 빨라 GPU 이용한 공격에 취약합니다.
Scrypt는 hashing 하는데 더 많은 자원이 필요합니다.
즉, 서비스가 감당할 수 있는 자원에는 한계 있고 한정된 자원에서 최적의 효과를 내기 위해 보편적인 서비스에 bcrypt를 사용하는 것 같습니다.
bcrypt사용법
1. build.gradle에 의존성 추가(https://mvnrepository.com/artifact/org.mindrot/jbcrypt/0.4)
implementation 'org.mindrot:jbcrypt:0.4'
2. 비밀번호 해싱화
public void hashingPassword(){ this.password = BCrypt.hashpw(this.password, BCrypt.gensalt()); }
String hashpw(String password, String salt)
3. 로그인 기능에 비밀번호 검증 적용
@Override public boolean login(MemberLoginRequestDTO memberLoginRequestDTO) { Optional<Member> user = memberRepository.findByUserId(memberLoginRequestDTO.getUserId()); String userPassword = user.orElseThrow(() -> new IllegalArgumentException()) .getPassword(); return BCrypt.checkpw(memberLoginRequestDTO.getPassword(), userPassword); }
boolean checkpw(String plaintext, String hashed)
4. 테스트 코드 작성
@Test @DisplayName("회원 비밀번호 해싱화") public void memberPasswordHashingTest(){ //given String originPassword = "Test"; MemberRequestDTO memberRequestDTO = MemberRequestDTO.builder() .userId("Test") .name("Test") .nickName("Test") .password(originPassword) .phoneNumber("Test") .address(new Address("a1", "a2", "a3")) .build(); //when memberRequestDTO.hashingPassword(); //then Assertions.assertThat(originPassword).isNotEqualTo(memberRequestDTO.getPassword()); }
조금 더 디테일하게
Salt는 매번 Random으로 생성되는데 Bcrypt.checkPw는 어떻게 로그인 검증을 진행할 수 있을까요?
우리가 넘겨준 hashed는 다음과 같은 구조로 이루어져 있습니다.
여기서 내부적으로 salt를 가져와서 이를 다시 해싱시키고 hashed 해싱된 값을 비교하여 같으면 true를 return 하게 됩니다.
출처
https://www.youtube.com/watch?v=67UwxR3ts2E
https://velog.io/@matisse/bcrypt
https://st-lab.tistory.com/100
https://junho94.tistory.com/30
'프로젝트 > 게시판 프로젝트' 카테고리의 다른 글
JWT란? JWT 원리, 사용법 (0) 2022.05.24 Optional을 올바르게 활용해보기 (0) 2022.05.24 intellij 자동 포맷팅 시 줄 바꿈 처리 적용해주기 (0) 2022.05.19 로그인 기능을 만들어보자 (0) 2022.05.18 JPA로 Update를 해보자! (0) 2022.05.14