Repository: incubator-gobblin Updated Branches: refs/heads/master c36f2c87b -> fc389522a
[GOBBLIN-293] Remove stream materialization in GPGFileDecryptor Closes #2333 from htran1/gpg_memory Project: http://git-wip-us.apache.org/repos/asf/incubator-gobblin/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-gobblin/commit/fc389522 Tree: http://git-wip-us.apache.org/repos/asf/incubator-gobblin/tree/fc389522 Diff: http://git-wip-us.apache.org/repos/asf/incubator-gobblin/diff/fc389522 Branch: refs/heads/master Commit: fc389522a6cfb15965f10480a21804fca6660605 Parents: c36f2c8 Author: Hung Tran <[email protected]> Authored: Mon Apr 16 11:24:20 2018 -0700 Committer: Hung Tran <[email protected]> Committed: Mon Apr 16 11:24:20 2018 -0700 ---------------------------------------------------------------------- .../apache/gobblin/crypto/GPGFileDecryptor.java | 155 ++++++++++++------- .../gobblin/crypto/GPGFileDecryptorTest.java | 75 ++++++++- .../test/resources/crypto/gpg/keyEncrypted.gpg | Bin 0 -> 1483 bytes .../resources/crypto/gpg/passwordEncrypted.gpg | Bin 0 -> 1198 bytes .../test/resources/crypto/gpg/testPrivate.key | 59 +++++++ .../test/resources/crypto/gpg/testPublic.key | 30 ++++ 6 files changed, 260 insertions(+), 59 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-gobblin/blob/fc389522/gobblin-modules/gobblin-crypto/src/main/java/org/apache/gobblin/crypto/GPGFileDecryptor.java ---------------------------------------------------------------------- diff --git a/gobblin-modules/gobblin-crypto/src/main/java/org/apache/gobblin/crypto/GPGFileDecryptor.java b/gobblin-modules/gobblin-crypto/src/main/java/org/apache/gobblin/crypto/GPGFileDecryptor.java index 7d62439..399b3e8 100644 --- a/gobblin-modules/gobblin-crypto/src/main/java/org/apache/gobblin/crypto/GPGFileDecryptor.java +++ b/gobblin-modules/gobblin-crypto/src/main/java/org/apache/gobblin/crypto/GPGFileDecryptor.java @@ -16,15 +16,11 @@ */ package org.apache.gobblin.crypto; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.security.Security; - import java.util.Iterator; -import lombok.SneakyThrows; -import lombok.experimental.UtilityClass; + import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; @@ -44,8 +40,8 @@ import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBu import org.bouncycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder; -import org.bouncycastle.util.io.Streams; +import lombok.experimental.UtilityClass; /** * A utility class that decrypts both password based and key based encryption files. @@ -67,23 +63,16 @@ public class GPGFileDecryptor { PGPEncryptedDataList enc = getPGPEncryptedDataList(inputStream); PGPPBEEncryptedData pbe = (PGPPBEEncryptedData) enc.get(0); - InputStream clear; + try { clear = pbe.getDataStream(new JcePBEDataDecryptorFactoryBuilder( new JcaPGPDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build()) .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(passPhrase.toCharArray())); JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(clear); - Object pgpfObject = pgpFact.nextObject(); - if (pgpfObject instanceof PGPCompressedData) { - PGPCompressedData cData = (PGPCompressedData) pgpfObject; - pgpFact = new JcaPGPObjectFactory(cData.getDataStream()); - pgpfObject = pgpFact.nextObject(); - } - PGPLiteralData ld = (PGPLiteralData) pgpfObject; - return ld.getInputStream(); + return new LazyMaterializeDecryptorInputStream(pgpFact); } catch (PGPException e) { throw new IOException(e); } @@ -97,55 +86,31 @@ public class GPGFileDecryptor { * @return * @throws IOException */ - @SneakyThrows (PGPException.class) public InputStream decryptFile(InputStream inputStream, InputStream keyIn, String passPhrase) throws IOException { + try { + PGPEncryptedDataList enc = getPGPEncryptedDataList(inputStream); + Iterator it = enc.getEncryptedDataObjects(); + PGPPrivateKey sKey = null; + PGPPublicKeyEncryptedData pbe = null; + PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(PGPUtil.getDecoderStream(keyIn), new BcKeyFingerprintCalculator()); + + while (sKey == null && it.hasNext()) { + pbe = (PGPPublicKeyEncryptedData) it.next(); + sKey = findSecretKey(pgpSec, pbe.getKeyID(), passPhrase); + } - PGPEncryptedDataList enc = getPGPEncryptedDataList(inputStream); - Iterator it = enc.getEncryptedDataObjects(); - PGPPrivateKey sKey = null; - PGPPublicKeyEncryptedData pbe =null; - PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection( - PGPUtil.getDecoderStream(keyIn), new BcKeyFingerprintCalculator()); - - while(sKey == null && it.hasNext()) { - pbe = (PGPPublicKeyEncryptedData)it.next(); - sKey = findSecretKey(pgpSec, pbe.getKeyID(), passPhrase); - } - - if (sKey == null) { - throw new IllegalArgumentException("secret key for message not found."); - } - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - - try (InputStream clear = pbe.getDataStream( - new JcePublicKeyDataDecryptorFactoryBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build(sKey))) { + if (sKey == null) { + throw new IllegalArgumentException("secret key for message not found."); + } + InputStream clear = pbe.getDataStream( + new JcePublicKeyDataDecryptorFactoryBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build(sKey)); JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(clear); - Object pgpfObject = pgpFact.nextObject(); - - while (pgpfObject != null) { - if (pgpfObject instanceof PGPCompressedData) { - PGPCompressedData cData = (PGPCompressedData) pgpfObject; - pgpFact = new JcaPGPObjectFactory(cData.getDataStream()); - pgpfObject = pgpFact.nextObject(); - } - if (pgpfObject instanceof PGPLiteralData) { - Streams.pipeAll(((PGPLiteralData) pgpfObject).getInputStream(), outputStream); - } else if (pgpfObject instanceof PGPOnePassSignatureList) { - throw new PGPException("encrypted message contains PGPOnePassSignatureList message - not literal data."); - } else if (pgpfObject instanceof PGPSignatureList) { - throw new PGPException("encrypted message contains PGPSignatureList message - not literal data."); - } else { - throw new PGPException("message is not a simple encrypted file - type unknown."); - } - pgpfObject = pgpFact.nextObject(); - } - return new ByteArrayInputStream(outputStream.toByteArray()); - } finally { - outputStream.close(); + return new LazyMaterializeDecryptorInputStream(pgpFact); + } catch (PGPException e) { + throw new IOException(e); } } @@ -191,4 +156,78 @@ public class GPGFileDecryptor { } return enc; } + + /** + * A class for reading the underlying {@link InputStream}s from the pgp object without pre-materializing all of them. + * The PGP object may present the decrypted data through multiple {@link InputStream}s, but these streams are sequential + * and the n+1 stream is not available until the end of the nth stream is reached, so the + * {@link LazyMaterializeDecryptorInputStream} keeps a reference to the {@link JcaPGPObjectFactory} and moves to new + * {@link InputStream}s as they are available + */ + private static class LazyMaterializeDecryptorInputStream extends InputStream { + JcaPGPObjectFactory pgpFact; + InputStream currentUnderlyingStream; + + public LazyMaterializeDecryptorInputStream(JcaPGPObjectFactory pgpFact) + throws IOException { + this.pgpFact = pgpFact; + + moveToNextInputStream(); + } + + @Override + public int read() + throws IOException { + int value = this.currentUnderlyingStream.read(); + + if (value != -1) { + return value; + } else { + moveToNextInputStream(); + + if (this.currentUnderlyingStream == null) { + return -1; + } + + return this.currentUnderlyingStream.read(); + } + } + + /** + * Move to the next {@link InputStream} if available, otherwise set {@link #currentUnderlyingStream} to null to + * indicate that there is no more data. + * @throws IOException + */ + private void moveToNextInputStream() throws IOException { + Object pgpfObject = this.pgpFact.nextObject(); + + // no more data + if (pgpfObject == null) { + this.currentUnderlyingStream = null; + return; + } + + if (pgpfObject instanceof PGPCompressedData) { + PGPCompressedData cData = (PGPCompressedData) pgpfObject; + + try { + this.pgpFact = new JcaPGPObjectFactory(cData.getDataStream()); + } catch (PGPException e) { + throw new IOException("Could not get the PGP data stream", e); + } + + pgpfObject = this.pgpFact.nextObject(); + } + + if (pgpfObject instanceof PGPLiteralData) { + this.currentUnderlyingStream = ((PGPLiteralData) pgpfObject).getInputStream(); + } else if (pgpfObject instanceof PGPOnePassSignatureList) { + throw new IOException("encrypted message contains PGPOnePassSignatureList message - not literal data."); + } else if (pgpfObject instanceof PGPSignatureList) { + throw new IOException("encrypted message contains PGPSignatureList message - not literal data."); + } else { + throw new IOException("message is not a simple encrypted file - type unknown."); + } + } + } } http://git-wip-us.apache.org/repos/asf/incubator-gobblin/blob/fc389522/gobblin-modules/gobblin-crypto/src/test/java/org/apache/gobblin/crypto/GPGFileDecryptorTest.java ---------------------------------------------------------------------- diff --git a/gobblin-modules/gobblin-crypto/src/test/java/org/apache/gobblin/crypto/GPGFileDecryptorTest.java b/gobblin-modules/gobblin-crypto/src/test/java/org/apache/gobblin/crypto/GPGFileDecryptorTest.java index f0ed6ac..964bebb 100644 --- a/gobblin-modules/gobblin-crypto/src/test/java/org/apache/gobblin/crypto/GPGFileDecryptorTest.java +++ b/gobblin-modules/gobblin-crypto/src/test/java/org/apache/gobblin/crypto/GPGFileDecryptorTest.java @@ -16,15 +16,18 @@ */ package org.apache.gobblin.crypto; -import com.google.common.base.Charsets; import java.io.File; import java.io.IOException; import java.io.InputStream; + import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; +import org.bouncycastle.openpgp.PGPException; import org.testng.Assert; import org.testng.annotations.Test; +import com.google.common.base.Charsets; + /** * Test class for {@link GPGFileDecryptor} @@ -58,4 +61,74 @@ public class GPGFileDecryptorTest { } } + /** + * Decrypt a large (~1gb) password encrypted file and check that memory usage does not blow up + * @throws IOException + * @throws PGPException + */ + @Test (enabled=true) + public void decryptLargeFileSym() throws IOException, PGPException { + System.gc(); + System.gc(); + + long startHeapSize = Runtime.getRuntime().totalMemory(); + + try(InputStream is = GPGFileDecryptor.decryptFile( + getClass().getResourceAsStream("/crypto/gpg/passwordEncrypted.gpg"), "test")) { + int value; + long bytesRead = 0; + + // the file contains only the character 'a' + while ((value = is.read()) != -1) { + bytesRead++; + Assert.assertTrue(value == 'a'); + } + + Assert.assertEquals(bytesRead, 1041981183L); + + System.gc(); + System.gc(); + long endHeapSize = Runtime.getRuntime().totalMemory(); + + // make sure the heap doesn't grow too much + Assert.assertTrue(endHeapSize - startHeapSize < 200 * 1024 * 1024, + "start heap " + startHeapSize + " end heap " + endHeapSize); + } + } + + /** + * Decrypt a large (~1gb) private key encrypted file and check that memory usage does not blow up + * @throws IOException + * @throws PGPException + */ + @Test (enabled=true) + public void decryptLargeFileAsym() throws IOException, PGPException { + System.gc(); + System.gc(); + + long startHeapSize = Runtime.getRuntime().totalMemory(); + + try(InputStream is = GPGFileDecryptor.decryptFile( + getClass().getResourceAsStream("/crypto/gpg/keyEncrypted.gpg"), + getClass().getResourceAsStream("/crypto/gpg/testPrivate.key"), "gobblin")) { + int value; + long bytesRead = 0; + + // the file contains only the character 'a' + while ((value = is.read()) != -1) { + bytesRead++; + Assert.assertTrue(value == 'a'); + } + + Assert.assertEquals(bytesRead, 1041981183L); + + System.gc(); + System.gc(); + long endHeapSize = Runtime.getRuntime().totalMemory(); + + // make sure the heap doesn't grow too much + Assert.assertTrue(endHeapSize - startHeapSize < 200 * 1024 * 1024, + "start heap " + startHeapSize + " end heap " + endHeapSize); + } + } } http://git-wip-us.apache.org/repos/asf/incubator-gobblin/blob/fc389522/gobblin-modules/gobblin-crypto/src/test/resources/crypto/gpg/keyEncrypted.gpg ---------------------------------------------------------------------- diff --git a/gobblin-modules/gobblin-crypto/src/test/resources/crypto/gpg/keyEncrypted.gpg b/gobblin-modules/gobblin-crypto/src/test/resources/crypto/gpg/keyEncrypted.gpg new file mode 100644 index 0000000..cb88a58 Binary files /dev/null and b/gobblin-modules/gobblin-crypto/src/test/resources/crypto/gpg/keyEncrypted.gpg differ http://git-wip-us.apache.org/repos/asf/incubator-gobblin/blob/fc389522/gobblin-modules/gobblin-crypto/src/test/resources/crypto/gpg/passwordEncrypted.gpg ---------------------------------------------------------------------- diff --git a/gobblin-modules/gobblin-crypto/src/test/resources/crypto/gpg/passwordEncrypted.gpg b/gobblin-modules/gobblin-crypto/src/test/resources/crypto/gpg/passwordEncrypted.gpg new file mode 100644 index 0000000..c017a35 Binary files /dev/null and b/gobblin-modules/gobblin-crypto/src/test/resources/crypto/gpg/passwordEncrypted.gpg differ http://git-wip-us.apache.org/repos/asf/incubator-gobblin/blob/fc389522/gobblin-modules/gobblin-crypto/src/test/resources/crypto/gpg/testPrivate.key ---------------------------------------------------------------------- diff --git a/gobblin-modules/gobblin-crypto/src/test/resources/crypto/gpg/testPrivate.key b/gobblin-modules/gobblin-crypto/src/test/resources/crypto/gpg/testPrivate.key new file mode 100644 index 0000000..530e2bf --- /dev/null +++ b/gobblin-modules/gobblin-crypto/src/test/resources/crypto/gpg/testPrivate.key @@ -0,0 +1,59 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v2.0.14 (GNU/Linux) + +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 +=27va +-----END PGP PRIVATE KEY BLOCK----- http://git-wip-us.apache.org/repos/asf/incubator-gobblin/blob/fc389522/gobblin-modules/gobblin-crypto/src/test/resources/crypto/gpg/testPublic.key ---------------------------------------------------------------------- diff --git a/gobblin-modules/gobblin-crypto/src/test/resources/crypto/gpg/testPublic.key b/gobblin-modules/gobblin-crypto/src/test/resources/crypto/gpg/testPublic.key new file mode 100644 index 0000000..ee68362 --- /dev/null +++ b/gobblin-modules/gobblin-crypto/src/test/resources/crypto/gpg/testPublic.key @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v2.0.14 (GNU/Linux) + +mQENBFrGokcBCAC7nwmzuuSZiWj03+mlcRuvQHCX0WEnOHxssgBXO30TEn4SeIoi +fdgsQzVKpUvLdJK6IowQ/uYOO1llG/5EkFZKYBV91+MXuaauo5cpmN7VaFL5Cex1 +5wT2cQq1QUWIsJb0sssnHYlsMU6gNTL/vg5wqtVjK2wvbi2K3q+d3Azph7dYHvUE +VB0IFFdaBweZHY9uMgtOy3EMB+LuNoRAwkh7go8UJNShI2xCFx/dKcHddRXDax3u +vXasxnp+bUawFCNx55l0vpsMCmeWCWrLEaSeKKAPEq/cyDFQkOoUSlqsb7LsIT1u +v6qgOc3Dfex7dwSJyJ/5UvYGyP64jJ0rB7wDABEBAAG0MEdvYmJsaW4gVGVzdCAo +Z29iYmxpbikgPGdvYmJsaW5AZ29iYmxpbi5nb2JibGluPokBOAQTAQIAIgUCWsai +RwIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQwnCTyiGofW8PEgf+NXJD +20j/ornWKPmDG7/duwDnSvKbOCCG8/u/WAm7q+mJ9nFSBhY/jKpy1VwtT0h7hy/I +f0qI8tiXzfE6aX2njIkUWTRj9DipB3I2SLz8d+ysGMTSa0gdmF2tX7hXQAh/gf8+ +GUR1HBIb9vvDHF9EHngP8kWlRvdiget0U77Z+FDRP+qOej1XYgxB6iCw4uI8dcNo +yghA0weY/1u/GEMoe8aI4mNsOWxAsEybu5q1O3TWkstkm2TQtEgZD6NkWQe5x69s +vun1oUq12s//7NdbGGfNbwYW4cm9PLmlNBapldfqjP9zmbbf4Y/Cm6ump0KQdeJN +s4tFWvxeDdWHu6EdEbkBDQRaxqJHAQgAxAnoam2a/drd62+1TXbLJKUmAWaEoWzE +onqrh0/L+v7CEluNPRArTCm7B54IcolYUJWejAFRRJaLz2zGBndwX4eyZvE2qWSe +ujZ14Ja2SXR8ZcHXpsPd5TdPixYGKgge1zNZWUEV5ncXWqjKCwISTuxoh0hQu3YJ +kolquZHJAs8LH3aKTZ9GUkyyS61rMKAarRIjr/+yURIasPPtvdnllYUf9bDOdNdT +UFHdXwVG16r2/qLL8RZDXaNlKJ5xqYvKo4TdJQj1Cq4dcyNwQntrFU5JJfETaQAD +ZMa15NSReoToKSMTX+IJ4Bsn8LGG3Qv42qWyBcubq0lM1tnyxmesQQARAQABiQEf +BBgBAgAJBQJaxqJHAhsMAAoJEMJwk8ohqH1vVzoH/j9b9VK/Sb6CW/SP5+4Xe2Rq +Ly9yilLJqSFWI27xRmCmvGMUm0sbFR/deitCW05e+BKIoO5yyOe8zAEpr1j+d4TJ +CqYSyw/9oGLVjAHJnid4SO7uDjGvTInIy8oardvPj8675Ud+H0OO7upWLKqvbrQf +WzxtS31geiUk2YKcB9dGn6MB3cZoLxjjlWSPkyuL3qbayCa0+Cz6uoKa+93Tt64S +JamGqZlvmytw1E8y4Tx/7QiNs/7qaVqrYTsBj5tmd7ESmIbEaBPxSQXAR/9zYKrZ +AEXotsW7COeATq1dC02PftnPugkH6Ue+NY9n8Ltm3+bNCZWOHJSatKGlvB4ODtI= +=02M+ +-----END PGP PUBLIC KEY BLOCK-----
