2016년 12월 6일

자바스크립트 CryptoJS 와 JAVA 간의 AES 암호화

CryptoJS 를 이용한 암호화


CryptoJS 를 사용하면 웹에서도 손쉽게 AES 암호화가 가능하다.

자바스크립트에서 AES 암호화 예 1

1
2
3
4
5
6
7
8
9
10
11
12
<script    src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js"></script>
<script    src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/sha256.js"></script>
<script>
    
    var message = "Led Zeppelin- Stairway to Heaven"
    var passphrase = "1234";
    var encrypt = CryptoJS.AES.encrypt(message, passphrase);
    var decrypted = CryptoJS.AES.decrypt(encrypt, passphrase );
 
    // 암호화 이전의 문자열은 toString 함수를 사용하여 추출할 수 있다.
    var text = decrypted.toString(CryptoJS.enc.Utf8);
</script>
cs

텍스트 형식의 비밀번호를 사용하고 싶지 않는 경우는 SHA-256 같은 단방향 해쉬 알고리즘을 사용 (암호화된 메시지) 다이제스트를 생성하여 사용할 수 도 있다. 대부분의 웹 프로그램들은 보통 텍스트 형식의 비밀번호를 추론하기 어렵게 단방향으로 암호화하여 비밀번호를 보호하고 있다. (원본 메시지를 알면 암호화된 메시지를 구하기는 쉽지만 암호화된 메시지로는 원본 메시지를 구할 수 없어야 하며 이를 '단방향성'이라고 한다.)

자바스크립트에서 AES 암호화 예 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script    src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js"></script>
<script    src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/sha256.js"></script>
<script>
    
    var message = "Led Zeppelin- Stairway to Heaven"
 
    // 생성된 해쉬 값을 사용하면 평문 사용을 피할 수 있다.
    var hashedPassword = CryptoJS.SHA256("1234").toString() ; 
 
    var encrypt = CryptoJS.AES.encrypt(message, hashedPassword);
    var decrypted = CryptoJS.AES.decrypt(encrypt, hashedPassword );
 
    // 암호화 이전의 문자열은 toString 함수를 사용하여 추출할 수 있다.
    var text = decrypted.toString(CryptoJS.enc.Utf8);
</script>
cs


자바에서 원본 메시지를 구하기 위해서는 CryptoJS 내부적으로 키생성을 위하여 내부적으로 사용하는 비표준 OpenSSL KDF 를 사용함을 알고 있어야 한다. (사실 이미 관련 자바 코드가 공개되어 있어 몰라도 상관없다.)

주의할 것은 자바에서는 미국 이외의 국가에는 암호화 관련 제약(AES에서 128bit(16byte)를 초과하는 길이의 key 사용불가)이 있으며 별도의 확장 패키지(JCE Unlimited Strength Jurisdiction Policy)를 설치를 통하여 해지가 가능하다. 참고로 SHA-256를 사용하지 않고 PBKDF2 키를 사용하면 확장 패키지 설치 없이 암호화 및 복호화가 가능하다.

Java SE Downloads



자바에서 복호화


다음은 자바에서 암호화된 메시지를 복호화하는 과정이다.

복호화 예제

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
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
 
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
 
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
 
public class CryptoTest {
    
    public static void main(String[] args) throws UnsupportedEncodingException, GeneralSecurityException, DecoderException {        
        String ciphertext = "U2FsdGVkX18+s94R2PY8hZ+HLrPiucqI33KNQHmkAvES41hGlh1HLgfTwGbwc9dkSid4RW5xj5yBn9hj0y7y0A==";
        String password = "1234";
        System.out.println ( decrypt(ciphertext, password) );
       }
    
    public static String decrypt(String ciphertext, String passphrase) {
        try {
            final int keySize = 256;
            final int ivSize = 128;
 
            // 텍스트를 BASE64 형식으로 디코드 한다.
            byte[] ctBytes = Base64.decodeBase64(ciphertext.getBytes("UTF-8"));
 
            // 솔트를 구한다. (생략된 8비트는 Salted__ 시작되는 문자열이다.) 
            byte[] saltBytes = Arrays.copyOfRange(ctBytes, 816);
            System.out.println( Hex.encodeHexString(saltBytes) );
            
            // 암호화된 테스트를 구한다.( 솔트값 이후가 암호화된 텍스트 값이다.)
            byte[] ciphertextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.length);
                       
            // 비밀번호와 솔트에서 키와 IV값을 가져온다.
            byte[] key = new byte[keySize / 8];
            byte[] iv = new byte[ivSize / 8];
            EvpKDF(passphrase.getBytes("UTF-8"), keySize, ivSize, saltBytes, key, iv);
            
            // 복호화 
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
            byte[] recoveredPlaintextBytes = cipher.doFinal(ciphertextBytes);
 
            return new String(recoveredPlaintextBytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
 
        return null;
    }
    
    private static byte[] EvpKDF(byte[] password, int keySize, int ivSize, byte[] salt, byte[] resultKey, byte[] resultIv) throws NoSuchAlgorithmException {
        return EvpKDF(password, keySize, ivSize, salt, 1"MD5", resultKey, resultIv);
    }
 
    private static byte[] EvpKDF(byte[] password, int keySize, int ivSize, byte[] salt, int iterations, String hashAlgorithm, byte[] resultKey, byte[] resultIv) throws NoSuchAlgorithmException {
        keySize = keySize / 32;
        ivSize = ivSize / 32;
        int targetKeySize = keySize + ivSize;
        byte[] derivedBytes = new byte[targetKeySize * 4];
        int numberOfDerivedWords = 0;
        byte[] block = null;
        MessageDigest hasher = MessageDigest.getInstance(hashAlgorithm);
        while (numberOfDerivedWords < targetKeySize) {
            if (block != null) {
                hasher.update(block);
            }
            hasher.update(password);            
            // Salting 
            block = hasher.digest(salt);
            hasher.reset();
            // Iterations : 키 스트레칭(key stretching)  
            for (int i = 1; i < iterations; i++) {
                block = hasher.digest(block);
                hasher.reset();
            }
            System.arraycopy(block, 0, derivedBytes, numberOfDerivedWords * 4, Math.min(block.length, (targetKeySize - numberOfDerivedWords) * 4));
            numberOfDerivedWords += block.length / 4;
        }
        System.arraycopy(derivedBytes, 0, resultKey, 0, keySize * 4);
        System.arraycopy(derivedBytes, keySize * 4, resultIv, 0, ivSize * 4);
        return derivedBytes; // key + iv
    }    
}
cs


PBKDF2 방식의 AES 암호화와 복호화


참고로 PBKDF2는 NIST(National Institute of Standards and Technology, 미국표준기술연구소)에 의해서 승인된 알고리즘이고, 미국 정부 시스템에서도 사용자 패스워드의 암호화된 다이제스트를 생성할 때 사용된다고 한다.

PBKDF2 키를 이용한 자바스크립트에서 AES 암호화 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script    src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js"></script>
<script    src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/pbkdf2.js"></script>
<script>
 
    var iv = CryptoJS.lib.WordArray.random(128/8).toString(CryptoJS.enc.Hex);
    var salt = CryptoJS.lib.WordArray.random(128/8).toString(CryptoJS.enc.Hex);
    var plainText = "Led Zeppelin- Stairway to Heaven";
    var keySize = 128;
    var iterationCount = 10000;
    var passPhrase = "1234";
    
    // PBKDF2 키 생성 
    var key128Bits100Iterations = 
            CryptoJS.PBKDF2(passPhrase, CryptoJS.enc.Hex.parse(salt),
            { keySize: keySize/32, iterations: iterationCount });
        
    var encrypted = CryptoJS.AES.encrypt(
            plainText,
            key128Bits100Iterations,
            { iv: CryptoJS.enc.Hex.parse(iv) });
 
    
});    
</script>
cs

128 비트 키를 사용하기 때문에 별도의 확장 패키지(JCE Unlimited Strength Jurisdiction Policy)를 설치필요하지 않는다.

PBKDF2 키를 이용한 자바에서 AES 복호화 예제

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
import java.security.spec.KeySpec;
 
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
 
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
 
public class Crypto2Test {
 
    public static void main(String args[]) throws Exception {
        String ciphertext = "rV2MW3IoCfcCHMa6cZuCcDJpOnOhHDS4R8cOXEL+Z4kqYiBqYbG+zVJNfNUZrKDI"
        String passPhrase = "1234";
        String salt = "18b00b2fc5f0e0ee40447bba4dabc952"
        String iv = "4378110db6392f93e95d5159dabdee9b";
        String decrypted = decrypt(salt, iv, passPhrase, ciphertext, 10000128);
        System.out.println(decrypted);
    }
    
    public static String decrypt(String salt, String iv, String passphrase, String ciphertext, int iterationCount, int keySize) throws Exception {        
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        KeySpec spec = new PBEKeySpec(passphrase.toCharArray(), Hex.decodeHex(salt.toCharArray()), iterationCount, keySize);
        SecretKey key = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");        
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(Hex.decodeHex(iv.toCharArray())));        
        byte[] decrypted = cipher.doFinal(Base64.decodeBase64(ciphertext));        
        return new String(decrypted, "UTF-8");
    }
}
cs


참고

스크립트와 자바 간의 AES 암호화는 비밀 키를 서로 알고있어야 한다는 키 관리 이슈를 가지고 있다. 
키관리를 위하여 HTTPS 와 유사하게 RSA 알고리즘을 적용하면 더욱 안전한 암호화를 구현할 수 있다. 

댓글 13개:

  1. 안녕하세요. 두번째로 작성해주신 PBKDF2 방식 이걸 보고있는데요.

    iterationCount 의 역할은 무엇인가요? 명칭으로봐선 무엇인가를 계속되풀이해서 작동시킬려고

    하는것 같은데 (1만번)

    해당 숫자를 줄여도 암호화에는 상관이 없을까요? 아니면 최소한의 요구치가 10000 이여서

    그렇게 코드를 짜신걸까요?

    답글삭제
    답글
    1. iterationCount 는 해시함수 반복 횟수를 의미하며,
      해시 함수의 반복 횟수는 임의로 선택할 수 있습니다.
      다만 OWASP 2015 의 Password Storage Cheat Sheet 항목을 참고하면
      해시 함수의 반복 횟수로 10000 을 권장하고 있습니다.

      삭제
    2. 네 감사합니다. 권고사항이 있었군요. 같은 코드들 참고하는 블로그나 페이지보면

      대부분 1000 으로 설정해놨길래 질문드렸습니다.

      답변감사합니다.

      삭제
    3. 추가로 대부분 1000 으로 설정하 것은 아마도 RFC 2898 - IETF 에서 1,000 정도를 언급했었고 당시에는 커뮤팅 파워가 지금과는 비교할 수 없었기 때문에 많은 개발자들이 성능을 고려하여 문서에서는 최소한의 값을 사용하지 않았나 추정합니다. 요즘은 가능한 최대 값을 사용하는 것을 권고하고 있습니다. 사실 10,000 도 충분하지는 않습니다. (대부분의 해커들은 수백대의 컴퓨팅 파워를 사용하고 있기때문이죠.)

      삭제
  2. PBKDF2 키를 이용한 자바에서 AES 복호화 예제 에서
    byte[] decrypted = cipher.doFinal(Base64.decodeBase64(ciphertext));
    해당 구문에서 decodeBase64 함수가 에러가 나오는거 같아요

    해당 에러:
    the method decodebase64(byte ) in the type base64 is not applicable for the arguments (string)

    math6646@naver.com

    답글삭제
    답글
    1. 환경을 확인해보시는 것이 좋을 것 같습니다. BASE64 인코딩과 디코딩을 위해서 저는 commons-codec-1.10.jar 를 사용했습니다.

      삭제
  3. 질문이 있습니다.

    String ciphertext = "U2FsdGVkX18+s94R2PY8hZ+HLrPiucqI33KNQHmkAvES41hGlh1HLgfTwGbwc9dkSid4RW5xj5yBn9hj0y7y0A==";

    ciphertext에 들어있는 문자열이 Led Zeppelin- Stairway to Heaven를 암호화 한 값인 가요?

    그렇다면 암호화하면 값이 실행할 때마다 바뀌는데 어떻게 알 수 있나요?

    jsp에서 넘긴 값을 받아서 복호화 하고 싶은데 ciphertext 값을 변경하면 에러가 나서 질문드립니다.

    답글삭제
    답글
    1. PBKDF2 키를 이용한 자바스크립트에서 AES 암호화 예제 코드를 보시면 salt 값과 iv 값을 랜덤하게 생성하는 코드를 확인하실수 있습니다. 즉 서버에서 복호화할 때 암호화할때 사용된 동일한 salt 값과 iv 값을 가지고 복호화를 실행하면 오류 없이 복호화됩니다.

      삭제
  4. 질문이 있습니다.

    바로 위의 댓글에 답변에서

    'salt 값과 iv 값을 랜덤하게 생성하는 코드를 확인하실수 있습니다. 즉 서버에서 복호화할 때 암호화할때 사용된 동일한 salt 값과 iv 값을 가지고 복호화를 실행하면 오류 없이 복호화됩니다.'

    라고 하셨는데, 랜덤하게 생성한 코드값을 java에서 파라미터로 받아서 복호화한다는 말씀인가요?

    난수발생 값이라 예제처럼 하드코딩을 하면 복호화 시 에러가 나고.

    파라미터로 받으면 패킷유출로 인해 암호화의 의미가 없는 것 같습니다.

    답글삭제
    답글
    1. 클라이언트와 서버 간의 암호화에 적용하는 경우는 언급하신 것과 같은 이슈가 있습니다. 저의 경우는 과거에 SSL을 사용할 수 없는 환경에서 암호화가 필요하여 랜덤생성된 iv 과 암호화된 값을 전달하고 salt 값은 로그인 된 사용자의 아이디를 사용하는 방식으로 구현한 경우가 있었습니다. 서버에서는 인자가 아닌 세션에서 아이디 값을 꺼내어 사용하였습니다.

      삭제
    2. 답변 감사합니다.

      도움이 많이 되었습니다ㅎ

      삭제
    3. 댓글을보고 궁금한게있는데요.
      salt값을 서버영역에서 사용자 아이디를 이용하는겨 이해가 가는데 스크립트 영역에서도 아이디값으로 변환시켜서 넘겨줘야 서버영역에서 비교할수있다고 생각이 되는데요.
      salt값은 넘기지 않는방식으로 구현이 가능한가요?

      삭제
    4. 서버 세션을 사용하는 경우 인증이 완료된 경우에는 이미 서버는 클라이언트 인증 사용자를 알고 있기 때문에 서버 세션에 저장된 값을 사용하면 됩니다. 다만 클라이언트는 아이디 값을 가지고 있어야 하겠지요. 인증 이전이라면 요청 파라메터가 아닌 헤더에 사용자 아이디를 추가하여 보내는 방식으로 처리했습니다.

      삭제