비밀번호 암호화 라이브러리

김지인·2022년 9월 14일
2
post-thumbnail

들어가기 앞서서..

웹페이지를 제작할 때 가장 중요한 것은 보안이다.
만약, 해킹을 통해 DB가 뚫렸을 때 사용자의 비밀번호가 그대로 저장이 되어있으면 어떻게 될까 ? 사람 심리란게 비밀번호가 다 비슷하기 때문에 해당 아이디와 비밀번호로 다른 사이트까지 해킹이 가능할 것이다.

따라서 사용자의 비밀번호를 암호화 시켜서 DB에 저장하고 그 암호화 코드를 이용해서 로그인도 할 수 있게 하는 라이브러리를 제작할 것이다.

그렇다면 생각해야 될 점은 무엇일까 ? 복호화가 불가능하게 단방향 암호로 만들어 만약 DB가 뚫려도 원본 비밀번호를 알 수 없게 해야할 것이다.

그래서 각 아이디의 고유 랜덤값을 주고, 그 해당 랜덤값과 비밀번호 더해 해쉬코드로 만들어 DB에 저장할 것이다.


사용자로 부터 값을 받아와서 코드 변환하기

CodeEncryptionOfOneWay.java

public class CodeEncryptionOfOneWay {
	private String idCode;
	private String pwdCode;
	private Salt ms = new Salt();
	private HashCode hc = new HashCode();
	
	public CodeEncryptionOfOneWay(String idCode, String pwdCode) {
		this.idCode = idCode;
		this.pwdCode = pwdCode;
	} -------- 1
	
	public ArrayList<CodeEntity> getEncryptingCode() { 
		return getArrayListOfCodeEncryptionOfOneWay();
	} -------- 2

	private ArrayList<CodeEntity> getArrayListOfCodeEncryptionOfOneWay() {
		ArrayList<CodeEntity> ceArr = new ArrayList<CodeEntity>();
		ce.add(actionOfEncryptingCode(idCode, pwdCode));
		return ceArr;
	} -------- 3
	
	private CodeEntity actionOfEncryptingCode(String idCode, String pwdCode) {
		CodeEntity cObj = new CodeEntity(idCode,
        								 ms.actionOfMakeSalt(),
                                         hc.actionOfMakeHash(ms.getSalt(), pwdCode));
		return cObj;
	} -------- 4
}
  1. 사용자로 부터 아이디와 비밀번호를 받아온다.
  2. CodeEntity타입의 ArrayList를 반환하는 메서드
  3. ArrayList에 값이 담긴 CodeEntity를 추가 하는 메서드
  4. CodeEntity객체에 아이디와, 랜덤값을 담당할 고유의 salt값을 Salt클래스의 actionOfMakeSalt()를 통해 가져와 담아주고, Salt클래스로 부터 생성된 salt값과 기존의 비밀번호를 합쳐 HashCode클래스의 actionOfMakeHash()를 통해 해쉬코드를 가져와 담는 메서드

각 아이디 마다 고유의 랜덤값 얻기(Salt)

Salt.java

public class Salt {
	
	public String actionOfMakeSalt() {
		byte[] randomByte = makeRandomByteArray();
		return new String(Base64.getEncoder().encode(randomByte));
	} ----- 1
	
	private byte[] makeRandomByteArray() {
		byte[] randomByte = null;
		try {
			SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
			randomByte = new byte[16];
			random.nextBytes(randomByte);
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		return randomByte;
	} ----- 2
	
	public String getSalt() {
		return actionOfMakeSalt();
	} ----- 3

}
  1. makeRandomByte()를 통해 임의의 난수값 배열을 받아온다. 그 다음 받아온 배열값을 코드화 하고 제어문자와 특수문자를 제외한 안전한 문자만 받아와 DB의 값에 저장하기 위해 BASE64로 인코딩을 진행한다. 그 후 그 값을 반환한다.
  2. SHA1PRNG알고리즘을 사용함으로써 복호화가 불가능하게 만들어 준다. 그리고 기존의 Random(48비트)보다 복호화가 더 힘든 SecureRandom(128비트)를 사용하여 난수를 생성해준다. 그렇게 생성된 난수를 byte배열인 randomByte에 담아서 반환한다.
  3. DB에는 해당 아이디와 고유의 랜덤값이 담겨야 하므로 salt 값을 반환해주는 메서드이다.

비밀번호 암호화

HashCode.java

public class HashCode {
	
	public String actionOfMakeHash(String salt, String pwdCode) {
		return actionOfEncryption(plusStr(salt, pwdCode));
	} ----- 1

	public String actionOfEncryption(String plusStr) {
		MessageDigest md = null;
		try {
			md = MessageDigest.getInstance("SHA-256");
			md.update(plusStr.getBytes());
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		return String.format("%064x", new BigInteger(1, md.digest()));
	} ----- 2

	public String plusStr(String salt, String pwdCode) {
		return salt+pwdCode;
	} ----- 3
	
}
  1. salt값과 비밀번호값을 받아와 합친 해쉬코드값을 반환하는 메서드
  2. Java에서 SHA를 이용한 알고리즘을 사용하기 위해 MessageDigest클래스를 사용하고 update()함수를 이용하여 지정된 바이트 데이터를 사용해 다이제스트를 갱신합니다. 그리고 그 해당 다이제스트값을 digest()로 해쉬를 바이트 배열로 반환하고 64자리의 16진수 값으로 패딩합니다.
    (Message : Hash 함수를 통과하기 전 원본데이터 / Digest : Hash 함수 통과 후 데이터)
  3. 랜덤값인 salt값과, 비밀번호를 합쳐주는 메서드

고유의 salt값을 이용해서 암호코드 찾기

CheckMachingCode.java

public class CheckMachingCode {

	private HashCode hc = new HashCode(); ----- 1

	public boolean checkMatchingCode(String pwdCode, String salt, String hashCode) {
		return checkMachingHashCode(pwdCode, salt, hashCode);
	}----- 2
	
	private boolean checkMatchingHashCode(String pwdCode, String salt, String hashCode) {
		if(hc.actionOfEncryption(hc.plusStr(salt, pwdCode)).equals(hashCode)){
			return true;
		}
		return false;
	}----- 3
}
  1. DB에 저장된 고유의 salt값을 해쉬코드화 하기 위해 HashCode를 전역객체로 선언한다.
  2. 사용자로부터 서버로부터 온 패스워드와 DB로부터 고유의 랜덤값과 암호화된 해쉬코드값을 받아온다.
  3. HashCode클래스의 actionOfEncryption()함수에 받아온 데이터 값을 넣어 해쉬코드화 시키고 DB로부터 온 해쉬코드와 값이 같은지 비교하여 boolean값을 반환한다.

적용시켜보기

기존에 했던 서블릿 + jsp 기반의 프로젝트에 해당 기능 라이브러리로 적용시켜보았다.
다만, 기존의 로직이 짜여진 상태에서 추가한 것 이기때문에 구조가 비효율적으로 보일 수 있다.

public class NoticeBoardMakeMemeberController extends HttpServlet{
	ArrayList<CodeEntity> hashCodeArr = new ArrayList<CodeEntity>(); 
	
.
.
    private ArrayList<CodeEntity> changePwdToHashCode(String userId, String userPwd) {
            ArrayList<CodeEntity> ceArr = new ArrayList<CodeEntity>();
            CodeEncryptionOfOneWay cw = new CodeEncryptionOfOneWay(userId, userPwd);
            arr = cw.getEncryptingCode();
            return ceArr;
        }
.
.
}
  • 기존 회원가입 컨트롤단 로직에 CodeEntity타입의 ArrayList를 반환하는 메서드를 만들어 주었다.
  • 그리고 비밀번호를 암호화 해주는 CodeEncryptionOfOneWay클래스에 인자로 클라이언트로 부터 받아온 아이디와 비밀번호를 인자로 넘겨 인스턴스를 생성한다.
  • 해당 인스턴스의 함수 getEncryptingCode()를 통해 CodeEntity타입의 ArrayList를 받아와 해당 리스트를 반환한다.
    (ArrayList<CodeEntity> = <userId, salt, hashCode>)
public class NoticeBoardMakeMemeberController extends HttpServlet{
	ArrayList<CodeEntity> hashCodeArr = new ArrayList<CodeEntity>(); 
	

	@Override
	protected void doPost(HttpServletRequest request, HttpServletResponse response) 
			throws ServletException, IOException {
		hashCodeArr = changePwdToHashCode(request.getParameter("user_ID"),
        								  request.getParameter("user_PW"));
		makeMemberAction(hashCodeArr.get(0).getIdCode(), 
        		........ hashCodeArr.get(0).getHashCode(), 
				........ hashCodeArr.get(0).getSaltCode());
	}
    
    
    private ArrayList<CodeEntity> changePwdToHashCode(String userId, String userPwd) {
            ArrayList<CodeEntity> ceArr = new ArrayList<CodeEntity>();
            CodeEncryptionOfOneWay cw = new CodeEncryptionOfOneWay(userId, userPwd);
            arr = cw.getEncryptingCode();
            return ceArr;
        }
.
.
}
  • 기존의 서블릿코드에 전역으로 선언된 CodeEntity타입의 ArrayList에 바로 전에 만들었던 changePwdToHashCode()메서드를 통해 CodeEntity객체를 담는다.
  • 그렇게 담겨진 리스트에서 id와 salt값과 hashcode값을 인자로 넘겨준다.

  • 그리고 해당 아이디의 고유값을 담아줄 테이블을 생성한다.
  • MEMBERID컬럼은 BOARDMEMBER의 USERID의 외래키가 될 것 이다.

  • MEMEBERIDCODE테이블에 아이디와 고유의 랜덤값이 저장되어 있는 것을 볼 수 있다.
  • 또한 BOARDMEMBER테이블에 패스워드가 암호화된 값으로 저장되어 있는 것을 볼 수 있다.

그렇다면 로그인할때는 어떻게 해야할까 ?

public class LoginDAO extends NoticeBoardProjectDAO{

	.
    .
    .
	private boolean checkPwd(Connection con, PreparedStatement pst, ResultSet rs,
    						String userId, String userPwd) {
		CheckMachingCode cmc = new CheckMachingCode();
		String salt = getSalt(con, pst, rs, userId);
		return cmc.checkMachingCode(userId, userPwd, salt, 
        							getHashCode(con,pst,rs,userId));
		}
    .
    .
    .
	}
  • 기존 로그인 DAO로직단에서 클라이언트로부터 온 userid와 userPwd, DB로부터 온 salt값과 암호화된 비밀번호 값을 받아와 CheckMachingCode클래스의 checkMachingCode()함수를 통해 다시 암호화로 변환 후 기존의 DB에 저장된 값과 일치하는지 알아본다.

    (salt값과 hashCode값은 단순히 디비에서 가져오도록 했다.)
  • 그 다음 반환타입을 boolean으로 해주어 일치하지 않으면 false, 일치하면 true를 반환 할 것이다.
public class LoginDAO extends NoticeBoardProjectDAO{ 
 
 public Token getTokenOfLoginCheckIfLogic(Connection con, PreparedStatement pst, ResultSet rs,String userId, String userPwd) {

              if(checkPwd(con,pst,rs,userId,userPwd)) {
                  JdbcClose(con, pst, rs);
                  return Token.LOGINSUCCESS;
              }
              return Token.LOGINFAIL;

          }
}
  • 그래서 최종적으로 checkPwd() 메서드의 반환값을 통해 토큰발행해서 로그인 성공 실패를 알린다.

profile
에러가 세상에서 제일 좋아

1개의 댓글

comment-user-thumbnail
2023년 1월 11일

잘 읽었습니다

답글 달기