Неверные начальные байты после расшифровки Java AES / CBC
Что не так в следующем примере?
Проблема в том, что первая часть расшифрованной строки является бессмыслицей. Однако остальное в порядке, я понимаю...
Result: `£eB6O�geS��i are you? Have a nice day.
@Test
public void testEncrypt() {
try {
String s = "Hello there. How are you? Have a nice day.";
// Generate key
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128);
SecretKey aesKey = kgen.generateKey();
// Encrypt cipher
Cipher encryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
encryptCipher.init(Cipher.ENCRYPT_MODE, aesKey);
// Encrypt
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, encryptCipher);
cipherOutputStream.write(s.getBytes());
cipherOutputStream.flush();
cipherOutputStream.close();
byte[] encryptedBytes = outputStream.toByteArray();
// Decrypt cipher
Cipher decryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivParameterSpec = new IvParameterSpec(aesKey.getEncoded());
decryptCipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec);
// Decrypt
outputStream = new ByteArrayOutputStream();
ByteArrayInputStream inStream = new ByteArrayInputStream(encryptedBytes);
CipherInputStream cipherInputStream = new CipherInputStream(inStream, decryptCipher);
byte[] buf = new byte[1024];
int bytesRead;
while ((bytesRead = cipherInputStream.read(buf)) >= 0) {
outputStream.write(buf, 0, bytesRead);
}
System.out.println("Result: " + new String(outputStream.toByteArray()));
}
catch (Exception ex) {
ex.printStackTrace();
}
}
Переведено автоматически
Ответ 1
Многие люди, включая меня, сталкиваются с множеством проблем при выполнении этой работы из-за отсутствия некоторой информации, например, забывания преобразовать в Base64, векторов инициализации, набора символов и т.д. Поэтому я подумал о создании полностью функционального кода.
Надеюсь, это будет полезно для всех вас: Для компиляции вам понадобится дополнительный jar-кодек Apache Commons, который доступен здесь: http://commons.apache.org/proper/commons-codec/download_codec.cgi
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
public class Encryptor {
public static String encrypt(String key, String initVector, String value) {
try {
IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
byte[] encrypted = cipher.doFinal(value.getBytes());
System.out.println("encrypted string: "
+ Base64.encodeBase64String(encrypted));
return Base64.encodeBase64String(encrypted);
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
public static String decrypt(String key, String initVector, String encrypted) {
try {
IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
byte[] original = cipher.doFinal(Base64.decodeBase64(encrypted));
return new String(original);
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
public static void main(String[] args) {
String key = "Bar12345Bar12345"; // 128 bit key
String initVector = "RandomInitVector"; // 16 bytes IV
System.out.println(decrypt(key, initVector,
encrypt(key, initVector, "Hello World")));
}
}
Ответ 2
В этом ответе я предпочитаю обратиться к основной теме "Простой пример шифрования / дешифрования Java AES", а не к конкретному вопросу отладки, потому что я думаю, что это принесет пользу большинству читателей.
Это простое изложение моего поста в блоге о шифровании AES в Java, поэтому я рекомендую прочитать его, прежде чем что-либо внедрять. Однако я все же приведу простой пример для использования и дам несколько советов, на что следует обратить внимание.
В этом примере я выберу использовать аутентифицированное шифрование с режимом Галуа / счетчика или режимом GCM. Причина в том, что в большинстве случаев вам нужна целостность и аутентичность в сочетании с конфиденциальностью (подробнее читайте в блоге).
Руководство по шифрованию / расшифровке AES-GCM
Вот шаги, необходимые для шифрования / дешифрования с помощью AES-GCM с архитектурой Java Cryptography Architecture (JCA). Не смешивайте с другими примерами, поскольку незначительные различия могут сделать ваш код крайне небезопасным.
1. Создайте ключ
Поскольку это зависит от вашего варианта использования, я предположу простейший случай: случайный секретный ключ.
SecureRandom secureRandom = new SecureRandom();
byte[] key = new byte[16];
secureRandom.nextBytes(key);
SecretKey secretKey = SecretKeySpec(key, "AES");
Важно:
- всегда используйте надежный генератор псевдослучайных чисел, например
SecureRandom
- используйте ключ длиной 16 байт / 128 бит (или больше - но больше редко требуется)
- если вы хотите получить ключ, полученный из пароля пользователя, загляните в хэш-функцию пароля (или KDF) со свойством растяжения, например PBKDF2 или bcrypt
- если вы хотите получить ключ из других источников, используйте соответствующую функцию получения ключа (KDF), например, HKDF (реализация Java здесь). Не используйте для этого простые криптографические хэши (например, SHA-256).
2. Создайте вектор инициализации
Используется вектор инициализации (IV), чтобы один и тот же секретный ключ создавал разные зашифрованные тексты.
byte[] iv = new byte[12]; //NEVER REUSE THIS IV WITH SAME KEY
secureRandom.nextBytes(iv);
Важно:
- никогда не повторно используйте один и тот же IV с одним и тем же ключом (очень важно в режиме GCM / CTR)
- IV должен быть уникальным (т.Е.. используйте случайный IV или счетчик)
- IV не обязательно должен быть секретным
- всегда используйте надежный генератор псевдослучайных чисел, например
SecureRandom
- 12 байт IV - правильный выбор для режима AES-GCM
3. Шифрование с помощью IV и ключа
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); //128 bit auth tag length
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
byte[] cipherText = cipher.doFinal(plainText);
Важно:
- используйте 16-байтовый / 128-битный тег аутентификации (используется для проверки целостности / аутентичности)
- тег аутентификации будет автоматически добавлен к зашифрованному тексту (в реализации JCA)
- поскольку GCM ведет себя как потоковый шифр, заполнение не требуется
- используется
CipherInputStream
при шифровании больших фрагментов данных - хотите проверить дополнительные (несекретные) данные, если они были изменены? Возможно, вы захотите использовать связанные данные с
cipher.updateAAD(associatedData);
More here .
3. Сериализовать в одно сообщение
Просто добавьте IV и зашифрованный текст. Как указано выше, IV не обязательно должен быть секретным.
ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + cipherText.length);
byteBuffer.put(iv);
byteBuffer.put(cipherText);
byte[] cipherMessage = byteBuffer.array();
Необязательно кодировать с помощью Base64, если вам нужно строковое представление. Используйте либо Android, либо встроенную в Java 8 реализацию (не используйте кодек Apache Commons - это ужасная реализация). Кодирование используется для "преобразования" массивов байтов в строковое представление, чтобы сделать его безопасным для ASCII, например:
String base64CipherMessage = Base64.getEncoder().encodeToString(cipherMessage);
4. Prepare Decryption: Deserialize
If you have encoded the message, first decode it to byte array:
byte[] cipherMessage = Base64.getDecoder().decode(base64CipherMessage)
Important:
- be careful to validate input parameters, so to avoid denial of service attacks by allocating too much memory.
5. Decrypt
Initialize the cipher and set the same parameters as with the encryption:
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
//use first 12 bytes for iv
AlgorithmParameterSpec gcmIv = new GCMParameterSpec(128, cipherMessage, 0, 12);
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmIv);
//use everything from 12 bytes on as ciphertext
byte[] plainText = cipher.doFinal(cipherMessage, 12, cipherMessage.length - 12);
Important:
- don't forget to add associated data with
cipher.updateAAD(associatedData);
if you added it during encryption.
A working code snippet can be found in this gist.
Note that most recent Android (SDK 21+) and Java (7+) implementations should have AES-GCM. Older versions may lack it. I still choose this mode, since it is easier to implement in addition to being more efficient compared to similar mode of Encrypt-then-Mac (with e.g. AES-CBC + HMAC). See this article on how to implement AES-CBC with HMAC.
Ответ 3
Вот решение без Apache Commons Codec
's Base64
:
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class AdvancedEncryptionStandard
{
private byte[] key;
private static final String ALGORITHM = "AES";
public AdvancedEncryptionStandard(byte[] key)
{
this.key = key;
}
/**
* Encrypts the given plain text
*
* @param plainText The plain text to encrypt
*/
public byte[] encrypt(byte[] plainText) throws Exception
{
SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher.doFinal(plainText);
}
/**
* Decrypts the given byte array
*
* @param cipherText The data to decrypt
*/
public byte[] decrypt(byte[] cipherText) throws Exception
{
SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
return cipher.doFinal(cipherText);
}
}
Пример использования:
byte[] encryptionKey = "MZygpewJsCpRrfOr".getBytes(StandardCharsets.UTF_8);
byte[] plainText = "Hello world!".getBytes(StandardCharsets.UTF_8);
AdvancedEncryptionStandard advancedEncryptionStandard = new AdvancedEncryptionStandard(
encryptionKey);
byte[] cipherText = advancedEncryptionStandard.encrypt(plainText);
byte[] decryptedCipherText = advancedEncryptionStandard.decrypt(cipherText);
System.out.println(new String(plainText));
System.out.println(new String(cipherText));
System.out.println(new String(decryptedCipherText));
С принтами:
Hello world!
դ;��LA+�ߙb*
Hello world!
Ответ 4
Мне кажется, что вы неправильно работаете со своим вектором инициализации (IV). Прошло много времени с тех пор, как я в последний раз читал об AES, IVS и цепочке блоков, но ваша строка
IvParameterSpec ivParameterSpec = new IvParameterSpec(aesKey.getEncoded());
похоже, что все не в порядке. В случае AES вы можете рассматривать вектор инициализации как "начальное состояние" экземпляра шифрования, и это состояние представляет собой бит информации, который вы можете получить не из своего ключа, а из фактического вычисления шифрующего кода. (Можно было бы утверждать, что если бы IV можно было извлечь из ключа, то это было бы бесполезно, поскольку ключ уже передан экземпляру cipher на этапе его инициализации).
Следовательно, вы должны получить IV в виде байта [] из экземпляра cipher в конце вашего шифрования
cipherOutputStream.close();
byte[] iv = encryptCipher.getIV();
и вы должны инициализировать свой Cipher
in DECRYPT_MODE
этим байтом [] :
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
Тогда ваша расшифровка должна быть в порядке.
Надеюсь, это поможет.