• 참고 URL
    • JCA로 이해하는 암호화와 보안 : http://d2.naver.com/helloworld/197937
    • JCA로 이해하는 암호화와 보안2 : http://d2.naver.com/helloworld/227016
    • 안전한 패스워드 저장 : http://d2.naver.com/helloworld/318732
    • 암호화 알고리즘 종류와 관련 용어 : http://modoris.blogspot.kr/2011/11/blog-post_08.html
    • [KISA] 암호기술 구현 안내서 2013
    • http://ohgyun.com/433
    • http://dinguri.tistory.com/entry/Base643216-Encodings
    • update, doFinal 메서드 차이
    • wikipedia
      • https://en.wikipedia.org/wiki/Cryptographic_hash_function
      • https://en.wikipedia.org/wiki/Hash-based_message_authentication_code
      • https://ko.wikipedia.org/wiki/%EB%B2%A0%EC%9D%B4%EC%8A%A464
    • bcrypt cryptography


개념은 알지만 막상 구현하려니 헷갈리던 암호화,, Java에서는 다행히도 javax.crypto 패키지 하위의 클래스를 통해 보안 관련 기능을 제공하고있다.
위의 URL들은 암호화/복호화의 개념을 잡을수 있도록 잘 정리된 페이지이며, 이를 바탕으로 나중에 참고하기 위해 간단히 정리해보고자 한다 :3
나름대로 정리한 자료지만 혹시 틀릴수도 있으니 참고 URL이나 미심쩍을때는 최신 정보를 검색해보는것을 추천함.


MesasgeDigest

MD5, SHA1과 같은 알고리즘이 MessageDigest(해시)의 대표적인 알고리즘이다.
다른말로는 체크섬(checksum)이라는 명칭으로도 불리며, 원본파일(메시지)가 그대로인지 파악하는 무결성 검사에 사용한다.
이 알고리즘들은 다양한 길이의 입력값을 고정길이의 해시값으로 출력하며, 단방향 알고리즘이므로 해시값에서 역으로 원본값을 추출할 수 없다.
때문에 패스워드를 암호화 할 때, 원본 패스워드의 노출을 막기 위해 해시값으로 변환하여 저장하고, 마찬가지로 비밀번호 검증시에도 해시화된 값이 동일한지로 비교하게된다.

[대표적인 해쉬함수] (출처 : KISA)

 

HAS-160 

SHA-1 

SHA-2 

SHA-224

SHA-256 

SHA-384 

 SHA-512

 해시값 길이(bit)

160 

160 

224 

256 

384 

512 

[보안강도별 해쉬함수 분류] (출처 : KISA)

 보안강도(*)

해쉬함수 

권고여부 

 80비트 미만

MD5, SHA-1 

권고하지 않음 

 80비트

HAS-160 

 112비트

SHA-224 

권고함 

128비트 

SHA-256 

 192비트

 SHA-384

 256비트

 SHA-512

* 보안강도 : 2^n번의 계산을 해야 비밀키 또는 알고리즘 취약성을 알아낼 수 있다는 것을 의미한다. (국내 권고수준은 112비트 이상)


비밀번호와 Salt

동일한 비밀번호이더라도 다른 해쉬결과를 얻기 위해 Salt라는 비밀값을 추가하여 해쉬함수를 적용한다.
사용자별로 다른 Salt값을 적용하면 비밀번호만 해싱하는것에 비해 비밀번호 사전공격 취약점을 해결하고, 동일비밀번호이더라도 다른 해쉬결과를 얻을 수 있어 암호를 유추하기 어려워진다.

salt를 추가하는 방법은 다음과 같이 다양한 방식이 존재한다.
비밀번호와 salt를 연접하는 방법은 가낭 흔히 사용하므로 다양한 방법을 응용하여 사용하는것이 좋다.
- hashing ( salt || password || salt)
- hashing (salt || hashing (password))
- hashing ( password || hashing (password || salt))
- hashing ( hashing (salt) || hashing (password))
- hashing1 (hashing2 (password || salt))


MessageDigest와 Cryptography

내가 헷갈린다는 것도 몰랐던 Digest와 암호화의 차이.. 사실 이미 구현된 결과만 놓고보면.. 둘다 알 수 없는 숫자 문자열의 나열들이니..
변명이지만 변명할 수도 있겠지. :3 나말고도 헷갈리는사람이 꽤 있는듯하다.

이 둘의 차이점은 복호화가 가능한지 여부인것으로 보인다. MessageDigest는 단방향 알고리즘으로 결과값에서 원본값을 추출할 수 없으나 Cryptography는 암호화 키(대칭키, 비대칭키)를 통해 결과값에서 원본값으로 복원을 할 수 있다.


MAC (Message Authentication Codes)

암호화관련 자료를 찾아보면서 낯선것은 Mac 이었다. 처음 봤을때는 A사의 사과제품을 떠올렸던.. (그..그럴수도 있지!!)

MAC은 초기화 비밀키(SecretKey, 대칭키)를 활용하여 해시값을 추출하며 MessageDigest와 유사하지만 다르다.
이 초기화 키로 인해 MessageDigest는 누구나 무결성 검사가 가능하지만(예: 파일다운로드시 checksum 활용), MAC은 초기화 키를 가지고 있어야만 동일한 해시값을 추출하여 무결성 검사가 가능하다. 

대표적인 알고리즘으로는 HMAC이 있다.

jca8

 [MAC 동작 흐름(이미지 출처) ]

Base64Encoding/Decoding

Base64는 8비트 이진 데이터를 문자코드에 영향받지 않는 공통 ASCII 영역 문자열로 바꾸는 인코딩 방식이다.
인코딩된 문자열은 알파벳 대소문자와 숫자, +, / 기호 64개로 이루어지며 =는 끝을 알리는 코드로 사용하며, 인코딩 된 문자열은 원본보다 약 4/3 정도 증가한다.


Cryptography

해시함수와 다르게 Cryptography는 암호화, 복호화가 가능한 양방향 알고리즘을 사용한다.
알고리즘에는 암호화를 위한 키가 필요하며, 키의 유형에 따라 대칭키(공유키), 비대칭키(공개키, 비밀키)로 나뉜다.

[암호화 알고리즘 분류(출처 : 모도리 블로그)

알고리즘 분류

대표 알고리즘 

 대칭형

 블록 암호

DES (Data Encryption Standard), 3-DES 
AES, SEED, ARIA

 스트림 암호

 MASK

 비대칭형

 RSA (Rivest Shamir Adleman)
DSA (Digital Signature Algorithm; 후에 Digital Signature Standard로 명명)

[대표적 블록암호(CBC) 알고리즘] (출처 : KISA)

 

 SEED

ARIA

AES 

 입출력크기(비트)

 128

128 

128 

 비밀키크기(비트)

128 

128 / 192 / 256 

128 / 192 / 256 

[보안강도별 블록암호 알고리즘 분류] (출처 : KISA)

보안강도

블록암호 알고리즘 

권고여부 

 80 비트 미만

DES 

권고하지 않음 

80 비트 

2TDEA 

 112 비트

3TDEA 

128 비트 

SEED, HIGHT, ARIA-128, AES-128 

 권고함

192 비트 

ARIA-192, AES-192 

256 비트 

ARIA-256, AES-256 



JCA (Java Cryptography Architecture)

Java에서는 JCA (Java Cryptography Architecture)라는 보안 플랫폼을 통해 해시, 암호화, 전자서명 등 다양한 보안기능을 제공하고있다.

Cipher

Cipher 클래스는 암호화/복호화 기능을 제공한다.

예) Cipher cipher = Cipher.getInstance("AES");
알고리즘명, 피드백모드, 패딩모드를 전달하여 생성된 Cipher 객체를 반환받는다.  (피드백모드, 패딩모드 생략 가능)
내부적으로는 전달된 알고리즘이 구현한 서비스객체와 피드백모드(default:ECB), 패딩모드(default : PKCS5Padding)를 설정하여 Cipher 객체를 생성 후 반환한다.

예) cipher.init(Cipher.ENCRYPT_MODE, key);
이 Cipher 객체를 사용하기 위해서 init 메서드를 통해 운영모드와 Key값을 전달하여 초기화한다. 
init 메서드는 인자로 opmode, key, (AlgorithmParameters)params, random을 전달받는데 운영모드에 따라 생략이 가능하며 생략된 값은
 내부적으로 자동 생성하여 사용한다. 기존에 설정된 변수값은 모두 삭제되므로 cipher객체를 초기화하여 사용하는경우, getParameters() 메서드를 호출하여 암호화시 자동으로 생성된 params값을 저장해두어야 한다. 이 값은 복호화시에 동일하게 전달되어야 복호화가 가능하기 때문이다.

  • Cipher 운영모드(opmode)
    • ENCRYPT_MODE : 암호화 모드. 
    • DECRYPT_MODE : 복호화 모드
    • WRAP_MODE : key를 안전하게 전송할 수 있도록 java.security.Key를 래핑하여 바이트 변환
    • UNWRAP_MODE : WRAP_MODE에서 래핑한 Key 를 java.security.Key 객체로 변환
예) cipher.update(byteArray); cipher.doFinal(lastByteArray);
암호화/복호화는 update(), doFinal() 메서드를 통해 수행하며 마지막 데이터(혹은 단일 바이트 데이터)인경우 doFinal을 통해 해당 작업이 완료되었음을 알린다. doFinal이 호출되면 cipher 객체는 모든 데이터에 대한 암호화가 완료되었음을 인지하고 데이터블록의 빈자리에 Padding작업을 수행한다.


jca6

[ Cipher 객체 동작 흐름(이미지 출처) ]

블록암호 피드백모드와 IV (Initial Value)

블록 암호화는 블록단위로 데이터를 암호화 하는데 입력데이터가 같으면 동일한 암호화된 결과값을 갖게되어 해킹공격에 취약점을 가지게된다. 이러한 점을 보완하고 암호화의 복잡도를 높이기 위해 피드백 모드가 도입되었다.

피드백모드는 이전 데이터블록을 활용해 다음 블록의 암호화를 진행한다. N번째 암호화 과정에서 N번째 입력 데이터 블록 (또는 N번째 암호화된 결과 데이터 블록)과 N-1번째 입력 데이터 블록(또는 N-1번째 암호화된 결과 데이터 블록)을 XOR 연산으로 조합하는 것이다. 이때 첫번째 블록의 경우는 N-1번째 블록이 존재하지 않기때문에 이 값을 대체하는 변수값이 IV(Initial Value) 이다. IV값을 통해 동일한 입력값이더라도 전혀다른 암호화된 결과값을 얻을 수 있다.

다음의 그림은 원본파일 / 피드백모드를 사용하지 않는 암호화 결과 / 피드백모드를 사용한 암호화 결과의 차이를 잘 나타내고 있다.

jca7

[ 이미지 암호화(이미지 출처: http://en.wikipedia.org/wiki/Modes_of_operation) ]



Simple Implements Examples (for JAVA)

Encode/Decode Codec Library Import

※ JVM의 encode, decode library를 사용하지 않는 이유
    - http://www.oracle.com/technetwork/java/faq-sun-packages-142232.html
    - java.*, javax.*, org.* 패키지에서 제공되는 클래스는 JVM에서 public API를 제공하고 관리되지만,
      sun.misc.*, com.sun.* 패키지의 기능은 그렇지 않으므로 언제든 변경되거나 사라질 수 있는 위험성을 내포하고 있다.

<!-- http://mvnrepository.com/artifact/commons-codec/commons-codec/1.9 -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.9</version>
</dependency>


Byte Array <-> Hex String 

import org.apache.commons.codec.binary.Hex;

Hex.encodeHexString(byteArray); // byte[] => hex string
Hex.decode(hexString); // hex string => byte[]


BASE64 Encoding/Decoding

import org.apache.commons.codec.binary.Base64; 

Base64.encodeBase64String(byteArray); // byte[] => base64 encoded string
Base64.decodeBase64(encodedString); // base64 encoded string => byte[] , new String(byteArray)를 통해 문자열로 변경


AES 암호화 (출처 : AES256 암호화 Java 샘플)

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class AES256Cipher {

    private final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";

    /** @return encryptData */
    public static byte[] encode(String key, String plainData, String iv) {
        SecretKey secretKey = new SecretKeySpec(key.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance(
AES_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv.getBytes()));

        return cipher.doFinal(plainData.getBytes("UTF-8"));
    }

    /** @return decryptData(plainData)  */
    public static String decode(String key, String encryptData, String iv) {
        SecretKey secretKey = new SecretKeySpec(key.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance(
AES_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv.getBytes()));

        return new String(cipher.doFinal(encryptData.getBytes(), "UTF-8");
    
}

}


RSA 암호화


import javax.crypto.Cipher;
import java.security.Key;

public class RSACipher {

    private final String RSA_ALGORITHM = "RSA";
    private final int KEY_SIZE = 2048;

    /** @return KeyPair */
    public static KeyPair generateRSAKeyPair() throws Exception {
        KeyPairGenerator generator = KeyPairGenerator.getInstance(
RSA_ALGORITHM);
        generator.initialize(KEY_SIZE);
        return generator.genKeyPair();
    }

    /** @return encryptData */
    public static byte[] encode(Key key, String plainData) {
        Cipher cipher = Cipher.getInstance(
RSA_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, key);

        return cipher.doFinal(plainData.getBytes("UTF-8"));
    }

    /** @return decryptData(plainData)  */
    public static String decode(Key key, String encryptData, String iv) {
        Cipher cipher = Cipher.getInstance(
RSA_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv.getBytes()));

        return new String(cipher.doFinal(encryptData.getBytes(), "UTF-8");
    
}

    public static void main(String[] args) {
        String data = "원본 데이터입니다.";

        KeyPair rsaKeyPair = generateRSAKeyPair();
        byte[] encrypted = encode(rsaKeyPair.getPrivate(), data);
        String decrypted = decode(rsaKeyPair.getPublic(), encrypted);

        assertEquals(data, decrypted);
    }
}





Posted by dreamhopp
,