내가이 질문을하는 이유 :
Android에서도 AES 암호화에 대해 많은 질문이있었습니다. 그리고 웹을 검색하면 많은 코드 조각이 있습니다. 그러나 모든 단일 페이지에서 모든 스택 오버플로 질문에서 큰 차이점이있는 또 다른 구현을 찾습니다.
그래서 “모범 사례”를 찾기 위해이 질문을 만들었습니다. 가장 중요한 요구 사항 목록을 수집하고 정말 안전한 구현을 설정할 수 있기를 바랍니다.
초기화 벡터와 솔트에 대해 읽었습니다. 내가 찾은 모든 구현에 이러한 기능이있는 것은 아닙니다. 그래서 필요합니까? 보안을 많이 강화합니까? 어떻게 구현합니까? 암호화 된 데이터를 해독 할 수없는 경우 알고리즘이 예외를 발생시켜야합니까? 아니면 안전하지 않고 읽을 수없는 문자열을 반환해야합니까? 알고리즘이 SHA 대신 Bcrypt를 사용할 수 있습니까?
내가 찾은이 두 가지 구현은 어떻습니까? 괜찮아? 완벽하거나 중요한 것이 누락 되었습니까? 이 중 어떤 것이 안전합니까?
알고리즘은 암호화를 위해 문자열과 “암호”를 가져온 다음 해당 암호로 문자열을 암호화해야합니다. 출력은 다시 문자열 (hex 또는 base64?)이어야합니다. 물론 복호화도 가능해야합니다.
Android를위한 완벽한 AES 구현은 무엇입니까?
구현 # 1 :
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
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;
public class AdvancedCrypto implements ICrypto {
public static final String PROVIDER = "BC";
public static final int SALT_LENGTH = 20;
public static final int IV_LENGTH = 16;
public static final int PBE_ITERATION_COUNT = 100;
private static final String RANDOM_ALGORITHM = "SHA1PRNG";
private static final String HASH_ALGORITHM = "SHA-512";
private static final String PBE_ALGORITHM = "PBEWithSHA256And256BitAES-CBC-BC";
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
private static final String SECRET_KEY_ALGORITHM = "AES";
public String encrypt(SecretKey secret, String cleartext) throws CryptoException {
try {
byte[] iv = generateIv();
String ivHex = HexEncoder.toHex(iv);
IvParameterSpec ivspec = new IvParameterSpec(iv);
Cipher encryptionCipher = Cipher.getInstance(CIPHER_ALGORITHM, PROVIDER);
encryptionCipher.init(Cipher.ENCRYPT_MODE, secret, ivspec);
byte[] encryptedText = encryptionCipher.doFinal(cleartext.getBytes("UTF-8"));
String encryptedHex = HexEncoder.toHex(encryptedText);
return ivHex + encryptedHex;
} catch (Exception e) {
throw new CryptoException("Unable to encrypt", e);
}
}
public String decrypt(SecretKey secret, String encrypted) throws CryptoException {
try {
Cipher decryptionCipher = Cipher.getInstance(CIPHER_ALGORITHM, PROVIDER);
String ivHex = encrypted.substring(0, IV_LENGTH * 2);
String encryptedHex = encrypted.substring(IV_LENGTH * 2);
IvParameterSpec ivspec = new IvParameterSpec(HexEncoder.toByte(ivHex));
decryptionCipher.init(Cipher.DECRYPT_MODE, secret, ivspec);
byte[] decryptedText = decryptionCipher.doFinal(HexEncoder.toByte(encryptedHex));
String decrypted = new String(decryptedText, "UTF-8");
return decrypted;
} catch (Exception e) {
throw new CryptoException("Unable to decrypt", e);
}
}
public SecretKey getSecretKey(String password, String salt) throws CryptoException {
try {
PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray(), HexEncoder.toByte(salt), PBE_ITERATION_COUNT, 256);
SecretKeyFactory factory = SecretKeyFactory.getInstance(PBE_ALGORITHM, PROVIDER);
SecretKey tmp = factory.generateSecret(pbeKeySpec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), SECRET_KEY_ALGORITHM);
return secret;
} catch (Exception e) {
throw new CryptoException("Unable to get secret key", e);
}
}
public String getHash(String password, String salt) throws CryptoException {
try {
String input = password + salt;
MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM, PROVIDER);
byte[] out = md.digest(input.getBytes("UTF-8"));
return HexEncoder.toHex(out);
} catch (Exception e) {
throw new CryptoException("Unable to get hash", e);
}
}
public String generateSalt() throws CryptoException {
try {
SecureRandom random = SecureRandom.getInstance(RANDOM_ALGORITHM);
byte[] salt = new byte[SALT_LENGTH];
random.nextBytes(salt);
String saltHex = HexEncoder.toHex(salt);
return saltHex;
} catch (Exception e) {
throw new CryptoException("Unable to generate salt", e);
}
}
private byte[] generateIv() throws NoSuchAlgorithmException, NoSuchProviderException {
SecureRandom random = SecureRandom.getInstance(RANDOM_ALGORITHM);
byte[] iv = new byte[IV_LENGTH];
random.nextBytes(iv);
return iv;
}
}
구현 # 2 :
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
/**
* Usage:
* <pre>
* String crypto = SimpleCrypto.encrypt(masterpassword, cleartext)
* ...
* String cleartext = SimpleCrypto.decrypt(masterpassword, crypto)
* </pre>
* @author ferenc.hechler
*/
public class SimpleCrypto {
public static String encrypt(String seed, String cleartext) throws Exception {
byte[] rawKey = getRawKey(seed.getBytes());
byte[] result = encrypt(rawKey, cleartext.getBytes());
return toHex(result);
}
public static String decrypt(String seed, String encrypted) throws Exception {
byte[] rawKey = getRawKey(seed.getBytes());
byte[] enc = toByte(encrypted);
byte[] result = decrypt(rawKey, enc);
return new String(result);
}
private static byte[] getRawKey(byte[] seed) throws Exception {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
sr.setSeed(seed);
kgen.init(128, sr); // 192 and 256 bits may not be available
SecretKey skey = kgen.generateKey();
byte[] raw = skey.getEncoded();
return raw;
}
private static byte[] encrypt(byte[] raw, byte[] clear) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(clear);
return encrypted;
}
private static byte[] decrypt(byte[] raw, byte[] encrypted) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] decrypted = cipher.doFinal(encrypted);
return decrypted;
}
public static String toHex(String txt) {
return toHex(txt.getBytes());
}
public static String fromHex(String hex) {
return new String(toByte(hex));
}
public static byte[] toByte(String hexString) {
int len = hexString.length()/2;
byte[] result = new byte[len];
for (int i = 0; i < len; i++)
result[i] = Integer.valueOf(hexString.substring(2*i, 2*i+2), 16).byteValue();
return result;
}
public static String toHex(byte[] buf) {
if (buf == null)
return "";
StringBuffer result = new StringBuffer(2*buf.length);
for (int i = 0; i < buf.length; i++) {
appendHex(result, buf[i]);
}
return result.toString();
}
private final static String HEX = "0123456789ABCDEF";
private static void appendHex(StringBuffer sb, byte b) {
sb.append(HEX.charAt((b>>4)&0x0f)).append(HEX.charAt(b&0x0f));
}
}
출처 : http://www.tutorials-android.com/learn/How_to_encrypt_and_decrypt_strings.rhtml
답변
질문에 제공 한 구현은 완전히 정확하지 않으며 제공하는 구현도 그대로 사용해서는 안됩니다. 다음에서는 Android의 암호 기반 암호화 측면에 대해 설명합니다.
키 및 해시
솔트를 사용하여 암호 기반 시스템에 대해 논의하기 시작합니다. 소금은 무작위로 생성 된 숫자입니다. 그것은 “추론”되지 않습니다. 구현 1에는 generateSalt()
강력한 암호화 난수를 생성 하는 방법이 포함됩니다 . 소금은 보안에 중요하므로 한 번만 생성하면되지만 생성 된 후에는 비밀로 유지해야합니다. 이것이 웹 사이트 인 경우 솔트 비밀을 유지하는 것이 상대적으로 쉽지만 설치된 응용 프로그램 (데스크톱 및 모바일 장치 용)의 경우 훨씬 더 어려울 것입니다.
이 메서드 getHash()
는 단일 문자열로 연결된 주어진 암호와 솔트의 해시를 반환합니다. 사용 된 알고리즘은 512 비트 해시를 반환하는 SHA-512입니다. 이 메서드는 문자열의 무결성을 확인하는 데 유용한 해시를 반환하므로 getHash()
단순히 두 매개 변수를 연결하기 때문에 암호 만 사용 하거나 솔트 만 사용하여 호출 하여 사용할 수도 있습니다 . 이 방법은 암호 기반 암호화 시스템에서 사용되지 않으므로 더 이상 논의하지 않겠습니다.
메서드 getSecretKey()
는에서 char
반환 된대로 암호 및 16 진수 인코딩 솔트 배열 에서 키를 파생합니다 generateSalt()
. 사용 된 알고리즘은 SHA-256을 해시 함수로 사용하는 PKCS5의 PBKDF1 (내 생각에는)이며 256 비트 키를 반환합니다. 무차별 대입 공격을 수행하는 데 필요한 시간을 늘리기 위해 getSecretKey()
암호, 솔트 및 카운터의 해시 ( PBE_ITERATION_COUNT
여기서는 100에 주어진 반복 횟수까지)를 반복적으로 생성하여 키를 생성합니다 . 솔트의 길이는 최소한 생성되는 키만큼 길어야합니다 (이 경우에는 최소한 256 비트). 반복 횟수는 비합리적인 지연없이 가능한 한 길게 설정해야합니다. 키 파생의 솔트 및 반복 횟수에 대한 자세한 내용은 RFC2898의 섹션 4를 참조 하세요 .
그러나 Java PBE의 구현은 암호에 유니 코드 문자 (즉, 8 비트 이상을 표시해야하는 문자)가 포함 된 경우 결함이 있습니다. 에서 언급했듯이 PBEKeySpec
“PKCS # 5에 정의 된 PBE 메커니즘은 각 문자의 하위 8 비트 만 살펴 봅니다.” 이 문제를 해결하려면 암호를 PBEKeySpec
. 예를 들어 “ABC”는 “004100420043”이됩니다. 또한 PBEKeySpec은 “암호를 문자 배열로 요청하므로 clearPassword()
완료되면 [ ] 로 덮어 쓸 수 있습니다 “. ( “메모리의 문자열 보호”와 관련 하여이 질문을 참조하십시오 .) 그래도 문제는 없습니다.
암호화
키가 생성되면이를 사용하여 텍스트를 암호화하고 해독 할 수 있습니다.
구현 1에서 사용 된 암호 알고리즘 AES/CBC/PKCS5Padding
은 PKCS # 5에 패딩이 정의 된 CBC (Cipher Block Chaining) 암호 모드의 AES입니다. (기타 AES 암호 모드에는 카운터 모드 (CTR), 전자 코드북 모드 (ECB) 및 Galois 카운터 모드 (GCM)가 포함됩니다. Stack Overflow 에 대한 또 다른 질문 에는 다양한 AES 암호 모드 및 사용 권장 모드에 대해 자세히 설명하는 답변이 포함되어 있습니다. 또한 CBC 모드 암호화에 대한 여러 공격이 있으며 그중 일부는 RFC 7457에 언급되어 있습니다.)
암호화 된 데이터의 무결성도 확인하는 암호화 모드를 사용해야합니다 (예 : RFC 5116에 설명 된 관련 데이터 를 사용한 인증 된 암호화 , AEAD). 그러나 AES/CBC/PKCS5Padding
는 무결성 검사를 제공하지 않으므로 단독으로 권장되지 않습니다 . AEAD 목적의 경우 관련 키 공격을 피하기 위해 일반 암호화 키보다 두 배 이상 긴 비밀을 사용하는 것이 좋습니다. 첫 번째 절반은 암호화 키로, 나머지 절반은 무결성 검사를위한 키 역할을합니다. (즉,이 경우 암호와 솔트에서 하나의 비밀을 생성하고 그 비밀을 둘로 분할합니다.)
자바 구현
구현 1의 다양한 기능은 알고리즘에 대해 특정 공급자, 즉 “BC”를 사용합니다. 그러나 일반적으로 지원 부족, 코드 중복 방지 또는 기타 이유로 모든 Java 구현에서 모든 공급자를 사용할 수있는 것은 아니기 때문에 특정 공급자를 요청하는 것은 권장되지 않습니다. 이 조언은 2018 년 초에 Android P 미리보기가 출시 된 이후로 특히 중요해졌습니다. ‘BC’제공 업체의 일부 기능이 지원 중단 되었기 때문입니다. Android 개발자 블로그의 ‘Android P의 암호화 변경’문서를 참조하세요. Oracle 공급자 소개를 참조하십시오 .
따라서 PROVIDER
존재하지 않아야하며에서 문자열 -BC
을 제거해야합니다 PBE_ALGORITHM
. 구현 2는이 점에서 정확합니다.
메서드가 모든 예외를 포착하는 것은 부적절하지만 가능한 예외 만 처리하는 것은 부적절합니다. 귀하의 질문에 제공된 구현은 다양한 확인 된 예외를 throw 할 수 있습니다. 메서드는 확인 된 예외 만 CryptoException으로 래핑하도록 선택하거나 throws
절 에서 확인 된 예외를 지정할 수 있습니다. 편의상 원래 예외를 CryptoException으로 래핑하는 것이 적절할 수 있습니다. 클래스가 throw 할 수있는 잠재적으로 많은 확인 된 예외가 있기 때문입니다.
SecureRandom
Android에서
Android 개발자 블로그의 “Some SecureRandom Thoughts”기사에 자세히 설명 된대로 java.security.SecureRandom
2013 년 이전에 Android 릴리스에서 구현 하면 제공하는 난수의 강도를 감소시키는 결함이 있습니다. 이 결함은 해당 기사에 설명 된대로 완화 할 수 있습니다.
답변
# 2는 암호에 “AES”(텍스트에 대한 ECB 모드 암호화를 의미 함) 만 사용하므로 절대 사용해서는 안됩니다. # 1에 대해 이야기하겠습니다.
첫 번째 구현은 암호화에 대한 모범 사례를 준수하는 것 같습니다. 상수는 일반적으로 괜찮지 만 PBE를 수행하기위한 반복 횟수와 솔트 크기는 모두 짧은 편입니다. 또한 PBE 키 생성이 하드 코딩 된 값으로 256을 사용하기 때문에 AES-256에 해당하는 것으로 보입니다 (모든 상수 이후에 수치심). 그것은 최소한 당신이 기대하는 CBC와 PKCS5Padding을 사용합니다.
인증 / 무결성 보호가 완전히 누락되어 공격자가 암호 텍스트를 변경할 수 있습니다. 이는 클라이언트 / 서버 모델에서 패딩 오라클 공격이 가능함을 의미합니다. 또한 공격자가 암호화 된 데이터를 변경하려고 시도 할 수 있음을 의미합니다. 패딩이나 콘텐츠가 응용 프로그램에서 허용되지 않기 때문에 어딘가에서 오류가 발생할 수 있지만 원하는 상황은 아닙니다.
예외 처리 및 입력 유효성 검사를 향상시킬 수 있으며 예외를 잡는 것은 항상 내 책에서 잘못되었습니다. 게다가이 클래스는 내가 모르는 ICrypt를 구현합니다. 클래스에 부작용이없는 메서드 만있는 것이 조금 이상하다는 것을 알고 있습니다. 일반적으로 정적으로 만들 것입니다. Cipher 인스턴스 등의 버퍼링이 없으므로 모든 필수 개체가 생성됩니다. 그러나 정의에서 ICrypto를 안전하게 제거 할 수 있습니다.이 경우 코드를 정적 메서드로 리팩터링 할 수도 있습니다 (또는 원하는 객체 지향으로 다시 작성).
문제는 모든 래퍼가 항상 사용 사례에 대해 가정한다는 것입니다. 래퍼가 옳고 그름을 말하는 것은 그러므로 벙크입니다. 이것이 제가 항상 래퍼 클래스 생성을 피하려고하는 이유입니다. 그러나 적어도 명백히 잘못된 것 같지는 않습니다.
답변
꽤 흥미로운 질문을하셨습니다. 모든 알고리즘과 마찬가지로 암호 키는 “비밀 소스”입니다. 일단 대중에게 알려지면 다른 모든 것도 마찬가지입니다. 따라서 Google에서이 문서에 대한 방법을 살펴 봅니다.
Google 인앱 결제 외에도 통찰력있는 보안에 대한 생각을 제공합니다.
답변
BouncyCastle Lightweight API를 사용하십시오. PBE 및 Salt와 함께 256 AES를 제공합니다.
다음은 파일을 암호화 / 복호화 할 수있는 샘플 코드입니다.
public void encrypt(InputStream fin, OutputStream fout, String password) {
try {
PKCS12ParametersGenerator pGen = new PKCS12ParametersGenerator(new SHA256Digest());
char[] passwordChars = password.toCharArray();
final byte[] pkcs12PasswordBytes = PBEParametersGenerator.PKCS12PasswordToBytes(passwordChars);
pGen.init(pkcs12PasswordBytes, salt.getBytes(), iterationCount);
CBCBlockCipher aesCBC = new CBCBlockCipher(new AESEngine());
ParametersWithIV aesCBCParams = (ParametersWithIV) pGen.generateDerivedParameters(256, 128);
aesCBC.init(true, aesCBCParams);
PaddedBufferedBlockCipher aesCipher = new PaddedBufferedBlockCipher(aesCBC, new PKCS7Padding());
aesCipher.init(true, aesCBCParams);
// Read in the decrypted bytes and write the cleartext to out
int numRead = 0;
while ((numRead = fin.read(buf)) >= 0) {
if (numRead == 1024) {
byte[] plainTemp = new byte[aesCipher.getUpdateOutputSize(numRead)];
int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
final byte[] plain = new byte[offset];
System.arraycopy(plainTemp, 0, plain, 0, plain.length);
fout.write(plain, 0, plain.length);
} else {
byte[] plainTemp = new byte[aesCipher.getOutputSize(numRead)];
int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
int last = aesCipher.doFinal(plainTemp, offset);
final byte[] plain = new byte[offset + last];
System.arraycopy(plainTemp, 0, plain, 0, plain.length);
fout.write(plain, 0, plain.length);
}
}
fout.close();
fin.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public void decrypt(InputStream fin, OutputStream fout, String password) {
try {
PKCS12ParametersGenerator pGen = new PKCS12ParametersGenerator(new SHA256Digest());
char[] passwordChars = password.toCharArray();
final byte[] pkcs12PasswordBytes = PBEParametersGenerator.PKCS12PasswordToBytes(passwordChars);
pGen.init(pkcs12PasswordBytes, salt.getBytes(), iterationCount);
CBCBlockCipher aesCBC = new CBCBlockCipher(new AESEngine());
ParametersWithIV aesCBCParams = (ParametersWithIV) pGen.generateDerivedParameters(256, 128);
aesCBC.init(false, aesCBCParams);
PaddedBufferedBlockCipher aesCipher = new PaddedBufferedBlockCipher(aesCBC, new PKCS7Padding());
aesCipher.init(false, aesCBCParams);
// Read in the decrypted bytes and write the cleartext to out
int numRead = 0;
while ((numRead = fin.read(buf)) >= 0) {
if (numRead == 1024) {
byte[] plainTemp = new byte[aesCipher.getUpdateOutputSize(numRead)];
int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
// int last = aesCipher.doFinal(plainTemp, offset);
final byte[] plain = new byte[offset];
System.arraycopy(plainTemp, 0, plain, 0, plain.length);
fout.write(plain, 0, plain.length);
} else {
byte[] plainTemp = new byte[aesCipher.getOutputSize(numRead)];
int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
int last = aesCipher.doFinal(plainTemp, offset);
final byte[] plain = new byte[offset + last];
System.arraycopy(plainTemp, 0, plain, 0, plain.length);
fout.write(plain, 0, plain.length);
}
}
fout.close();
fin.close();
} catch (Exception e) {
e.printStackTrace();
}
}
답변
여기 멋진 구현을 발견
http://nelenkov.blogspot.fr/2012/04/using-password-based-encryption-on.html
및
https://github.com/nelenkov/android-pbe
도 도움이되었다 Android 용 AES 구현에 대한 내 탐구에서
답변
