S>아래 내용은 보안상 검증된 것이 아닙니다. 잘 확인하고 사용하세요.

웹 페이지에서 SSL 없이 RSA 암호화 로그인 하기

사용자의 비밀번호를 전송할 때는 SSL 등의 처리를 하지 않으면 해당 비밀번호를 중간에 가로채서 보는 것이 가능하다. 그러나 비영리 싸이트 혹은 SSL 인증서 구매가 어려운 경우에 JavaScript로 RSA 암호화를 이용해서 암호화된 로그인이 가능하다.

RSA는 비대칭 방식으로 암호화는 공개키(누구나 볼 수 있다)로 하고 복호화는 개인키를 가진쪽만 가능한 형태이다.

사용자가 로그인 폼을 채우면 사용자 ID와 비밀번호를 RSA 공개키로 암호화해서 전송하여, 중간에 패킷을 가로채도 해석이 불가능하게 만드는 것이다.

 

이와 같은 것을 구현하고 싶어진 계기는 회사 내부적으로 사용하는 운영용 싸이트의 로그인 정보를 암호화해야 겠다는 생각이 들었기 때문이다. 그러면서  돈 안 쓰고 특정 브라우저에 종속되지 않으면서도 안전한 사이트 이용을 가능하게 하는 방법을 찾다가 메가박스 홈페이지가 2010년 6월 현재 SSL과 ActiveX없이 그렇게 구현돼 있는 것을 보았다.

이와 같은 로그인 및 데이터 전송 방식에 대해서는 알아야 막는다 자바 JSP 해킹과 보안 책에서 정보를 얻어서 BigIntegers and RSA in JavaScript 라이브러리를 사용하였다.

참고로 해당 라이브러리에서 제공하는 BASE64 인코더에 문제가 있는 것으로 보인다. 책에서 소개한대로 이 BASE64 인코딩 라이브러리를 사용하면 FireFox에서는 오동작을 한다. 그래서 BASE64를 사용하지 않고 암호화된 바이트 배열을 그냥 16진 문자열(hex)로 서버에 전송한다.

알아야 막는다 자바 JSP 해킹과 보안 책의 방식대로 하면 세션에 무한정 PrivateKey를 저장해서 메모리 누수를 일으킬 수 있다. 이 책 그대로 따라하면 안된다.

 

사실 그냥 HTTPS 사용하면 더 안전하게 아무 처리 없이 다 해결되는 문제들이다. 하지만 내부 적으로만 사용하는 서비스들에 대해서 일일이 다 인증서를 등록할 수는 없으니 지금 소개하는 방식이 가장 쉽고 돈 안들이면서 안전하게 로그인 할 수 있는 방법이 되어 줄 것이다.

 

기본 작동 원리

  1. [서버] 서버측에서 RSA 공개키와 개인키(비밀키)를 생성하여, 개인키는 세션에 저장하고 공개키는 자바스크립트 로그인 폼이 있는 페이지로 출력한다.
  2. [클라이언트] 로그인폼은 자바스크립트가 실행되지 않는 환경에서는 발행(submit)이 되면 안된다.
  3. [클라이언트] 로그인폼에 사용자의 ID와 비밀번호를 넣고 발행을 누르면 자바스크립트가 이를 가로챈다.

    1. 사용자 ID를 RSA로 암호화하여 화면에 안보이는 새로운 폼에 저장한다.
    2. 비밀번호를 RSA로 암호화하여 화면에 안보이는 새로운 폼에 저장한다.
    3. 이제 화면에 안보이는 해당 폼을 서버로 발행한다.
  4. [서버] 서버측에서 세션에 저장된 RSA개인키로 암호화된 사용자ID와 비밀번호를 복호화한다.
  5. [서버] 데이터베이스/혹은 기타 저장소에 저장된 사용자 ID와 비밀번호가 일치하는지 확인한다.

 

주의할 점. 암호화 된 값은 byte 배열이다. 이를 문자열 폼으로 전송하기 위해 16진 문자열(hex)로 변경한다. 서버측에서도 값을 받을 때 hex 문자열을 받아서 이를 다시 byte 배열로 바꾼 뒤에 복호화 과정을 수행한다.

 

Java 로그인 폼 측 키생성

로그인 폼을 보여주는 화면을 출력할 때, 그와 동시에 공개키와 개인키를 생성해서 공개키는 HTML/Javascript에서 접근 가능하게 노출하고 개인키는 HTTP 세션에 보관하여 암호를 풀 때 사용하도록 처리한다.

  1. KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
    generator.initialize(KEY_SIZE);

    KeyPair keyPair = generator.genKeyPair();
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");

    PublicKey publicKey = keyPair.getPublic();
    PrivateKey privateKey = keyPair.getPrivate();


    HttpSession session = request.getSession();
    // 세션에 공개키의 문자열을 키로하여 개인키를 저장한다.
    session.setAttribute("__rsaPrivateKey__", privateKey);

    // 공개키를 문자열로 변환하여 JavaScript RSA 라이브러리 넘겨준다.
    RSAPublicKeySpec publicSpec = (RSAPublicKeySpec) keyFactory.getKeySpec(publicKey, RSAPublicKeySpec.class);

    String publicKeyModulus = publicSpec.getModulus().toString(16);
    String publicKeyExponent = publicSpec.getPublicExponent().toString(16);

    request.setAttribute("publicKeyModulus", publicKeyModulus);
    request.setAttribute("publicKeyExponent", publicKeyExponent);

    request.getRequestDispatcher("/WEB-INF/views/loginForm.jsp").forward(request, response);

 

loginForm.jsp 에서 실제로 폼을 출력하는 역할을 한다.

 

HTML 폼

  1.         <!-- script 태그에서 가져오는 자바스크립트 파일의 순서에 주의해야한다! 순서가 틀릴경우 자바스크립트 오류가 발생한다. -->
            <script type="text/javascript" src="<%=request.getContextPath()%>/js/rsa/jsbn.js"></script>
            <script type="text/javascript" src="<%=request.getContextPath()%>/js/rsa/rsa.js"></script>
            <script type="text/javascript" src="<%=request.getContextPath()%>/js/rsa/prng4.js"></script>
            <script type="text/javascript" src="<%=request.getContextPath()%>/js/rsa/rng.js"></script>

            <script type="text/javascript" src="<%=request.getContextPath()%>/js/login.js"></script>
        </head>
        <body>
            <div>
                <label for="username">사용자ID : <input type="text" id="username" size="16"/></label>
                <label for="password">비밀번호 : <input type="password" id="password" size="16" /></label>
                <input type="hidden" id="rsaPublicKeyModulus" value="<%=publicKeyModulus%>" />
                <input type="hidden" id="rsaPublicKeyExponent" value="<%=publicKeyExponent%>" />

                <a href="<%=request.getContextPath()%>/loginFailure.jsp" onclick="validateEncryptedForm(); return false;">로그인</a>
            </div>
            <form id="securedLoginForm" name="securedLoginForm" action="<%=request.getContextPath()%>/login" method="post" style="display: none;">
                <input type="hidden" name="securedUsername" id="securedUsername" value="" />
                <input type="hidden" name="securedPassword" id="securedPassword" value="" />
            </form>

        </body>

 

폼을 이중으로 만들었는데, 이유가 있다. 폼에 submit 버튼을 두게되면 사용자가 그냥 엔터를 쳐도 폼이 제출되게 된다. 이렇게 되면 사용자의 웹 브라우저가 Javascript를 지원하지 않아도 폼이 제출되므로 사용자가 쓴 아이디와 비밀번호가 그냥 전송되게 돼 버린다.

사용자가 ID와 비밀번호를 친 뒤에 무조건 Javascript를 타게 만들기 위해 입력용 폼과 Javascript로 암호화하여 실제로 제출하는 폼을 분리한 것이다.

만약 사용자의 브라우저가 javascript를 지원하지 않는다면, 로그인 링크의 loginFailure.jsp 페이지가 보여지게 된다.

 

자바스크립트 암호화 처리

  1. function validateEncryptedForm() {
        var username = document.getElementById("username").value;
        var password = document.getElementById("password").value;
        if (!username || !password) {
            alert("ID/비밀번호를 입력해주세요.");
            return false;
        }

        try {
            var rsaPublicKeyModulus = document.getElementById("rsaPublicKeyModulus").value;
            var rsaPublicKeyExponent = document.getElementById("rsaPublicKeyExponent").value;
            submitEncryptedForm(username,password, rsaPublicKeyModulus, rsaPublicKeyExponent);
        } catch(err) {
            alert(err);
        }
        return false;
    }

    function submitEncryptedForm(username, password, rsaPublicKeyModulus, rsaPpublicKeyExponent) {
        var rsa = new RSAKey();
        rsa.setPublic(rsaPublicKeyModulus, rsaPpublicKeyExponent);

        // 사용자ID와 비밀번호를 RSA로 암호화한다.
        var securedUsername = rsa.encrypt(username);
        var securedPassword = rsa.encrypt(password);

        // POST 로그인 폼에 값을 설정하고 발행(submit) 한다.
        var securedLoginForm = document.getElementById("securedLoginForm");
        securedLoginForm.securedUsername.value = securedUsername;
        securedLoginForm.securedPassword.value = securedPassword;
        securedLoginForm.submit();

 

 

실질적인 암호화는 submitEncryptedForm에서 이뤄진다. 공개키의 rsaPublicKeyModulus와 rsaPublicKeyExponent 값을 읽어 RSA 객체를 구성하고 이를 가지고 사용자의 ID와 비밀번호를 모두 암호화하여 전송용 폼에 암호화된 값을 지정하고 폼을 제출(submit)한다.

 

Java 측 복호화하여 사용자 ID,비밀번호 확인

  1.     /**
         * 암호화된 비밀번호를 복호화 한다.
         */
        protected void processRequest(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            String securedUsername = request.getParameter("securedUsername");
            String securedPassword = request.getParameter("securedPassword");

            HttpSession session = request.getSession();
            PrivateKey privateKey = (PrivateKey) session.getAttribute("__rsaPrivateKey__");
            session.removeAttribute("__rsaPrivateKey__"); // 키의 재사용을 막는다. 항상 새로운 키를 받도록 강제.


            if (privateKey == null) {
                throw new RuntimeException("암호화 비밀키 정보를 찾을 수 없습니다.");
            }
            try {
                String username = decryptRsa(privateKey, securedUsername);
                String password = decryptRsa(privateKey, securedPassword);
                request.setAttribute("username", username);
                request.setAttribute("password", password);
                request.getRequestDispatcher("/WEB-INF/views/login.jsp").forward(request, response);
            } catch (Exception ex) {
                throw new ServletException(ex.getMessage(), ex);
            }
        }

        private String decryptRsa(PrivateKey privateKey, String securedValue) throws Exception {
            System.out.println("will decrypt : " + securedValue);
            Cipher cipher = Cipher.getInstance("RSA");
            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;
        }

 

사용자의 로그인 정보를 받아서 실제 사용자가 맞는지 확인하는쪽 컨트롤러이다. 여기서는 그냥 사용자의 입력을 화면에 다시 출력해주도록 만들었다.

HTTP 세션에서 앞서 저장한 개인키를 읽는다. 만약 변조된 공개키로 암호화를 했거나 개인키가 존재하지 않는 상황이라면 물론 오류가 발생한다.

그리고 세션에서 개인키를 지워버린다. 절대로 동일 개인키로 두 번이상 로그인 할 수 없게 만들었다.

 

어떻게 전송되나

아래 이미지는 실제 전송되는 데이터를 Charles 라는 HTTP 프록시 툴로 살펴본 것이다. 사용자의 ID와 비밀번호가 모두 암호화되어 원형을 알아 볼 수 없음을 볼 수 있다.

 

 

이상이 끝. 관련 파일을 모두 함께 올려둔다. WAR 는 실행하고서 http://localhost:8080/RSATest 로 접속하면 된다.


RSATest.war

 

기타

사실 정말로 원한 것은 AES로 사용자의 정보를 암호화하고 AES키를 RSA공개키로 암호화하여 서버에 전달 한뒤, 서버에서 RSA 개인키로 AES키를 복호화해 알아내고, 그 AES키로 다시 사용자 정보를 복호화하는 이중 암호화를 시도했었다.

그러나 그닥 녹록치 않아서, 일단 RSA 기반의 단일 암호화로 만족한 상태이다.


1. 제가 간편장부 작성해서 신고할시 이런 부분들 감안해서

신고시 식대,차량유지,소모품 구입 배우자 공제 등등 카드 내역 다 뒤져서  신고하는게 나을까요??

아니면 그냥 세무서에 돈주는거 감안해도 맡기는 편이 나을까요?

(세무서에 맡길시 비용 15마넌 정도 한다는 군요....)

 

프리랜서의 경우 2012년 소득으로 2013년의 종합소득세 신고 유형(단순경비율과 간편장부, 기준경비율) 여부를 결정합니다. 2012년에 2,400만원 이상의 사업소득이 있으셨다면 간편장부or 기준경비율로 신고하셔야 합니다.

 

즉 신고 방법에는 단순경비율, 기준경비율, 간편장부 세가지가 있다고 보셔야 합니다.

 

프리랜서의 경우 주로 단순경비율 또는 간편장부로 신고하게 됩니다.

 

2.세무서에 맡길경우 제가 낸 서류(신용카드내역 등)로 세무서에서 간편장부 이런거 작성해서 신고 해주는 건가요??   아니면 단순 기준경비율로 계산해서 신고만 해주는 건가요??

 

> 1번에서 말했듯 2012년 사업소득을 기준으로 신고유형을 정한 후 각 방법을 비교하여 세금이 적게나오는 방법으로 신고합니다.

 

3. 대략적인 기준경비율로 했을 경우와 간편장부로 했을경우, 세무서에 맡겼을경우 차이를 알고 싶습니다.

 

> 세무사에게 맡기셔도 큰 차이는 없을 것 입니다. 다만, 신고하시면서 생기는 귀찮음, 잘못 신고했을 경우의 가산세 등을 생각하신다면 맡기는 것이 마음 편할 것 입니다. 판단은 질문자에게 맡깁니다.

 

4. 환급 가능 금액이 어느정도 될까요?

(물론 정확한 금액이 힘들겠지만 대략적으로 오차 포함해서 알고 싶습니다.)

 

> 이미납부한 세금 : 4,400만원 x 3.3% = 1,452,000원

  

2012년 사업소득이 2,400만원 미만 (프로그래머 가정 단순경비율)

  

사업소득금액 : 44,000,000원

(-) 필요경비  : 40,000,000x64.1% + 4,000,000x46.1% = 27,484,000원 (4천만원 초과분에 초과율 적용)

종합소득금액 : 16,516,000

(-) 기본공제  : 3,000,000원 (본인1, 배우자1) 

과세표준       : 13,516,000

산출세액       : 947,400

기납부세액    : 1,452,000

납부(환급)세액 : (504,600)  대략 계산으로 2013년 경비율의 변동, 공제에 따라 오차가 있을 수 있습니다.

 

2012년 사업소득에 2,400만원 이상 (간편장부 or 기준경비율)

 

사업소득금액 : 44백만원

(-) 필요경비  : '신용카드 내역등 서류 금액합계'의 크기에 따라 위와 같은 과정을 통해 세액이 결정 됩니다.

 

* 서류 금액의 합계가 단순경비율의 경우보다 크다면 더 많은 세금을 환급받고

   작다면 더 적은 세금을 환급받거나 납부해야할 수 도 있습니다.

 

도움이 되셨으면 합니다. 

http://jungle-e.tistory.com/335

* 나중에 다시 추가 하여 정리하나 기본적인 내용이니 까먹지말자...



request.setAttribute() 와 request.getAttribute()


request.setParameter() 와 getParameter()를 이용하면 String의 값 밖엔 받을 수 없다. 


List를 받기 위해서는 setAttribute()와 getAttribute()를 써야 한다.

 이때 type이 Object 이기 때문에 반드시 형변환을 해줘야 한다.

 

혹시나 해서 문법도 쓴다.

 action에서 객체를 request에 담을 때.

request.setAttribute("객체명", 객체);

 

이렇게 해서 jsp를 호출하면 jsp에서 "객체명"을 이용해서 객체를 받을 수 있다.


< %

Object x = request.getAttribute("객체명");

% >

 

 > Object 형으로 받는 다는 것. 핵심 포인트.




* java to jsp : 


java: 

String str = "안녕하세요"

int num = 100;

request.setAttribute("STR",str);   //Attribute 에 호출명 STR 로 저장 

request.setAttribute("NUM",num);   //Attribute 에 호출명 NUM 로 저장

jsp : ${STR} // ${NUM}     

출력 화면: 안녕하세요 100


* jsp to java : 



function fn_submit(){

var form = document.test;

form.action = "addressSearch.do";

form.submit();

}

jsp: <input type="button" value="서브밋----" onclick="fn_submit()" />

     <form name="test" id="test" method="post">

<input type="text" name="test1" value="testtest11" />

<input type="text" name="test2" value="testtest22" />

<input type="text" name="test3" value="testtset33" />

     </form>


jsp: ajax : url: java.do? 에 값을 붙여준다.



java: String testx1 = request.getParameter("test1");

         String testx2 = request.getParameter("test2");

         String testx3 = request.getParameter("test3");



ajax post(json) 형식 값넘길때 한글 깨짐 현상.


JSP

var inputValue = $("#addressSearchBox").val();

inputValue = escape(encodeURIComponent(inputValue));


JAVA

String searchValue = java.net.URLDecoder.decode(request.getParameter("searchValue"),"UTF-8");     


++++

String alpha = LeftMenu.getAlpha(); //클래스명.함수명();

System.out.println(alpha);


public class LeftMenu {

public static String getAlpha() throws DoException {

//static 변수형 함수명(받는값);

String def = "get!!!";

return def;//리턴

}


++


파라메터 받기

String addressType =  request.getParameter("addressType"); // 지번,도로명 라디오버튼 값

String authority_code = "40"; //권한 osp:20, sp:30, cp:40


값 저장

request.setAttribute("authority_code", authority_code);

+ Recent posts