웹 페이지에서 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와 비밀번호가 모두 암호화되어 원형을 알아 볼 수 없음을 볼 수 있다.

 

rsa_login_charles.png

 

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

RSATest.war LoginServlet.java LoginFormServlet.java

 

기타

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

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


출처 - http://kwon37xi.egloos.com/4427199






개요

일반적인 form 전송 방식은 모든 데이타가 인터네상에 깔끔하게 보이게 된다. 즉, 비밀번호 1234를 입력하면 1234을 그대로 알 수가 있다. 항상 이 부분을 찜찜하게 생각하고 있었는데 일반적인 해결책은 HTTPS를 이용하면 되는데 호스팅 업체에서 이걸 지원 해줄 리도 없기에 그냥 아쉬운 데로 그냥 사용하고 있었다.

 

회사에 security 관련 업무를 잠깐 할 일이 있었는데 그 일을 하면서 RSA 라는 것을 접하게 되었고 그걸 적용하면 이 문제를 해결 할 수 있을 것 같아 적용해 보았다. 여기서 사용한 방식은 단순히 재미로 구현 한 것으로 검증절차나 보안상에 취약점에 대한 고려는 전혀 하지 않은 것임을 알아줬으면 한다.

 

기본 개념

암호화에 대한 정의나 알고리즘들은 인터넷상에서 쉽게 찾을 수 있고 제가 여기선 그런 개념을 언급 하는 것 또한 무의미 할 것이다. 사실은 누구에게 설명 해줄만큼 잘 알질 못한다. 아무 개념도 없다면 RSA, 공개키, 개인키, 쌍키, AES, DES  등으로 인터넷 검색을 해 보면 될 것이다.

여기서는 구현을 위해 찾아두었던 두었던 사이트 링크로 대신한다.

http://wiki.kldp.org/wiki.php/DocbookSgml/SSL-Certificates-HOWTO SSL 인증서 HOWTO

http://kwon37xi.egloos.com/4427199 RSA기반 웹페이지 암호화 로그인 : 내가 하고자 하던 내용에 대한 기본 작업, 마지막 항목에 나와 있는 이중 암호화를 추가함

http://ohdave.com/rsa/ 자바 스크립트로 구현한 RSA

http://www.fourmilab.ch/javascrypt/ 자바 스크립트 기반 암호화 툴들

http://people.eku.edu/styere/ 자바스크립트 기반 암호화 예제

http://www-cs-students.stanford.edu/~tjw/jsbn/ RSA, BigInteger 소스 : 이곳 자바스크립트 소스를 RSA Encrypt용으로 사용함

http://www.movable-type.co.uk/ 자바스크립트 암호화 : TEA 라는 암호화 방식으로 이중암호화용으로 사용함

 

전체 구조

클라이언트에서 form 데이타로 전송되는 text data를 보호하기 위하여 TEA(Tiny Encryption Algorithm)을 이용하여 암호화 하였다. 이때 암호화를 위한 키는 클라이언트(자바스크립트)에서 랜덤하게 생성을 한다.

TEA 암호화는 대칭키 알고리즘이기에 암호화한 키를 이용하여 복호화가 가능하다. 그렇기 때문에 이렇게 생성된 키를 RSA Public Key를 이용하여 암호화를 한 후 키를 서버로 전달한다.  이때 사용 된 RSA Public Key는 로그인 폼 페이지 요청시 서버에서 클라이언트로 전달이 된다. 서버에는 이 Public Key에 해당되는 Private Key를 세션에 저장을 해 두고 클라이언트에서 Request가 오면 복호화에 이용하다.

 

왜 굳이 RSA Public Key를 이용하여 암호화를 하지 않느냐는 의문이 들 수 있는데 - 내가 들었기에.. 그래서 대략 찾아본 것임, 정확한지는 모르겠음, 의심이 되면 찾아보시길 - RSA키는 공개키 방식으로 암호화, 복호화 연산 수행시간이 오래 걸린다. 단순히 로그인과 같은 짧은 텍스트인 경우에는 크게 문제가 되지 않지만 그 크기가 커질 수록 수행시간이 오래 걸린다. 

 

전첵적인 흐름은 대략 다음과 같다. 구글 Docs에 있는 Drawing 도구를 이용해 보았다.

 

 

구현

구현함에 있어 항상 고려해야 할 대상은 클라이언트는 자바스크립트를 사용하고 서버는 자바를 이용해서 구현을 해야 한다는 것이다.

이러한 고려 사항 때문에 AES가 아닌 TEA라는 암호화를 사용하였다. 자바스크립트, 자바 둘 다 암호화 관련된 지식과 소스를 갖고 있질 않아 가장 구현하기 쉬운 것을 선택하였다. 아무래도 AES가 TEA보다 보안레벨이 높겠지만 여기서는 그 정도 수준까지 필요하지도 않고 단순히 인터넷상에서 내가 보내는 데이타가 보이지만 않을 정도이면 만족한다.

 

TEA 암호화

http://www.movable-type.co.uk/scripts/tea-block.html

위의 사이트 소스를 라이브러리로 하여 Tea.encrypt 함수를 이용하였으며 다음과 같이 키를 생성 함수를 추가하여 암호화를 하였다.

1
2
3
4
5
6
7
8
9
function GenerateKey(){
    time = new Date().getTime();
    random = Math.floor(65536*Math.random());
    return (time*random).toString();   
}
 
function EncryptTEA(k, text){  
    return Tea.encrypt(text, k);   
}

TEA를 이용한 가장 큰 이유가 서버에서의 복호화 과정을 쉽게 구현 할 수 있었기 때문이다. 자바스크립트 소스를 이용하여 자바 소스를 생성하였다.

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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import java.util.Arrays;
import ks.util.Base64;
 
public class TEA {
 
    private final int delta = 0x9E3779B9;
 
    private int[] S = new int[4];
 
    /**
     * Initialize the cipher for encryption or decryption.
     * @param key a 16 byte (128-bit) key
     */
    public TEA(byte[] key) {
        if (key == null)
            throw new RuntimeException("Invalid key: Key was null");
        if (key.length < 16)
            throw new RuntimeException("Invalid key: Length was less than 16 bytes");
        for (int off=0, i=0; i<4; i++) {
            S[i] = ((key[off++] & 0xff)) |
            ((key[off++] & 0xff) <<  8) |
            ((key[off++] & 0xff) << 16) |
            ((key[off++] & 0xff) << 24);
        }
 
//      System.out.println("KEY:" + Arrays.toString(S));
    }
     
    public TEA(String key){
        this(key.getBytes());
    }
 
    /*
     * encrypt text using Corrected Block TEA (xxtea) algorithm
     *
     * @param {string} plaintext String to be encrypted (multi-byte safe)
     * @param {string} password  Password to be used for encryption (1st 16 chars)
     * @returns {string} encrypted text
     */
    public byte[] encrypt(byte[] clear){
 
        int[] v = strToLongs(clear);       
        int n = v.length;
 
        // ---- <TEA coding> ----
        int z = v[n-1];
        int y = v[0];
 
        int mx, e;
        int q = 6 + 52/n;
        int sum = 0;
 
        while (q-- > 0) {  // 6 + 52/n operations gives between 6 & 32 mixes on each word
            sum += delta;
            e = sum>>>2 & 3;
            for (int p = 0; p < n; p++) {
                y = v[(p+1)%n];
                mx = (z>>>5 ^ y<<2) + (y>>>3 ^ z<<4) ^ (sum^y) + (S[p&3 ^ e] ^ z);
                z = v[p] += mx;
            }
        }
        // ---- </TEA> ----
        return longsToStr(v);
    }
 
    /*
     * decrypt text using Corrected Block TEA (xxtea) algorithm
     *
     * @param {byte[]} ciphertext byte arrays to be decrypted
     * @returns {byte[]} decrypted array
     */
    public byte[] decrypt(byte[] crypt){
        int[] v = strToLongs(crypt);
        int n = v.length;
 
        // ---- <TEA decoding> ----
        int z = v[n-1];
        int y = v[0];
 
        int mx, e;
        int q = 6 + 52/n;
        int sum = q*delta;
 
        while (sum != 0) {
            e = sum>>>2 & 3;
            for (int p = n-1; p >= 0; p--) {
                z = v[p>0 ? p-1 : n-1];
                mx = (z>>>5 ^ y<<2) + (y>>>3 ^ z<<4) ^ (sum^y) + (S[p&3 ^ e] ^ z);
                y = v[p] -= mx;
            }
            sum -= delta;
        }
 
        // ---- </TEA> ----
         
        byte[] plainBytes = longsToStr(v);
         
        // strip trailing null chars resulting from filling 4-char blocks:
        int len;       
        for(len=0; len<plainBytes.length; len++){
            if(plainBytes[len] == 0)
                break;
        }
         
        byte[] plainTrim = new byte[len];
        System.arraycopy(plainBytes, 0, plainTrim, 0, len);
                 
        return plainTrim;
    }
     
     
    /*
     * decrypt text using Corrected Block TEA (xxtea) algorithm
     *
     * @param {string} ciphertext String to be decrypted
     * @returns {string} decrypted text
     */
    public String decrypt(String ciphertext){
        String plainText = null;       
        byte[] plainTextBytes = decrypt(Base64.decode(ciphertext));
        try{
            plainText = new String(plainTextBytes, "UTF-8");
        } catch(Exception e){}
         
        return plainText;      
    }
 
    private int[] strToLongs(byte[] s) {  // convert string to array of longs, each containing 4 chars
        // note chars must be within ISO-8859-1 (with Unicode code-point < 256) to fit 4/long
        int[] l = new int[(s.length + 3)/4];
 
        for (int i=0; i<l.length; i++) {
            // note little-endian encoding - endianness is irrelevant as long as
            // it is the same in longsToStr()
            l[i] = (s[i*4+0]&0xff)<<0 |
            (s[i*4+1]&0xff)<<8 | 
            (s[i*4+2]&0xff)<<16 |
            (s[i*4+3]&0xff)<<24;
        }
 
        return l;  // note running off the end of the string generates nulls since
    }
 
    private byte[] longsToStr(int[] l){ // convert array of longs back to string
        byte[] a = new byte[l.length*4];
 
        for (int i=0; i<l.length; i++) {
            a[i*4+0] = (byte)((l[i]>>0)&0xff);
            a[i*4+1] = (byte)((l[i]>>8)&0xff);
            a[i*4+2] = (byte)((l[i]>>16)&0xff);
            a[i*4+3] = (byte)((l[i]>>24)&0xff);
        }
 
        return a;
    }
}

 

Base64 소스

 

RSA 암호화

RSA 키는 클라이언트 세션이 이루어질 때 생성을 한다. 자바에서 RSA 관련함수를 제공하는 패키지가 존재하는데 여기서는 BigInteger Class만을 이용하여 구현하여 사용하였다.

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
import java.math.BigInteger;
import java.security.SecureRandom;
 
public class RSAKey {
    private BigInteger privateExponent;
    private BigInteger publicExponent;
    private BigInteger modulus;
 
    public RSAKey(BigInteger d, BigInteger e, BigInteger m){
        this.privateExponent = d;
        this.publicExponent = e;
        this.modulus = m;
    }
 
    public BigInteger getPrivateExponent(){ return this.privateExponent; }
    public BigInteger getPublicExponent(){ return this.publicExponent; }
    public BigInteger getModulus(){ return this.modulus; }
     
    public static RSAKey generate(int nbit){
        // generate an N-bit (roughly) public and private key
 
        SecureRandom random = new SecureRandom();
        BigInteger one      = new BigInteger("1");
 
        BigInteger p = BigInteger.probablePrime(nbit/2, random);
        BigInteger q = BigInteger.probablePrime(nbit/2, random);
        BigInteger phi = (p.subtract(one)).multiply(q.subtract(one));
 
        BigInteger m = p.multiply(q);
        BigInteger e = new BigInteger("65537");     // common value in practice = 2^16 + 1
        BigInteger d = e.modInverse(phi);
         
        return new RSAKey(d, e, m);
    }
     
    public static String toHex (BigInteger value) {
        byte b[] = value.toByteArray();
         
        StringBuffer strbuf = new StringBuffer(b.length * 2);
        int i;
 
        for (i = 0; i < b.length; i++) {
            if (((int) b[i] & 0xff) < 0x10)
                strbuf.append("0");
 
            strbuf.append(Long.toString((int) b[i] & 0xff, 16));
        }
 
        return strbuf.toString();
    }
}

 

클라이언트측의 RSA 암호화 관련된 함수는 http://www-cs-students.stanford.edu/~tjw/jsbn/ 를 참조하여 구현하였다.

서버측에서 복호화는 다음과 같이 구현된 함수를 이용하였다.

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
import java.math.BigInteger;
 
public class RSA {
 
    private RSAKey key;
     
    public RSA(int nbit){
        key = RSAKey.generate(nbit);
    }
     
    public RSA(){
        this(1024);
    }
     
    public RSA(RSAKey key){
        this.key = key;
    }
         
    public String decrypt(String encText){
        BigInteger modulus = key.getModulus();
        BigInteger privateExponent = key.getPrivateExponent();
          
        BigInteger enc = new BigInteger(encText, 16);
        BigInteger dec = enc.modPow(privateExponent, modulus);
                 
        String plainText =  new String(dec.toByteArray());
         
        return plainText;
    }
 
    public static String asHex (byte buf[]) {
        StringBuffer strbuf = new StringBuffer(buf.length * 2);
        int i;
 
        for (i = 0; i < buf.length; i++) {
            if (((int) buf[i] & 0xff) < 0x10)
                strbuf.append("0");
 
            strbuf.append(Long.toString((int) buf[i] & 0xff, 16));
        }
        return strbuf.toString();
    }  
}

 

 

마무리

단순히 붙여 넣기만 하는데도 생각보다 오랜 시간이 소요가 되었으며 에디터가 생각만큼 제대로 동작을 하지 않는다. 소스 하이라이트 기능과 목록 기능을 이용하기에는 많이 부족한 듯 하다.
 
차후에 기회가 된다면 다시 한번 정리를 할 것이며 혹시라도 이런 구조를 이용해보고 싶은 분이 있으시다면 여기서 사용된 전체 소스들을 전달해 드리도록 하겠습니다.
 
* 사용하고 있는 암호화 관련 javascript, login form, Java 소스들을 대략 뽑아서 첨부합니다. 구현하는데 도움이 되길 바랍니다


출처 - http://tvnuri.tistory.com/entry/%EC%95%94%ED%98%B8%ED%99%94%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9B%B9%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%B3%B4%EC%95%88




Posted by linuxism
,


Jesper M. Johansson, Ph.D., ISSAP, CISSP

보안 프로그램 관리자

Microsoft Corporation

정보 보안은 매우 흥미로운 논쟁들을 불러 일으키고 있습니다. 중요도에 따라 분류가 되겠지만 확실한 것은 이들 모두가 여전히 진행 중이고 무엇보다 매우 흥미롭다는 점입니다. 그럼 지금부터 이들 중 몇 가지 논쟁에 대해 정리하고 그에 대한 제 개인적인 의견들을 제시해 보도록 하겠습니다. 먼저 암호와 관련해 Pass phrase와 암호(Password)에 관한 문제를 살펴 보고자 합니다.

"Pass phrase vs. Password"는 "또 하나의 큰 쟁점"이라고 할 수도 있는 반면, 어떤 이들에게는 "할 일 없는 몇몇 사람들이 관심을 쏟는" 논쟁 거리에 지나지 않을 수도 있습니다. 둘 중 어느 쪽에 속하든 간에, Pass phrase와 Password 중 보다 안전한 방식은 어느 쪽일까요? 그러나 대답은 보는 것처럼 그리 간단하지가 않습니다.

Pass phrase와 Password의 문제를 보다 잘 분석하기 위해 저는 이 글을 세 부분으로 나누어 진행하고자 합니다. 첫 번째 도입부에서는 Pass phrase와 Password의 개념과 원리에 대해 다루고 이들이 저장되는 방식 등에 대해 설명하겠습니다. 그리고 다음 달에는 각각의 장점에 대해서 논하고, 어느 쪽이 보다 강력한가를 결정하기 위해 약간의 수학적인 방식들을 적용해 보도록 하겠습니다. 그런 다음 마지막 달에는 이 시리즈를 마무리 하면서 암호를 선택하는 방법과 암호 정책을 구현하는 방법에 대한 몇 가지 지침들을 제시할 계획입니다.

 

 

기본 개념

먼저, Password와 Pass phrase의 차이에 대해 이해하는 것이 매우 중요합니다. 

Pass phrase와 Password의 가장 큰 차이점은 다음과 같습니다

  • Pass phrase 는 중간에 띄어 쓰기가 있지만 Password 는 그렇지 않습니다.
  • Pass phrase는 대부분의 단어들보다 훨씬 길고, 무엇보다 일반인들이 기억할 수 있는 정도의 글자의 나열보다 깁니다.

Pass phrase 는 단순히 매우 긴 암호 정도로 여겨질 수도 있지만, 기본적으로 이는 단어나 단어와 비슷한 것들의 조합입니다. 그리고 여기서 설명하고 있는 Pass phrase는 Windows 2000 이상 버전에서 공식적으로 사용이 가능합니다. 다른 제품들은 다른 방식의 Pass phrase를 사용하며 아예 지원하지 않는 경우도 있습니다.

둘째, Password guessing(암호 추측)과 Password cracking(암호 크래킹)의 차이점에 대해 이해해야 합니다. Password guessing(암호 추측)은 누군가 해당 기기에서, 또는 원격 기기를 통해 암호를 알아 내려고 시도하는 것입니다. 사실 Password guessing(암호 추측)은 이 글과는 관련이 없습니다. 이미 이 글은 계정에 비교적 복잡한 암호를 설정했다는 것은 전제로 기술하고 있으며 그 경우 Password guessing(암호 추측)은 거의 불가능하기 때문입니다. 만약 Password guessing(암호 추측)이 성공한다면 그것은 공격자가 정말 운이 좋다거나 암호가 너무 쉽기 때문일 것입니다.

예를 들어 봅시다. 먼저 4가지 종류의 기호, 대문자, 소문자, 숫자, 문자나 숫자가 아닌 기호를 사용한 암호를 생각해 보십시오. 기호에는 키보드 상에 나타나 있는 모든 기호들은 물론, 물론 유니코드 기호처럼 키보드에 나와 있지 않은 기호들도 포함됩니다. 어떤 이들은 유니코드 기호와 키보드 상의 기호들을 두 개의 전혀 다른 범주로 분류하기도 합니다. 그러나 이 글에서는 이들을 하나의 범주로 보겠습니다. 그리고 "기호(character)"도 위의 4가지 모두를 지칭하는 것으로 하겠습니다.

 

이제 이 4가지 기호 중 적어도 3가지를 포함하고 있고, 70일 후에 만료되는 8자리 암호들이 있다고 가정해 봅시다. 이 암호에 대한 사전 지식이 전혀 없는 한 공격자가 암호 만료 전에 이들 중 하나를 추측해 내려면 컴퓨터의 네트워크 대역폭이 53,000 T-3 (각각 44.736 Mbps)은 되어야 합니다. 이는 가능성이 있는 암호들의 절반 정도를 시도해 보는 데 필요한 인증 트래픽 전송에 요구되는 수준입니다. 우리는 추측에 사용되는 기호들을 제한하였고, 암호가 76개의 일반적인 기호들 중에서 임의로 선정되었다고 그 범위를 한정했습니다. 그러나 이 경우에만 해도 추측 가능한 8자리 암호는 1.11 x 10^15 개에 달합니다.

 

공격자가 초당 300개씩을 맞춰 본다고 해도(이는 가장 최적을 성능을 갖춘 프로그램에서도 거의 불가능하지만), 이 모두를 맞춰 보는 데에는 58,783년이 걸릴 것입니다. 또 "net use" 구문을 작성한다면, 초당 2~3개 정도를 시도해 볼 수 있을 것이고, 모두를 맞춰 보려면 5,878,324년 정도의 시간이 걸릴 것입니다.

한편, 크래킹은 raw hash(도출된 해시값에 어떠한 변형도 하지 않은 해시, 원래의 해시값)를 확보한 경우 이루어집니다(해시는 암호를 저장하는 데 종종 사용되는 수학적 설명입니다. 보다 자세한 사항에 대해서는 아래에서 설명하도록 하겠습니다). 공격자는 테스트 암호를 생성하여 이를 해시한 다음 저장된 해시와 그 결과를 비교합니다.

 

크래킹은 추측에 비해 훨씬 빠릅니다. 적절한 하드웨어를 사용하는 경우 초당 3,000,000개의 암호를 생성하여 테스트해 볼 수 있습니다. 76개의 기호 중 8개를 골라 만들 수 있는 가능한 모든 암호들에 대한 크래킹 공격은 테스트 시간을 기준으로 6년이 걸립니다. 물론 수많은 암호들이 이보다 짧은 시간에 밝혀질 것이고, 특정 암호를 찾는 데에는 그나마 이 시간의 절반 정도 밖에는 걸리지 않을 것입니다. 그리고 암호가 7자리인 경우에는 모든 암호 조합을 크랙하는 데 약 28일 밖에 걸리지 않을 것입니다.

크래킹이 주된 문제이긴 하지만, 공격자가 raw hash(도출된 해시값에 어떠한 변형도 하지 않은 해시, 원래의 해시값)를 확보하는 것 또한 문제입니다. Windows의 경우 공격자가 도메인 암호를 크랙하려면 도메인 컨트롤러에 시스템 수준의 액세스를 할 수 있어야 합니다. 그러나 실제로 공격자가 도메인 컨트롤러에 침입했다면 사실상 암호 크래킹은 더 이상 중요한 문제가 아닙니다. 어찌되었든, 대부분의 공격자들은 암호를 크랙합니다. 왜일까요?

 

대부분 공격자들은 해당 계정의 사용자가 다른 도메인의 또 다른 시스템에 같은 사용자 이름의, 같은 암호를 가진 다른 계정을 갖고 있길 바랍니다. 이는 관리 종속성(administrative dependency)이라고 알려져 있는데 이 주제에 관해서는 다음에 다루기로 하겠습니다. 그리고 Challenge/Response 프로토콜에서 사용되는 유일한 비밀 정보는 암호 해시 자체이고, 공격자가 계정에 액세스하는 데 필요한 것도 오로지 해시뿐이기 때문에 엄밀히 말해 이 경우 암호 크래킹은 별 의미가 없습니다. 그렇기는 해도 공격자들이 암호를 크랙하고 이들의 성공 확률이 높아진다는 것은 여전히 심각한 문제입니다.

오늘날 운영 체제들은 일반 텍스트 암호나 Pass phrase를 사용하지 않는다는 사실을 기억하십시오. 대개 저장된 값은 해시와 같은 일방 함수(one-way function)에 따른 것입니다. 그러나 Windows NT 기반 운영 체제(Windows 2000, XP, Server 2003 등)에서 암호는 몇 가지 다른 방법으로 저장됩니다. 대표적인 방법 중 하나가 LM 해시와 NT 해시입니다. 물론 이 문서에서는 이들의 작동 방법에 대해 정확하게 알 필요는 없지만, 그래도 다음 세 가지는 기억해야 합니다

  • LM 해시는 대, 소문자를 구분하지 않는 반면, NT 해시는 대, 소문자를 구분합니다.
  • LM 해시는 142개로 그 사용 기호들이 제한되지만, NT 해시는 65,536개나 되는 거의 대부분의 유니코드 기호들을 지원합니다.
  • NT 해시는 사용자가 입력한 전체 암호를 기반으로 해시를 파악하지만, LM 해시는 암호를 두 개의 7자리 청크(chunk)로 분리합니다.

두 해시 모두 128 비트로 저장되는 값을 생성합니다. 대부분의 암호 크래커들은 먼저 LM 해시를 크랙하고, LM 해시에 의해 크랙된 대, 소문자 비 구분 암호의 모든 대문자 및 소문자 조합을 가지고 NT 해시를 크랙합니다. LM 해시는 암호 저장에 사용되는 매우 취약한 일방 함수입니다. 원래 LAN 관리자 운영 체제를 위해 고안된 LM 해시는 이전 버전과의 호환을 위해 Windows NT에 포함되었으며, 여전히 유지되고 있습니다. LM 해시가 산출되는 방식 때문에, LM 해시를 가진 암호는 142개 기호들 중 원하는 기호를 선택해 만든 7자리 암호에 비해 강력하지 않습니다.


 

LM 해시 제거

LM 해시가 저장되지 않게 하는 몇 가지 방법이 있습니다. 그 중 하나는 14자 이상의 Password나 Pass phrase를 사용하는 것입니다. 또한 NoLMHash 스위치를 사용할 수도 있습니다. Windows Server 2003과 Windows XP의 그룹 정책에는 "Network security: Do not store LAN Manager hash value on next password change (네트워크 보안: 다음 번 암호 변경 시 LAN 관리자 해시 값을 저장하지 마십시오)."라고 되어 있습니다. 이 스위치를 전체적으로 사용하면 모든 계정에 대한 저장 LM 해시가 기능하지 못합니다. 현재나 과거의 암호를 위한 LM 해시들은 해당 스위치를 삭제하면 자동으로 제거됩니다. 그리고 스위치가 제대로 작동하지 않는다는 것은 LM 해시를 저장하지 않음으로써 나타날 수 있는 잠재적인 상호 운영성 문제를 즉시 인식하지 못할 것이라는 뜻입니다. 더 자세한 사항은 기술 자료 기사 299656 을 참조하시기 바랍니다. 기술 자료 기사에는 그 밖에 Windows 2000에서의 NoLMHash 스위치 사용에 관한 정보도 나와 있습니다.

암호에 특정 기호를 사용하여 LM 해시를 제거할 수도 있습니다. 암호에 "ALT 기호"를 사용해 LM 해시의 생성을 막는 것은 널리 알려진 사실입니다. 그러나 실제로는 특정 유니코드 기호 몇 개만이 LM 해시들을 없앨 수 있습니다. 예를 들어 0128과 0159 사이의 유니코드 기호들은 LM 해시의 생성을 막습니다. 그리고 일부 유니코드 기호들은 해시되기 전에 다른 기호로 변환됩니다. 그러나 LM 해시를 제거하는 것은 문제가 될 수 있습니다.

 

LM 해시를 기본적으로 제거하지 않고 남겨 두는 이유는 이들을 제거하는 경우 RPC에 대해 UDP 기반 인증을 사용하는 어플리케이션들이 손상되기 때문입니다. 여기에는 Windows Cluster Services나 Real Time Communications Server 등 여러 가지가 해당됩니다. 이러한 문제는 Windows Server 2003 그룹 정책에 "Network security: Minimum session security for NTLM SSP based (including secure RPC) clients (네트워크 보안: (안전한 RPC를 포함한) NTLM SSP 기반 클라이언트를 위한 최소 세션 보안)"라고 나와 있는 NtlmMinClientSec을 활성화함으로써 해결할 수 있습니다.

NtlmMinClientSec은 메시지 무결성과 NTLMv2 세션 보안(0x80010)을 요구하도록 설정되어야 합니다. RPC가 NTLMv2 인증을 활용하도록 설정된 경우 NT 해시를 사용합니다. (LM 해시가 없는 경우 나타날 수 있는 클러스터 문제에 관한 자세한 사항은 기술 자료 기사 828861 를 참조하시기 바랍니다.) 다른 어플리케이션들 역시 LM 해시가 없는 경우 손상될 것입니다. 예를 들어 매킨토시용 Outlook 2001은 사용하는 모든 계정들이 LM 해시를 포함할 것을 요구합니다. Windows 3.x은 LM 해시가 없는 경우 100% 손상되며, Windows 95와 98의 경우는 일부 상황에서 손상될 것입니다. 그리고 NAS(network attached storage) 장치들과 같은 일부 타사 제품들도 LM 해시가 필요합니다.

 

Jesper M. Johansson, Ph.D., ISSAP, CISSP

보안 프로그램 관리자

Microsoft Corporation

"Pass phrase vs. Password"와 관련된 두 번째 기사입니다. 첫 번째 기사에 서는 Password와 Pass phrase의 기본적인 내용들과 저장 방식 등에 대해 다뤘었습니다. 이번에는 각각의 상대적 장점에 대해 설명하고, 이해를 돕기 위해 몇 가지 수학적인 방식들을 사용해 보겠습니다. 그리고 마지막 세 번째 기사에서는 이 시리즈를 마무리하면서 암호를 선택하는 방법과 암호 정책을 구성하는 방법에 대한 지침들을 제시할 예정입니다.

 

다양한 주장들: 찬성론 vs. 반대론

Pass phrase는 몇 분만에 수많은 Password들을 크랙하는 도구들이 개발되고 있다든가 하는 몇 가지 이유에서 점점 유행하기 시작하고 있습니다. 그러나 사실 이러한 도구들은 새로운 것이 아닙니다. Quakenbush Password Appraiser 같은 경우는 이미 1998년에 Password를 크랙하였습니다. 다만 새로운 것은 Phillippe Oechslin 박 사의 Space-time tradeoff를 뒷받침하는 이론과 실례들입니다. Time-space tradeoff 는 모든 해시를 저장하지 않는다는 것을 뜻합니다. 그리고, 예를 들어 NT 해시를 저장할 경우 그 모든 해시를 저장한다는 것은 현재로서는 불가능한 일입니다.

 

76개 문자 조합에서 14개를 선택해 만든 Password의 모든 NT 해시들을 저장하려면 5,652,897,009 바이트의 저장 공간이 필요하고 현재 제공되고 있는 파일 시스템 중에는 이를 저장할 만한 용량을 갖춘 시스템이 전혀 없습니다. 설령 모든 LM 해시들을 저장하는 데 310 테라바이트 정도만 필요하다고 해도 이를 모두 저장하는 것은 여전히 불가능한 일입니다. Oechslin박사는 이 딜레마를 해결하기 위해 해시의 일부와 이와 관련된 Password들만을 저장하는 Time-space tradeoff 이론을 내놓았습니다.

 

이는 저장 요구 사항을 현저하게 줄여 주어 위와 같은 조건의 LM 해시들을 저장하는 데 단 17 기가바이트의 저장소 공간 밖에는 필요하지 않습니다. 앞으로 살펴 보겠지만, Pass phrase에 대한 주요 논쟁점 중 하나가 바로 저장소 요구 조건이 지나치며 사전 계산된 해시 공격을 일으킬 수 있다는 것입니다.

 

 

주장 1: 사용자는 Pass Phrase를 기억할 수 있습니다

Pass phrase를 지지하는 쪽에서 내세우는 첫 번째 주장은 사용자들이 (10자 이상의) 긴 Password보다 Pass phrase를 더 기억하기 쉽다는 것입니다. 물론 그럴지도 모르지만, 10자 이상의 Password를 사용하는 사용자들이 매우 적다는 것을 감안할 때 이는 그다지 설득력이 있어 보이지는 않습니다. 그러나 위 질문에 대한 답을 제시하기 위해 저는 사용자들이 10자짜리 Password를 기억할 수 있는지에 대해 지극히 비 과학적인 연구를 수행하였습니다.

 

관리자들을 대상으로 조사를 실시하였는데, 그들 중 99%가 사용자들이 쉽게 10자짜리 Password를 잊어버릴 뿐 아니라 이처럼 긴 Password의 사용에 대해 반감을 표시할 것이라고 답했습니다. 그렇다면 10자로 된 문장 하나는 기억할 수 있을까요? 아마도 가능할 거라 생각합니다. 왜냐하면 한 문장은 단지 몇 개의 상징(이 경우에는 단어)들로 이루어지기 때문입니다. 제가 자주 인용하는 유명한 논문 중 하나가 Miller가 1956년에 발표한 "The Magical Number Seven, Plus or Minus Two: Some Limits On Our Capacity For Processing Information(7플러스, 마이너스 2: 정보를 처리하는 능력의 한계)." 입니다. 제목을 읽는 것만으로도 충분히 의미가 있는 이 대단한 논문에서 내건 전제가 바로 인간의 정보 처리 능력이 제한적이라는 것입니다.

 

이에 따르면 인간은 한 번에 7 플러스, 마이너스 2의 정보 단위만을 기억할 수 있다고 합니다. 그러나 여기서 중요한 것은 숫자 7이 아닙니다. 우리가 주목해야 할 것은 바로 인간의 정보 처리 능력이 제한적이란 그 사실입니다. 어떤 사람들은 그 수치를 5플러스, 마이너스 2라고 주장하기도 합니다. 물론 그 수를 3으로 잡는 사람도 드물지만 본 적이 있습니다. 어느 경우든, 중요한 사실은 인간의 정보 처리 능력이 지극히 한정적이라는 것입니다.

 

"청크(단위)"에 대한 정의 또한 우리가 시도하는 작업에 따라 달라집니다. 임의의 10자짜리 Password 에서 청크는 기호가 되며, Miller의 경우 대부분의 사람들이 임의로 고른 10개의 기호는 기억할 수가 없다고 이야기하고 있습니다. 그러나 이에 반해 2~3개의 단어나 청크로 구성된 10자짜리 Pass phrase는 기억할 가능성이 훨씬 많습니다.

 

따라서 사용자가 7개의 청크나 단어, 기호를 기억할 수 있다고 가정하면 사용자가 사용할 수 있는 가장 긴 Password는 9자리로 제한될 것입니다. 그리고 이는 실질적인 실험을 통해 입증되었습니다. Password의 강점을 파악하기 위해 대규모 도메인에서 28,000개의 Password에 대해 크랙을 시도하였습니다. 이중 83%인 23,311개를 완벽하게 크랙할 수 있었고, 13.16%는 부분적으로 크랙이 가능하였습니다.

 

물론 이것이 모든 Password를 완전히 대표하진 못하더라도, 이 글의 나머지 부분에 나와 있는 여러 통계 수치는 이 23,311개의 크랙된 Password들에 대한 분석 내용을 기반으로 하고 있습니다. 그리고 그 분석 내용은 9자리 한계 이론을 어느 정도 뒷받침합니다. 도메인 상에서 크랙된 Password들 중 64%는 최소 7자리 이상 최대 9자리 이하였습니다.

 

또 모든 도메인 Password들 중 90.37%가 15자리가 미만이었습니다. (15자리 미만의 Password가 정확히 몇 개였는지를 밝힌다는 것은 모든 Password들이 일반 텍스트로 캡처되지 않은 이상은 불가능합니다. 따라서 일단은 LM 해시가 없는 Password들을 15자리 이상의 긴 Password라고 추정하였습니다. 그러나 여기에는 다른 이유 때문에 LM 해시가 부족한 Password들도 일부 포함되어 있습니다.)

 

Pass phrase에서는 각 단어가 청크가 됩니다. 영어 단어의 평균 길이는 5자입니다. 영어에서는 분당 단어수로 표시되는 타자 속도를 잴 때 이 5자짜리 단어를 표준으로 합니다. 이와 마찬가지로 a 1995 survey of 45 PGP users (45 명의 PGP 사용자들을 대상으로 한 연구 조사, 1995)에서 Arnold Reinhold는 평균 PGP Pass phrase는 5.3개의 글자로 이루어진다는 사실을 발견하였습니다. 또한 Reinhold는 자신이 조사한 전체 단어들 중 8분의 5가 영어 사전에 실려 있는 단어들이었다는 흥미로운 사실도 보고하였습니다. 물론 위의 사례는 그 규모가 너무 작아서 실제 과학적으로는 설득력이 별로 없지만, 그나마 부족한 자료들 중에서는 최고라 할 수 있습니다.

 

다시 Miller로 돌아와서, 7개의 단어로 이루어진 문장을 기억하는 사용자는 41자짜리 Password를 사용할 수 있습니다. 물론 여기에는 몇 가지 주의해야 할 점들이 있습니다. 먼저, 실제 Pass phrase는 그렇게까지 길지는 않습니다. 예를 들어 제가 현재 사용하고 있는 Pass phrase(네, 저는 Pass phrase를 사용하고 있습니다)도 35자 밖에 되지 않습니다. 그리고 이미 이것이 귀찮아지기 시작했습니다. 또한 Reinhold는 일반적인 Pass phrase의 경우 평균 4단어 정도로만 구성된다는 사실도 발견했습니다.

 

 

주장 2: 길수록 강력합니다

Pass phrase에 대한 또 다른 주장은 이들이 길고, 그래서 더욱 강력하다는 것입니다. 아직도 Pass phrase의 길이와 Password의 길이는 직접 비교할 수가 없습니다. 다만 Password의 강력함을 평가하는 일반적인 방식이 이를 크랙하는 데 걸리는 시간이기 때문에 보다 긴 암호가 보다 나은 것으로 여겨집니다.

 

예를 들어 전에 살펴 본 것처럼 7자리 Password에 비해 8자리 Password를 크랙하는 데에는 5년하고도 11개월이 더 걸립니다. 다만 이는 Password가 확실하게 임의로 만들어졌고, 모든 기호들에 있어 Password에 포함될 수 있는 가능성이 동일한 경우에만 해당합니다. 따라서 Password가 완벽하게 무작위로 선택되지 않았을 경우에는 이 계산 내용이 달라질 수 있습니다.

 

보다 긴 Password에 대한 또 다른 주장은 14자보다 긴 Password는 LM 해시를 생성하지 않는다는 것입니다. 물론 다른 방식으로도 LM 해시를 제거할 수 있다는 점을 생각해 보면 단순히 LM 해시의 제거는 Pass phrase에 대한 장점이 될 수 없습니다. 그렇다면 이 모든 내용을 종합해 볼 때, 길이가 장점이 될 수 있을까요? 글쎄요, 별로 그런 것 같지가 않군요. 현재 기호를 크랙하기 위해 많은 Password 크래커들이 고안되고 있습니다.

 

그러나 나중엔 기호 대신 단어를 사용할 수도 있습니다. 앞으로도 기호 대신 단어를 사용하는 일은 없을 거라고 말할 근거가 전혀 없기 때문입니다. 실제로 몇몇 사람들이 충분히 그럴 가능성이 있다고 보고 있습니다. 따라서 이제 긴 Password는 더 이상 불변의 장점이 되지 못합니다. 다만 오늘날 사용되고 있는 크래킹 도구들을 막는 데 도움이 되는 정도일 뿐입니다.

 

 

주장 3: Pass Phrase가 보다 임의적입니다

Pass phrase의 확실한 장점 중 하나가 높은 엔트로피입니다. 엔트로피란 무작위성을 평가하는 일반적 단위입니다. 엔트로피에는 세 가지 구성 요소가 있습니다. 선택한 항목의 수, 선택한 대상의 규모, 그리고 각각의 개별 항목들이 선택될 가능성이 그것입니다. Pass phrase가 Password보다 더 길기 때문에 엔트로피가 높을 가능성도 더 많습니다.

 

이 둘이 같은 기호 모음에서 선택된 것들일지라도 말입니다. 이는 Password 크래커들이 확률적으로 운영되도록 설계되었다는 점에서 특히 주목할 만합니다. Password 크래커들은 단순히 Password에서 가능한 모든 문자 조합을 대상으로 크랙을 하는 것이 아니라 사전에서 찾을 수 있는 일반적인 조합에서부터 시작해서 빈번히 사용되는 문자를 기준으로 (단어 조합) 순서를 변경하는 방식으로 진행해 갑니다. 따라서 7자리 Password들을 크랙하는 데 걸린다는 28일이라는 기간이 정확하지 않을 수도 있습니다.

 

실제로 몇 초 만에도 수많은 Password들을 크랙하는 것을 종종 볼 수 있습니다. 이 모든 것이 Password의 조합 방식에 따라 달라지기 때문입니다. 따라서 엔트로피는 단순히 길이나 기호 모음보다 Password의 장점을 파악하는 데 있어 훨씬 나은 방법이라고 할 수 있습니다.

 

몇 가지 예를 살펴 봅시다. 테스트 결과 실험 대상 Password의 83%가 문자, 숫자, 기호(!@#$%^&*()-_+=)들로 이루어진 것으로 나타났습니다. 이러한 조합은 영어의 경우 26+26+10+14=76개의 기호를 포함하며, 다른 언어의 경우 더 늘어날 수도 있습니다. 더불어 이들 Password에 사용된 기호들 중 80%가 그러한 76개의 기호 중 32개만을 대상으로 선택된 것들이었습니다.

 

76개 기호 집합의 기본 엔트로피, 혹은 절대 비율은 각 기호당 R=Log2L = 6.25 비트입니다. 절대 비율은 일반적으로 엔트로피의 최대값으로 여겨지며, 각 기호들이 동일한 값의 선택 가능성을 갖고 있다는 것을 전제로 합니다. C.E. Shannon은 그럼에도 불구하고 8개 글자로 이루어진 영어 단어(청크)의 각 문자당 엔트로피를 계산하여 문자당 2.3 비트라는 결과를 얻었습니다(Shannon, C.E., "Predication and Entropy in Printed English," Bell System Technical Journal, v. 30, n. 1, 1951, pp. 50-64). Shannon의 작업은 76개의 기호 조합이 아닌 26개의 문자 조합을 사용하는 영어 단어를 기반으로 했다는 점을 기억하시기 바랍니다.

 

앞서 사용자들이 오직 32개의 기호들 중에서만 대부분의 기호들을 선택하고 있다는 사실을 살펴 보았습니다. 어떤 경우든 Password의 기호당 실제 엔트로피는 Shannon이 계산한 2.3이라는 수치보다는 크고, 6.25라는 절대 비율보다는 작을 것입니다. Log2 32 = 5가 -개인적으로는 다소 높다고 생각하곤 있지만- Password의 비트당 엔트로피 최대값(추정 계산값)이 될 것입니다. 따라서 평균 Password의 길이가 9.16개, 약 9개이기 때문에 엔트로피는 9*5=45 비트를 넘지 않습니다.

 

Pass phrase에 대한 논쟁 중 대부분의 사람들이 문장을 구사할 때 76개 이상의 단어를 사용한다는 내용이 있습니다. 대개 Pass phrase는 하나의 언어로 이루어진다고 생각됩니다. 옥스포드 영어 사전에는 616,500 개의 단어가 들어 있습니다. 스펠링 비(spelling bee) 참가자들이라든가 대학 입학 시험을 준비하는 학생들이 이들 중 614,000개를 다루게 됩니다. 그러나 실질적으로 미국인들의 평균 어휘력을 언어학자 Richard Lederer의 경우는 10,000-20,000 개, James L. Fidelholtz의 경우는 50,000-70,000 개 로 추정하고 있습니다. 그리고 두 언어학자 모두 이들 대부분이 연상 어휘라는 점에 동의합니다. 즉, 여러분이 듣게 되는 모든 단어들을 인지한다고 해서 이들 모두를 사용하는 것은 아니라는 뜻입니다. 보통 사람들의 경우 이러한 어휘들의 일부만을 사용할 뿐입니다.

 

Pass phrase가 오직 300 단어만을 기준으로 한다고 가정해 봅시다. 이는 매우 조심스러운 평가가 될 수도 있지만, 그러한 단어들의 대부분은 특정한 방식으로 함께 사용될 때에만 의미가 있으며, 이는 Pass phrase의 무작위성을 현저하게 감소시킵니다.

 

Pass phrase의 실제 엔트로피를 계산하기 위해서는 얼마나 많은 단어들이 사용되는지를 알아야 합니다. 앞서 참고한 PGP 연구에서의 단어의 중간값은 4였고 이는 평균보다 높습니다. Miller의 주장을 잠시 뒤로 하고, 이번엔 5단어로 이루어진 Pass phrase에 대해 생각해 봅시다.

 

단어당 글자가 5개라고 하면 4개의 공란(띄어쓰기)을 포함해 25+4=29개의 기호로 이루어진 Pass phrase를 갖게 됩니다. 이 Pass phrase가 갖게 될 엔트로피는 누구의 산출 방식을 활용하느냐에 따라 달라집니다. Shannon의 산출 방식에서는 8자로 이루어진 단어에서 문자당 엔트로피가 2.3비트였으므로 총 엔트로피는 29*2.3=66.7 비트가 됩니다. 산출된 66.7 비트라는 값은 Pass phrase의 엔트로피에 있어 적정한 최대값이 될 것입니다. 그리고 엔트로피가 45비트인 9자리 Password에 비해 훨씬 낫습니다. 최저값의 경우에는 Thomas Cover의 연구 결과를 바탕으로, Bruce Schneier의 산출 값인 문자당 1.3비트를 사용할 수 있습니다(B. Schneier, "Applied Cryptography, 2nd Edition," Wiley, 1996).

 

Shannon은 16자로 된 단어들을 대상으로 문자당 1.3비트라는 값을 계산해 냈습니다. 따라서 이를 우리의 5자짜리 단어에 완벽하게 적용하기란 다소 어려울 것입니다. 어쨌든 엔트로피 계산을 위해 1.3을 사용하는 경우 29*1.3= 37.7이라는 값을 얻을 수 있고, 이는 확실히 9자짜리 Password에 비해 훨씬 떨어집니다. 이 수치를 기반으로 볼 때, 9자짜리 Password와 비슷한 엔트로피를 확보하려면 6 단어짜리 Pass phrase가 필요합니다.

 

Pass phrase 엔트로피에 대한 계산값은 우리가 지금 다루고 있는 어휘를 고려한 것은 아닙니다. 그러나 Pass phrase가 일반적인 사항이 되면 공격자들도 기본 단위로 기호가 아닌 단어를 쓰는 "Pass phrase 크래커"를 사용하기 시작할 것입니다. 이러한 상황은 Password의 무작위성 계산법을 완전히 바꿀 것입니다. 기본 단위로 단어를 사용하는 것이 기호로 단어를 구성하는 문자를 사용하는 것에 비해 훨씬 적절할 수도 있습니다.

 

Pass phrase에 300개의 어휘를 사용하고 이를 임의로 조합할 수 있다고 가정한다면, 단어당 Log2300 = 8.23 비트라는 절대 비율을 확보하게 됩니다. 따라서 5 단어짜리 Pass phrase를 사용하는 경우에는 8.23*5= 41.2 비트의 엔트로피를 얻게 되고, 6단어짜리 Pass phrase를 사용하는 경우에는 49.4 비트의 엔트로피를 얻게 됩니다.

 

기본 단위로 단어를 사용하는 것이 Password에 비해 Pass phrase를 꺼리게 만들 수도 있습니다. 그러나 실제로 5~6개 단어로 이루어진 Pass phrase는 9자짜리 Password만큼 강력합니다. 물론 이는 과학적으로 증명된 결과는 아닙니다. 이와 같은 엔트로피 계산값을 입증하기 위해 향후 별도의 연구가 진행되어야 할 것입니다. 

 

출처: Microsoft TechNet

http://www.microsoft.com/korea/technet/security/secnews/articles/itproviewpoint091004.asp


출처 - http://blog.naver.com/PostView.nhn?blogId=yechan75&logNo=140007269874&viewDate=&currentPage=1&listtype=0&from=postList



'Security > Common' 카테고리의 다른 글

crytography - key  (0) 2012.12.24
naver login process  (0) 2012.12.23
패스워드 암호화  (0) 2012.05.24
SASL(Simple Authentication and Security Layer)  (0) 2012.03.02
XSS(Cross-Site Scripting)  (0) 2012.02.29
Posted by linuxism
,



[java] java.security.InvalidKeyException: Illegal key size 오류 해결 방법  java 프로그래밍 

2010/11/20 00:06

복사http://blog.naver.com/websearch/70097615280

java.security.InvalidKeyException: Illegal key size 오류는 Java 기본 패키지의 key size 크기 제한 때문입니다. 이를 해결하는 방법은 다음과 같습니다.

 

* 아래의 웹페이지에서 Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 6 를 다운로드 받는다.

  - http://www.oracle.com/technetwork/java/javase/downloads/index.html

 

* 위에서 다운로드 받은 파일의 압축을 해제한 후, java 실행 폴더 하위의 lib\security 폴더에 복사한다.

  - java.exe 가 여러 곳에 존재할 수 있으므로 현재 실행되는 java 의 폴더를 확인한 후, 복사해 넣어야 한다.

 

Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 6 의 Readme.txt 에 아래와 같은 소개의 글이 있군요.

 

Due to import control restrictions, the version of JCE policy files that are bundled in the JDK(TM) 6 environment allow "strong" but limited cryptography to be used. This download bundle (the one including this README file) provides "unlimited strength" policy files which contain no restrictions on cryptographic strengths.

 

이 문제 때문에 몇 시간을 허비하였군요. ㅡ.ㅡ

 

[참고자료] http://charithaka.blogspot.com/2008/08/how-to-avoid-javasecurityinvalidkeyexce.html


출처 - http://blog.naver.com/PostView.nhn?blogId=websearch&logNo=70097615280


기존에 파일이 있다면 덮어 쓰기 한다.



Posted by linuxism
,