2019 년 12 월 12 일 업데이트
CBC와 같은 다른 모드와 달리 GCM 모드에서는 IV를 예측할 수 없습니다. 유일한 요구 사항은 IV가 주어진 키를 가진 각 호출에 대해 고유해야한다는 것입니다. 주어진 키에 대해 한 번 반복되면 보안이 손상 될 수 있습니다. 이를 달성하는 쉬운 방법은 아래에 표시된 것처럼 강력한 의사 난수 생성기에서 난수 IV를 사용하는 것입니다.
시퀀스 또는 타임 스탬프를 IV로 사용하는 것도 가능하지만 들리는 것처럼 사소한 것은 아닙니다. 예를 들어, 시스템이 지속적 저장소에서 IV로 이미 사용 된 시퀀스를 올바르게 추적하지 않으면 시스템을 재부팅 한 후 호출로 IV를 반복 할 수 있습니다. 마찬가지로, 완벽한 시계는 없습니다. 컴퓨터 시계 등 조정
또한 2 ^ 32 호출마다 키를 회전해야합니다. IV 요구 사항에 대한 자세한 내용은이 답변 과 NIST 권장 사항을 참조하십시오 .
이것은 다음 사항을 고려하여 방금 Java 8로 작성한 암호화 및 암호 해독 코드입니다. 누군가가 이것을 유용하게 사용하기를 바랍니다.
암호화 알고리즘 : 256 비트 키를 가진 블록 암호 AES는 충분히 안전한 것으로 간주됩니다. 완전한 메시지를 암호화하려면 모드를 선택해야합니다. 기밀성과 무결성을 모두 제공하는 인증 된 암호화가 권장됩니다. GCM, CCM 및 EAX는 가장 일반적으로 사용되는 인증 된 암호화 모드입니다. GCM이 일반적으로 선호되며 GCM 전용 지침을 제공하는 인텔 아키텍처에서 성능이 우수합니다. 이 세 가지 모드는 모두 CTR 기반 (카운터 기반) 모드이므로 패딩이 필요하지 않습니다. 결과적으로 패딩 관련 공격에 취약하지 않습니다.
GCM에는 초기화 벡터 (IV)가 필요합니다. IV는 비밀이 아닙니다. 유일한 요구 사항은 무작위이거나 예측할 수 없어야합니다. Java SecuredRandom
에서이 클래스는 암호화 적으로 강력한 의사 난수를 생성합니다. 의사 난수 생성 알고리즘은 getInstance()
메소드 에서 지정할 수 있습니다 . 그러나 Java 8 이후로 권장되는 방법은 getInstanceStrong()
구성 및 제공하는 가장 강력한 알고리즘 을 사용하는 방법 을 사용 하는 것입니다.Provider
NIST는 상호 운용성, 효율성 및 단순 설계를 촉진하기 위해 GCM에 96 비트 IV를 권장합니다.
추가적인 보안을 보장하기 위해, 다음 구현 SecureRandom
에서 의사 임의 바이트 생성의 2 ^ 16 바이트마다 생성 된 후에 다시 시드됩니다.
수신자는 IV를 알아야 암호 텍스트를 해독 할 수 있습니다. 따라서 IV는 암호문과 함께 전송되어야합니다. 일부 구현에서는 IV를 AD (Associated Data)로 전송합니다. 즉, 암호 텍스트와 IV 모두에서 인증 태그가 계산됩니다. 그러나 필수는 아닙니다. 의도적 인 공격이나 네트워크 / 파일 시스템 오류로 인해 전송 중에 IV가 변경되면 인증 태그 유효성 검사가 실패하기 때문에 IV에 암호 텍스트를 미리 붙일 수 있습니다.
문자열을 변경할 수 없으므로 문자열을 사용하여 일반 텍스트 메시지 나 키를 보관할 수 없습니다. 따라서 사용 후에는 문자열을 지울 수 없습니다. 이러한 정리되지 않은 문자열은 메모리에 남아 힙 덤프에 표시 될 수 있습니다. 같은 이유로, 이러한 암호화 또는 암호 해독 방법을 호출하는 클라이언트는 더 이상 필요하지 않은 메시지 또는 키를 보유한 모든 변수 또는 배열을 지워야합니다.
일반적인 권장 사항에 따라 코드에 제공자를 하드 코딩하지 않았습니다.
마지막으로 네트워크 또는 스토리지를 통한 전송을 위해서는 키 또는 암호 텍스트가 Base64 인코딩을 사용하여 인코딩되어야합니다. Base64에 대한 자세한 내용은 여기를 참조하십시오 . Java 8 접근 방식을 따라야합니다
바이트 배열은 다음을 사용하여 지울 수 있습니다.
Arrays.fill(clearTextMessageByteArray, Byte.MIN_VALUE);
그러나, 자바 (8)로, 취소하는 쉬운 방법이 없습니다 SecretKeyspec
및 SecretKey
이 두 인터페이스의 구현으로 방법을 구현하지 않는 것 destroy()
인터페이스를 Destroyable
. 다음 코드에서는 리플렉션 SecretKeySpec
과 SecretKey
사용 을 지우는 별도의 메서드가 작성되었습니다 .
아래 언급 된 두 가지 접근 방법 중 하나를 사용하여 키를 생성해야합니다.
키는 비밀번호와 같은 비밀이지만 사람이 사용하는 비밀번호와 달리 키는 암호화 알고리즘에 의해 사용되므로 위의 방법으로 만 생성해야합니다.
package com.sapbasu.javastudy;
import java.lang.reflect.Field;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class Crypto {
private static final int AUTH_TAG_SIZE = 128; // bits
// NIST recommendation: "For IVs, it is recommended that implementations
// restrict support to the length of 96 bits, to
// promote interoperability, efficiency, and simplicity of design."
private static final int IV_LEN = 12; // bytes
// number of random number bytes generated before re-seeding
private static final double PRNG_RESEED_INTERVAL = Math.pow(2, 16);
private static final String ENCRYPT_ALGO = "AES/GCM/NoPadding";
private static final List<Integer> ALLOWED_KEY_SIZES = Arrays
.asList(new Integer[] {128, 192, 256}); // bits
private static SecureRandom prng;
// Used to keep track of random number bytes generated by PRNG
// (for the purpose of re-seeding)
private static int bytesGenerated = 0;
public byte[] encrypt(byte[] input, SecretKeySpec key) throws Exception {
Objects.requireNonNull(input, "Input message cannot be null");
Objects.requireNonNull(key, "key cannot be null");
if (input.length == 0) {
throw new IllegalArgumentException("Length of message cannot be 0");
}
if (!ALLOWED_KEY_SIZES.contains(key.getEncoded().length * 8)) {
throw new IllegalArgumentException("Size of key must be 128, 192 or 256");
}
Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);
byte[] iv = getIV(IV_LEN);
GCMParameterSpec gcmParamSpec = new GCMParameterSpec(AUTH_TAG_SIZE, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, gcmParamSpec);
byte[] messageCipher = cipher.doFinal(input);
// Prepend the IV with the message cipher
byte[] cipherText = new byte[messageCipher.length + IV_LEN];
System.arraycopy(iv, 0, cipherText, 0, IV_LEN);
System.arraycopy(messageCipher, 0, cipherText, IV_LEN,
messageCipher.length);
return cipherText;
}
public byte[] decrypt(byte[] input, SecretKeySpec key) throws Exception {
Objects.requireNonNull(input, "Input message cannot be null");
Objects.requireNonNull(key, "key cannot be null");
if (input.length == 0) {
throw new IllegalArgumentException("Input array cannot be empty");
}
byte[] iv = new byte[IV_LEN];
System.arraycopy(input, 0, iv, 0, IV_LEN);
byte[] messageCipher = new byte[input.length - IV_LEN];
System.arraycopy(input, IV_LEN, messageCipher, 0, input.length - IV_LEN);
GCMParameterSpec gcmParamSpec = new GCMParameterSpec(AUTH_TAG_SIZE, iv);
Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);
cipher.init(Cipher.DECRYPT_MODE, key, gcmParamSpec);
return cipher.doFinal(messageCipher);
}
public byte[] getIV(int bytesNum) {
if (bytesNum < 1) throw new IllegalArgumentException(
"Number of bytes must be greater than 0");
byte[] iv = new byte[bytesNum];
prng = Optional.ofNullable(prng).orElseGet(() -> {
try {
prng = SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Wrong algorithm name", e);
}
return prng;
});
if (bytesGenerated > PRNG_RESEED_INTERVAL || bytesGenerated == 0) {
prng.setSeed(prng.generateSeed(bytesNum));
bytesGenerated = 0;
}
prng.nextBytes(iv);
bytesGenerated = bytesGenerated + bytesNum;
return iv;
}
private static void clearSecret(Destroyable key)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, SecurityException {
Field keyField = key.getClass().getDeclaredField("key");
keyField.setAccessible(true);
byte[] encodedKey = (byte[]) keyField.get(key);
Arrays.fill(encodedKey, Byte.MIN_VALUE);
}
}
암호화 키는 주로 두 가지 방법으로 생성 할 수 있습니다.
비밀번호없이
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(KEY_LEN, SecureRandom.getInstanceStrong());
SecretKey secretKey = keyGen.generateKey();
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(),
"AES");
Crypto.clearSecret(secretKey);
// After encryption or decryption with key
Crypto.clearSecret(secretKeySpec);
비밀번호로
SecureRandom random = SecureRandom.getInstanceStrong();
byte[] salt = new byte[32];
random.nextBytes(salt);
PBEKeySpec keySpec = new PBEKeySpec(password, salt, iterations,
keyLength);
SecretKeyFactory keyFactory =
SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
SecretKey secretKey = keyFactory.generateSecret(keySpec);
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(),
"AES");
Crypto.clearSecret(secretKey);
// After encryption or decryption with key
Crypto.clearSecret(secretKeySpec);
주석을 기반으로 업데이트
@MaartenBodewes가 지적한 것처럼 내 대답은 String
질문에 필요한 것을 처리하지 못했습니다 . 따라서 누군가 가이 답변을 우연히 발견하고 처리에 대해 궁금해하는 경우를 대비하여 그 격차를 메우려 고합니다 String
.
답변의 앞부분에서 알 수 있듯이 a에서 민감한 정보를 처리하는 String
것은 일반적으로 String
불변이므로 사용 후에는 정보를 지울 수 없습니다. 그리고 우리가 알다시피, String
강력한 참조가 없더라도 가비지 수집기는 즉시 힙을 제거하기 위해 서두르지 않습니다. 따라서 String
프로그램에 액세스 할 수없는 경우에도 메모리에서 알 수없는 시간 동안 계속 유지됩니다. 그 문제는 해당 기간 동안 힙 덤프가 중요한 정보를 공개한다는 것입니다. 따라서 모든 민감한 정보를 바이트 배열 또는 char 배열로 처리 한 다음 목적이 달성되면 배열을 0으로 채우는 것이 항상 좋습니다.
그러나 모든 지식을 가지고 암호화 할 민감한 정보가에있는 상황에서 여전히 끝내면 String
먼저 바이트 배열로 변환하고 위에서 소개 한 encrypt
및 decrypt
함수를 호출해야합니다 . 다른 입력 키는 위에 제공된 코드 스 니펫을 사용하여 생성 할 수 있습니다.
String
다음과 같은 방법으로 A 를 바이트로 변환 할 수 있습니다.
byte[] inputBytes = inputString.getBytes(StandardCharsets.UTF_8);
Java 8 String
부터는 UTF-16
인코딩 과 함께 내부적으로 힙에 저장됩니다 . 그러나 UTF-8
여기서는 UTF-16
특히 ASCII 문자의 경우보다 적은 공간을 차지하므로 여기에서 사용 했습니다 .
마찬가지로, 암호화 된 바이트 배열은 다음과 같이 문자열로 변환 될 수도 있습니다.
String encryptedString = new String(encryptedBytes, StandardCharsets.UTF_8);