본문으로 바로가기
반응형

웹 취약점 (RSA 알고리즘) 로그인 아이디, 패스워드 암호화





해커로 부터 웹 사이트를 방어하는 일은 생각보다 까다롭고 귀찮은 작업입니다.

HTTPS를 적용한다고 하더라도 인증서 교환 방식, Proxy 방식등 여러 기법들을 이용해서 해커는 사용자의 중요한 정보(패스워드, 계좌번호, 개인정보 등)를 훔쳐가게 됩니다.


여기서 다룰 내용은 완벽하게 해커가 해킹을 하지 못하게 한다기 보다는 해커가 중요정보를 가져가는 행위를 조금더 복잡하고 어렵게 하기 위한 방법입니다.

결국, 암호화라는 방식도 언젠가는 복호화가 되기 마련입니다.(키가 업더라도...) 단, 복호화에 들어가는 시간이 얼마나 걸리냐의 문제겠지요.


RSA는 비대칭 방식으로 암호화 하여 공개키(공개된 키)와, 개인키를 이용한 암호화 방식입니다.


사용자가 로그인 시 아이디, 패스워드를 RSA공개키로 암호화해서, 중간에 패킷 스니핑, 패킷 덤프를 하더라도 사용자가 입력한 아이디, 패스워드를 해석이 불가능 하게 만드는 방법 입니다.


[구현 흐름도]

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

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


저는 위에 방식과 sha256 해시 알고리즘을 이용해서 DB에 저장되는 패스워드 엮시 단방향 해시 값으로 저장을 했습니다.

데이터베이스(DB) 해킹 시 사용자의 패스워드를 알아볼 수 없도록 단방향 해시를 적용한 것입니다.



[준비물]

RSA.zip



[소스]

1) 로그인 화면 (세션을 이용하여 공개키의 문자열을 개인키로 저장 합니다)

@RequestMapping(value = "/login.do", method = RequestMethod.GET)

@Description("로그인 페이지")

public String login(Locale locale, Model model, final HttpServletResponse response, final HttpServletRequest request, final HttpSession session) throws Exception {

KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");

generator.initialize(2048);


KeyPair keyPair = generator.genKeyPair();

KeyFactory keyFactory = KeyFactory.getInstance("RSA");


PublicKey publicKey = keyPair.getPublic();

PrivateKey privateKey = keyPair.getPrivate();


// 세션에 공개키의 문자열을 키로하여 개인키를 저장한다.

session.setAttribute("_RSA_WEB_Key_", privateKey);


// 공개키를 문자열로 변환하여 JavaScript RSA 라이브러리 넘겨준다.

RSAPublicKeySpec publicSpec = 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);


return "/login";


2) 로그인 화면 (javascript) 순서를 정확히 맞춰야 함. (RSA.zip 파일 참조)

<script type="text/javascript" src="<c:url value="/resources/js/jsbn.js"/>"></script>

<script type="text/javascript" src="<c:url value="/resources/js/rsa.js"/>"></script>

<script type="text/javascript" src="<c:url value="/resources/js/prng4.js"/>"></script>

<script type="text/javascript" src="<c:url value="/resources/js/rng.js"/>"></script>


3) 로그인 화면 (html)

            <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>
 
        <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> 

위 html처럼 작업 해도 되고, 저의 경우 Ajax 방식을 이요해서 별도의 form 처리는 하지 않았습니다.



4) 로그인 버튼 클릭 시(submit)

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);


    var securedLoginForm = document.getElementById("securedLoginForm");
    securedLoginForm.securedUsername.value = securedUsername;
    securedLoginForm.securedPassword.value = securedPassword;
    securedLoginForm.submit();
}

중요한 부분은 bold 처리된 부분입니다. 사용자가 입력한 아이디, 패스워드 부분을 RSA로 암호화 해서 form submit 하는 부분 입니다.


5) 암호화된 아이디, 패스워드 복호화(java)

   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;
    }



결론적으론, 세션에 개인키를 저장하는 방식의 RSA 암호화 기법을 사용하므로 세션 해킹 기법을 이용한다면, 위에 언급한 RSA 방식도 안전하진 못하지만 세션 해킹 기법을 막기위한 token 기법과 동시에 적용 한다면, 만족한 형태의 보안 처리가 될 것 같다.


CC인증을 하기 위해서 위에 내용은 필수이므로, 반드시 로그인 처리 시 적용을 해야 한다.










반응형