diff --git a/java/google/registry/rde/Ghostryde.java b/java/google/registry/rde/Ghostryde.java index 57dc61a71..2660473d8 100644 --- a/java/google/registry/rde/Ghostryde.java +++ b/java/google/registry/rde/Ghostryde.java @@ -21,9 +21,10 @@ import static google.registry.rde.RydeCompression.openDecompressor; import static google.registry.rde.RydeEncryption.GHOSTRYDE_USE_INTEGRITY_PACKET; import static google.registry.rde.RydeEncryption.openDecryptor; import static google.registry.rde.RydeEncryption.openEncryptor; +import static google.registry.rde.RydeFileEncoding.openPgpFileReader; +import static google.registry.rde.RydeFileEncoding.openPgpFileWriter; import static java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.bouncycastle.openpgp.PGPLiteralData.BINARY; import com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; @@ -35,12 +36,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; -import javax.annotation.WillNotClose; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPLiteralDataGenerator; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.joda.time.DateTime; @@ -113,9 +110,6 @@ import org.joda.time.DateTime; */ public final class Ghostryde { - /** Size of the buffer used by the intermediate streams. */ - static final int BUFFER_SIZE = 64 * 1024; - /** * For backwards compatibility reasons, we wrap the data in a PGP file, which preserves the * original filename and modification time. However, these values are never used, so we just @@ -178,8 +172,7 @@ public final class Ghostryde { * output. See {@link #readLength}. */ public static ImprovedOutputStream encoder( - OutputStream output, PGPPublicKey encryptionKey, @Nullable OutputStream lengthOutput) - throws IOException { + OutputStream output, PGPPublicKey encryptionKey, @Nullable OutputStream lengthOutput) { // We use a Closer to handle the stream .close, to make sure it's done correctly. Closer closer = Closer.create(); @@ -188,8 +181,7 @@ public final class Ghostryde { openEncryptor(output, GHOSTRYDE_USE_INTEGRITY_PACKET, ImmutableList.of(encryptionKey))); OutputStream kompressor = closer.register(openCompressor(encryptionLayer)); OutputStream fileLayer = - closer.register( - openPgpFileOutputStream(kompressor, INNER_FILENAME, INNER_MODIFICATION_TIME)); + closer.register(openPgpFileWriter(kompressor, INNER_FILENAME, INNER_MODIFICATION_TIME)); return new ImprovedOutputStream("GhostrydeEncoder", fileLayer) { @Override @@ -211,8 +203,7 @@ public final class Ghostryde { * @param output where to write the encrypted data * @param encryptionKey the encryption key to use */ - public static ImprovedOutputStream encoder(OutputStream output, PGPPublicKey encryptionKey) - throws IOException { + public static ImprovedOutputStream encoder(OutputStream output, PGPPublicKey encryptionKey) { return encoder(output, encryptionKey, null); } @@ -222,15 +213,14 @@ public final class Ghostryde { * @param input from where to read the encrypted data * @param decryptionKey the decryption key to use */ - public static ImprovedInputStream decoder(InputStream input, PGPPrivateKey decryptionKey) - throws IOException, PGPException { + public static ImprovedInputStream decoder(InputStream input, PGPPrivateKey decryptionKey) { // We use a Closer to handle the stream .close, to make sure it's done correctly. Closer closer = Closer.create(); InputStream decryptionLayer = closer.register(openDecryptor(input, GHOSTRYDE_USE_INTEGRITY_PACKET, decryptionKey)); InputStream decompressor = closer.register(openDecompressor(decryptionLayer)); - InputStream fileLayer = closer.register(openPgpFileInputStream(decompressor)); + InputStream fileLayer = closer.register(openPgpFileReader(decompressor)); return new ImprovedInputStream("GhostryderDecoder", fileLayer) { @Override @@ -242,45 +232,4 @@ public final class Ghostryde { } private Ghostryde() {} - - /** - * Opens an {@link OutputStream} to which the actual data should be written (Writing Step 3/3) - * - *

This is the third and final step in creating a ghostryde file. You'll want to write data to - * the returned object. - * - *

TODO(b/110465985): merge with the RyDE version. - * - * @param os is the value returned by {@link #openCompressor}. - * @param name is a filename for your data which gets written in the literal tag. - * @param modified is a timestamp for your data which gets written to the literal tags. - * @throws IOException - */ - @CheckReturnValue - private static ImprovedOutputStream openPgpFileOutputStream( - @WillNotClose OutputStream os, String name, DateTime modified) throws IOException { - return new ImprovedOutputStream( - "GhostrydePgpFileOutput", - new PGPLiteralDataGenerator() - .open(os, BINARY, name, modified.toDate(), new byte[BUFFER_SIZE])); - } - - /** - * Opens a new decoder for reading the original contents (Reading Step 3/3) - * - *

This is the final step in reading a ghostryde file. After calling this method, you should - * call the read methods on the returned {@link InputStream}. - * - *

TODO(b/110465985): merge with the RyDE version. - * - * @param input is the value returned by {@link #openDecompressor}. - * @throws IOException - * @throws PGPException - */ - @CheckReturnValue - private static ImprovedInputStream openPgpFileInputStream(@WillNotClose InputStream input) - throws IOException, PGPException { - PGPLiteralData literal = PgpUtils.readSinglePgpObject(input, PGPLiteralData.class); - return new ImprovedInputStream("GhostrydePgpFileInputStream", literal.getDataStream()); - } } diff --git a/java/google/registry/rde/RydeEncoder.java b/java/google/registry/rde/RydeEncoder.java index f8ae81670..34c5d9804 100644 --- a/java/google/registry/rde/RydeEncoder.java +++ b/java/google/registry/rde/RydeEncoder.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static google.registry.rde.RydeCompression.openCompressor; import static google.registry.rde.RydeEncryption.RYDE_USE_INTEGRITY_PACKET; import static google.registry.rde.RydeEncryption.openEncryptor; +import static google.registry.rde.RydeFileEncoding.openPgpFileWriter; import com.google.common.collect.ImmutableList; import com.google.common.io.Closer; @@ -73,8 +74,7 @@ public final class RydeEncoder extends FilterOutputStream { signer = closer.register(new RydePgpSigningOutputStream(checkNotNull(rydeOutput), signingKey)); encryptLayer = closer.register(openEncryptor(signer, RYDE_USE_INTEGRITY_PACKET, receiverKeys)); kompressor = closer.register(openCompressor(encryptLayer)); - fileLayer = - closer.register(new RydePgpFileOutputStream(kompressor, modified, filenamePrefix + ".tar")); + fileLayer = closer.register(openPgpFileWriter(kompressor, filenamePrefix + ".tar", modified)); tarLayer = closer.register( new RydeTarOutputStream(fileLayer, dataLength, modified, filenamePrefix + ".xml")); diff --git a/java/google/registry/rde/RydeFileEncoding.java b/java/google/registry/rde/RydeFileEncoding.java new file mode 100644 index 000000000..ecb11e953 --- /dev/null +++ b/java/google/registry/rde/RydeFileEncoding.java @@ -0,0 +1,107 @@ +// Copyright 2018 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.rde; + +import static org.bouncycastle.openpgp.PGPLiteralData.BINARY; +import static org.joda.time.DateTimeZone.UTC; + +import google.registry.util.ImprovedInputStream; +import google.registry.util.ImprovedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import javax.annotation.CheckReturnValue; +import javax.annotation.WillNotClose; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.joda.time.DateTime; + +/** + * Input/Output stream for reading/writing PGP literal data layer. + * + *

OpenPGP messages are like an onion; there can be many layers like compression and encryption. + * It's important to wrap out plaintext in a literal data layer such as this so the code that's + * unwrapping the onion knows when to stop. + * + *

According to escrow spec, the PGP message should contain a single tar file. + */ +final class RydeFileEncoding { + + private static final int BUFFER_SIZE = 64 * 1024; + + /** + * Creates an OutputStream that encodes the data as a PGP file blob. + * + *

TODO(b/110465964): document where the input comes from / output goes to. Something like + * documenting that os is the result of openCompressor and the result is used for the actual file + * data (Ghostryde) / goes in to openTarEncoder (RyDE). + * + * @param os where to write the file blob. Is not closed by this object. + * @param filename the filename to set in the file's metadata. + * @param modified the modification time to set in the file's metadata. + */ + @CheckReturnValue + static ImprovedOutputStream openPgpFileWriter( + @WillNotClose OutputStream os, String filename, DateTime modified) { + try { + return new ImprovedOutputStream( + "PgpFileWriter", + new PGPLiteralDataGenerator() + .open(os, BINARY, filename, modified.toDate(), new byte[BUFFER_SIZE])); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** Input stream to a PGP file's data that also holds the file's metadata. */ + static class PgpFileInputStream extends ImprovedInputStream { + private final String filename; + private final DateTime modified; + + private PgpFileInputStream(PGPLiteralData literal) { + super("PgpFileReader", literal.getDataStream()); + filename = literal.getFileName(); + modified = new DateTime(literal.getModificationTime(), UTC); + } + + /** Returns the name of the original file. */ + String getFilename() { + return filename; + } + + /** Returns the time this file was created or modified. */ + DateTime getModified() { + return modified; + } + } + + /** + * Opens an InputStream to a PGP file blob's data. + * + *

The result includes the file's metadata - the file name and modification time. + * + *

TODO(b/110465964): document where the input comes from / output goes to. Something like + * documenting that input is the result of openDecompressor and the result is the final file + * (Ghostryde) / goes into openTarDecoder (RyDE). + * + * @param input from where to read the file blob. + */ + @CheckReturnValue + static PgpFileInputStream openPgpFileReader(@WillNotClose InputStream input) { + return new PgpFileInputStream(PgpUtils.readSinglePgpObject(input, PGPLiteralData.class)); + } + + private RydeFileEncoding() {} +} diff --git a/java/google/registry/rde/RydePgpFileOutputStream.java b/java/google/registry/rde/RydePgpFileOutputStream.java deleted file mode 100644 index 31d63fe6b..000000000 --- a/java/google/registry/rde/RydePgpFileOutputStream.java +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2017 The Nomulus Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package google.registry.rde; - -import static com.google.common.base.Preconditions.checkArgument; -import static org.bouncycastle.openpgp.PGPLiteralData.BINARY; - -import google.registry.util.ImprovedOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import javax.annotation.WillNotClose; -import org.bouncycastle.openpgp.PGPLiteralDataGenerator; -import org.joda.time.DateTime; - -/** - * OpenPGP literal data layer generator that wraps {@link OutputStream}. - * - *

OpenPGP messages are like an onion; there can be many layers like compression and encryption. - * It's important to wrap out plaintext in a literal data layer such as this so the code that's - * unwrapping the onion knows when to stop. - * - *

According to escrow spec, the PGP message should contain a single tar file. - */ -public class RydePgpFileOutputStream extends ImprovedOutputStream { - - private static final int BUFFER_SIZE = 64 * 1024; - - /** - * Creates a new instance for a particular file. - * - * @param os is the upstream {@link OutputStream} which is not closed by this object - * @throws IllegalArgumentException if {@code filename} isn't a {@code .tar} file - * @throws RuntimeException to rethrow {@link IOException} - */ - public RydePgpFileOutputStream( - @WillNotClose OutputStream os, - DateTime modified, - String filename) { - super("RydePgpFileOutputStream", createDelegate(os, modified, filename)); - } - - private static OutputStream - createDelegate(OutputStream os, DateTime modified, String filename) { - try { - checkArgument(filename.endsWith(".tar"), - "Ryde PGP message should contain a tar file."); - return new PGPLiteralDataGenerator().open( - os, BINARY, filename, modified.toDate(), new byte[BUFFER_SIZE]); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/javatests/google/registry/rde/RydeFileEncodingTest.java b/javatests/google/registry/rde/RydeFileEncodingTest.java new file mode 100644 index 000000000..96a580298 --- /dev/null +++ b/javatests/google/registry/rde/RydeFileEncodingTest.java @@ -0,0 +1,53 @@ +// Copyright 2018 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.rde; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.io.ByteStreams; +import google.registry.testing.ShardableTestCase; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import org.joda.time.DateTime; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class RydeFileEncodingTest extends ShardableTestCase { + + @Test + public void testEncodeDecode() throws Exception { + byte[] expectedContent = "Testing 1, 2, 3".getBytes(UTF_8); + String expectedFilename = "myFile.txt"; + DateTime expectedModified = DateTime.parse("2015-12-25T06:30:00.000Z"); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (OutputStream encoder = + RydeFileEncoding.openPgpFileWriter(output, expectedFilename, expectedModified)) { + encoder.write(expectedContent); + } + byte[] encoded = output.toByteArray(); + + ByteArrayInputStream input = new ByteArrayInputStream(encoded); + try (RydeFileEncoding.PgpFileInputStream decoder = RydeFileEncoding.openPgpFileReader(input)) { + assertThat(decoder.getFilename()).isEqualTo(expectedFilename); + assertThat(decoder.getModified()).isEqualTo(expectedModified); + assertThat(ByteStreams.toByteArray(decoder)).isEqualTo(expectedContent); + } + } +}