Password 암호화 왜 궁금했을까❓
- SSAFY 프로젝트를 진행하면서 법적으로 정해져 있는 비밀번호 일방향 암호화를 준수하기 위해 사용자 비밀번호를 SHA512를 통해 해시하여 DB에 저장하였다.
- 해당 기능에 대해 흐름이나 작동 방식은 이해하고 있지만, 내가 구현한 부분이 아니였기에 정확하게 각 코드들이 어떤 역할을 하는 지 알지 못하여 학습해보려 한다.
Password 암호화 왜 해야할까❓
- 단순 Plain Text로 DB에 저장하는 것은 보안적인 측면에서 위험하다고 할 수 있다. 그 이유는 DB에 있는 password 정보가 SQL Injection이나 개발자의 부주의로 의해 조회가 된다면 계정 탈취에 대한 위협이 존재하게 되고 보통 다른 사이트에서도 동일한 password를 사용하기 때문에 영향을 끼칠 수 있을 것이다.
- 이러한 이유 때문에, password를 해싱하여 예상치 못한 password 노출에도 보안적인 요소를 첨가하여 사용자의 비밀번호를 보호할 수 있게 된다.
- 하지만, 암호화 방식에는 단방향과 양방향이 있는데 왜 단방향 암호화를 진행하는 것일까라는 의문이 생길 수 있다. password 같은 경우에는 로그인, 비밀번호 재설정 등에서만 사용이 되어 복호화 될 이유가 없다. 또한, 플랫폼을 운영하는 운영자 또한 비밀번호를 검증만 하면 되므로 복호화 과정이 필요 없어 해시 함수를 사용한다.
단방향 암호화 함수의 한계점
- 정보보호학과 학부 시절에 해싱에 대해서 배우며 해시 함수 적용시 발생 가능한 공격들인 레인보우(rainbow attack) 공격이 생각났다.
- 레인보우 공격의 경우 해시 함수가 동일한 입력에 대해 동일한 출력이 나온다는 점을 이용하여 공격자가 가능한 많은 해싱 결과를 모아 레인보우 테이블을 만들고 이를 바탕으로 해싱전의 값을 유추하는 것이다. 관련하여 복호화 사이트가 있을 정도로 해시값 탈취 시 password 유추가 가능하여 보안적인 측면에서 좋지 않다.
- 이 외에도 충돌 공격, 역상 공격 등 많은 공격들이 존재하여 일반적인 형태로 사용하는 것은 보안적인 측면에서 좋지 않다.
- 이러한 공격들에 대응하기 위해, Salt라는 랜덤의 값을 원본 메시지와 합쳐 해싱하면 앞서 말한 것들을 해결할 수 있다.
Salting을 통한 Password 생성
- Salt + 원본 메시지를 Salting이라하며 프로젝트에 어떻게 구현을 했는 지 작성해보려고 한다.
@Override
public void createUser(UserDto dto) {
byte[] salt = getSalt();
byte[] byteDigestPsw = getSaltHashSHA512(dto.getUserPassword(), salt);
String strDigestPsw = toHex(byteDigestPsw);
String strSalt = toHex(salt);
dto.setUserPassword(strDigestPsw);
dto.setSalt(strSalt);
try {
userMapper.createUser(dto);
} catch (DataIntegrityViolationException e) {
throw new RestApiException(CustomResponseCode.INVALID_USER_INFO);
}
}
- 회원가입을 하는 Service 코드인데 우선 password를 만들기 위해서는 salt 값이 필요하여 getSalt() 함수를 통해 salt 값을 받아오고 있다.
private byte[] getSalt() {
SecureRandom sr = null;
try {
sr = SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
throw new RestApiException(CustomResponseCode.PASSWORD_NOT_CREATED);
}
byte[] salt = new byte[16];
sr.nextBytes(salt);
return salt;
}
- SecureRandom.getInstanceStrong()을 통해서 난수를 생성하고 해시를 할 때 byte 자료형의 salt 값이 필요하여 byte 값으로 변환하는 과정이다.
private byte[] getSaltHashSHA512(String userPassword, byte[] salt) {
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA-512");
} catch (NoSuchAlgorithmException e) {
throw new RestApiException(CustomResponseCode.PASSWORD_NOT_CREATED);
}
md.update(salt);
byte[] byteData = md.digest(userPassword.getBytes());
md.reset();
return byteData;
}
- getSaltHashSHA512() 함수를 통해 사용자 입력 Password와 salt를 SHA-512 알고리즘으로 해싱하고 결과 값을 추출한다.
- byte[]를 hex로 바꾸어 줬는데, 자바에서 byte 자료형의 범위는 -128 ~ 127이다. 맨 앞의 비트는 부호코드로 인식되기 때문에 이것을 방지하기 위해 16진수로 변환해 주었다.
- 로그인을 시도할 때는 DB에서 salt를 가져오고 사용자가 입력한 값과 salting을 통해 나온 값과 DB에 저장되어 있는 password를 비교하여 성공 여부를 보내주게 된다.
@Override
public UserDto login(UserDto dto) {
String strSalt = getSaltById(dto.getUserId());
byte[] salt = fromHex(strSalt);
byte[] byteDigestPsw = getSaltHashSHA512(dto.getUserPassword(), salt);
String strDigestPsw = toHex(byteDigestPsw);
dto.setUserPassword(strDigestPsw);
return userMapper.login(dto)
.orElseThrow(() -> new RestApiException(CommonResponseCode.UNAUTHORIZED_REQUEST));
}