java.security 라이브러리를 사용해서 MD5, SHA-256 으로 해시 하는 방법과 AES-256 으로 암호화 하고 복호화 하는 방법을 알아 봅니다.
MD5와 SHA-256은 단뱡향 암호화로 비밀번호를 암호화 하거나 데이터 전송등에서 무결성을 체크하는데 사용됩니다. MD5는 128bit로 서로 다른 값에 같은 해시가 발생하는 충돌이 확인 되었고, 빠르게 해시가 가능하므로 비밀번호를 만드는데는 안전하지 않다고합니다. 이제는 SHA-256을 사용하기는 권장하고 있습니다.
MD5이던 SHA-256 이던 적절한 길이의 salt와 bcrypt, scrypt 또는 pbkdf2와 같은 느린 알고리즘을 적용하여 무작위 대입 공격에 대한 대비를 해야만 안전한 비밀번호를 만들 수 있습니다.
이글의 예제 에서는 MD5와, SHA-256에는 salt와 해시의 반복 적용을 하지 않았습니다. 비밀번호를 만들기 위해서 사용한다면 관련 기능을 추가해야 할것입니다.
전체소스는 글 하단에 첨부해 두었습니다.
1. MD5 해시
- MessageDigest객체 생성시 알고리즘을 "MD5"로 해서 만듭니다. 해시된 데이터는 바이트 배열의 바이너리 데이터이므로 16진수 문자열로 변환해 줍니다.
public static String md5(String msg) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(msg.getBytes());
return CryptoUtil.byteToHexString(md.digest());
}
2. SHA-256으로 해시
- MessageDigest객체 생성시 알고리즘을 "SHA-256"으로 해서 만듭니다. 해시된 데이터는 바이트 배열의 바이너리 데이터이므로 16진수 문자열로 변환해 줍니다.
public static String sha256(String msg) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(msg.getBytes());
return CryptoUtil.byteToHexString(md.digest());
}
3. 바이트 배열을 HEX 문자열로 변환
- 바이트 배열을 16진수 문자열로 변환합니다.
public static String byteToHexString(byte[] data) {
StringBuilder sb = new StringBuilder();
for(byte b : data) {
sb.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
}
return sb.toString();
}
4. AES 256 으로 암호화
- AES 256은 키가 256bit 즉 32바이트 문자열 이어야 합니다.
- 이 예제에서는 임의의 길이의 키 문자열을 받아서 랜덤 salt 를 첨가해서 해시하여 256bit 키를 생성합니다.
- 암호화 모드는 CBC를 사용하고, 길이를 일정하게 하는데 PKCS5 패딩을 사용합니다.
- salt를 사용하므로 동일한 값을 암호화 하더라도 암호된 값이 동일하지 않습니다.
- 결과값에는 salt와 iv값을 추가하여 Base64로 엔코딩 하여 반환합니다.
- Java 8에는 Base64 기능이 포함되어 있지만, 그 이전 버전의 JDK를 사용한다면 apache common codec 라이브러리 등을 사용하여 Base64 엔코딩 기능을 사용할 수 있습니다.
public static String encryptAES256(String msg, String key) throws Exception {
SecureRandom random = new SecureRandom();
byte bytes[] = new byte[20];
random.nextBytes(bytes);
byte[] saltBytes = bytes;
// Password-Based Key Derivation function 2
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
// 70000번 해시하여 256 bit 길이의 키를 만든다.
PBEKeySpec spec = new PBEKeySpec(key.toCharArray(), saltBytes, 70000, 256);
SecretKey secretKey = factory.generateSecret(spec);
SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), "AES");
// 알고리즘/모드/패딩
// CBC : Cipher Block Chaining Mode
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secret);
AlgorithmParameters params = cipher.getParameters();
// Initial Vector(1단계 암호화 블록용)
byte[] ivBytes = params.getParameterSpec(IvParameterSpec.class).getIV();
byte[] encryptedTextBytes = cipher.doFinal(msg.getBytes("UTF-8"));
byte[] buffer = new byte[saltBytes.length + ivBytes.length + encryptedTextBytes.length];
System.arraycopy(saltBytes, 0, buffer, 0, saltBytes.length);
System.arraycopy(ivBytes, 0, buffer, saltBytes.length, ivBytes.length);
System.arraycopy(encryptedTextBytes, 0, buffer, saltBytes.length + ivBytes.length, encryptedTextBytes.length);
return Base64.getEncoder().encodeToString(buffer);
}
5. 암호화된 내용을 복호화
- 앞에서 암호화된 내용을 Base64 디코드 합니다.
- 붙였던 salt, iv와 데이터를 분리합니다.
- 복호화를 수행하고 복호화된 바이트 배열을 문자열로 만들어 반환합니다.
public static String decryptAES256(String msg, String key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
ByteBuffer buffer = ByteBuffer.wrap(Base64.getDecoder().decode(msg));
byte[] saltBytes = new byte[20];
buffer.get(saltBytes, 0, saltBytes.length);
byte[] ivBytes = new byte[cipher.getBlockSize()];
buffer.get(ivBytes, 0, ivBytes.length);
byte[] encryoptedTextBytes = new byte[buffer.capacity() - saltBytes.length - ivBytes.length];
buffer.get(encryoptedTextBytes);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
PBEKeySpec spec = new PBEKeySpec(key.toCharArray(), saltBytes, 70000, 256);
SecretKey secretKey = factory.generateSecret(spec);
SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), "AES");
cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(ivBytes));
byte[] decryptedTextBytes = cipher.doFinal(encryoptedTextBytes);
return new String(decryptedTextBytes);
}
6. 실행 예제 프로그램입니다.
- 키값으로 "secret key" 문자열을 사용하여 "Hello, World!" 문자열을 해시해보고, 암/복호화해 봅니다.
package com.tistory.offbyone.main;
import com.tistory.offbyone.crypto.CryptoUtil;
public class CryptoTest {
public static void main(String[] args) throws Exception {
String plainText = "Hello, World!";
String key = "secret key";
System.out.println("MD5 : " + plainText + " - " + CryptoUtil.md5(plainText));
System.out.println("SHA-256 : " + plainText + " - " + CryptoUtil.sha256(plainText));
String encrypted = CryptoUtil.encryptAES256("Hello, World!", key);
System.out.println("AES-256 : enc - " + encrypted);
System.out.println("AES-256 : dec - " + CryptoUtil.decryptAES256(encrypted, key));
}
}
실행결과 입니다.
※ 참고
JDK 8u161 이전 버전을 사용중이라면 AES-256 암호화 작업중에 다음 예외가 발생할 것입니다.
java.security.InvalidKeyException: Illegal key size or default parameters
이전 버전에서는 키 길이에 제한이 걸려 있었습니다. JDK 8u161 업데이트 릴리즈 부터 제한 하지 않는것이 기본값으로 설정되어 있습니다.
이전 버전의 JDK 를 사용할 때는 AES-256을 사용하기 위해서 jce policy 파일을 패치해야 합니다.
오라클 사이트에서 Java SE 다운로드 페이지에서 Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy 파일들을 다운로드 받아서 jre/lib/security/ 아래에 local_policy.jar, US_export_policy.jar 파일을 교체해 주면 됩니다.
※ 전체예제소스
'프로그래밍 > 자바' 카테고리의 다른 글
JUnit 4 기능 찾아보기 (1) | 2018.05.03 |
---|---|
Oracle JDK 라이센스와 OpenJDK (2) | 2018.04.26 |
자바(Java) - static import 사용하기 (0) | 2018.04.23 |
Java 에서 난수(random number) 생성하기 (6) | 2018.04.20 |
자바문법 - 배열 사용하기 (0) | 2018.04.18 |