암호화
최근에 암호화에 대해서 많이 배우고 있었는데요
모르고 헷갈리고 정신없는 부분들이 너무 많아서
조금 이해하기 쉽게(?) 정리하면 좋겠어가지고 정리하게 되었습니다
사실 네이버 블로그나 여러 군데에 정말 많은 글들이 있는데요
내용이 워낙에 어려운지라
헷갈려가지고 따로 정리하게 되었네요 ㅎㅎ
먼저 암호화라는 개념은 너무나 간단합니다
내가 가진 원문의 메세지를 상대방이 해석할 수 없게 하는 것이 바로 암호화의 목적이죠
예를 들자면, 원문의 메세지는 일반적인 집주소, 전화번호 와 같은 개인정보들이 될 수도 있구요!
이제 암호화를 어떻게 하는지에 대해 알아보도록 할게요!
암호화 하는 방법
우리가 컴퓨터를 공부하다보면 암호화하는 방법들에 대해 간략히 배우게 되는데요
대표적인 종류로는 대칭키 암호, 비대칭키 암호, 해쉬 함수 등이 있습니다
해쉬함수
제일 쉬운 해쉬함수부터 알아보도록 할까요?
그림과 같이 특정한 메세지를 해쉬값으로 변환하는 과정을 거치게 되는데요.
대표적인 예로는 MD5, SHA-256 이라는 알고리즘이 있습니다
해쉬 함수를 이용하는 가장 바람직한 방법은
"Hello World" -> "bc6fdsfjiowejklsdfm92"
이라는 단방향성적인 특성만 가지게 하는 것인데요
뒤에 있는 "bc6fdsfjiowejklsdfm92" 는 더 이상 "Hello World" 로 변환하지 못하게 하는 방법입니다
엇 그렇다면? 더 이상 해독하지 못하니까 안전한 암호화 방식이 아닌가요?
그렇지 않습니다
컴퓨터의 연산속도가 보통 빠르다는 것을 다들 아실텐데요.
이는 브루트 포스(Brute Force) = 모든 경우의 수를 고려하여 답을 찾는 방식
에 매우 취약하게 됩니다
해커가 마음 먹고 시도한다면,
위 해쉬함수에 대해서 모든 경우의 수를 고려하여 해쉬 해독을 하게 되고,
데이터가 점점 쌓인 해커는 일정한 패턴을 파악하여 원본 메세지를 파악할 수 있게 됩니다
그렇다면 해쉬 함수보다 더 안전한 방법은 없을까요?
Salting
제일 쉬운 방법은 임의의 문자열을 더 붙이는 것입니다
아까 예제에서 "Hello World" 라는 문자열의 앞 혹은 뒤에
임의의 문자열 ( "tempfewfsdfds" ) 와 같은 문자열을 붙이는 것이죠
이 방법을 사용하게 되면, 해커는 문자열도 파악해야 되고, 붙인 문자열(솔트) 도 알아내야 되죠
그래서 각각의 암호화 하고 싶은 대상마다
고유한 솔트값(문자열) 을 가지고 해당 솔트값은 32bit 이상이어야 안전하게 보관할 수 있다고 합니다!
Key Stratching
위에서 모든 경우의 수를 고려하는 방식(브루트 포스)에 해쉬가 많이 취약하다고 했었죠?
그렇다면, 모든 경우의 수를 고려하는 것에 대한 속도를 늦추게 하는 방법이 있습니다
//Java Code
//해당 키를 가지고 Salt 생성 후 SHA512 적용하여, 다이제스트 생성
MessageDigest digest = MessageDigest.getInstance("SHA-512");
byte[] keyBytes = password.getBytes("UTF-8");
byte[] saltBytes = digest.digest(keyBytes);
// in Java (65536번 해싱)
PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray(), saltBytes, 65536, 256);
한마디로 엄~~~~~~~~~청 해쉬를 돌려대는 것이에요
코드상의 PBEKeySpec 이 최종적으로 해쉬 연산에 포함되는 Spec을 의미하는 것이구요
Adaptive Key Function
위에서 설명드린 2가지 방식 ( key stratching , salting ) 들을 혼합해서
엄청 하는것이 Adaptive Key Function 이에요
자세한 내용은 역시 네이버에 너무 정리가 잘 되어있어요
d2.naver.com/helloworld/318732
그래서 여기까지가 Hash Function에 대한 설명이었구요.
대칭키 비대칭키 암호화 방식은 사실 많은 글들이 있는 것 같아서
제가 설명은 생략할게요 ㅠㅠ
쉽게 설명하자면
대칭키 방식은 하나의 키(열쇠)를 바탕으로 암호화/복호화 하는 방식이구요
비대칭키 방식은 2개의 키들(열쇠) 바탕으로 암호화/복호화 하는 방식입니다.
우리는 이제 대칭키 암호화 알고리즘인 AES 에 대해서 알아보도록 하겠습니다
AES 알고리즘
사실 위의 Flow가 전체적인 암호화 복호화 과정에 대한 Flow 인데요
이것만으로는 어렵습니다
뭐가 어렵냐면
이해하기가 너무 힘들어요 ㅠㅠ
- S-Box : 치환연산
- Shift Row : 자리바꿈
- Mix Column : 치환연산
- Add RoundKey : XOR 연산을 이용한 연산
뭐가 뭐가 연산이 많습니다
우리는 알고리즘을 공부하고 싶지만 너무 읽기에는 어렵습니다
그래서 간단히 요약하면, 치환하고 자리 바꾸고 치환하고 또 연산한다
라고 요약하시면 될 것 같아요
그리고 보시면 Key 라는 항목이 존재하죠?
저 Key 가 바로 대칭키가 되는 것입니다
위 연산에 대해서는 넘어가고
어떤 단위로 암호화 되는지가 더 중요하기 때문에 ( 알고리즘 개발자는 아니니까 )
방식을 한번 알아보도록 해요
Block Cipher Mode
용어는 엄청 어려워요
그냥 요약하면, 특정 블록 단위로 암호화가 이루어진다 라고 생각하면 됩니다
각 블록들을 암호화 하기 위해서 IV(Initializing Vector) 라는 개념이 존재하구요,
이 IV 는 각각의 암호화된 블록들이 암호화하는 과정이 이루어 질때마다 다른 결과가 나오도록 하기 위해서 사용하게 됩니다
쉽게 설명해볼까요?
"Hello World" -> "bc6fdsfjiowejklsdfm92"
아까 이러한 예제가 있었는데요
만일 "Hello World" 라는 텍스트를 AES 알고리즘과 함께 IV 를 적용하게 된다면,
다음 암호화를 진행했을 때
"Hello World" -> "bc6fdsfjiowejklsdfm92" 가 아닌
"Hello World" -> "dfiouiwoernm22jkopfd" 다른 값으로 결과가 나오게 된다는 것을 의미합니다
Block Cipher Mode에는 크게 2가지가 있는데요
바로 ECB 와 CBC 입니다
ECB
그냥 딱봐도 보이시나요?
각각의 Block 들이 같은 Key 를 가지고 암호화를 진행하게 되는 것입니다
그렇기 때문에 보안에 취약하죠
왜냐하면 ECB 를 적용하게 되면, 아까 같은
"Hello World" -> "bc6fdsfjiowejklsdfm92"
연산을 매번 할때마다 동일하게 나오게 되는 것입니다!
CBC
반면에 CBC는 어떤가요?
각각의 Block을 암호화 할때마다 서로 다른 Key 를 가지고 암호화하게 됩니다
딱 보시면 아시겠지만..
만일 계속 똑같은 IV 로 암호화하게 된다면 암호화에 대한 결과값이 매번 같겠죠?
그렇기 때문에 암호화를 할 때마다 매번 다른 IV 값을 지정하는 것이 매우 중요합니다
Padding
위에서 언급한 블록단위로 암호화를 하고 싶은데,
블록이 끊어지는 경우도 있겠죠?
그럴때 모자른 부분을 채우는 것이 바로 Padding 입니다
보통은 암호 블록 사이즈가 8바이트에 맞춰져 있는데요
마지막 부분이 8바이트보다 작다면, 나머지 부분을 채우게 되는 것이에요!
마지막 문자열이 딱 채워진다고 해도 더 채워지게 되는 방식이에요
AA 07 07 07 07 07 07 07 : 1바이트 데이터 + 7바이트 패딩
AA BB 06 06 06 06 06 06 : 2바이트 데이터 + 6바이트 패딩
AA BB CC 05 05 05 05 05 : 3바이트 데이터 + 5바이트 패딩
요런식으로요!!
그래서, PKCS#5 와 PKCS#7이 있는데요
두 개의 차이라 하면, PKCS#5는 8바이트 고정길이, PKCS#7은 1~255바이트의 가변길이에요
이제 모든 이론을 다 배웠으니 바로 구현해보도록 해요!
구현하는 방법 - Java
package example;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.Base64;
public class Decryption {
private static final String algorithm = "AES";
//Java에서는 PKCS#5 = PKCS#7이랑 동일
//자세한 내용은 http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html 참고.
private static final String blockNPadding = algorithm + "/CBC/PKCS5Padding";
private static final String password = "This is Key";
private static final String IV = "This is Vector";
private static IvParameterSpec ivSpec;
private static Key keySpec;
public static void setIvSpec(IvParameterSpec ivSpec) {
Decryption.ivSpec = ivSpec;
}
public static void setKeySpec(Key keySpec) {
Decryption.keySpec = keySpec;
}
public static void main(String[] args) throws Exception {
MakeKey(password);
MakeVector(IV);
// Test-file "100 Sales Records" (5KB zip-file) downloaded at http://eforexcel.com/wp/downloads-18-sample-csv-files-data-sets-for-testing-sales/
// and encrypted (100-Sales-RecordsEncrypted.enc) using the unchanged C# code
new Decryption().decrypt(new File("C:/test/100-Sales-RecordsEncrypted.enc"), new File("C:/test/100-Sales-RecordsDecrypted.zip"));
}
/**
* 32자리의 키값을 이용하여 SecretKeySpec 생성
* @param password 절대 유출되서는 안되는 키 값이며, 이것으로 키스펙을 생성
* @throws UnsupportedEncodingException 지원되지 않는 인코딩 사용시 발생
* @throws NoSuchAlgorithmException 잘못된 알고리즘을 입력하여 키를 생성할 경우 발생
* @throws InvalidKeySpecException 잘못된 키 스펙이 생성될 경우 발생
*/
public static void MakeKey(String password)
throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeySpecException {
//암호키를 생성하는 팩토리 객체 생성
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
//다이제스트를 이용하여, SHA-512로 단방향 해시 생성 (salt 생성용)
MessageDigest digest = MessageDigest.getInstance("SHA-512");
// C# : byte[] keyBytes = System.Text.Encoding.UTF8.GetBytes(password);
byte[] keyBytes = password.getBytes("UTF-8");
// C# : byte[] saltBytes = SHA512.Create().ComputeHash(keyBytes);
byte[] saltBytes = digest.digest(keyBytes);
// 256bit (AES256은 256bit의 키, 128bit의 블록사이즈를 가짐.)
PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray(), saltBytes, 65536, 256);
Key secretKey = factory.generateSecret(pbeKeySpec);
// 256bit = 32byte
byte[] key = new byte[32];
System.arraycopy(secretKey.getEncoded(), 0, key, 0, 32);
//AES 알고리즘을 적용하여 암호화키 생성
SecretKeySpec secret = new SecretKeySpec(key, "AES");
setKeySpec(secret);
}
/**
* 16자리 초기화벡터 입력하여 ivSpec을 생성한다.
* @param IV 절대 유출되서는 안되는 키 값이며, 이것으로 키스펙을 생성
* @throws UnsupportedEncodingException 지원되지 않는 인코딩 사용시 발생
* @throws NoSuchAlgorithmException 잘못된 알고리즘을 입력하여 키를 생성할 경우 발생
* @throws InvalidKeySpecException 잘못된 키 스펙이 생성될 경우 발생
* @
*/
public static void MakeVector(String IV)
throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeySpecException {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
MessageDigest digest = MessageDigest.getInstance("SHA-512");
byte[] vectorBytes = IV.getBytes("UTF-8");
byte[] saltBytes = digest.digest(vectorBytes);
// 128bit
PBEKeySpec pbeKeySpec = new PBEKeySpec(IV.toCharArray(), saltBytes, 65536, 128);
Key secretIV = factory.generateSecret(pbeKeySpec);
// 128bit = 16byte
byte[] iv = new byte[16];
System.arraycopy(secretIV.getEncoded(), 0, iv, 0, 16);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
setIvSpec(ivSpec);
}
/**
* 원본 파일을 복호화해서 대상 파일을 만든다.
* @param source 원본 파일
* @param dest 대상 파일
* @throws Exception
*/
public void decrypt(File source, File dest) throws Exception {
Cipher c = Cipher.getInstance(blockNPadding);
c.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
fileProcessing(source, dest, c);
}
/**
* 파일 복호화 처리
* @param source 원본 파일
* @param dest 대상 파일
* @param c 생성된 Cipher 객체 전달
* @throws Exception
* @Step
* 1. 생성한 파일의 버퍼를 읽어들임.
* 2. Base64 인코딩된 문자열 -> Base64 디코딩 Byte[]로 변환
* 3. Base64 디코딩 Byte[] -> Cipher.update를 사용하여 AES256 Decryption 실행
* 4. Cipher.doFinal()로 마지막 Padding을 추가.
*/
public void fileProcessing(File source, File dest, Cipher c) throws Exception {
InputStream input = null;
OutputStream output = null;
try {
input = new BufferedInputStream(new FileInputStream(source));
output = new BufferedOutputStream(new FileOutputStream(dest));
byte[] buffer = new byte[4 * (input.available() / 4)];
int read = -1;
while ((read = input.read(buffer)) != -1) {
byte[] bufferEncoded = buffer;
if (read != buffer.length) {
bufferEncoded = Arrays.copyOf(buffer, read); //버퍼에 읽힌 값을 bufferEncoded에 Array Copy
}
byte[] bufferDecoded = Base64.getDecoder().decode(bufferEncoded); //Base64 Decode
output.write(c.update(bufferDecoded)); //AES256 Decryption
}
output.write(c.doFinal()); // Last Padding add
} catch (BadPaddingException e){
e.printStackTrace();
} finally {
if (output != null) {
try {
output.close();
} catch (IOException e) {
}
}
if (input != null) {
try {
input.close();
} catch (IOException e) {
}
}
}
}
}
요렇게 보면
위에서 말씀 드린
Salting 방식과 Key Stratching 방식을 혼합한 방식이 바로 SecretKeySpec 객체이며,
매번 다른 IV 를 생성하는 방식이 바로 IvParameterSpec 객체임을 볼 수 있습니다
정리하며
사실 제 글의 대부분이 아래 참고링크에서 퍼온 글들이 엄청 많은데요
이해하기 힘든 부분들을 지우고, 다시 정리한 글임을 사전에 미리 양해드립니다 :)
원작자분이 너무 정리를 잘해주셔서 감동을 많이 받았아요 ㅎㅎ
조금은 이해가 되셨을기를 바랍니다 :)
참고
안전한 암호화를 위한 AES 알고리즘에 대한 이해와 구현코드
'Developer > Kotlin & Java' 카테고리의 다른 글
Kotlin Collection Util Method - 코틀린의 Collection Util 함수들을 파헤쳐보자 (2) | 2021.05.30 |
---|---|
Kotlin High Function - 고차 함수 람다 함수에 대해 알아보자 (0) | 2021.02.11 |
Kotlin - Null 을 다루는 방법 / 체이닝 / lateinit (0) | 2021.01.16 |
JVM과 Garbage Collection - G1GC vs ZGC (6) | 2020.11.02 |
Java8 자바 안정적인 비동기 처리 - CompletableFuture (0) | 2020.05.22 |