diff --git a/src/main/java/org/c02e/jpgpj/Decryptor.java b/src/main/java/org/c02e/jpgpj/Decryptor.java index 8ae59f1..2915a8b 100644 --- a/src/main/java/org/c02e/jpgpj/Decryptor.java +++ b/src/main/java/org/c02e/jpgpj/Decryptor.java @@ -14,6 +14,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Objects; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.openpgp.PGPCompressedData; @@ -293,40 +294,54 @@ public void clearSecrets() { */ public FileMetadata decrypt(File ciphertext, File plaintext) throws IOException, PGPException { - if (ciphertext.equals(plaintext)) + if (Objects.equals(ciphertext.getAbsoluteFile(), plaintext.getAbsoluteFile())) throw new IOException("cannot decrypt " + ciphertext + " over itself"); // delete old output file - plaintext.delete(); + if (plaintext.delete()) { + log.debug("decrypt({}) deleted {}", ciphertext, plaintext); + } - InputStream input = null; - OutputStream output = null; - try { - int bestBufferSize = - Util.bestFileBufferSize(ciphertext.length(), maxFileBufferSize); - input = new BufferedInputStream( - new FileInputStream(ciphertext), bestBufferSize); - output = new BufferedOutputStream( - new FileOutputStream(plaintext), bestBufferSize); + long inputSize = ciphertext.length(); + try (InputStream sourceStream = new FileInputStream(ciphertext); + InputStream input = wrapSourceInputStream(sourceStream, inputSize); + OutputStream targetStream = new FileOutputStream(plaintext); + OutputStream output = wrapTargetOutputStream(targetStream, inputSize)) { return decrypt(input, output); } catch (Exception e) { // delete output file if anything went wrong - if (output != null) - try { - output.close(); - plaintext.delete(); - } catch (Exception ee) { - log.error("failed to delete bad output file {}", - plaintext, ee); - } + if (!plaintext.delete()) { + log.warn("decrypt({}) cannot clean up {}", ciphertext, plaintext); + } throw e; - } finally { - try { output.close(); } catch (Exception e) {} - try { input.close(); } catch (Exception e) {} } } + /** + * @param sourceStream Original source (ciphertext) {@link InputStream} + * @param inputSize Expected input (ciphertext) size + * @return A wrapper buffered stream optimized for the input size according to + * the current encryptor settings + * @throws IOException If failed to generate the wrapper + */ + public InputStream wrapSourceInputStream(InputStream sourceStream, long inputSize) throws IOException { + int bestFileBufferSize = Util.bestFileBufferSize(inputSize, getMaxFileBufferSize()); + return new BufferedInputStream(sourceStream, bestFileBufferSize); + } + + /** + * @param targetStream Original target (plaintext) {@link OutputStream} + * @param inputSize Expected input (ciphertext) size + * @return A wrapper buffered stream optimized for the input size according to + * the current encryptor settings + * @throws IOException If failed to generate the wrapper + */ + public OutputStream wrapTargetOutputStream(OutputStream targetStream, long inputSize) throws IOException { + int bestFileBufferSize = Util.bestFileBufferSize(inputSize, getMaxFileBufferSize()); + return new BufferedOutputStream(targetStream, bestFileBufferSize); + } + /** * Decrypts the specified PGP message into the specified output stream, * and (if {@link #isVerificationRequired}) verifies the message @@ -537,7 +552,7 @@ protected InputStream decrypt(Iterator data) * Decrypts the encrypted data as the returned input stream. */ protected InputStream decrypt(PGPPublicKeyEncryptedData data, Subkey subkey) - throws IOException, PGPException { + throws IOException, PGPException { if (data == null || subkey == null) throw new DecryptionException("no suitable decryption key found"); @@ -680,7 +695,7 @@ protected PBEDataDecryptorFactory buildSymmetricKeyDecryptor(char[] passphraseCh new BcPGPDigestCalculatorProvider()); } - protected byte[] getCopyBuffer() { + public byte[] getCopyBuffer() { return new byte[getCopyFileBufferSize()]; } diff --git a/src/main/java/org/c02e/jpgpj/Encryptor.java b/src/main/java/org/c02e/jpgpj/Encryptor.java index a69fe54..b23b165 100644 --- a/src/main/java/org/c02e/jpgpj/Encryptor.java +++ b/src/main/java/org/c02e/jpgpj/Encryptor.java @@ -603,7 +603,7 @@ public void clearSecrets() { */ public void encrypt(File plaintext, File ciphertext) throws IOException, PGPException { - if (plaintext.equals(ciphertext)) + if (Objects.equals(plaintext.getAbsoluteFile(), ciphertext.getAbsoluteFile())) throw new IOException("cannot encrypt " + plaintext + " over itself"); @@ -612,33 +612,47 @@ public void encrypt(File plaintext, File ciphertext) log.debug("encrypt({}) deleted {}", plaintext, ciphertext); } - InputStream input = null; - OutputStream output = null; - try { - int bestFileBufferSize = - Util.bestFileBufferSize(plaintext.length(), maxFileBufferSize); - input = new BufferedInputStream( - new FileInputStream(plaintext), bestFileBufferSize); - output = new BufferedOutputStream( - new FileOutputStream(ciphertext), - estimateOutFileSize(plaintext.length())); - encrypt(input, output, new FileMetadata(plaintext)); + FileMetadata meta = new FileMetadata(plaintext); + long inputSize = meta.getLength(); + try (InputStream sourceStream = new FileInputStream(plaintext); + InputStream input = wrapSourceInputStream(sourceStream, inputSize); + OutputStream targetStream = new FileOutputStream(ciphertext); + OutputStream output = wrapTargetOutputStream(targetStream, inputSize)) { + encrypt(input, output, meta); } catch (Exception e) { // delete output file if anything went wrong - if (output != null) - try { - output.close(); - ciphertext.delete(); - } catch (Exception ee) { - log.error("failed to delete bad output file " + plaintext, ee); - } + if (!ciphertext.delete()) { + log.warn("encrypt({}) cannot clean up {}", plaintext, ciphertext); + } throw e; - } finally { - try { output.close(); } catch (Exception e) {} - try { input.close(); } catch (Exception e) {} } } + /** + * @param sourceStream Original source (plaintext) {@link InputStream} + * @param inputSize Expected input (plaintext) size + * @return A wrapper buffered stream optimized for the input size according to + * the current encryptor settings + * @throws IOException If failed to generate the wrapper + */ + public InputStream wrapSourceInputStream(InputStream sourceStream, long inputSize) throws IOException { + int bestFileBufferSize = Util.bestFileBufferSize(inputSize, getMaxFileBufferSize()); + return new BufferedInputStream(sourceStream, bestFileBufferSize); + } + + /** + * @param targetStream Original target (ciphertext) {@link OutputStream} + * @param inputSize Expected input (plaintext) size + * @return A wrapper buffered stream optimized for the input size according to + * the current encryptor settings + * @throws IOException If failed to generate the wrapper + * @see #estimateOutFileBufferSize(long) + */ + public OutputStream wrapTargetOutputStream(OutputStream targetStream, long inputSize) throws IOException { + int bestFileBufferSize = estimateOutFileBufferSize(inputSize); + return new BufferedOutputStream(targetStream, bestFileBufferSize); + } + /** * Signs, compresses, and encrypts the specified content as a PGP message * into the specified output stream (with no optional metadata). @@ -1082,27 +1096,47 @@ protected PGPContentSignerBuilder buildSignerBuilder(int keyAlgorithm, int hashA return new BcPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm); } - protected byte[] getEncryptionBuffer(FileMetadata meta) { - return new byte[bestPacketSize(meta)]; + public byte[] getEncryptionBuffer(FileMetadata meta) { + return getEncryptionBuffer((meta == null) ? 0L : meta.getLength()); } - protected byte[] getCompressionBuffer(FileMetadata meta) { - return new byte[bestPacketSize(meta)]; + public byte[] getEncryptionBuffer(long inputSize) { + return new byte[bestPacketSize(inputSize)]; } - protected byte[] getLiteralBuffer(FileMetadata meta) { - return new byte[bestPacketSize(meta)]; + public byte[] getCompressionBuffer(FileMetadata meta) { + return getCompressionBuffer((meta == null) ? 0L : meta.getLength()); } - protected byte[] getCopyBuffer(FileMetadata meta) { - int len = (meta == null) ? 0 : (int) meta.getLength(); + public byte[] getCompressionBuffer(long inputSize) { + return new byte[bestPacketSize(inputSize)]; + } + + public byte[] getLiteralBuffer(FileMetadata meta) { + return getLiteralBuffer((meta == null) ? 0L : meta.getLength()); + } + + public byte[] getLiteralBuffer(long inputSize) { + return new byte[bestPacketSize(inputSize)]; + } + + public byte[] getCopyBuffer(FileMetadata meta) { + return getCopyBuffer((meta == null) ? 0L : meta.getLength()); + } + + public byte[] getCopyBuffer(long inputSize) { + int len = (int) inputSize; if (len <= 0 || len > MAX_ENCRYPT_COPY_BUFFER_SIZE) len = MAX_ENCRYPT_COPY_BUFFER_SIZE; return new byte[len]; } - protected int bestPacketSize(FileMetadata meta) { - int len = (int) meta.getLength(); + public int bestPacketSize(FileMetadata meta) { + return bestPacketSize((meta == null) ? 0L : meta.getLength()); + } + + public int bestPacketSize(long inputSize) { + int len = (int) inputSize; if (len > 0) { // add some extra space for packet flags @@ -1112,13 +1146,19 @@ protected int bestPacketSize(FileMetadata meta) { } // cap size at 64k - if (len <= 0 || len > 0x10000) - len = 0x10000; + if (len <= 0 || len > MAX_ENCRYPT_COPY_BUFFER_SIZE) { + len = MAX_ENCRYPT_COPY_BUFFER_SIZE; + } return len; } - protected int estimateOutFileSize(long inFileSize) { + /** + * @param inFileSize Input (plaintext) file size + * @return The recommended buffering for the target (ciphertext) output stream + * @see #getMaxFileBufferSize() + */ + public int estimateOutFileBufferSize(long inFileSize) { int maxBufSize = getMaxFileBufferSize(); if (inFileSize >= maxBufSize) return maxBufSize; @@ -1145,7 +1185,7 @@ protected int estimateOutFileSize(long inFileSize) { protected class SigningOutputStream extends FilterOutputStream { protected final AtomicBoolean finished = new AtomicBoolean(false); - protected FileMetadata meta; + protected final FileMetadata meta; protected List sigs; public SigningOutputStream(OutputStream out, List keys, FileMetadata meta) diff --git a/src/test/groovy/org/c02e/jpgpj/EncryptorSpec.groovy b/src/test/groovy/org/c02e/jpgpj/EncryptorSpec.groovy index 2cf0a23..79b9c8d 100644 --- a/src/test/groovy/org/c02e/jpgpj/EncryptorSpec.groovy +++ b/src/test/groovy/org/c02e/jpgpj/EncryptorSpec.groovy @@ -779,7 +779,7 @@ hQEMAyne546XDHBhAQ... setup: def encryptor = new Encryptor(); expect: - encryptor.estimateOutFileSize(inputSize) == outputSize + encryptor.estimateOutFileBufferSize(inputSize) == outputSize where: inputSize << [ -1, 0, 1, @@ -800,7 +800,7 @@ hQEMAyne546XDHBhAQ... def encryptor = new Encryptor(); encryptor.asciiArmored = true expect: - encryptor.estimateOutFileSize(inputSize) == outputSize + encryptor.estimateOutFileBufferSize(inputSize) == outputSize where: inputSize << [ -1, 0, 1, @@ -822,7 +822,7 @@ hQEMAyne546XDHBhAQ... def encryptor = new Encryptor(new Ring(stream('test-ring.asc'))) encryptor.ring.findAll('key-1')*.signing*.forSigning = false expect: - encryptor.estimateOutFileSize(inputSize) == outputSize + encryptor.estimateOutFileBufferSize(inputSize) == outputSize where: inputSize << [ -1, 0, 1, @@ -849,7 +849,7 @@ hQEMAyne546XDHBhAQ... when: def plainIn = new ByteArrayInputStream(new byte[inputSize]) encryptor.encrypt plainIn, cipherOut - def estimate = encryptor.estimateOutFileSize(inputSize) + def estimate = encryptor.estimateOutFileBufferSize(inputSize) def actual = cipherOut.size() then: estimate > actual @@ -870,7 +870,7 @@ hQEMAyne546XDHBhAQ... when: def plainIn = new ByteArrayInputStream(new byte[inputSize]) encryptor.encrypt plainIn, cipherOut - def estimate = encryptor.estimateOutFileSize(inputSize) + def estimate = encryptor.estimateOutFileBufferSize(inputSize) def actual = cipherOut.size() then: estimate > actual