개요
일반적인 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; } } |
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(); } } |