diff --git a/java/google/registry/config/RegistryConfig.java b/java/google/registry/config/RegistryConfig.java index 6a6ec3a9a..4519ca242 100644 --- a/java/google/registry/config/RegistryConfig.java +++ b/java/google/registry/config/RegistryConfig.java @@ -638,17 +638,6 @@ public final class RegistryConfig { return projectId + "-rde-import"; } - /** - * Size of Ghostryde buffer in bytes for each layer in the pipeline. - * - * @see google.registry.rde.Ghostryde - */ - @Provides - @Config("rdeGhostrydeBufferSize") - public static Integer provideRdeGhostrydeBufferSize() { - return 64 * 1024; - } - /** * Amount of time between RDE deposits. * diff --git a/java/google/registry/rde/BrdaCopyAction.java b/java/google/registry/rde/BrdaCopyAction.java index 3f1fbc256..7c5ffbe34 100644 --- a/java/google/registry/rde/BrdaCopyAction.java +++ b/java/google/registry/rde/BrdaCopyAction.java @@ -16,7 +16,6 @@ package google.registry.rde; import static google.registry.model.rde.RdeMode.THIN; import static google.registry.request.Action.Method.POST; -import static java.nio.charset.StandardCharsets.UTF_8; import com.google.appengine.tools.cloudstorage.GcsFilename; import com.google.common.flogger.FluentLogger; @@ -29,7 +28,6 @@ import google.registry.request.Action; import google.registry.request.Parameter; import google.registry.request.RequestParameters; import google.registry.request.auth.Auth; -import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -66,7 +64,6 @@ public final class BrdaCopyAction implements Runnable { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @Inject GcsUtils gcsUtils; - @Inject Ghostryde ghostryde; @Inject RydePgpCompressionOutputStreamFactory pgpCompressionFactory; @Inject RydePgpFileOutputStreamFactory pgpFileFactory; @Inject RydePgpEncryptionOutputStreamFactory pgpEncryptionFactory; @@ -102,10 +99,7 @@ public final class BrdaCopyAction implements Runnable { logger.atInfo().log("Writing %s", rydeFile); byte[] signature; try (InputStream gcsInput = gcsUtils.openInputStream(xmlFilename); - Ghostryde.Decryptor decryptor = ghostryde.openDecryptor(gcsInput, stagingDecryptionKey); - Ghostryde.Decompressor decompressor = ghostryde.openDecompressor(decryptor); - Ghostryde.Input ghostInput = ghostryde.openInput(decompressor); - BufferedInputStream xmlInput = new BufferedInputStream(ghostInput); + InputStream ghostrydeDecoder = Ghostryde.decoder(gcsInput, stagingDecryptionKey); OutputStream gcsOutput = gcsUtils.openOutputStream(rydeFile); RydePgpSigningOutputStream signLayer = pgpSigningFactory.create(gcsOutput, signingKey)) { try (OutputStream encryptLayer = pgpEncryptionFactory.create(signLayer, receiverKey); @@ -113,7 +107,7 @@ public final class BrdaCopyAction implements Runnable { OutputStream fileLayer = pgpFileFactory.create(compressLayer, watermark, prefix + ".tar"); OutputStream tarLayer = tarFactory.create(fileLayer, xmlLength, watermark, prefix + ".xml")) { - ByteStreams.copy(xmlInput, tarLayer); + ByteStreams.copy(ghostrydeDecoder, tarLayer); } signature = signLayer.getSignature(); } @@ -127,7 +121,7 @@ public final class BrdaCopyAction implements Runnable { /** Reads the contents of a file from Cloud Storage that contains nothing but an integer. */ private long readXmlLength(GcsFilename xmlLengthFilename) throws IOException { try (InputStream input = gcsUtils.openInputStream(xmlLengthFilename)) { - return Long.parseLong(new String(ByteStreams.toByteArray(input), UTF_8).trim()); + return Ghostryde.readLength(input); } } } diff --git a/java/google/registry/rde/Ghostryde.java b/java/google/registry/rde/Ghostryde.java index 4ff5cbf46..433a42f5c 100644 --- a/java/google/registry/rde/Ghostryde.java +++ b/java/google/registry/rde/Ghostryde.java @@ -17,15 +17,16 @@ package google.registry.rde; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.bouncycastle.bcpg.CompressionAlgorithmTags.ZLIB; import static org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags.AES_128; import static org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME; import static org.bouncycastle.openpgp.PGPLiteralData.BINARY; -import static org.joda.time.DateTimeZone.UTC; import com.google.common.flogger.FluentLogger; import com.google.common.io.ByteStreams; -import google.registry.config.RegistryConfig.Config; +import com.google.common.io.Closer; import google.registry.util.ImprovedInputStream; import google.registry.util.ImprovedOutputStream; import java.io.ByteArrayInputStream; @@ -38,11 +39,7 @@ import java.security.ProviderException; import java.security.SecureRandom; import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; -import javax.annotation.WillCloseWhenClosed; import javax.annotation.WillNotClose; -import javax.annotation.concurrent.Immutable; -import javax.annotation.concurrent.NotThreadSafe; -import javax.inject.Inject; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPCompressedDataGenerator; import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; @@ -68,39 +65,42 @@ import org.joda.time.DateTime; * eyes of anyone with access to the Google Cloud * Console. * - *

This class has an unusual API that's designed to take advantage of Java 7 try-with-resource - * statements to the greatest extent possible, while also maintaining security contracts at - * compile-time. + *

The encryption is similar to the "regular" RyDE RDE deposit file encryption. The main + * difference (and the reason we had to create a custom encryption) is that the RDE deposit has a + * tar file in the encoding. A tar file needs to know its final size in the header, which means we + * have to create the entire deposit before we can start encoding it. + * + *

Deposits are big, and there's no reason to hold it all in memory. Instead, save a "staging" + * version encrypted with Ghostryde instead of "RyDE" (the RDE encryption/encoding), using the + * "rde-staging" keys. We also remember the actual data size during the staging creation. + * + *

Then when we want to create the actual deposits, we decrypt the staging version, and using the + * saved value for the data size we can encrypt with "RyDE" using the receiver key. * *

Here's how you write a file: * *

   {@code
- *   File in = new File("lol.txt");
- *   File out = new File("lol.txt.ghostryde");
- *   Ghostryde ghost = new Ghostryde(1024);
- *   try (OutputStream output = new FileOutputStream(out);
- *       Ghostryde.Encryptor encryptor = ghost.openEncryptor(output, publicKey);
- *       Ghostryde.Compressor kompressor = ghost.openCompressor(encryptor);
- *       OutputStream go = ghost.openOutput(kompressor, in.getName(), DateTime.now(UTC));
- *       InputStream input = new FileInputStream(in)) {
- *     ByteStreams.copy(input, go);
- *   }}
+ * File in = new File("lol.txt"); + * File out = new File("lol.txt.ghostryde"); + * File lengthOut = new File("lol.length.ghostryde"); + * try (OutputStream output = new FileOutputStream(out); + * OutputStream lengthOutput = new FileOutputStream(lengthOut); + * OutputStream ghostrydeEncoder = Ghostryde.encoder(output, publicKey, lengthOut); + * InputStream input = new FileInputStream(in)) { + * ByteStreams.copy(input, ghostrydeEncoder); + * }} * *

Here's how you read a file: * *

   {@code
- *   File in = new File("lol.txt.ghostryde");
- *   File out = new File("lol.txt");
- *   Ghostryde ghost = new Ghostryde(1024);
- *   try (InputStream fileInput = new FileInputStream(in);
- *       Ghostryde.Decryptor decryptor = ghost.openDecryptor(fileInput, privateKey);
- *       Ghostryde.Decompressor decompressor = ghost.openDecompressor(decryptor);
- *       Ghostryde.Input input = ghost.openInput(decompressor);
- *       OutputStream fileOutput = new FileOutputStream(out)) {
- *     System.out.println("name = " + input.getName());
- *     System.out.println("modified = " + input.getModified());
- *     ByteStreams.copy(input, fileOutput);
- *   }}
+ * File in = new File("lol.txt.ghostryde"); + * File out = new File("lol.txt"); + * Ghostryde ghost = new Ghostryde(1024); + * try (InputStream fileInput = new FileInputStream(in); + * InputStream ghostrydeDecoder = new Ghostryde.decoder(fileInput, privateKey); + * OutputStream fileOutput = new FileOutputStream(out)) { + * ByteStreams.copy(ghostryderDecoder, fileOutput); + * }} * *

Simple API

* @@ -108,10 +108,11 @@ import org.joda.time.DateTime; * static methods more convenient: * *
   {@code
- *   byte[] data = "hello kitty".getBytes(UTF_8);
- *   byte[] blob = Ghostryde.encode(data, publicKey, "lol.txt", DateTime.now(UTC));
- *   Ghostryde.Result result = Ghostryde.decode(blob, privateKey);
- *   }
+ * byte[] data = "hello kitty".getBytes(UTF_8); + * byte[] blob = Ghostryde.encode(data, publicKey); + * byte[] result = Ghostryde.decode(blob, privateKey); + * + * } * *

GhostRYDE Format

* @@ -122,11 +123,13 @@ import org.joda.time.DateTime; *

Ghostryde is different from RyDE in the sense that ghostryde is only used for internal * storage; whereas RyDE is meant to protect data being stored by a third-party. */ -@Immutable public final class Ghostryde { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + /** Size of the buffer used by the intermediate streams. */ + static final int BUFFER_SIZE = 64 * 1024; + /** * Compression algorithm to use when creating ghostryde files. * @@ -162,22 +165,27 @@ public final class Ghostryde { */ static final String RANDOM_SOURCE = "NativePRNG"; + /** + * 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 + * set them to a constant value. + */ + static final String INNER_FILENAME = "file.xml"; + static final DateTime INNER_MODIFICATION_TIME = DateTime.parse("2000-01-01TZ"); + /** * Creates a ghostryde file from an in-memory byte array. * * @throws PGPException * @throws IOException */ - public static byte[] encode(byte[] data, PGPPublicKey key, String name, DateTime modified) + public static byte[] encode(byte[] data, PGPPublicKey key) throws IOException, PGPException { checkNotNull(data, "data"); checkArgument(key.isEncryptionKey(), "not an encryption key"); - Ghostryde ghost = new Ghostryde(1024 * 64); ByteArrayOutputStream output = new ByteArrayOutputStream(); - try (Encryptor encryptor = ghost.openEncryptor(output, key); - Compressor kompressor = ghost.openCompressor(encryptor); - OutputStream go = ghost.openOutput(kompressor, name, modified)) { - go.write(data); + try (OutputStream encoder = encoder(output, key)) { + encoder.write(data); } return output.toByteArray(); } @@ -188,191 +196,106 @@ public final class Ghostryde { * @throws PGPException * @throws IOException */ - public static DecodeResult decode(byte[] data, PGPPrivateKey key) + public static byte[] decode(byte[] data, PGPPrivateKey key) throws IOException, PGPException { checkNotNull(data, "data"); - Ghostryde ghost = new Ghostryde(1024 * 64); ByteArrayInputStream dataStream = new ByteArrayInputStream(data); ByteArrayOutputStream output = new ByteArrayOutputStream(); - String name; - DateTime modified; - try (Decryptor decryptor = ghost.openDecryptor(dataStream, key); - Decompressor decompressor = ghost.openDecompressor(decryptor); - Input input = ghost.openInput(decompressor)) { - name = input.getName(); - modified = input.getModified(); - ByteStreams.copy(input, output); + try (InputStream ghostrydeDecoder = decoder(dataStream, key)) { + ByteStreams.copy(ghostrydeDecoder, output); } - return new DecodeResult(output.toByteArray(), name, modified); + return output.toByteArray(); } - /** Result class for the {@link Ghostryde#decode(byte[], PGPPrivateKey)} method. */ - @Immutable - public static final class DecodeResult { - private final byte[] data; - private final String name; - private final DateTime modified; - - DecodeResult(byte[] data, String name, DateTime modified) { - this.data = checkNotNull(data, "data"); - this.name = checkNotNull(name, "name"); - this.modified = checkNotNull(modified, "modified"); - } - - /** Returns the decoded ghostryde content bytes. */ - public byte[] getData() { - return data; - } - - /** Returns the name of the original file, taken from the literal data packet. */ - public String getName() { - return name; - } - - /** Returns the time this file was created or modified, take from the literal data packet. */ - public DateTime getModified() { - return modified; - } + /** Reads the value of a length stream - see {@link #encoder}. */ + public static long readLength(InputStream lengthStream) throws IOException { + return Long.parseLong(new String(ByteStreams.toByteArray(lengthStream), UTF_8).trim()); } /** - * PGP literal file {@link InputStream}. + * Creates a Ghostryde Encoder. * - * @see Ghostryde#openInput(Decompressor) + *

Optionally can also save the total length of the data written to an OutputStream. + * + *

This is necessary because the RyDE format uses a tar file which requires the total length in + * the header. We don't want to have to decrypt the entire ghostryde file to determine the length, + * so we just save it separately. + * + * @param output where to write the encrypted data + * @param encryptionKey the encryption key to use + * @param lengthOutput if not null - will save the total length of the data written to this + * output. See {@link #readLength}. */ - @NotThreadSafe - public static final class Input extends ImprovedInputStream { - private final String name; - private final DateTime modified; + public static ImprovedOutputStream encoder( + OutputStream output, PGPPublicKey encryptionKey, @Nullable OutputStream lengthOutput) + throws IOException, PGPException { - Input(@WillCloseWhenClosed InputStream input, String name, DateTime modified) { - super("Input", input); - this.name = checkNotNull(name, "name"); - this.modified = checkNotNull(modified, "modified"); - } + // We use a Closer to handle the stream .close, to make sure it's done correctly. + Closer closer = Closer.create(); + OutputStream encryptionLayer = closer.register(openEncryptor(output, encryptionKey)); + OutputStream kompressor = closer.register(openCompressor(encryptionLayer)); + OutputStream fileLayer = + closer.register( + openPgpFileOutputStream(kompressor, INNER_FILENAME, INNER_MODIFICATION_TIME)); - /** Returns the name of the original file, taken from the literal data packet. */ - public String getName() { - return name; - } - - /** Returns the time this file was created or modified, take from the literal data packet. */ - public DateTime getModified() { - return modified; - } - } - - /** - * PGP literal file {@link OutputStream}. - * - *

This class isn't needed for ordering safety, but is included regardless for consistency and - * to improve the appearance of log messages. - * - * @see Ghostryde#openOutput(Compressor, String, DateTime) - */ - @NotThreadSafe - public static final class Output extends ImprovedOutputStream { - Output(@WillCloseWhenClosed OutputStream os) { - super("Output", os); - } - } - - /** - * Encryption {@link OutputStream}. - * - *

This type exists to guarantee {@code open*()} methods are called in the correct order. - * - * @see Ghostryde#openEncryptor(OutputStream, PGPPublicKey) - */ - @NotThreadSafe - public static final class Encryptor extends ImprovedOutputStream { - Encryptor(@WillCloseWhenClosed OutputStream os) { - super("Encryptor", os); - } - } - - /** - * Decryption {@link InputStream}. - * - *

This type exists to guarantee {@code open*()} methods are called in the correct order. - * - * @see Ghostryde#openDecryptor(InputStream, PGPPrivateKey) - */ - @NotThreadSafe - public static final class Decryptor extends ImprovedInputStream { - private final PGPPublicKeyEncryptedData crypt; - - Decryptor(@WillCloseWhenClosed InputStream input, PGPPublicKeyEncryptedData crypt) { - super("Decryptor", input); - this.crypt = checkNotNull(crypt, "crypt"); - } - - /** - * Verifies that the ciphertext wasn't corrupted or tampered with. - * - *

Note: If {@link Ghostryde#USE_INTEGRITY_PACKET} is {@code true}, any ghostryde file - * without an integrity packet will be considered invalid and an exception will be thrown. - * - * @throws IllegalStateException to propagate {@link PGPException} - * @throws IOException - */ - @Override - protected void onClose() throws IOException { - if (USE_INTEGRITY_PACKET) { - try { - if (!crypt.verify()) { - throw new PGPException("ghostryde integrity check failed: possible tampering D:"); - } - } catch (PGPException e) { - throw new IllegalStateException(e); + return new ImprovedOutputStream("GhostrydeEncoder", fileLayer) { + @Override + public void onClose() throws IOException { + // Close all the streams we opened + closer.close(); + // Optionally also output the size of the encoded data - which is needed for the RyDE + // encoding. + if (lengthOutput != null) { + lengthOutput.write(Long.toString(getBytesWritten()).getBytes(US_ASCII)); } } - } + }; } /** - * Compression {@link OutputStream}. + * Creates a Ghostryde Encoder. * - *

This type exists to guarantee {@code open*()} methods are called in the correct order. - * - * @see Ghostryde#openCompressor(Encryptor) + * @param output where to write the encrypted data + * @param encryptionKey the encryption key to use */ - @NotThreadSafe - public static final class Compressor extends ImprovedOutputStream { - Compressor(@WillCloseWhenClosed OutputStream os) { - super("Compressor", os); - } + public static ImprovedOutputStream encoder(OutputStream output, PGPPublicKey encryptionKey) + throws IOException, PGPException { + return encoder(output, encryptionKey, null); } /** - * Decompression {@link InputStream}. + * Creates a Ghostryde decoder. * - *

This type exists to guarantee {@code open*()} methods are called in the correct order. - * - * @see Ghostryde#openDecompressor(Decryptor) + * @param input from where to read the encrypted data + * @param decryptionKey the decryption key to use */ - @NotThreadSafe - public static final class Decompressor extends ImprovedInputStream { - Decompressor(@WillCloseWhenClosed InputStream input) { - super("Decompressor", input); - } + public static ImprovedInputStream decoder(InputStream input, PGPPrivateKey decryptionKey) + throws IOException, PGPException { + + // 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, decryptionKey)); + InputStream decompressor = closer.register(openDecompressor(decryptionLayer)); + InputStream fileLayer = closer.register(openPgpFileInputStream(decompressor)); + + return new ImprovedInputStream("GhostryderDecoder", fileLayer) { + @Override + public void onClose() throws IOException { + // Close all the streams we opened + closer.close(); + } + }; } - private final int bufferSize; - - /** Constructs a new {@link Ghostryde} object. */ - @Inject - public Ghostryde( - @Config("rdeGhostrydeBufferSize") int bufferSize) { - checkArgument(bufferSize > 0, "bufferSize"); - this.bufferSize = bufferSize; - } + private Ghostryde() {} /** - * Opens a new {@link Encryptor} (Writing Step 1/3) + * Opens a new encryptor (Writing Step 1/3) * - *

This is the first step in creating a ghostryde file. After this method, you'll want to - * call {@link #openCompressor(Encryptor)}. + *

This is the first step in creating a ghostryde file. After this method, you'll want to call + * {@link #openCompressor}. + * + *

TODO(b/110465985): merge with the RyDE version. * * @param os is the upstream {@link OutputStream} to which the result is written. * @param publicKey is the public encryption key of the recipient. @@ -380,19 +303,20 @@ public final class Ghostryde { * @throws PGPException */ @CheckReturnValue - public Encryptor openEncryptor(@WillNotClose OutputStream os, PGPPublicKey publicKey) - throws IOException, PGPException { + private static ImprovedOutputStream openEncryptor( + @WillNotClose OutputStream os, PGPPublicKey publicKey) throws IOException, PGPException { PGPEncryptedDataGenerator encryptor = new PGPEncryptedDataGenerator( new JcePGPDataEncryptorBuilder(CIPHER) .setWithIntegrityPacket(USE_INTEGRITY_PACKET) .setSecureRandom(getRandom()) .setProvider(PROVIDER_NAME)); encryptor.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(publicKey)); - return new Encryptor(encryptor.open(os, new byte[bufferSize])); + return new ImprovedOutputStream( + "GhostrydeEncryptor", encryptor.open(os, new byte[BUFFER_SIZE])); } /** Does stuff. */ - private SecureRandom getRandom() { + private static SecureRandom getRandom() { SecureRandom random; try { random = SecureRandom.getInstance(RANDOM_SOURCE); @@ -403,44 +327,57 @@ public final class Ghostryde { } /** - * Opens a new {@link Compressor} (Writing Step 2/3) + * Opens a new compressor (Writing Step 2/3) * - *

This is the second step in creating a ghostryde file. After this method, you'll want to - * call {@link #openOutput(Compressor, String, DateTime)}. + *

This is the second step in creating a ghostryde file. After this method, you'll want to call + * {@link #openPgpFileOutputStream}. + * + *

TODO(b/110465985): merge with the RyDE version. * * @param os is the value returned by {@link #openEncryptor(OutputStream, PGPPublicKey)}. * @throws IOException * @throws PGPException */ @CheckReturnValue - public Compressor openCompressor(@WillNotClose Encryptor os) throws IOException, PGPException { + private static ImprovedOutputStream openCompressor(@WillNotClose OutputStream os) + throws IOException, PGPException { PGPCompressedDataGenerator kompressor = new PGPCompressedDataGenerator(COMPRESSION_ALGORITHM); - return new Compressor(kompressor.open(os, new byte[bufferSize])); + return new ImprovedOutputStream( + "GhostrydeCompressor", kompressor.open(os, new byte[BUFFER_SIZE])); } /** * 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. + *

This is the third and final step in creating a ghostryde file. You'll want to write data to + * the returned object. * - * @param os is the value returned by {@link #openCompressor(Encryptor)}. + *

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 - public Output openOutput(@WillNotClose Compressor os, String name, DateTime modified) - throws IOException { - return new Output(new PGPLiteralDataGenerator().open( - os, BINARY, name, modified.toDate(), new byte[bufferSize])); + 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 {@link Decryptor} (Reading Step 1/3) + * Opens a new decryptor (Reading Step 1/3) * - *

This is the first step in opening a ghostryde file. After this method, you'll want to - * call {@link #openDecompressor(Decryptor)}. + *

This is the first step in opening a ghostryde file. After this method, you'll want to call + * {@link #openDecompressor}. + * + *

Note: If {@link Ghostryde#USE_INTEGRITY_PACKET} is {@code true}, any ghostryde file without + * an integrity packet will be considered invalid and an exception will be thrown. + * + *

TODO(b/110465985): merge with the RyDE version. * * @param input is an {@link InputStream} of the ghostryde file data. * @param privateKey is the private encryption key of the recipient (which is us!) @@ -448,60 +385,81 @@ public final class Ghostryde { * @throws PGPException */ @CheckReturnValue - public Decryptor openDecryptor(@WillNotClose InputStream input, PGPPrivateKey privateKey) - throws IOException, PGPException { + private static ImprovedInputStream openDecryptor( + @WillNotClose InputStream input, PGPPrivateKey privateKey) throws IOException, PGPException { checkNotNull(privateKey, "privateKey"); PGPObjectFactory fact = new BcPGPObjectFactory(checkNotNull(input, "input")); - PGPEncryptedDataList crypts = pgpCast(fact.nextObject(), PGPEncryptedDataList.class); - checkState(crypts.size() > 0); - if (crypts.size() > 1) { - logger.atWarning().log("crypts.size() is %d (should be 1)", crypts.size()); + PGPEncryptedDataList ciphertexts = pgpCast(fact.nextObject(), PGPEncryptedDataList.class); + checkState(ciphertexts.size() > 0); + if (ciphertexts.size() > 1) { + logger.atWarning().log("crypts.size() is %d (should be 1)", ciphertexts.size()); } - PGPPublicKeyEncryptedData crypt = pgpCast(crypts.get(0), PGPPublicKeyEncryptedData.class); - if (crypt.getKeyID() != privateKey.getKeyID()) { + PGPPublicKeyEncryptedData cyphertext = + pgpCast(ciphertexts.get(0), PGPPublicKeyEncryptedData.class); + if (cyphertext.getKeyID() != privateKey.getKeyID()) { throw new PGPException(String.format( "Message was encrypted for keyid %x but ours is %x", - crypt.getKeyID(), privateKey.getKeyID())); + cyphertext.getKeyID(), privateKey.getKeyID())); } - return new Decryptor( - crypt.getDataStream(new BcPublicKeyDataDecryptorFactory(privateKey)), - crypt); + + // We want an input stream that also verifies ciphertext wasn't corrupted or tampered with when + // the stream is closed. + return new ImprovedInputStream( + "GhostrydeDecryptor", + cyphertext.getDataStream(new BcPublicKeyDataDecryptorFactory(privateKey))) { + @Override + protected void onClose() throws IOException { + if (USE_INTEGRITY_PACKET) { + try { + if (!cyphertext.verify()) { + throw new PGPException("ghostryde integrity check failed: possible tampering D:"); + } + } catch (PGPException e) { + throw new IllegalStateException(e); + } + } + } + }; } /** - * Opens a new {@link Decompressor} (Reading Step 2/3) + * Opens a new decompressor (Reading Step 2/3) * - *

This is the second step in reading a ghostryde file. After this method, you'll want to - * call {@link #openInput(Decompressor)}. + *

This is the second step in reading a ghostryde file. After this method, you'll want to call + * {@link #openPgpFileInputStream}. + * + *

TODO(b/110465985): merge with the RyDE version. * * @param input is the value returned by {@link #openDecryptor}. * @throws IOException * @throws PGPException */ @CheckReturnValue - public Decompressor openDecompressor(@WillNotClose Decryptor input) + private static ImprovedInputStream openDecompressor(@WillNotClose InputStream input) throws IOException, PGPException { PGPObjectFactory fact = new BcPGPObjectFactory(checkNotNull(input, "input")); PGPCompressedData compressed = pgpCast(fact.nextObject(), PGPCompressedData.class); - return new Decompressor(compressed.getDataStream()); + return new ImprovedInputStream("GhostrydeDecompressor", compressed.getDataStream()); } /** - * Opens a new {@link Input} for reading the original contents (Reading Step 3/3) + * 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 - public Input openInput(@WillNotClose Decompressor input) throws IOException, PGPException { + private static ImprovedInputStream openPgpFileInputStream(@WillNotClose InputStream input) + throws IOException, PGPException { PGPObjectFactory fact = new BcPGPObjectFactory(checkNotNull(input, "input")); PGPLiteralData literal = pgpCast(fact.nextObject(), PGPLiteralData.class); - DateTime modified = new DateTime(literal.getModificationTime(), UTC); - return new Input(literal.getDataStream(), literal.getFileName(), modified); + return new ImprovedInputStream("GhostrydePgpFileInputStream", literal.getDataStream()); } /** Safely extracts an object from an OpenPGP message. */ diff --git a/java/google/registry/rde/RdeReportAction.java b/java/google/registry/rde/RdeReportAction.java index 6ce58246a..076f6574d 100644 --- a/java/google/registry/rde/RdeReportAction.java +++ b/java/google/registry/rde/RdeReportAction.java @@ -23,7 +23,6 @@ import static google.registry.request.Action.Method.POST; import static google.registry.util.DateTimeUtils.isBeforeOrAt; import com.google.appengine.tools.cloudstorage.GcsFilename; -import com.google.common.flogger.FluentLogger; import com.google.common.io.ByteStreams; import google.registry.config.RegistryConfig.Config; import google.registry.gcs.GcsUtils; @@ -59,10 +58,7 @@ public final class RdeReportAction implements Runnable, EscrowTask { static final String PATH = "/_dr/task/rdeReport"; - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - @Inject GcsUtils gcsUtils; - @Inject Ghostryde ghostryde; @Inject EscrowTaskRunner runner; @Inject Response response; @Inject RdeReporter reporter; @@ -101,10 +97,8 @@ public final class RdeReportAction implements Runnable, EscrowTask { /** Reads and decrypts the XML file from cloud storage. */ private byte[] readReportFromGcs(GcsFilename reportFilename) throws IOException, PGPException { try (InputStream gcsInput = gcsUtils.openInputStream(reportFilename); - Ghostryde.Decryptor decryptor = ghostryde.openDecryptor(gcsInput, stagingDecryptionKey); - Ghostryde.Decompressor decompressor = ghostryde.openDecompressor(decryptor); - Ghostryde.Input xmlInput = ghostryde.openInput(decompressor)) { - return ByteStreams.toByteArray(xmlInput); + InputStream ghostrydeDecoder = Ghostryde.decoder(gcsInput, stagingDecryptionKey)) { + return ByteStreams.toByteArray(ghostrydeDecoder); } } } diff --git a/java/google/registry/rde/RdeStagingReducer.java b/java/google/registry/rde/RdeStagingReducer.java index ff3ceb4e0..fadbd48f5 100644 --- a/java/google/registry/rde/RdeStagingReducer.java +++ b/java/google/registry/rde/RdeStagingReducer.java @@ -23,7 +23,6 @@ import static google.registry.model.common.Cursor.getCursorTimeOrStartOfTime; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.xml.ValidationMode.LENIENT; import static google.registry.xml.ValidationMode.STRICT; -import static java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.appengine.tools.cloudstorage.GcsFilename; @@ -74,7 +73,6 @@ public final class RdeStagingReducer extends Reducer instead of JSch to prevent fetching of rdeSsh*Keys before we know we're @@ -213,9 +211,7 @@ public final class RdeUploadAction implements Runnable, EscrowTask { throws Exception { logger.atInfo().log("Uploading XML file '%s' to remote path '%s'.", xmlFile, uploadUrl); try (InputStream gcsInput = gcsUtils.openInputStream(xmlFile); - Ghostryde.Decryptor decryptor = ghostryde.openDecryptor(gcsInput, stagingDecryptionKey); - Ghostryde.Decompressor decompressor = ghostryde.openDecompressor(decryptor); - Ghostryde.Input xmlInput = ghostryde.openInput(decompressor)) { + InputStream ghostrydeDecoder = Ghostryde.decoder(gcsInput, stagingDecryptionKey)) { try (JSchSshSession session = jschSshSessionFactory.create(lazyJsch.get(), uploadUrl); JSchSftpChannel ftpChan = session.openSftpChannel()) { byte[] signature; @@ -231,7 +227,7 @@ public final class RdeUploadAction implements Runnable, EscrowTask { OutputStream fileLayer = pgpFileFactory.create(kompressor, watermark, name + ".tar"); OutputStream tarLayer = tarFactory.create(fileLayer, xmlLength, watermark, name + ".xml")) { - ByteStreams.copy(xmlInput, tarLayer); + ByteStreams.copy(ghostrydeDecoder, tarLayer); } signature = signer.getSignature(); logger.atInfo().log("uploaded %,d bytes: %s.ryde", signer.getBytesWritten(), name); @@ -247,7 +243,7 @@ public final class RdeUploadAction implements Runnable, EscrowTask { /** Reads the contents of a file from Cloud Storage that contains nothing but an integer. */ private long readXmlLength(GcsFilename xmlLengthFilename) throws IOException { try (InputStream input = gcsUtils.openInputStream(xmlLengthFilename)) { - return Long.parseLong(new String(ByteStreams.toByteArray(input), UTF_8).trim()); + return Ghostryde.readLength(input); } } diff --git a/java/google/registry/tools/GhostrydeCommand.java b/java/google/registry/tools/GhostrydeCommand.java index 24c9a6a97..4f7f30302 100644 --- a/java/google/registry/tools/GhostrydeCommand.java +++ b/java/google/registry/tools/GhostrydeCommand.java @@ -16,7 +16,6 @@ package google.registry.tools; import static com.google.common.base.Preconditions.checkArgument; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; -import static org.joda.time.DateTimeZone.UTC; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; @@ -31,13 +30,11 @@ import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.attribute.FileTime; import javax.inject.Inject; import javax.inject.Provider; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; -import org.joda.time.DateTime; /** Command to encrypt/decrypt {@code .ghostryde} files. */ @Parameters(separators = " =", commandDescription = "Encrypt/decrypt a ghostryde file.") @@ -61,16 +58,14 @@ final class GhostrydeCommand implements RemoteApiCommand { @Parameter( names = {"-o", "--output"}, - description = "Output file. If this is a directory, then in --encrypt mode, the output " - + "filename will be the input filename with '.ghostryde' appended, and in --decrypt " - + "mode, the output filename will be determined based on the name stored within the " - + "archive.", + description = + "Output file. If this is a directory: (a) in --encrypt mode, the output " + + "filename will be the input filename with '.ghostryde' appended, and will have an " + + "extra '.length' file with the original file's length; (b) In --decrypt " + + "mode, the output filename will be the input filename with '.decrypt' appended.", validateWith = PathParameter.class) private Path output = Paths.get("/dev/stdout"); - @Inject - Ghostryde ghostryde; - @Inject @Key("rdeStagingEncryptionKey") Provider rdeStagingEncryptionKey; @@ -93,30 +88,23 @@ final class GhostrydeCommand implements RemoteApiCommand { Path outFile = Files.isDirectory(output) ? output.resolve(input.getFileName() + ".ghostryde") : output; + Path lenOutFile = + Files.isDirectory(output) ? output.resolve(input.getFileName() + ".length") : null; try (OutputStream out = Files.newOutputStream(outFile); - Ghostryde.Encryptor encryptor = - ghostryde.openEncryptor(out, rdeStagingEncryptionKey.get()); - Ghostryde.Compressor kompressor = ghostryde.openCompressor(encryptor); - Ghostryde.Output ghostOutput = - ghostryde.openOutput(kompressor, input.getFileName().toString(), - new DateTime(Files.getLastModifiedTime(input).toMillis(), UTC)); + OutputStream lenOut = lenOutFile == null ? null : Files.newOutputStream(lenOutFile); + OutputStream ghostrydeEncoder = + Ghostryde.encoder(out, rdeStagingEncryptionKey.get(), lenOut); InputStream in = Files.newInputStream(input)) { - ByteStreams.copy(in, ghostOutput); + ByteStreams.copy(in, ghostrydeEncoder); } } private void runDecrypt() throws IOException, PGPException { try (InputStream in = Files.newInputStream(input); - Ghostryde.Decryptor decryptor = - ghostryde.openDecryptor(in, rdeStagingDecryptionKey.get()); - Ghostryde.Decompressor decompressor = ghostryde.openDecompressor(decryptor); - Ghostryde.Input ghostInput = ghostryde.openInput(decompressor)) { - Path outFile = Files.isDirectory(output) - ? output.resolve(ghostInput.getName()) - : output; - Files.copy(ghostInput, outFile, REPLACE_EXISTING); - Files.setLastModifiedTime(outFile, - FileTime.fromMillis(ghostInput.getModified().getMillis())); + InputStream ghostDecoder = Ghostryde.decoder(in, rdeStagingDecryptionKey.get())) { + Path outFile = + Files.isDirectory(output) ? output.resolve(input.getFileName() + ".decrypt") : output; + Files.copy(ghostDecoder, outFile, REPLACE_EXISTING); } } } diff --git a/java/google/registry/tools/ValidateEscrowDepositCommand.java b/java/google/registry/tools/ValidateEscrowDepositCommand.java index b9ae4be0b..4e224ebc4 100644 --- a/java/google/registry/tools/ValidateEscrowDepositCommand.java +++ b/java/google/registry/tools/ValidateEscrowDepositCommand.java @@ -65,13 +65,10 @@ final class ValidateEscrowDepositCommand implements Command { @Override public void run() throws Exception { if (input.toString().endsWith(".ghostryde")) { - Ghostryde ghostryde = new Ghostryde(64 * 1024); try (InputStream in = Files.newInputStream(input); - Ghostryde.Decryptor decryptor = - ghostryde.openDecryptor(in, keyring.getRdeStagingDecryptionKey()); - Ghostryde.Decompressor decompressor = ghostryde.openDecompressor(decryptor); - Ghostryde.Input ghostInput = ghostryde.openInput(decompressor)) { - validateXmlStream(ghostInput); + InputStream ghostrydeDecoder = + Ghostryde.decoder(in, keyring.getRdeStagingDecryptionKey())) { + validateXmlStream(ghostrydeDecoder); } } else { try (InputStream inputStream = Files.newInputStream(input)) { diff --git a/javatests/google/registry/rde/BrdaCopyActionTest.java b/javatests/google/registry/rde/BrdaCopyActionTest.java index ac77a9fc5..272960e1f 100644 --- a/javatests/google/registry/rde/BrdaCopyActionTest.java +++ b/javatests/google/registry/rde/BrdaCopyActionTest.java @@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertWithMessage; import static google.registry.testing.GcsTestingUtils.readGcsFile; import static google.registry.testing.SystemInfo.hasCommand; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.joda.time.DateTimeZone.UTC; import static org.junit.Assume.assumeTrue; import com.google.appengine.tools.cloudstorage.GcsFilename; @@ -101,7 +100,6 @@ public class BrdaCopyActionTest extends ShardableTestCase { @Before public void before() throws Exception { action.gcsUtils = gcsUtils; - action.ghostryde = new Ghostryde(23); action.pgpCompressionFactory = new RydePgpCompressionOutputStreamFactory(() -> 1024); action.pgpEncryptionFactory = new RydePgpEncryptionOutputStreamFactory(() -> 1024); action.pgpFileFactory = new RydePgpFileOutputStreamFactory(() -> 1024); @@ -116,8 +114,7 @@ public class BrdaCopyActionTest extends ShardableTestCase { action.stagingDecryptionKey = decryptKey; byte[] xml = DEPOSIT_XML.read(); - GcsTestingUtils.writeGcsFile(gcsService, STAGE_FILE, - Ghostryde.encode(xml, encryptKey, "lobster.xml", new DateTime(UTC))); + GcsTestingUtils.writeGcsFile(gcsService, STAGE_FILE, Ghostryde.encode(xml, encryptKey)); GcsTestingUtils.writeGcsFile(gcsService, STAGE_LENGTH_FILE, Long.toString(xml.length).getBytes(UTF_8)); } diff --git a/javatests/google/registry/rde/GhostrydeGpgIntegrationTest.java b/javatests/google/registry/rde/GhostrydeGpgIntegrationTest.java index da31d92e4..2fd025190 100644 --- a/javatests/google/registry/rde/GhostrydeGpgIntegrationTest.java +++ b/javatests/google/registry/rde/GhostrydeGpgIntegrationTest.java @@ -34,7 +34,6 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import org.bouncycastle.openpgp.PGPPublicKey; -import org.joda.time.DateTime; import org.junit.Rule; import org.junit.experimental.theories.DataPoints; import org.junit.experimental.theories.Theories; @@ -61,18 +60,6 @@ public class GhostrydeGpgIntegrationTest extends ShardableTestCase { new GpgCommand("gpg2"), }; - @DataPoints - public static BufferSize[] bufferSizes = new BufferSize[] { - new BufferSize(1), - new BufferSize(7), - }; - - @DataPoints - public static Filename[] filenames = new Filename[] { - new Filename("lol.txt"), - // new Filename("(◕‿◕).txt"), // gpg displays this with zany hex characters. - }; - @DataPoints public static Content[] contents = new Content[] { new Content("(◕‿◕)"), @@ -82,21 +69,16 @@ public class GhostrydeGpgIntegrationTest extends ShardableTestCase { }; @Theory - public void test(GpgCommand cmd, BufferSize bufferSize, Filename filename, Content content) - throws Exception { + public void test(GpgCommand cmd, Content content) throws Exception { assumeTrue(hasCommand(cmd.get() + " --version")); Keyring keyring = new FakeKeyringModule().get(); PGPPublicKey publicKey = keyring.getRdeStagingEncryptionKey(); File file = new File(gpg.getCwd(), "love.gpg"); byte[] data = content.get().getBytes(UTF_8); - DateTime mtime = DateTime.parse("1984-12-18T00:30:00Z"); - Ghostryde ghost = new Ghostryde(bufferSize.get()); try (OutputStream output = new FileOutputStream(file); - Ghostryde.Encryptor encryptor = ghost.openEncryptor(output, publicKey); - Ghostryde.Compressor kompressor = ghost.openCompressor(encryptor); - OutputStream os = ghost.openOutput(kompressor, filename.get(), mtime)) { - os.write(data); + OutputStream ghostrydeEncoder = Ghostryde.encoder(output, publicKey)) { + ghostrydeEncoder.write(data); } Process pid = gpg.exec(cmd.get(), "--list-packets", "--keyid-format", "long", file.getPath()); @@ -106,13 +88,13 @@ public class GhostrydeGpgIntegrationTest extends ShardableTestCase { assertThat(stdout).contains(":compressed packet:"); assertThat(stdout).contains(":encrypted data packet:"); assertThat(stdout).contains("version 3, algo 1, keyid A59C132F3589A1D5"); - assertThat(stdout).contains("name=\"" + filename.get() + "\""); + assertThat(stdout).contains("name=\"" + Ghostryde.INNER_FILENAME + "\""); assertThat(stderr).contains("encrypted with 2048-bit RSA key, ID A59C132F3589A1D5"); pid = gpg.exec(cmd.get(), "--use-embedded-filename", file.getPath()); stderr = CharStreams.toString(new InputStreamReader(pid.getErrorStream(), UTF_8)); assertWithMessage(stderr).that(pid.waitFor()).isEqualTo(0); - File dataFile = new File(gpg.getCwd(), filename.get()); + File dataFile = new File(gpg.getCwd(), Ghostryde.INNER_FILENAME); assertThat(dataFile.exists()).isTrue(); assertThat(slurp(dataFile)).isEqualTo(content.get()); } @@ -133,30 +115,6 @@ public class GhostrydeGpgIntegrationTest extends ShardableTestCase { } } - private static class BufferSize { - private final int value; - - BufferSize(int value) { - this.value = value; - } - - int get() { - return value; - } - } - - private static class Filename { - private final String value; - - Filename(String value) { - this.value = value; - } - - String get() { - return value; - } - } - private static class Content { private final String value; diff --git a/javatests/google/registry/rde/GhostrydeTest.java b/javatests/google/registry/rde/GhostrydeTest.java index c0373d21b..319f0ef06 100644 --- a/javatests/google/registry/rde/GhostrydeTest.java +++ b/javatests/google/registry/rde/GhostrydeTest.java @@ -26,17 +26,17 @@ import static org.junit.Assume.assumeThat; import com.google.common.io.ByteStreams; import google.registry.keyring.api.Keyring; -import google.registry.rde.Ghostryde.DecodeResult; import google.registry.testing.BouncyCastleProviderRule; import google.registry.testing.FakeKeyringModule; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.InputStream; import java.io.OutputStream; +import java.util.Base64; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyPair; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; -import org.joda.time.DateTime; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; @@ -53,18 +53,6 @@ public class GhostrydeTest { @Rule public final BouncyCastleProviderRule bouncy = new BouncyCastleProviderRule(); - @DataPoints - public static BufferSize[] bufferSizes = new BufferSize[] { - new BufferSize(1), - new BufferSize(7), - }; - - @DataPoints - public static Filename[] filenames = new Filename[] { - new Filename("lol.txt"), - // new Filename("(◕‿◕).txt"), // gpg displays this with zany hex characters. - }; - @DataPoints public static Content[] contents = new Content[] { new Content("hi"), @@ -75,46 +63,34 @@ public class GhostrydeTest { }; @Theory - public void testSimpleApi(Filename filename, Content content) throws Exception { + public void testSimpleApi(Content content) throws Exception { Keyring keyring = new FakeKeyringModule().get(); byte[] data = content.get().getBytes(UTF_8); - DateTime mtime = DateTime.parse("1984-12-18T00:30:00Z"); PGPPublicKey publicKey = keyring.getRdeStagingEncryptionKey(); PGPPrivateKey privateKey = keyring.getRdeStagingDecryptionKey(); - byte[] blob = Ghostryde.encode(data, publicKey, filename.get(), mtime); - DecodeResult result = Ghostryde.decode(blob, privateKey); + byte[] blob = Ghostryde.encode(data, publicKey); + byte[] result = Ghostryde.decode(blob, privateKey); - assertThat(result.getName()).isEqualTo(filename.get()); - assertThat(result.getModified()).isEqualTo(mtime); - assertThat(new String(result.getData(), UTF_8)).isEqualTo(content.get()); + assertThat(new String(result, UTF_8)).isEqualTo(content.get()); } @Theory - public void testStreamingApi(BufferSize bufferSize, Filename filename, Content content) - throws Exception { + public void testStreamingApi(Content content) throws Exception { Keyring keyring = new FakeKeyringModule().get(); byte[] data = content.get().getBytes(UTF_8); - DateTime mtime = DateTime.parse("1984-12-18T00:30:00Z"); PGPPublicKey publicKey = keyring.getRdeStagingEncryptionKey(); PGPPrivateKey privateKey = keyring.getRdeStagingDecryptionKey(); - Ghostryde ghost = new Ghostryde(bufferSize.get()); ByteArrayOutputStream bsOut = new ByteArrayOutputStream(); - try (Ghostryde.Encryptor encryptor = ghost.openEncryptor(bsOut, publicKey); - Ghostryde.Compressor kompressor = ghost.openCompressor(encryptor); - OutputStream output = ghost.openOutput(kompressor, filename.get(), mtime)) { - output.write(data); + try (OutputStream encoder = Ghostryde.encoder(bsOut, publicKey)) { + encoder.write(data); } ByteArrayInputStream bsIn = new ByteArrayInputStream(bsOut.toByteArray()); bsOut.reset(); - try (Ghostryde.Decryptor decryptor = ghost.openDecryptor(bsIn, privateKey); - Ghostryde.Decompressor decompressor = ghost.openDecompressor(decryptor); - Ghostryde.Input input = ghost.openInput(decompressor)) { - assertThat(input.getName()).isEqualTo(filename.get()); - assertThat(input.getModified()).isEqualTo(mtime); - ByteStreams.copy(input, bsOut); + try (InputStream decoder = Ghostryde.decoder(bsIn, privateKey)) { + ByteStreams.copy(decoder, bsOut); } assertThat(bsOut.size()).isEqualTo(data.length); @@ -122,51 +98,20 @@ public class GhostrydeTest { } @Theory - public void testEncryptOnly(Content content) throws Exception { + public void testStreamingApi_withSize(Content content) throws Exception { Keyring keyring = new FakeKeyringModule().get(); byte[] data = content.get().getBytes(UTF_8); PGPPublicKey publicKey = keyring.getRdeStagingEncryptionKey(); - PGPPrivateKey privateKey = keyring.getRdeStagingDecryptionKey(); - Ghostryde ghost = new Ghostryde(1024); ByteArrayOutputStream bsOut = new ByteArrayOutputStream(); - try (Ghostryde.Encryptor encryptor = ghost.openEncryptor(bsOut, publicKey)) { - encryptor.write(data); + ByteArrayOutputStream lenOut = new ByteArrayOutputStream(); + try (OutputStream encoder = Ghostryde.encoder(bsOut, publicKey, lenOut)) { + encoder.write(data); } - ByteArrayInputStream bsIn = new ByteArrayInputStream(bsOut.toByteArray()); - bsOut.reset(); - try (Ghostryde.Decryptor decryptor = ghost.openDecryptor(bsIn, privateKey)) { - ByteStreams.copy(decryptor, bsOut); - } - - assertThat(new String(bsOut.toByteArray(), UTF_8)).isEqualTo(content.get()); - } - - @Theory - public void testEncryptCompressOnly(Content content) throws Exception { - Keyring keyring = new FakeKeyringModule().get(); - PGPPublicKey publicKey = keyring.getRdeStagingEncryptionKey(); - PGPPrivateKey privateKey = keyring.getRdeStagingDecryptionKey(); - byte[] data = content.get().getBytes(UTF_8); - - Ghostryde ghost = new Ghostryde(1024); - ByteArrayOutputStream bsOut = new ByteArrayOutputStream(); - try (Ghostryde.Encryptor encryptor = ghost.openEncryptor(bsOut, publicKey); - Ghostryde.Compressor kompressor = ghost.openCompressor(encryptor)) { - kompressor.write(data); - } - - assertThat(new String(bsOut.toByteArray(), UTF_8)).isNotEqualTo(content.get()); - - ByteArrayInputStream bsIn = new ByteArrayInputStream(bsOut.toByteArray()); - bsOut.reset(); - try (Ghostryde.Decryptor decryptor = ghost.openDecryptor(bsIn, privateKey); - Ghostryde.Decompressor decompressor = ghost.openDecompressor(decryptor)) { - ByteStreams.copy(decompressor, bsOut); - } - - assertThat(new String(bsOut.toByteArray(), UTF_8)).isEqualTo(content.get()); + assertThat(Ghostryde.readLength(new ByteArrayInputStream(lenOut.toByteArray()))) + .isEqualTo(data.length); + assertThat(Long.parseLong(new String(lenOut.toByteArray(), UTF_8))).isEqualTo(data.length); } @Theory @@ -177,26 +122,22 @@ public class GhostrydeTest { PGPPublicKey publicKey = keyring.getRdeStagingEncryptionKey(); PGPPrivateKey privateKey = keyring.getRdeStagingDecryptionKey(); byte[] data = content.get().getBytes(UTF_8); - DateTime mtime = DateTime.parse("1984-12-18T00:30:00Z"); - Ghostryde ghost = new Ghostryde(1024); ByteArrayOutputStream bsOut = new ByteArrayOutputStream(); - try (Ghostryde.Encryptor encryptor = ghost.openEncryptor(bsOut, publicKey); - Ghostryde.Compressor kompressor = ghost.openCompressor(encryptor); - OutputStream output = ghost.openOutput(kompressor, "lol", mtime)) { - output.write(data); + try (OutputStream encoder = Ghostryde.encoder(bsOut, publicKey)) { + encoder.write(data); } byte[] ciphertext = bsOut.toByteArray(); - korruption(ciphertext, ciphertext.length / 2); + korruption(ciphertext, ciphertext.length - 1); ByteArrayInputStream bsIn = new ByteArrayInputStream(ciphertext); IllegalStateException thrown = assertThrows( IllegalStateException.class, () -> { - try (Ghostryde.Decryptor decryptor = ghost.openDecryptor(bsIn, privateKey)) { - ByteStreams.copy(decryptor, ByteStreams.nullOutputStream()); + try (InputStream decoder = Ghostryde.decoder(bsIn, privateKey)) { + ByteStreams.copy(decoder, ByteStreams.nullOutputStream()); } }); assertThat(thrown).hasMessageThat().contains("tampering"); @@ -210,14 +151,10 @@ public class GhostrydeTest { PGPPublicKey publicKey = keyring.getRdeStagingEncryptionKey(); PGPPrivateKey privateKey = keyring.getRdeStagingDecryptionKey(); byte[] data = content.get().getBytes(UTF_8); - DateTime mtime = DateTime.parse("1984-12-18T00:30:00Z"); - Ghostryde ghost = new Ghostryde(1024); ByteArrayOutputStream bsOut = new ByteArrayOutputStream(); - try (Ghostryde.Encryptor encryptor = ghost.openEncryptor(bsOut, publicKey); - Ghostryde.Compressor kompressor = ghost.openCompressor(encryptor); - OutputStream output = ghost.openOutput(kompressor, "lol", mtime)) { - output.write(data); + try (OutputStream encoder = Ghostryde.encoder(bsOut, publicKey)) { + encoder.write(data); } byte[] ciphertext = bsOut.toByteArray(); @@ -227,28 +164,58 @@ public class GhostrydeTest { assertThrows( PGPException.class, () -> { - try (Ghostryde.Decryptor decryptor = ghost.openDecryptor(bsIn, privateKey)) { - ByteStreams.copy(decryptor, ByteStreams.nullOutputStream()); + try (InputStream decoder = Ghostryde.decoder(bsIn, privateKey)) { + ByteStreams.copy(decoder, ByteStreams.nullOutputStream()); } }); } + @Test + public void testFullEncryption() throws Exception { + // Check that the full encryption hasn't changed. All the other tests check that encrypting and + // decrypting results in the original data, but not whether the encryption method has changed. + FakeKeyringModule keyringModule = new FakeKeyringModule(); + PGPKeyPair dsa = keyringModule.get("rde-unittest@registry.test", ENCRYPT); + PGPPrivateKey privateKey = dsa.getPrivateKey(); + + // Encryption is inconsistent because it uses a random state. But decryption is consistent! + // + // If the encryption has legitimately changed - uncomment the following code, and copy the new + // encryptedInputBase64 from the test error: + // + // assertThat( + // Base64.getMimeEncoder() + // .encodeToString( + // Ghostryde.encode("Some data!!!111!!!".getBytes(UTF_8), dsa.getPublicKey()))) + // .isEqualTo("expect error"); + + String encryptedInputBase64 = + " hQEMA6WcEy81iaHVAQgAnn9bS6IOCTW2uZnITPWH8zIYr6K7YJslv38c4YU5eQqVhHC5PN0NhM2l\n" + + " i89U3lUE6gp3DdEEbTbugwXCHWyRL4fYTlpiHZjBn2vZdSS21EAG+q1XuTaD8DTjkC2G060/sW6i\n" + + " 0gSIkksqgubbSVZTxHEqh92tv35KCqiYc52hjKZIIGI8FHhpJOtDa3bhMMad8nrMy3vbv5LiYNh5\n" + + " j3DUCFhskU8Ldi1vBfXIonqUNLBrD/R471VVJyQ3NoGQTVUF9uXLoy+2dL0oBLc1Avj1XNP5PQ08\n" + + " MWlqmezkLdY0oHnQqTHYhYDxRo/Sw7xO1GLwWR11rcx/IAJloJbKSHTFeNJUAcKFnKvPDwBk3nnr\n" + + " uR505HtOj/tZDT5weVjhrlnmWXzaBRmYASy6PXZu6KzTbPUQTf4JeeJWdyw7glLMr2WPdMVPGZ8e\n" + + " gcFAjSJZjZlqohZyBUpP\n"; + + byte[] result = + Ghostryde.decode(Base64.getMimeDecoder().decode(encryptedInputBase64), privateKey); + + assertThat(new String(result, UTF_8)).isEqualTo("Some data!!!111!!!"); + } + @Test public void testFailure_keyMismatch() throws Exception { FakeKeyringModule keyringModule = new FakeKeyringModule(); byte[] data = "Fanatics have their dreams, wherewith they weave.".getBytes(UTF_8); - DateTime mtime = DateTime.parse("1984-12-18T00:30:00Z"); PGPKeyPair dsa1 = keyringModule.get("rde-unittest@registry.test", ENCRYPT); PGPKeyPair dsa2 = keyringModule.get("rde-unittest-dsa@registry.test", ENCRYPT); PGPPublicKey publicKey = dsa1.getPublicKey(); PGPPrivateKey privateKey = dsa2.getPrivateKey(); - Ghostryde ghost = new Ghostryde(1024); ByteArrayOutputStream bsOut = new ByteArrayOutputStream(); - try (Ghostryde.Encryptor encryptor = ghost.openEncryptor(bsOut, publicKey); - Ghostryde.Compressor kompressor = ghost.openCompressor(encryptor); - OutputStream output = ghost.openOutput(kompressor, "lol", mtime)) { - output.write(data); + try (OutputStream encoder = Ghostryde.encoder(bsOut, publicKey)) { + encoder.write(data); } ByteArrayInputStream bsIn = new ByteArrayInputStream(bsOut.toByteArray()); @@ -256,8 +223,8 @@ public class GhostrydeTest { assertThrows( PGPException.class, () -> { - try (Ghostryde.Decryptor decryptor = ghost.openDecryptor(bsIn, privateKey)) { - ByteStreams.copy(decryptor, ByteStreams.nullOutputStream()); + try (InputStream decoder = Ghostryde.decoder(bsIn, privateKey)) { + ByteStreams.copy(decoder, ByteStreams.nullOutputStream()); } }); assertThat(thrown) @@ -270,7 +237,6 @@ public class GhostrydeTest { public void testFailure_keyCorruption() throws Exception { FakeKeyringModule keyringModule = new FakeKeyringModule(); byte[] data = "Fanatics have their dreams, wherewith they weave.".getBytes(UTF_8); - DateTime mtime = DateTime.parse("1984-12-18T00:30:00Z"); PGPKeyPair rsa = keyringModule.get("rde-unittest@registry.test", ENCRYPT); PGPPublicKey publicKey = rsa.getPublicKey(); @@ -282,17 +248,14 @@ public class GhostrydeTest { rsa.getPrivateKey().getPublicKeyPacket(), rsa.getPrivateKey().getPrivateKeyDataPacket()); - Ghostryde ghost = new Ghostryde(1024); ByteArrayOutputStream bsOut = new ByteArrayOutputStream(); - try (Ghostryde.Encryptor encryptor = ghost.openEncryptor(bsOut, publicKey); - Ghostryde.Compressor kompressor = ghost.openCompressor(encryptor); - OutputStream output = ghost.openOutput(kompressor, "lol", mtime)) { - output.write(data); + try (OutputStream encoder = Ghostryde.encoder(bsOut, publicKey)) { + encoder.write(data); } ByteArrayInputStream bsIn = new ByteArrayInputStream(bsOut.toByteArray()); - try (Ghostryde.Decryptor decryptor = ghost.openDecryptor(bsIn, privateKey)) { - ByteStreams.copy(decryptor, ByteStreams.nullOutputStream()); + try (InputStream decoder = Ghostryde.decoder(bsIn, privateKey)) { + ByteStreams.copy(decoder, ByteStreams.nullOutputStream()); } } @@ -304,30 +267,6 @@ public class GhostrydeTest { } } - private static class BufferSize { - private final int value; - - BufferSize(int value) { - this.value = value; - } - - int get() { - return value; - } - } - - private static class Filename { - private final String value; - - Filename(String value) { - this.value = value; - } - - String get() { - return value; - } - } - private static class Content { private final String value; diff --git a/javatests/google/registry/rde/RdeReportActionTest.java b/javatests/google/registry/rde/RdeReportActionTest.java index 36ed1dee3..e4fe37f4f 100644 --- a/javatests/google/registry/rde/RdeReportActionTest.java +++ b/javatests/google/registry/rde/RdeReportActionTest.java @@ -26,7 +26,6 @@ import static google.registry.testing.GcsTestingUtils.writeGcsFile; import static google.registry.testing.JUnitBackports.assertThrows; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_OK; -import static org.joda.time.DateTimeZone.UTC; import static org.joda.time.Duration.standardDays; import static org.joda.time.Duration.standardSeconds; import static org.mockito.Matchers.any; @@ -105,7 +104,6 @@ public class RdeReportActionTest { reporter.retrier = new Retrier(new FakeSleeper(new FakeClock()), 3); RdeReportAction action = new RdeReportAction(); action.gcsUtils = new GcsUtils(gcsService, 1024); - action.ghostryde = new Ghostryde(1024); action.response = response; action.bucket = "tub"; action.tld = "test"; @@ -125,10 +123,7 @@ public class RdeReportActionTest { Cursor.create(RDE_REPORT, DateTime.parse("2006-06-06TZ"), Registry.get("test"))); persistResource( Cursor.create(RDE_UPLOAD, DateTime.parse("2006-06-07TZ"), Registry.get("test"))); - writeGcsFile( - gcsService, - reportFile, - Ghostryde.encode(REPORT_XML.read(), encryptKey, "darkside.xml", DateTime.now(UTC))); + writeGcsFile(gcsService, reportFile, Ghostryde.encode(REPORT_XML.read(), encryptKey)); } @Test diff --git a/javatests/google/registry/rde/RdeStagingActionTest.java b/javatests/google/registry/rde/RdeStagingActionTest.java index c612ce750..cc7e3f8a2 100644 --- a/javatests/google/registry/rde/RdeStagingActionTest.java +++ b/javatests/google/registry/rde/RdeStagingActionTest.java @@ -136,7 +136,6 @@ public class RdeStagingActionTest extends MapreduceTestCase { new FakeLockHandler(true), 0, // gcsBufferSize "rde-bucket", // bucket - 31337, // ghostrydeBufferSize Duration.standardHours(1), // lockTimeout PgpHelper.convertPublicKeyToBytes(encryptKey), // stagingKeyBytes false); // lenient @@ -330,9 +329,9 @@ public class RdeStagingActionTest extends MapreduceTestCase { action.run(); executeTasksUntilEmpty("mapreduce", clock); - XjcRdeDeposit deposit = unmarshal( - XjcRdeDeposit.class, - Ghostryde.decode(readGcsFile(gcsService, XML_FILE), decryptKey).getData()); + XjcRdeDeposit deposit = + unmarshal( + XjcRdeDeposit.class, Ghostryde.decode(readGcsFile(gcsService, XML_FILE), decryptKey)); XjcRdeHeader header = extractAndRemoveContentWithType(XjcRdeHeader.class, deposit); assertThat(header.getTld()).isEqualTo("lol"); @@ -361,9 +360,9 @@ public class RdeStagingActionTest extends MapreduceTestCase { action.run(); executeTasksUntilEmpty("mapreduce", clock); - XjcRdeDeposit deposit = unmarshal( - XjcRdeDeposit.class, - Ghostryde.decode(readGcsFile(gcsService, XML_FILE), decryptKey).getData()); + XjcRdeDeposit deposit = + unmarshal( + XjcRdeDeposit.class, Ghostryde.decode(readGcsFile(gcsService, XML_FILE), decryptKey)); assertThat(deposit.getType()).isEqualTo(XjcRdeDepositTypeType.FULL); assertThat(deposit.getId()).isEqualTo(RdeUtil.timestampToId(DateTime.parse("2000-01-01TZ"))); assertThat(deposit.getWatermark()).isEqualTo(DateTime.parse("2000-01-01TZ")); @@ -403,9 +402,9 @@ public class RdeStagingActionTest extends MapreduceTestCase { action.run(); executeTasksUntilEmpty("mapreduce", clock); - XjcRdeDeposit deposit = unmarshal( - XjcRdeDeposit.class, - Ghostryde.decode(readGcsFile(gcsService, XML_FILE), decryptKey).getData()); + XjcRdeDeposit deposit = + unmarshal( + XjcRdeDeposit.class, Ghostryde.decode(readGcsFile(gcsService, XML_FILE), decryptKey)); XjcRdeRegistrar registrar1 = extractAndRemoveContentWithType(XjcRdeRegistrar.class, deposit); XjcRdeRegistrar registrar2 = extractAndRemoveContentWithType(XjcRdeRegistrar.class, deposit); XjcRdeHeader header = extractAndRemoveContentWithType(XjcRdeHeader.class, deposit); @@ -492,9 +491,9 @@ public class RdeStagingActionTest extends MapreduceTestCase { for (GcsFilename filename : asList( new GcsFilename("rde-bucket", "fop_1971-01-01_full_S1_R0.xml.ghostryde"), new GcsFilename("rde-bucket", "fop_1971-01-05_thin_S1_R0.xml.ghostryde"))) { - XjcRdeDeposit deposit = unmarshal( - XjcRdeDeposit.class, - Ghostryde.decode(readGcsFile(gcsService, filename), decryptKey).getData()); + XjcRdeDeposit deposit = + unmarshal( + XjcRdeDeposit.class, Ghostryde.decode(readGcsFile(gcsService, filename), decryptKey)); XjcRdeRegistrar registrar1 = extractAndRemoveContentWithType(XjcRdeRegistrar.class, deposit); XjcRdeRegistrar registrar2 = extractAndRemoveContentWithType(XjcRdeRegistrar.class, deposit); XjcRdeHeader header = extractAndRemoveContentWithType(XjcRdeHeader.class, deposit); @@ -524,9 +523,9 @@ public class RdeStagingActionTest extends MapreduceTestCase { executeTasksUntilEmpty("mapreduce", clock); GcsFilename filename = new GcsFilename("rde-bucket", "fop_2000-01-01_full_S1_R0.xml.ghostryde"); - XjcRdeDeposit deposit = unmarshal( - XjcRdeDeposit.class, - Ghostryde.decode(readGcsFile(gcsService, filename), decryptKey).getData()); + XjcRdeDeposit deposit = + unmarshal( + XjcRdeDeposit.class, Ghostryde.decode(readGcsFile(gcsService, filename), decryptKey)); XjcRdeDomain domain = extractAndRemoveContentWithType(XjcRdeDomain.class, deposit); XjcRdeIdn firstIdn = extractAndRemoveContentWithType(XjcRdeIdn.class, deposit); XjcRdeHeader header = extractAndRemoveContentWithType(XjcRdeHeader.class, deposit); @@ -566,7 +565,7 @@ public class RdeStagingActionTest extends MapreduceTestCase { action.run(); executeTasksUntilEmpty("mapreduce", clock); - byte[] deposit = Ghostryde.decode(readGcsFile(gcsService, XML_FILE), decryptKey).getData(); + byte[] deposit = Ghostryde.decode(readGcsFile(gcsService, XML_FILE), decryptKey); assertThat(Integer.parseInt(new String(readGcsFile(gcsService, LENGTH_FILE), UTF_8))) .isEqualTo(deposit.length); } @@ -818,7 +817,7 @@ public class RdeStagingActionTest extends MapreduceTestCase { private String readXml(String objectName) throws IOException, PGPException { GcsFilename file = new GcsFilename("rde-bucket", objectName); - return new String(Ghostryde.decode(readGcsFile(gcsService, file), decryptKey).getData(), UTF_8); + return new String(Ghostryde.decode(readGcsFile(gcsService, file), decryptKey), UTF_8); } private diff --git a/javatests/google/registry/rde/RdeUploadActionTest.java b/javatests/google/registry/rde/RdeUploadActionTest.java index afbefad31..1bcc90b9d 100644 --- a/javatests/google/registry/rde/RdeUploadActionTest.java +++ b/javatests/google/registry/rde/RdeUploadActionTest.java @@ -187,7 +187,6 @@ public class RdeUploadActionTest { RdeUploadAction action = new RdeUploadAction(); action.clock = clock; action.gcsUtils = new GcsUtils(gcsService, BUFFER_SIZE); - action.ghostryde = new Ghostryde(BUFFER_SIZE); action.lazyJsch = () -> JSchModule.provideJSch( @@ -239,18 +238,12 @@ public class RdeUploadActionTest { createTld("tld"); PGPPublicKey encryptKey = new FakeKeyringModule().get().getRdeStagingEncryptionKey(); - writeGcsFile(gcsService, GHOSTRYDE_FILE, - Ghostryde.encode(DEPOSIT_XML.read(), encryptKey, "lobster.xml", clock.nowUtc())); - writeGcsFile(gcsService, GHOSTRYDE_R1_FILE, - Ghostryde.encode(DEPOSIT_XML.read(), encryptKey, "lobster.xml", clock.nowUtc())); - writeGcsFile(gcsService, LENGTH_FILE, - Long.toString(DEPOSIT_XML.size()).getBytes(UTF_8)); - writeGcsFile(gcsService, LENGTH_R1_FILE, - Long.toString(DEPOSIT_XML.size()).getBytes(UTF_8)); - writeGcsFile(gcsService, REPORT_FILE, - Ghostryde.encode(REPORT_XML.read(), encryptKey, "dieform.xml", clock.nowUtc())); - writeGcsFile(gcsService, REPORT_R1_FILE, - Ghostryde.encode(REPORT_XML.read(), encryptKey, "dieform.xml", clock.nowUtc())); + writeGcsFile(gcsService, GHOSTRYDE_FILE, Ghostryde.encode(DEPOSIT_XML.read(), encryptKey)); + writeGcsFile(gcsService, GHOSTRYDE_R1_FILE, Ghostryde.encode(DEPOSIT_XML.read(), encryptKey)); + writeGcsFile(gcsService, LENGTH_FILE, Long.toString(DEPOSIT_XML.size()).getBytes(UTF_8)); + writeGcsFile(gcsService, LENGTH_R1_FILE, Long.toString(DEPOSIT_XML.size()).getBytes(UTF_8)); + writeGcsFile(gcsService, REPORT_FILE, Ghostryde.encode(REPORT_XML.read(), encryptKey)); + writeGcsFile(gcsService, REPORT_R1_FILE, Ghostryde.encode(REPORT_XML.read(), encryptKey)); ofy() .transact( () -> { diff --git a/javatests/google/registry/tools/GhostrydeCommandTest.java b/javatests/google/registry/tools/GhostrydeCommandTest.java index db5580bda..4236e3919 100644 --- a/javatests/google/registry/tools/GhostrydeCommandTest.java +++ b/javatests/google/registry/tools/GhostrydeCommandTest.java @@ -19,15 +19,12 @@ import static java.nio.charset.StandardCharsets.UTF_8; import google.registry.keyring.api.Keyring; import google.registry.rde.Ghostryde; -import google.registry.rde.Ghostryde.DecodeResult; import google.registry.testing.BouncyCastleProviderRule; import google.registry.testing.FakeKeyringModule; import google.registry.testing.InjectRule; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.attribute.FileTime; -import org.joda.time.DateTime; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -35,7 +32,6 @@ import org.junit.Test; /** Unit tests for {@link GhostrydeCommand}. */ public class GhostrydeCommandTest extends CommandTestCase { - private static final DateTime MODIFIED_TIME = DateTime.parse("1984-12-18T04:20:00Z"); private static final byte[] SONG_BY_CHRISTINA_ROSSETTI = ("" + "When I am dead, my dearest, \n" + " Sing no sad songs for me; \n" @@ -66,7 +62,6 @@ public class GhostrydeCommandTest extends CommandTestCase { @Before public void before() { keyring = new FakeKeyringModule().get(); - command.ghostryde = new Ghostryde(1024); command.rdeStagingDecryptionKey = keyring::getRdeStagingDecryptionKey; command.rdeStagingEncryptionKey = keyring::getRdeStagingEncryptionKey; } @@ -76,14 +71,10 @@ public class GhostrydeCommandTest extends CommandTestCase { Path inFile = Paths.get(tmpDir.newFile("atrain.txt").toString()); Path outFile = Paths.get(tmpDir.newFile().toString()); Files.write(inFile, SONG_BY_CHRISTINA_ROSSETTI); - Files.setLastModifiedTime(inFile, FileTime.fromMillis(MODIFIED_TIME.getMillis())); runCommand("--encrypt", "--input=" + inFile, "--output=" + outFile); - DecodeResult decoded = Ghostryde.decode( - Files.readAllBytes(outFile), - keyring.getRdeStagingDecryptionKey()); - assertThat(decoded.getData()).isEqualTo(SONG_BY_CHRISTINA_ROSSETTI); - assertThat(decoded.getName()).isEqualTo("atrain.txt"); - assertThat(decoded.getModified()).isEqualTo(MODIFIED_TIME); + byte[] decoded = + Ghostryde.decode(Files.readAllBytes(outFile), keyring.getRdeStagingDecryptionKey()); + assertThat(decoded).isEqualTo(SONG_BY_CHRISTINA_ROSSETTI); } @Test @@ -91,45 +82,34 @@ public class GhostrydeCommandTest extends CommandTestCase { Path inFile = Paths.get(tmpDir.newFile("atrain.txt").toString()); Path outDir = Paths.get(tmpDir.newFolder().toString()); Files.write(inFile, SONG_BY_CHRISTINA_ROSSETTI); - Files.setLastModifiedTime(inFile, FileTime.fromMillis(MODIFIED_TIME.getMillis())); runCommand("--encrypt", "--input=" + inFile, "--output=" + outDir); + Path lenOutFile = outDir.resolve("atrain.txt.length"); + assertThat(Ghostryde.readLength(Files.newInputStream(lenOutFile))) + .isEqualTo(SONG_BY_CHRISTINA_ROSSETTI.length); Path outFile = outDir.resolve("atrain.txt.ghostryde"); - DecodeResult decoded = Ghostryde.decode( - Files.readAllBytes(outFile), - keyring.getRdeStagingDecryptionKey()); - assertThat(decoded.getData()).isEqualTo(SONG_BY_CHRISTINA_ROSSETTI); - assertThat(decoded.getName()).isEqualTo("atrain.txt"); - assertThat(decoded.getModified()).isEqualTo(MODIFIED_TIME); + byte[] decoded = + Ghostryde.decode(Files.readAllBytes(outFile), keyring.getRdeStagingDecryptionKey()); + assertThat(decoded).isEqualTo(SONG_BY_CHRISTINA_ROSSETTI); } @Test public void testDecrypt_outputIsAFile_writesToFile() throws Exception { Path inFile = Paths.get(tmpDir.newFile().toString()); Path outFile = Paths.get(tmpDir.newFile().toString()); - Files.write(inFile, Ghostryde.encode( - SONG_BY_CHRISTINA_ROSSETTI, - keyring.getRdeStagingEncryptionKey(), - "atrain.txt", - MODIFIED_TIME)); + Files.write( + inFile, Ghostryde.encode(SONG_BY_CHRISTINA_ROSSETTI, keyring.getRdeStagingEncryptionKey())); runCommand("--decrypt", "--input=" + inFile, "--output=" + outFile); assertThat(Files.readAllBytes(outFile)).isEqualTo(SONG_BY_CHRISTINA_ROSSETTI); - assertThat(Files.getLastModifiedTime(outFile)) - .isEqualTo(FileTime.fromMillis(MODIFIED_TIME.getMillis())); } @Test - public void testDecrypt_outputIsADirectory_writesToFileFromInnerName() throws Exception { - Path inFile = Paths.get(tmpDir.newFile().toString()); + public void testDecrypt_outputIsADirectory_AppendsDecryptExtension() throws Exception { + Path inFile = Paths.get(tmpDir.newFolder().toString()).resolve("atrain.ghostryde"); Path outDir = Paths.get(tmpDir.newFolder().toString()); - Files.write(inFile, Ghostryde.encode( - SONG_BY_CHRISTINA_ROSSETTI, - keyring.getRdeStagingEncryptionKey(), - "atrain.txt", - MODIFIED_TIME)); + Files.write( + inFile, Ghostryde.encode(SONG_BY_CHRISTINA_ROSSETTI, keyring.getRdeStagingEncryptionKey())); runCommand("--decrypt", "--input=" + inFile, "--output=" + outDir); - Path outFile = outDir.resolve("atrain.txt"); + Path outFile = outDir.resolve("atrain.ghostryde.decrypt"); assertThat(Files.readAllBytes(outFile)).isEqualTo(SONG_BY_CHRISTINA_ROSSETTI); - assertThat(Files.getLastModifiedTime(outFile)) - .isEqualTo(FileTime.fromMillis(MODIFIED_TIME.getMillis())); } }