본문 바로가기
JAVA

[JAVA] RSA 암호화 방식 적용 방법

by GoodDayDeveloper 2021. 9. 2.
반응형

공공기관 프로젝트 진행 도중,

로그인 이용 시에 민감 데이터.. 즉 계정 정보가 평문 전송된다는 지적을 받았습니다.

화면에서 서버로 데이터를 전송할때 데이터가 보여지기에 보안 취약점으로 지적을 받은 겁니다...

확실히 일반 프로젝트에 비해서 공공기간 프로젝트가 여러 심의를 거치는 듯 했습니다.

 

 

 

 

아래는 'wireshark'라는 프로그램으로 패킷(데이터) 분석이 가능한 프로그램입니다.

이 프로그램으로 확인해 봤더니 정말 아이디랑 패스워드가 나타나더라구요... 정말 깜짝 놀랬습니다...ㅜㅜ

 

 

 

그리고 아래와 같이 조치 권고 사항으로 '통신 암호화 적용' 적용하라는 문구까지 친절히 설명되어 있습니다......

항상 느끼는 것이지만 프로젝트는 끝나도 끝이 아니네요 ㅜㅜ

 

(2차 인증 OTP 구현방법을 확인하시려면 클릭해주세요)

 

그래서 보편적으로 사용되고 있는 RSA 암호화 방식에 대해 정리 및 구현방법을 소개하려 합니다.

 


 

RSA 암호화 : 

RSA의 약자는 Ron Rivert, Adi Shamir, Leonard Adleman이 3명의 연구원 이름의 앞글자를 딴 것이라고 하네요.

(이름의 앞글자로 알고리즘명을 만들다니... 엄청 친했나 봅니다..)

공개키 암호시스템의 하나로, 암호화뿐만 아니라 전자서명이 가능한 최초의 알고리즘으로 알려져 있다고 합니다.

RSA는 두개의 키를 사용하는데, 모두에게 공개하는 공개키 공개해선 안되는 개인키로 구성되며, 

공개키는 메세지를 암호화할때, 개인키는 암호화된 메세지를 복호화할때 사용됩니다.

 

RSA 알고리즘 방식(로그인 구현) : 

1. 서버가 공개키와 개인키를 만들어 개발자 공개키를 보냅니다. 

2. 개발자서버로부터 받은 공개키를 이용하여 아이디와 패스워드를 서버로 보냅니다.

3. 서버가 암호화된 정보를 받고 개인키를 이용하여 암호를 해독합니다.

 

이 방식으로 구현을 해보겠습니다.

 

 

 


 

 

 

Controller (로그인 폼)

 

맨 위에 전역변수로 개인키와 RSA 인스턴스를 생성해줍니다.

그리고 로그인 폼안에 있는 initRsa함수에 HttpServletRequest 객체인 request를 넣어줍니다.

initRsa함수에서는 RSA 개인키를 session에 저장하며, RSA modulus과 RSA exponent를 request에 넣어줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
private static String RSA_WEB_KEY = "3213213"// 개인키 session key
private static String RSA_INSTANCE = "RSA_INSTANCE"// rsa transformation
 
 
//로그인 화면
@RequestMapping(value = "/account/login.do")
public String login(
        @ModelAttribute("searchVO") LoginVO searchVO,
        RedirectAttributes redirectAttributes,
        HttpServletRequest request,
        ModelMap model) throws Exception {
 
initRsa(request);
 
model.addAttribute("requestURL", request.getAttribute("requestURL"));
return "tiles:bsite/account/login/login";
 
}
 
 
public void initRsa(HttpServletRequest request) {
HttpSession session = request.getSession();
 
KeyPairGenerator generator;
try {
    generator = KeyPairGenerator.getInstance(LoginController.RSA_INSTANCE);
    generator.initialize(1024);
 
    KeyPair keyPair = generator.genKeyPair();
    KeyFactory keyFactory = KeyFactory.getInstance(LoginController.RSA_INSTANCE);
    PublicKey publicKey = keyPair.getPublic();
    PrivateKey privateKey = keyPair.getPrivate();
 
    session.setAttribute(LoginController.RSA_WEB_KEY, privateKey); // session에 RSA 개인키를 세션에 저장
 
    RSAPublicKeySpec publicSpec = (RSAPublicKeySpec) keyFactory.getKeySpec(publicKey, RSAPublicKeySpec.class);
    String publicKeyModulus = publicSpec.getModulus().toString(16);
    String publicKeyExponent = publicSpec.getPublicExponent().toString(16);
 
    request.setAttribute("RSAModulus", publicKeyModulus); // rsa modulus 를 request 에 추가
    request.setAttribute("RSAExponent", publicKeyExponent); // rsa exponent 를 request 에 추가
catch (Exception e) {
    e.printStackTrace();
}
 
}
 
cs

 

관련 JS 자료입니다.

RSA javascript.zip
0.01MB

 

 

 

반응형

 

 

login.jsp (로그인 폼)

 

여기서는 우선 RSA관한 JS를 선언해주는데 순서에 맞게 선언해줘하는 것이 특징입니다.

그리고 서버에서 가져온 RSAModulus / RSAExponent와

실질적으로 서버로 넘겨줄 ID와 PASSWORD를 form태그 밑에 hidden type으로 선언해줍니다.

그리고 스크립트에서 rsa객체에 RSAModulus 과 RSAExponent를 넣어준 다음,

rsa.encrypt를 통해서 ID와 PASSWORD를 암호화해줍니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="ui" uri="http://egovframework.gov/ctl/ui"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles"%>
<c:set var="path" value="${pageContext.request.contextPath}" />
 
//순서에 주의합니다.
<script type="text/javascript" src="${pageContext.request.contextPath}/js/rsa/rsa.js"></script>
<script type="text/javascript" src="${pageContext.request.contextPath}/js/rsa/jsbn.js"></script>
<script type="text/javascript" src="${pageContext.request.contextPath}/js/rsa/prng4.js"></script>
<script type="text/javascript" src="${pageContext.request.contextPath}/js/rsa/rng.js"></script>
 
 
<script type="text/javascript">
 
function frmCheck() {
    if ($("#xid").val() == "") {
        alert("아이디를 입력해주세요.");
        $("#xid").focus();
        return false;
    }
    if ($("#xpassword").val() == "") {
        alert("비밀번호를 입력해주세요.");
        $("#xpassword").focus();
        return false;
    }
    
    var id = $("#xid").val();
    var password = $("#xpassword").val();
    
    //RSA 암호화
    var rsa = new RSAKey();
    rsa.setPublic($('#RSAModulus').val(),$('#RSAExponent').val());
    
    $("#ID").val(rsa.encrypt(id));
    $("#PASSWORD").val(rsa.encrypt(password));
    
    id.val("");
    password.val("");
    
    }
</script>
 
 
<div id="sign-body">
<div class="form-signin">
<div class="form-text text-cetner">
<h1 class="signin-logo"><img src="${pageContext.request.contextPath}/css/bsite/img/map/admin_logo.png"></h1>
<h3 class="mt_20">관리자 시스템<br />로그인</h3>
</div>
 
<div class="signin-box">   
<div id="member-box" class="login-wrap text-cetner">
<div class="form-box ">
    <!-- login-top -->        
    <div class="login-top">
        <h3 class="mb_10 fw-normal" style="font-weight:bold;"><i class="xi-mouse"></i>아이디/비밀번호 입력</h3>
    </div>
    <!-- login-top //-->        
    
    <!-- login-form -->              
    <div class="login-form">
        <form name="loginform" action ="${path}/account/actionLogin.do" method="post" onsubmit="return frmCheck();">
          <input type="hidden" id="RSAModulus" value="${RSAModulus}"/>
                  <input type="hidden" id="RSAExponent" value="${RSAExponent}"/>
                  <input type="hidden" id="ID" name="ID">
                  <input type="hidden" id="PASSWORD" name="PASSWORD">
            <ul class="login">
                <li>
                    <label for="user_id" class="hide">아이디</label>
                    <input type="text" class="form-control" id="xid" placeholder="아이디를 입력해주세요" />
                </li>
                <li>
                    <label for="user_password" class="hide">비밀번호</label>
                    <input type="password" class="form-control" id="xpassword" placeholder="비밀번호를 입력해주세요" />
                </li>
            </ul>
            <input type="submit" class="btn btn-lg btn-dark" value="로그인">
        </form>
    </div>
    <!-- login-form //-->    
</div>    
</div>  
</div>
</div>
</div>
cs

 

Controller (로그인 Action(확인) 폼)

 

requst로 ID와 PASSWORD 값을 가져옵니다.

전연변수로 선언한 개인키 (RSA_WEB_KEY) 를 사용해서 ID와 PASSWORD를 복호화해주고 개인키를 삭제합니다.

마지막으로 복호화를 해제한 값을 searchVO에 넣어준 다음 로그인정보를 확인하는 작업을 해주면 끝입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//로그인 실행
@RequestMapping(value = "/account/actionLogin.do", method = RequestMethod.POST)
public String actionLogin(
    @ModelAttribute("searchVO") LoginVO searchVO,
    HttpServletRequest request,
    HttpSession session,
    RedirectAttributes redirectAttributes,
    HttpServletResponse httpServletResponse,
    ModelMap model) throws Exception {
 
  String Id = (String) request.getParameter("ID");
  String Password = (String) request.getParameter("PASSWORD");
 
  PrivateKey privateKey = (PrivateKey) session.getAttribute(LoginController.RSA_WEB_KEY);
 
  // 복호화
  Id = decryptRsa(privateKey, Id);
  Password = decryptRsa(privateKey, Password);
 
  // 개인키 삭제
  session.removeAttribute(LoginController.RSA_WEB_KEY);
  
  
  searchVO.setId(Id);
  searchVO.setPassword(Password);
  
  //로그인 정보 조회
  LoginVO loginVO = loginService.actionLogin(searchVO);
  
  //로그인 실패 시
  if(loginVO == null){
      redirectAttributes.addFlashAttribute("actionResult""fail");
      return "redirect:/account/login.do";
  }
  
return "redirect:/account/otp.do";
}
 
//복호화
private String decryptRsa(PrivateKey privateKey, String securedValue) throws Exception {
    Cipher cipher = Cipher.getInstance(LoginController.RSA_INSTANCE);
    byte[] encryptedBytes = hexToByteArray(securedValue);
    cipher.init(Cipher.DECRYPT_MODE, privateKey);
    byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
    String decryptedValue = new String(decryptedBytes, "utf-8"); // 문자 인코딩 주의.
    return decryptedValue;
}
 
 
//16진 문자열을 BYTE 배열로 변환
public static byte[] hexToByteArray(String hex) {
    if (hex == null || hex.length() % 2 != 0) { return new byte[] {}; }
 
    byte[] bytes = new byte[hex.length() / 2];
    for (int i = 0; i < hex.length(); i += 2) {
        byte value = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
        bytes[(int) Math.floor(i / 2)] = value;
    }
    return bytes;
}
cs

 

그러면 패킷 분석 프로그램에 정보가 나타나지 않는 것을 확인 할 수 있습니다!

 

반응형

댓글