Я пытаюсь написать класс шифратора/дешифратора, который шифрует и расшифровывает файлы. Мой сервер получает файл как InputStream, и я хочу сохранить его по определенному пути, поэтому это входные данные для моего метода encrypt(). В целях тестирования encrypt() генерирует новый ключ для каждого файла и сохраняет его в карте с путем к файлу. Мой метод decrypt() просто берет имя файла и пытается распечатать его содержимое в виде строки. encrypt() кажется работает нормально. Проблема, с которой я столкнулся, заключается в том, что когда я вызываю decrypt() с именем файла, я получаю исключение ShortBufferException. Я пытался изменить размер буфера в своем BufferedReader, но безрезультатно. Я думаю, что мне не хватает чего-то фундаментального в том, как CipherInputStream или InputStream работают в целом. Любые мысли будут очень признательны! Вот мой FileEncrypterDecrypter класс:
public class FileEncrypterDecrypter {
private static final int ALGORITHM_NONCE_SIZE = 12;
private static final String ALGORITHM_NAME = "AES/GCM/NoPadding";
private final static int ALGORITHM_TAG_SIZE = 128;
private final static HashMap<String, SecretKey> fileToKey = new HashMap<>();
FileEncrypterDecrypter() {}
public static void encrypt(InputStream plaintext, String filePath) throws IOException, InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException {
// Generate a new SecretKey for this file
SecretKey secretKey = KeyGenerator.getInstance("AES").generateKey();
fileToKey.put(filePath, secretKey);
// Generate a 96-bit nonce using a CSPRNG.
SecureRandom rand = new SecureRandom();
byte[] nonce = new byte[ALGORITHM_NONCE_SIZE];
rand.nextBytes(nonce);
System.out.println(Arrays.toString(nonce));
// Create the cipher instance and initialize.
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(ALGORITHM_TAG_SIZE, nonce));
// Write to the file using a CipherOutputStream
FileOutputStream fileOut = new FileOutputStream(filePath);
CipherOutputStream cipherOut = new CipherOutputStream(fileOut, cipher);
fileOut.write(nonce);
plaintext.transferTo(cipherOut);
}
public static String decrypt(String filePath) throws InvalidKeyException, IOException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
// Get the SecretKey for this file
SecretKey secretKey = fileToKey.get(filePath);
// Read nonce from the file
FileInputStream fileIn = new FileInputStream(filePath);
byte[] nonce = new byte[ALGORITHM_NONCE_SIZE];
fileIn.read(nonce);
System.out.println(Arrays.toString(nonce));
fileIn = new FileInputStream(filePath);
// Create the cipher instance and initialize
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, nonce));
CipherInputStream cipherIn = new CipherInputStream(fileIn, cipher);
InputStreamReader inputReader = new InputStreamReader(cipherIn);
BufferedReader reader = new BufferedReader(inputReader);
// Decrypt and return result.
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
}
И мой вывод ошибки:
Exception in thread "main" java.io.IOException: javax.crypto.ShortBufferException: Output buffer invalid
at java.base/javax.crypto.CipherInputStream.getMoreData(CipherInputStream.java:148)
at java.base/javax.crypto.CipherInputStream.read(CipherInputStream.java:261)
at java.base/sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:270)
at java.base/sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:313)
at java.base/sun.nio.cs.StreamDecoder.read(StreamDecoder.java:188)
at java.base/java.io.InputStreamReader.read(InputStreamReader.java:177)
at java.base/java.io.BufferedReader.fill(BufferedReader.java:162)
at java.base/java.io.BufferedReader.readLine(BufferedReader.java:329)
at java.base/java.io.BufferedReader.readLine(BufferedReader.java:396)
at FileEncrypterDecrypter.decrypt(FileEncrypterDecrypter.java:64)
at Main.main(Main.java:22)
Caused by: javax.crypto.ShortBufferException: Output buffer invalid
at java.base/com.sun.crypto.provider.GaloisCounterMode$GCMDecrypt.doFinal(GaloisCounterMode.java:1366)
at java.base/com.sun.crypto.provider.GaloisCounterMode.engineDoFinal(GaloisCounterMode.java:432)
at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2152)
at java.base/javax.crypto.CipherInputStream.getMoreData(CipherInputStream.java:145)
... 10 more
Вот мой основной метод:
public static void main (String[] args) throws BadPaddingException, IllegalBlockSizeException, IOException, InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException {
InputStream is = new ByteArrayInputStream(Charset.forName("UTF-8").encode("HELLO WORLD").array());
FileEncrypterDecrypter encrypterDecrypter = new FileEncrypterDecrypter();
encrypterDecrypter.encrypt(is, "./message");
encrypterDecrypter.decrypt("./message");
}
Я нашел подобные проблемы при переполнении стека, но ни одна из них не применима именно к моей ситуации.
Может ли это быть так просто, как неправильное или отсутствующее дополнение? Это может привести к сбою последнего чтения из-за короткого буфера.




Вы не закрываете OutputStream в своем коде шифрования, что предотвращает запуск кода завершения шифрования. Ваша функция encrypt должна заканчиваться на
plaintext.close();
cipherOut.close();
Реализация GCM в Java не вернет никакого шифра до тех пор, пока не будет вызван метод Cipher базового объекта doFinal, после чего он возвращает весь шифр и тег. Это происходит только при вызове метода CipherOutputStream.close().
Ваш код расшифровки странен тем, что он дважды открывает шифр, не закрывая его. Он правильно считывает одноразовый номер, хотя даже это не выполняется надежно, а затем повторно открывает файл во второй раз и обрабатывает одноразовый номер, как если бы это был зашифрованный текст. Это приведет к сбою расшифровки при попытке проверки тега. Тег GCM снова приходит на помощь!
У меня код расшифровки работает успешно, если я просто удалю второй fileIn = new FileInputStream(filePath);.
Вот некоторые другие улучшения, которые я предлагаю:
byte[] nonce = new byte[ALGORITHM_NONCE_SIZE];
fileIn.read(nonce);
с
byte [] nonce = fileIn.readNBytes(ALGORITHM_NONCE_SIZE);
encrypt и decrypt должны проявлять симметрию. В данном случае это сложно, поскольку encrypt принимает в качестве параметра уже открытый InputStream. InputStream — это источник неинтерпретированных байтов, а не символов. Таким образом, максимально сохраняя симметрию, результатом decrypt должны быть те же байты, которые были прочитаны во время encrypt, а не строка.Что-то вроде следующего:
public static byte[] decrypt2(String filePath) throws Exception {
SecretKey secretKey = fileToKey.get(filePath);
// Read nonce from the file
FileInputStream fileIn = new FileInputStream(filePath);
byte[] nonce = fileIn.readNBytes(ALGORITHM_NONCE_SIZE);
System.out.println(Arrays.toString(nonce));
// Create the cipher instance and initialize
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, nonce));
CipherInputStream cipherIn = new CipherInputStream(fileIn, cipher);
return cipherIn.readAllBytes();
}
Преобразование байтов в строки может происходить вне функции decrypt.
Круто, это исправило. Спасибо за помощь и объяснения президенту Джеймсу К. Полку :)
Почему именно другие решения не применимы?