javascript - java 간 RSA 를 이용해서 암호화 복호화 하기 / 암호화 로그인 / 평문전송

2017. 4. 7. 14:00language/jsp


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

 

 

  • 프로필사진
    감사합니다2017.07.11 20:47

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

  • 프로필사진
    Favicon of https://fehead.tistory.com BlogIcon 늑대랑2017.10.31 17:25 신고

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

  • 프로필사진
    오펠리시스2018.07.23 09:34

    좋은 코드 감사합니다.
    암복호화는 성공적으로 되었는데 가끔씩 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 https://cofs.tistory.com BlogIcon 개발자 CofS2018.07.23 13:08 신고

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

  • 프로필사진
    이지성2018.09.06 16:58

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

    • 프로필사진
      Favicon of https://cofs.tistory.com BlogIcon 개발자 CofS2018.09.06 19:27 신고

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

    • 프로필사진
      이지성2018.09.07 09:56

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

    • 프로필사진
      Favicon of https://cofs.tistory.com BlogIcon 개발자 CofS2018.09.07 10:19 신고

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

    • 프로필사진
      이지성2018.09.07 10:43

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

  • 프로필사진
    조영필2019.10.23 17:23

    Message too long for RSA 에러는 뭐예요..?

    • 프로필사진
      Favicon of https://cofs.tistory.com BlogIcon 개발자 CofS2019.10.23 17:56 신고

      전체 오류로그나 개발 내용을 보지않고 에러메시지 하나로만 판단하기에는 어렵네요 ^^;

  • 프로필사진
    여노2020.02.05 15:08

    rsa.encrypt(id.val()); 호출 후
    ## rsa.js파일
    var m = pkcs1pad2(text,(this.n.bitLength()+7>>3));

    위 내용 처럼 pkcs1pad2(s,n) 함수 처리시 (this.n.bitLength()+7>>3) 값이 3으로만 찍히고 있어 아래 if문에 걸려 오류가 발생 하고 있어 해결을 못하고 있는 상황입니다. 조언 좀 부탁 드립니다.

    if(n < s.length + 11) { // TODO: fix for utf-8
    alert("Message too long for RSA");
    return null;
    }

    • 프로필사진
      Favicon of https://cofs.tistory.com BlogIcon 개발자 CofS2020.02.05 15:23 신고

      rsa.js 파일은 손대는 것이 아닙니다만???;;;;

    • 프로필사진
      여노2020.02.05 15:33

      rsa.js 파일을 수정한것은 없습니다. 값을 alert으로 찍어보기만한거라 alret으로 Message too long for RSA 메시지가 찍히길래 어떤부분에서 해당 alert값을 찍는지만 확인해 본거에요.

      출처: https://cofs.tistory.com/297 [CofS]

    • 프로필사진
      Favicon of https://cofs.tistory.com BlogIcon 개발자 CofS2020.02.05 17:46 신고

      예상하건데 흠...
      예제를 그대로 하셨으면 문제는 없을거라고 생각이 듭니다.
      설정하는 부분이나 사용하는 부분에서 다른점이 있는지부터 찾아보는게 맞을 것 같습니다.
      혹시나 버전문제라던지 버그가 있다면 피드백 부탁드리겠습니다 !!!

  • 프로필사진
    감사합니다2020.06.30 11:47

    안녕하세요. 일단 너무감사드립니다.
    참고할 소스가 있어 이렇게 사용까지 하게 되다니,,

    다른 개발자 분들께서도 말씀주셨던 내용이지만,
    동일하게 이런 error가 나오고있네요.
    Cannot read property 'length' of undefined

    예제에있는 내용을 그대로 대입하여 실행했는데 이렇게 나와서 조금당황스럽습니다 ㅠ
    rsa.encrypt(id.val()); 호출 후
    ## rsa.js파일
    var m = pkcs1pad2(text,(this.n.bitLength()+7>>3));

    위 내용 처럼 pkcs1pad2(s,n) 함수 처리시 (this.n.bitLength()+7>>3) 값이 3으로만 찍히고 있어 아래 if문에 걸려 오류가 발생 하고 있어 해결을 못하고 있는 상황입니다. 조언 좀 부탁 드립니다.

    if(n < s.length + 11) { // TODO: fix for utf-8
    alert("Message too long for RSA");
    return null;
    }

    여노님처럼 저도 이런상황인데, 따로 js file은 건들지 않았습니다.
    감이 안잡히는데 방향성이라도 제시해주시면 안될까요?
    왜 이런 에러가 나오는지,,

    감사합니다. 점심 맛있게드세용

    • 프로필사진
      Favicon of https://cofs.tistory.com BlogIcon 개발자 CofS2020.06.30 13:22 신고

      안녕하세요 ㅎㅎ
      같은 문제가 또한분 발생하네요 ㅠㅠ
      제가 지금 테스트를 해볼 상황은 아니라서 정확하진 않지만 혹시나 하고 답변드립니다.

      로그인 페이지에서 rsa.setPublic()
      함수 호출 할 때 RSAModulus, RSAExponent 값이 정상적으로 로딩된 상태에서 호출하는지 확인해볼 수 있을 것 같습니다ㅠㅠ...

    • 프로필사진
      감사합니다.2020.06.30 13:36

      안녕하세요. 오래전글임에도 바로바로 피드백 주셔서 감사합니다.

      방향성을 잡아주셔서 감사합니다.

    • 프로필사진
      Favicon of https://cofs.tistory.com BlogIcon 개발자 CofS2020.06.30 13:38 신고

      혹시라도 다른 문제가 또 있다면 알려주세요 ㅎㅎ 다시한번 보겠습니다.

      해결되시면 피드백도 한번 부탁드립니다 ㅎㅎ 감사합니다 ^^

  • 프로필사진
    생강차2020.07.21 15:30

    안녕하세요 글 감사합니다.
    내용을 따라하다가 저도 Message too long for RSA 에러에 걸렸네요.
    해결하신 분은 조언 좀 부탁드립니다.

    • 프로필사진
      Favicon of https://cofs.tistory.com BlogIcon 개발자 CofS2020.07.21 15:52 신고

      Message too long for RSA 이슈가 좀 있네요 ㅠㅠㅠ
      해당 상황을 좀 재현 가능하면 도와드릴텐데 ㅠㅠ....

    • 프로필사진
      BlogIcon 세상살이2020.08.25 09:49

      저도 이문제 때문에 찾아보니 암호 키 보다 큰 문자열을 암호화 하려 할때 생기는 문제라고 나오네요
      키 생성시 1024를 2048로 줘도 마찬가지 입니다
      결론은 문자열이 긴 경우에는 잘라서 암호화 해야 한다는 결론에....
      인터넷 뒤져보니 이 문제는 당연한 문제라고 나오네요...

    • 프로필사진
      Favicon of https://cofs.tistory.com BlogIcon 개발자 CofS2020.08.25 10:12 신고

      그런 이슈가 있었군요...

      피드백 감사합니다 ㅎㅎ!!!

    • 프로필사진
      BlogIcon 세상살이2020.08.25 10:21

      긴 문자열의 암호화 관련은 https://mohading.tistory.com/m/3
      이분의 글을 참고 하시는게 좋을듯 합니다
      RSA 암호키의 길이가 245 바이트 이기에 이 길이를 넘어가는 문자열은 에러가 난다고 하네요
      저도 이 블로그 보고 도움을 얻다 에러가 나서 확인 해보니 이런 문제도 있네요
      저도 이번에 많은걸 배워 갑니다

  • 프로필사진
    UnkNowN2020.07.31 14:22

    안녕하세요.

    위에 댓글 중에 crypto.BaddPaddingException 관련해서

    "rsa javascript 라이브러리가 암호화시에 앞에 0 값이 들어간 한 바이트를 추가하는 것 같다는 글을 보았습니다.
    해당 0을 제거하니 정상작동했다고 하니 참고해보시기 바랍니다 ㅎㅎ"

    라는 대댓을 달아주셨는데요.
    0 값이 어디에 들어가는 것이고 어떻게 빼야하는지 알 수 있을까요? ㅠㅠ

    • 프로필사진
      Favicon of https://cofs.tistory.com BlogIcon 개발자 CofS2020.07.31 14:43 신고

      안녕하세요ㅎㅎ
      0값이 들어가는 곳은 암호화된 문자열이고 해당증상이 0이추가되서 나는 에러라면 직접 0을 제거해보심이 어떨지 싶습니다ㅎㅎ

  • 프로필사진
    2020.09.20 14:21

    위 댓글들 중 Cannot read property 'length' of undefined에 대한 에러로 허덕이다가 해결방안을 찾았습니다.

    var encUserPw = rsa.encrypt(userPw)

    encrypt 함수 호출 시 아래와 같이 String형으로 지정해서 넘기시기 바랍니다.

    var encUserPw = rsa.encrypt(String(userPw))

    타입이 제대로 정해지지 않아서 length인식을 못하는 문제였습니다.