javascript - java 간 RSA 를 이용해서 암호화 복호화 하기

 

 

 

 

JSP 기반으로 설명함.

JSP가 아니라도 적용가능함.

 

 

 

 

프로젝트를 진행중에 로그인 시 로그인 정보의 평문전송에 대해서 이슈가 있었다.

 

RSA 를 사용하여 평문전송 이슈를 해결하였다.

 

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

 

RSA에 관한 이론적인 설명은 없고 사용하는 방법과 소스를 첨부한다.

 

 

본 방법만을 가지고 보안처리를 하게 되면 CSRF attack, Session Hijacking, XSS 취약점에 노출 될 수 있습니다.

 

CSRF attack, Session Hijacking, XSS 의 취약점에 대해서도 대응해야 합니다.

 



 

요약하자면 다음과 같다.

controller 에서 공개키와 개인키를 생성한다.

개인키는 session 에 저장한다.

공개키를 사용하여 modulus, exponent 를 생성하고 request 에 담는다.

loginForm.jsp를 호출한다.

입력받은 id, pw 등을 modulus, exponent 사용하여 rsa 암호화 한 후 전송한다.

javascript 에서 rsa를 사용하기 위한 js 파일들 -> (biginteger javascript 링크 또는 biginteger javascript.zip)

전송받은 controller 에서 session 에 저장했던 개인키를 활용하여 복호화 한다.

 

 

 

 

소스 설명은 주석으로 대신한다.

 

Login Controller

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.RSAPublicKeySpec;
 
import javax.crypto.Cipher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
 
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
 
@Controller("loginController")
public class LoginController {
 
    private static String RSA_WEB_KEY = "_RSA_WEB_Key_"// 개인키 session key
    private static String RSA_INSTANCE = "RSA"// rsa transformation
 
    // 로그인 폼 호출
    @RequestMapping("/loginForm.do")
    public ModelAndView loginForm(HttpServletRequest request, HttpServletResponse response) throws Exception {
 
        // RSA 키 생성
        initRsa(request);
 
        ModelAndView mav = new ModelAndView();
        mav.setViewName("loginForm");
        return mav;
    }
 
    // 로그인
    @RequestMapping("/login.do")
    public ModelAndView login(HttpServletRequest request, HttpServletResponse response) throws Exception {
 
        String userId = (String) request.getParameter("USER_ID");
        String userPw = (String) request.getParameter("USER_PW");
 
        HttpSession session = request.getSession();
        PrivateKey privateKey = (PrivateKey) session.getAttribute(LoginController.RSA_WEB_KEY);
 
        // 복호화
        userId = decryptRsa(privateKey, userId);
        userPw = decryptRsa(privateKey, userPw);
 
        // 개인키 삭제
        session.removeAttribute(LoginController.RSA_WEB_KEY);
 
        // 로그인 처리
        /*
          
         ...  
           
         */
 
        ModelAndView mav = new ModelAndView();
        mav.setViewName("index");
        return mav;
    }
 
    /**
     * 복호화
     * 
     * @param privateKey
     * @param securedValue
     * @return
     * @throws Exception
     */
    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 배열로 변환한다.
     * 
     * @param hex
     * @return
     */
    public static byte[] hexToByteArray(String hex) {
        if (hex == null || hex.length() % != 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;
    }
 
    /**
     * rsa 공개키, 개인키 생성
     * 
     * @param request
     */
    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) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
cs

 

 



JSP

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
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
    String ctxPath = (String) request.getContextPath();
%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Login</title>
<script type="text/javascript" src="<%=ctxPath %>/script/jquery/jquery-1.11.0.min.js"></script>
<!-- 순서에 유의 -->
<script type="text/javascript" src="<%=ctxPath %>/script/RSA/rsa.js"></script>
<script type="text/javascript" src="<%=ctxPath %>/script/RSA/jsbn.js"></script>
<script type="text/javascript" src="<%=ctxPath %>/script/RSA/prng4.js"></script>
<script type="text/javascript" src="<%=ctxPath %>/script/RSA/rng.js"></script>
 
<script type="text/javascript">    
    function login(){
        var id = $("#USER_ID_TEXT");
        var pw = $("#USER_PW_TEXT");
    
        if(id.val() == ""){
            alert("아이디를 입력 해주세요.");
            id.focus();
            return false;
        }
        
        if(pw.val() == ""){
            alert("비밀번호를 입력 해주세요.");
            pw.focus();
            return false;
        }
        
        // rsa 암호화
        var rsa = new RSAKey();
        rsa.setPublic($('#RSAModulus').val(),$('#RSAExponent').val());
        
        $("#USER_ID").val(rsa.encrypt(id.val()));
        $("#USER_PW").val(rsa.encrypt(pw.val()));
        
        id.val("");
        pw.val("");
 
        return true;
    }
</script>
</head>
<body>
    <form name="frm" method="post" action="<%=ctxPath%>/login.do" onsubmit="return login()">
        <input type="hidden" id="RSAModulus" value="${RSAModulus}"/>
        <input type="hidden" id="RSAExponent" value="${RSAExponent}"/>    
        <input type="text" placeholder="아이디" id="USER_ID_TEXT" name="USER_ID_TEXT" />
        <input type="password" placeholder="비밀번호" id="USER_PW_TEXT" name="USER_PW_TEXT" />
        <input type="hidden" id="USER_ID" name="USER_ID">
        <input type="hidden" id="USER_PW" name="USER_PW">
        <input type="submit" value="로그인" />
    </form>
</body>
</html>
cs

 

 

by 개발자 CofS 2017.04.07 14:00
  • 감사합니다 2017.07.11 20:47 신고 ADDR EDIT/DEL REPLY

    널값떨어져서 몇시간 삽질끝에 덕분에 해결했습니다

  • Favicon of http://fehead.tistory.com BlogIcon 늑대랑 2017.10.31 17:25 신고 ADDR EDIT/DEL REPLY

    좋은 예제, 친절한 설명 감사합니다.
    삽질하지 않고 이 예제로 바로 해결 하였습니다.
    감사합니다.

    • Favicon of http://cofs.tistory.com BlogIcon 개발자 CofS 2017.10.31 17:29 신고 EDIT/DEL

      도움이 되셨다니

      기분이 좋네요 ㅎㅎ

      추운데 감기조심하세요 !!

  • 오펠리시스 2018.07.23 09:34 신고 ADDR EDIT/DEL REPLY

    좋은 코드 감사합니다.
    암복호화는 성공적으로 되었는데 가끔씩 Exception이 발생합니다.

    StackTrace : javax.crypto.BadPaddingException: Data must start with zero
    at sun.security.rsa.RSAPadding.unpadV15(RSAPadding.java:325)
    at sun.security.rsa.RSAPadding.unpad(RSAPadding.java:272)
    at com.sun.crypto.provider.RSACipher.doFinal(RSACipher.java:356)
    at com.sun.crypto.provider.RSACipher.engineDoFinal(RSACipher.java:382)
    at javax.crypto.Cipher.doFinal(Cipher.java:2087)
    at com.firstbrain.spring.UserLoginSuccessHandler.decryptRsa(UserLoginSuccessHandler.java:262)
    at com.firstbrain.spring.UserLoginSuccessHandler.onAuthenticationSuccess(UserLoginSuccessHandler.java:71)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.successfulAuthentication(AbstractAuthenticationProcessingFilter.java:331)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.successfulAuthentication(AbstractAuthenticationProcessingFilter.java:298)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:235)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:110)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:50)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.session.ConcurrentSessionFilter.doFilter(ConcurrentSessionFilter.java:125)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:344)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:261)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:219)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:110)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:506)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:169)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:962)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:445)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1115)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:637)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:318)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:722)

    어디가 문제인지를 모르겠습니다...

    • Favicon of http://cofs.tistory.com BlogIcon 개발자 CofS 2018.07.23 13:08 신고 EDIT/DEL

      아직 그런 경우는 못봤네요ㅠ
      그래서 찾아봤더니 rsa javascript 라이브러리가 암호화시에 앞에 0 값이 들어간 한 바이트를 추가하는 것 같다는 글을 보았습니다.
      해당 0을 제거하니 정상작동했다고 하니 참고해보시기 바랍니다 ㅎㅎ

  • 이지성 2018.09.06 16:58 신고 ADDR EDIT/DEL REPLY

    안녕하세요~ CSRF attack, XSS 취약점에는 왜 노출되는지 자세하게 알 수 있을까요? 궁금합니다!

    • Favicon of http://cofs.tistory.com BlogIcon 개발자 CofS 2018.09.06 19:27 신고 EDIT/DEL

      두 취약점은 아주 대중적임으로 조금만 검색해 보셔도 많은 내용이 나옵니다
      협소한 답글공간에서 설명해 드리기보단 검색해보시기 바랍니다ㅎㅎ

    • 이지성 2018.09.07 09:56 신고 EDIT/DEL

      저 암호 모듈이 특정 취약점에 노출된다구 작성되어있어서요~ 어떤 결함때문에 그런지 궁금하네요.

    • Favicon of http://cofs.tistory.com BlogIcon 개발자 CofS 2018.09.07 10:19 신고 EDIT/DEL

      그 뜻이 아니라 어플리케이션에 다른보안장치들도 필요하다는 의미입니다ㅜㅜ
      글재주가없어서 죄송합니다
      rsa는 다른취약점들이 존재하는데 이는 위키에서 잘 설명하고있습니다ㅎㅎ

    • 이지성 2018.09.07 10:43 신고 EDIT/DEL

      네 알겠습니다~~ 감사합니다!^-^