[Spring] 비밀번호 암호화에 대한 고찰

600g (Kim Dong Geun)·2020년 6월 9일
4

그냥 쓰는 글이다 😂

로그인 서버를 제작하다보면 문득 드는 생각이 있다.
유저의 비밀번호를 어떻게 저장할 것인가

내가 비밀번호를 password로 보냈다고 해도 디비에 그대로 password로 저장하면 안될것이다.
아니 내가 한다고 해도 현행법상 DB에 그대로 비밀번호를 넣어서 서비스하면 불법으로 걸릴 것이다.

그래서 비밀번호를 암호화하는데 어떻게 하면 괜찮을까 라는 생각이 들었다.

비밀번호 암호화 - 해쉬 함수


이미지 출처 : https://needjarvis.tistory.com/239

해쉬함수는 단방향 암호화 방식이다.
(아니 엄연히 따지면 해쉬함수는 무결성 체크를 위해 나온 것이라한다😅)

비밀번호를 암호화하는데 있어 해쉬 함수를 사용하는 것은 다음과 같은 이유가 있다.

  • 복호화가 어렵다.
  • 비용이 적게 든다 (RSA에 비해)

뭐 위와 같은 이유로 사용되는 암호화 알고리즘이 여럿 있다.

해쉬함수는 다음과 같은 특성이 있다.

하나의 평문에는 언제나 같은 해쉬 함수를 내뱉는다.

무결성을 체크하기 위해서는 굉장히 좋은 원리이다.
(파일의 일부가 변경되거나 누가 파일을 수정하면 파일의 해쉬값이 달라지기 때문이다.)

이것이 암호화에서 해쉬함수의 약점으로 존재하게 된다.
모든 비밀번호에 대한 해쉬값을 테이블로 만들어서 무차별적으로 대입 공격을 해버린다면?

이렇게 모든 평문에 대해 해쉬값을 기록한 것을 레인보우 테이블이라 하고,
레인보우 테이블을 이용하여 무차별적인 로그인 시도를 하는 것을 레인보우 테이블 어택이라한다.

즉 레인보우 테이블 어택을 하면 시간이 걸릴뿐이지, 언젠가는 뚫리게 된다.

P.s) MD5의 예를 들어보자
MD5는 속도가 너무 빠른 탓에 1초에 56억개의 해쉬함수를 생성하고 대입할 수 있다.
즉, 현대 컴퓨터로 레인보우 테이블을 구성할 수 있다. 라는 것이다.
(실제로 MD5는 파일의 무결성 검사에서만 사용된다.)

다 된 해쉬함수에 소금치기 (Salting)

해쉬 함수를 만들기 전에 평문값에 SALT라는 랜덤 값을 추가로 넣어서 해쉬함수를 만들겠다는 것이다.

그럼 기존의 비밀번호와는 좀 더 복잡한 형태의 해쉬함수가 탄생을 해버리게 된다.
즉, 어떤 Salt를 사용했는지 공격하는 측에선 알 수 없기 때문에, 레인보우 테이블을 만드는데 오래 걸린다.

이런 해쉬함수에 Salt를 치게 만드는 방법은 PDKDF2와 BCrypt가 존재한다.

PBKDF2와 BCRPYT가 사용된다.
https://d2.naver.com/helloworld/318732 여기에 설명이 정말 잘 나와있음

간략하게 이야기 해보자면, PBKDF2는

PBKDF2

Salting을 하고 해쉬함수 생성하고 결과가 나오면 그걸 또 해쉬함수로 만들어버린다는 뜻이다.
사용자는 몇 번 해쉬 함수를 깜쌌는지 알 수 없거니와, Salting된 함수조차 알 수 없다.

BCrypt

음.. 이부분을 쓰고 싶은데 관련 내용을 찾아볼 수 가 없다.
애초부터 암호화를 위한 해쉬함수로써 구성이 되어있고 그 효과가 매우 강력크 하다는 것!

스프링으로 구현해 보기

실제로 서버에 구현을 해보겠다.

여기서 사용자 마다 다른 유저의 Salt를 어디서 보관할 것인가라는 물음표를 던진다.
무슨 말이냐면, 사용자 모델에 Salt를 넣어 보관할 것인가? 따로 보관할 것인가?

사실 어느 것도 상관없을 것 같다 (사실 나도 이부분 잘 모른다)

나는 따로 보관하는 것을 추천한다.

아니 좀 더 완벽하게 보안을 짤려면 Salt와 유저 모델을 보관하는 데이터 서버를 물리적으로 분리 시켜 놓을 것을 추천한다. (그럼 뚫릴 일이 없으니)

해당코드는 여기서 볼 수 있다.
https://github.com/ehdrms2034/SpringBootWithJava/tree/master/Spring_React_Login

  • Salt.java
@Entity
@Getter
@Setter
public class Salt {

    @Id
    @GeneratedValue
    private int id;

    @NotNull()
    private String salt;

    public Salt() {
    }

    public Salt(String salt) {
        this.salt = salt;
    }
}
  • Member.java
@Entity
@Table(name = "Members")
@Getter
@Setter
public class Member {
    //... 코드들

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "salt_id")
    private Salt salt;
    
    //...코드들
    }

위와 같이 구성을 해놓고, 아래는 솔트 함수를 관리하는 모듈이다.
(나중에서야 한건데 BCrypt에 CheckPassword라는 함수가 지원한다..)

  • SaltUtil.java
@Service
public class SaltUtil {

    public String encodePassword(String salt, String password){
        return BCrypt.hashpw(password,salt);
    }

    public String genSalt(){
        return BCrypt.gensalt();
    }

}

위 모듈들을 이용해서 다음과 같은 서비스를 제작한다.

  • AuthServiceImpl.java
@Service
@Slf4j
public class AuthServiceImpl implements AuthService {

    @Autowired
    private MemberRepository memberRepository;

    @Autowired
    private SaltRepository saltRepository;

    @Autowired
    private SaltUtil saltUtil;

    @Override
    @Transactional
    public void signUpUser(Member member) {
        String password = member.getPassword();
        String salt = saltUtil.genSalt();
        log.info(salt);
        member.setSalt(new Salt(salt));
        member.setPassword(saltUtil.encodePassword(salt,password));
        memberRepository.save(member);
    }

    @Override
    public Member loginUser(String id, String password) throws Exception{
        Member member = memberRepository.findByUsername(id);
        if(member==null) throw new Exception ("멤버가 조회되지 않음");
        String salt = member.getSalt().getSalt();
        password = saltUtil.encodePassword(salt,password);
        if(!member.getPassword().equals(password))
            throw new Exception ("비밀번호가 틀립니다.");
        return member;
    }


}

그리고 테스트 코드를 작성해봤는데 잘돌아간다

@SpringBootTest
@Slf4j
public class AuthServiceTest {

    @Autowired
    private AuthService authService;

    @Test
    public void signUp(){
        Member member = new Member();
        member.setUsername("user222");
        member.setPassword("a1234");
        member.setName("김동근");
        member.setEmail("403.forbidden@kakao.com");
        member.setAddress("대한민국 어디광역시 땡땡로 땡땡길 101동 1001호");
        authService.signUpUser(member);
    }

    @Test
    public void login(){
        RequestLoginUser loginUser = new RequestLoginUser("user222","a1234");
        try{
            authService.loginUser(loginUser.getUsername(),loginUser.getPassword());
            log.info("로그인 성공");
        }catch(Exception e){
            e.printStackTrace();
        }
    }


}

끝!


DB에도 잘저장되고 로그인도 잘된다. 물론 평문 전송은 HTTPS로 감싸줘야 되겠지만 말이다..

profile
수동적인 과신과 행운이 아닌, 능동적인 노력과 치열함

2개의 댓글

comment-user-thumbnail
2022년 1월 26일

좋은 게시글 감사합니다.

답글 달기
comment-user-thumbnail
2022년 11월 2일

와 시큐리티랑 jwt 관련해서 다양한 글 참고하고 있는데 동근님 게시글 너무 유익합니다.. 잘보고갑니다

답글 달기