From 8a8cd9f0d2815fa7e036a6c80649198fabb5867b Mon Sep 17 00:00:00 2001 From: guyben Date: Thu, 19 Jul 2018 08:34:59 -0700 Subject: [PATCH] Move the RDE encryption to a dedicated file Merges the encryptor creation between RyDE and Ghostryde. The new file - RydeEncryption.java - is a merge of the removed functions in Ghostryde.java and the RydePgpEncryptionOutputStream.java. This is one of a series of CLs - each merging a single "part" of the encoding. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=205246053 --- java/google/registry/rde/Ghostryde.java | 150 +------------ .../registry/rde/RdeStagingReducer.java | 7 +- java/google/registry/rde/RydeEncoder.java | 4 +- java/google/registry/rde/RydeEncryption.java | 203 ++++++++++++++++++ .../rde/RydePgpEncryptionOutputStream.java | 118 ---------- .../google/registry/rde/GhostrydeTest.java | 22 +- .../registry/rde/RydeEncryptionTest.java | 155 +++++++++++++ 7 files changed, 388 insertions(+), 271 deletions(-) create mode 100644 java/google/registry/rde/RydeEncryption.java delete mode 100644 java/google/registry/rde/RydePgpEncryptionOutputStream.java create mode 100644 javatests/google/registry/rde/RydeEncryptionTest.java diff --git a/java/google/registry/rde/Ghostryde.java b/java/google/registry/rde/Ghostryde.java index 2d0167f84..57dc61a71 100644 --- a/java/google/registry/rde/Ghostryde.java +++ b/java/google/registry/rde/Ghostryde.java @@ -18,12 +18,14 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static google.registry.rde.RydeCompression.openCompressor; 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 java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.UTF_8; -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 com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; import com.google.common.io.Closer; import google.registry.util.ImprovedInputStream; @@ -33,25 +35,14 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.security.NoSuchAlgorithmException; -import java.security.ProviderException; -import java.security.SecureRandom; -import java.util.Optional; -import java.util.stream.Collectors; import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import javax.annotation.WillNotClose; -import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; -import org.bouncycastle.openpgp.PGPEncryptedDataList; 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.bouncycastle.openpgp.PGPPublicKeyEncryptedData; -import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder; import org.joda.time.DateTime; /** @@ -125,32 +116,6 @@ public final class Ghostryde { /** Size of the buffer used by the intermediate streams. */ static final int BUFFER_SIZE = 64 * 1024; - /** - * Symmetric encryption cipher to use when creating ghostryde files. - * - *

We're going to use AES-128 just like {@link RydePgpEncryptionOutputStream}, although we - * aren't forced to use this algorithm by the ICANN RFCs since this is an internal format. - * - * @see org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags - */ - static final int CIPHER = AES_128; - - /** - * Unlike {@link RydePgpEncryptionOutputStream}, we're going to enable the integrity packet - * because it makes GnuPG happy. It's also probably necessary to prevent tampering since we - * don't sign ghostryde files. - */ - static final boolean USE_INTEGRITY_PACKET = true; - - /** - * The source of random bits. You are strongly discouraged from changing this value because at - * Google it's configured to use {@code /dev/{,u}random} in production and somehow - * magically go fast and not drain entropy in the testing environment. - * - * @see SecureRandom#getInstance(String) - */ - 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 @@ -214,11 +179,13 @@ public final class Ghostryde { */ public static ImprovedOutputStream encoder( OutputStream output, PGPPublicKey encryptionKey, @Nullable OutputStream lengthOutput) - throws IOException, PGPException { + throws IOException { // 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 encryptionLayer = + closer.register( + openEncryptor(output, GHOSTRYDE_USE_INTEGRITY_PACKET, ImmutableList.of(encryptionKey))); OutputStream kompressor = closer.register(openCompressor(encryptionLayer)); OutputStream fileLayer = closer.register( @@ -245,7 +212,7 @@ public final class Ghostryde { * @param encryptionKey the encryption key to use */ public static ImprovedOutputStream encoder(OutputStream output, PGPPublicKey encryptionKey) - throws IOException, PGPException { + throws IOException { return encoder(output, encryptionKey, null); } @@ -260,7 +227,8 @@ public final class Ghostryde { // 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 decryptionLayer = + closer.register(openDecryptor(input, GHOSTRYDE_USE_INTEGRITY_PACKET, decryptionKey)); InputStream decompressor = closer.register(openDecompressor(decryptionLayer)); InputStream fileLayer = closer.register(openPgpFileInputStream(decompressor)); @@ -275,43 +243,6 @@ public final class Ghostryde { private Ghostryde() {} - /** - * 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}. - * - *

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. - * @throws IOException - * @throws PGPException - */ - @CheckReturnValue - 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 ImprovedOutputStream( - "GhostrydeEncryptor", encryptor.open(os, new byte[BUFFER_SIZE])); - } - - /** Does stuff. */ - private static SecureRandom getRandom() { - SecureRandom random; - try { - random = SecureRandom.getInstance(RANDOM_SOURCE); - } catch (NoSuchAlgorithmException e) { - throw new ProviderException(e); - } - return random; - } - /** * Opens an {@link OutputStream} to which the actual data should be written (Writing Step 3/3) * @@ -334,65 +265,6 @@ public final class Ghostryde { .open(os, BINARY, name, modified.toDate(), new byte[BUFFER_SIZE])); } - /** - * 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}. - * - *

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!) - * @throws IOException - * @throws PGPException - */ - @CheckReturnValue - private static ImprovedInputStream openDecryptor( - @WillNotClose InputStream input, PGPPrivateKey privateKey) throws IOException, PGPException { - checkNotNull(privateKey, "privateKey"); - PGPEncryptedDataList ciphertextList = - PgpUtils.readSinglePgpObject(input, PGPEncryptedDataList.class); - // Go over all the possible decryption keys, and look for the one that has our key ID. - Optional cyphertext = - PgpUtils.stream(ciphertextList, PGPPublicKeyEncryptedData.class) - .filter(ciphertext -> ciphertext.getKeyID() == privateKey.getKeyID()) - .findAny(); - // If we can't find one with our key ID, then we can't decrypt the file! - if (!cyphertext.isPresent()) { - String keyIds = - PgpUtils.stream(ciphertextList, PGPPublicKeyEncryptedData.class) - .map(ciphertext -> Long.toHexString(ciphertext.getKeyID())) - .collect(Collectors.joining(",")); - throw new PGPException( - String.format( - "Message was encrypted for keyids [%s] but ours is %x", - keyIds, privateKey.getKeyID())); - } - - // 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.get().getDataStream(new BcPublicKeyDataDecryptorFactory(privateKey))) { - @Override - protected void onClose() throws IOException { - if (USE_INTEGRITY_PACKET) { - try { - if (!cyphertext.get().verify()) { - throw new PGPException("ghostryde integrity check failed: possible tampering D:"); - } - } catch (PGPException e) { - throw new IllegalStateException(e); - } - } - } - }; - } - /** * Opens a new decoder for reading the original contents (Reading Step 3/3) * diff --git a/java/google/registry/rde/RdeStagingReducer.java b/java/google/registry/rde/RdeStagingReducer.java index fadbd48f5..3adb9931c 100644 --- a/java/google/registry/rde/RdeStagingReducer.java +++ b/java/google/registry/rde/RdeStagingReducer.java @@ -57,7 +57,6 @@ import java.util.Optional; import java.util.concurrent.Callable; import javax.inject.Inject; import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.joda.time.DateTime; import org.joda.time.Duration; @@ -134,7 +133,7 @@ public final class RdeStagingReducer extends ReducerThis uses 128-bit AES (Rijndael) as the symmetric encryption algorithm. This is the only key + * strength ICANN allows. The other valid algorithms are TripleDES and CAST5 per RFC 4880. It's + * probably for the best that we're not using AES-256 since it's been weakened over the years to + * potentially being worse than AES-128. + * + *

The key for the symmetric algorithm is generated by a random number generator which SHOULD + * come from {@code /dev/random} (see: {@link sun.security.provider.NativePRNG}) but Java doesn't + * offer any guarantees that {@link SecureRandom} isn't pseudo-random. + * + *

The asymmetric algorithm is whatever one is associated with the {@link PGPPublicKey} object + * you provide. That should be either RSA or DSA, per the ICANN escrow spec. The underlying {@link + * PGPEncryptedDataGenerator} class uses PGP Cipher Feedback Mode to chain blocks. No integrity + * packet is used. + * + * @see RFC 4880 (OpenPGP Message Format) + * @see AES (Wikipedia) + */ +final class RydeEncryption { + + private static final int BUFFER_SIZE = 64 * 1024; + + /** + * The symmetric encryption algorithm to use. Do not change this value without checking the RFCs + * to make sure the encryption algorithm and strength combination is allowed. + * + * @see org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags + */ + private static final int CIPHER = AES_128; + + /** + * This option adds an additional checksum to the OpenPGP message. From what I can tell, this is + * meant to fix a bug that made a certain type of message tampering possible. GPG will actually + * complain on the command line when decrypting a message without this feature. + * + *

However I'm reasonably certain that this is not required if you have a signature (and + * remember to use it!) and the ICANN requirements document do not mention this. So we're going to + * leave it out. + */ + static final boolean RYDE_USE_INTEGRITY_PACKET = false; + + /** + * Unlike Ryde, we're going to enable the integrity packet because it makes GnuPG happy. It's also + * probably necessary to prevent tampering since we don't sign ghostryde files. + */ + static final boolean GHOSTRYDE_USE_INTEGRITY_PACKET = true; + + /** + * The source of random bits. This should not be changed at Google because it uses dev random in + * production, and the testing environment is configured to make this go fast and not drain system + * entropy. + * + * @see SecureRandom#getInstance(String) + */ + private static final String RANDOM_SOURCE = "NativePRNG"; + + /** + * Creates an OutputStream that encrypts data for the owners of {@code receiverKeys}. + * + *

TODO(b/110465964): document where the input comes from / output goes to. Something like + * documenting that os is the upstream OutputStream and the result goes into openCompressor. + * + * @param os where to write the encrypted data. Is not closed by this object. + * @param withIntegrityPacket whether to add the integrity packet to the encrypted data. Not + * allowed in RyDE. + * @param receiverKeys at least one encryption key. The message will be decryptable with any of + * the given keys. + * @throws IllegalArgumentException if {@code publicKey} is invalid + * @throws RuntimeException to rethrow {@link PGPException} and {@link IOException} + */ + @CheckReturnValue + static ImprovedOutputStream openEncryptor( + @WillNotClose OutputStream os, + boolean withIntegrityPacket, + Collection receiverKeys) { + try { + PGPEncryptedDataGenerator encryptor = + new PGPEncryptedDataGenerator( + new JcePGPDataEncryptorBuilder(CIPHER) + .setWithIntegrityPacket(withIntegrityPacket) + .setSecureRandom(SecureRandom.getInstance(RANDOM_SOURCE)) + .setProvider(PROVIDER_NAME)); + checkArgument(!receiverKeys.isEmpty(), "Must give at least one receiver key"); + receiverKeys.forEach( + key -> encryptor.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(key))); + return new ImprovedOutputStream("RydeEncryptor", encryptor.open(os, new byte[BUFFER_SIZE])); + } catch (NoSuchAlgorithmException e) { + throw new ProviderException(e); + } catch (IOException | PGPException e) { + throw new RuntimeException(e); + } + } + + /** + * Creates an InputStream that encrypts data for the owners of {@code receiverKeys}. + * + *

TODO(b/110465964): document where the input comes from / output goes to. Something like + * documenting that input is upstream InputStream and the result goes into openDecompressor. + * + * @param input from where to read the encrypted data. Is not closed by this object. + * @param checkIntegrityPacket whether to check the integrity packet on the encrypted data. Only + * use if the integrety packet was created when encrypting. + * @param privateKey the private counterpart of one of the receiverKeys used to encrypt. + * @throws IllegalArgumentException if {@code publicKey} is invalid + * @throws RuntimeException to rethrow {@link PGPException} and {@link IOException} + */ + @CheckReturnValue + static ImprovedInputStream openDecryptor( + @WillNotClose InputStream input, boolean checkIntegrityPacket, PGPPrivateKey privateKey) { + try { + PGPEncryptedDataList ciphertextList = + PgpUtils.readSinglePgpObject(input, PGPEncryptedDataList.class); + // Go over all the possible decryption keys, and look for the one that has our key ID. + Optional cyphertext = + PgpUtils.stream(ciphertextList, PGPPublicKeyEncryptedData.class) + .filter(ciphertext -> ciphertext.getKeyID() == privateKey.getKeyID()) + .findAny(); + // If we can't find one with our key ID, then we can't decrypt the file! + if (!cyphertext.isPresent()) { + String keyIds = + PgpUtils.stream(ciphertextList, PGPPublicKeyEncryptedData.class) + .map(ciphertext -> Long.toHexString(ciphertext.getKeyID())) + .collect(Collectors.joining(",")); + throw new PGPException( + String.format( + "Message was encrypted for keyids [%s] but ours is %x", + keyIds, privateKey.getKeyID())); + } + + InputStream dataStream = + cyphertext.get().getDataStream( + new JcePublicKeyDataDecryptorFactoryBuilder() + .setProvider(PROVIDER_NAME) + .build(privateKey)); + if (!checkIntegrityPacket) { + return new ImprovedInputStream("RydeDecryptor", dataStream); + } + // We want an input stream that also verifies ciphertext wasn't corrupted or tampered with + // when the stream is closed. + return new ImprovedInputStream("RydeDecryptor", dataStream) { + @Override + protected void onClose() throws IOException { + try { + if (!cyphertext.get().verify()) { + throw new PGPException("integrity check failed: possible tampering D:"); + } + } catch (PGPException e) { + throw new IllegalStateException(e); + } + } + }; + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + + private RydeEncryption() {} +} diff --git a/java/google/registry/rde/RydePgpEncryptionOutputStream.java b/java/google/registry/rde/RydePgpEncryptionOutputStream.java deleted file mode 100644 index ca108dd3c..000000000 --- a/java/google/registry/rde/RydePgpEncryptionOutputStream.java +++ /dev/null @@ -1,118 +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.bcpg.SymmetricKeyAlgorithmTags.AES_128; -import static org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME; - -import google.registry.util.ImprovedOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.security.NoSuchAlgorithmException; -import java.security.ProviderException; -import java.security.SecureRandom; -import java.util.Collection; -import javax.annotation.WillNotClose; -import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator; - -/** - * OpenPGP encryption service that wraps an {@link OutputStream}. - * - *

This uses 128-bit AES (Rijndael) as the symmetric encryption algorithm. This is the only key - * strength ICANN allows. The other valid algorithms are TripleDES and CAST5 per RFC 4880. It's - * probably for the best that we're not using AES-256 since it's been weakened over the years to - * potentially being worse than AES-128. - * - *

The key for the symmetric algorithm is generated by a random number generator which SHOULD - * come from {@code /dev/random} (see: {@link sun.security.provider.NativePRNG}) but Java doesn't - * offer any guarantees that {@link SecureRandom} isn't pseudo-random. - * - *

The asymmetric algorithm is whatever one is associated with the {@link PGPPublicKey} object - * you provide. That should be either RSA or DSA, per the ICANN escrow spec. The underlying - * {@link PGPEncryptedDataGenerator} class uses PGP Cipher Feedback Mode to chain blocks. No - * integrity packet is used. - * - * @see RFC 4880 (OpenPGP Message Format) - * @see AES (Wikipedia) - */ -public class RydePgpEncryptionOutputStream extends ImprovedOutputStream { - - private static final int BUFFER_SIZE = 64 * 1024; - - /** - * The symmetric encryption algorithm to use. Do not change this value without checking the - * RFCs to make sure the encryption algorithm and strength combination is allowed. - * - * @see org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags - */ - private static final int CIPHER = AES_128; - - /** - * This option adds an additional checksum to the OpenPGP message. From what I can tell, this is - * meant to fix a bug that made a certain type of message tampering possible. GPG will actually - * complain on the command line when decrypting a message without this feature. - * - *

However I'm reasonably certain that this is not required if you have a signature (and - * remember to use it!) and the ICANN requirements document do not mention this. So we're going - * to leave it out. - */ - private static final boolean USE_INTEGRITY_PACKET = false; - - /** - * The source of random bits. This should not be changed at Google because it uses dev random - * in production, and the testing environment is configured to make this go fast and not drain - * system entropy. - * - * @see SecureRandom#getInstance(String) - */ - private static final String RANDOM_SOURCE = "NativePRNG"; - - /** - * Creates a new instance that encrypts data for the owner of {@code receiverKey}. - * - * @param os is the upstream {@link OutputStream} which is not closed by this object - * @throws IllegalArgumentException if {@code publicKey} is invalid - * @throws RuntimeException to rethrow {@link PGPException} and {@link IOException} - */ - public RydePgpEncryptionOutputStream( - @WillNotClose OutputStream os, - Collection receiverKeys) { - super("RydePgpEncryptionOutputStream", createDelegate(os, receiverKeys)); - } - - private static OutputStream createDelegate( - OutputStream os, Collection receiverKeys) { - try { - PGPEncryptedDataGenerator encryptor = new PGPEncryptedDataGenerator( - new JcePGPDataEncryptorBuilder(CIPHER) - .setWithIntegrityPacket(USE_INTEGRITY_PACKET) - .setSecureRandom(SecureRandom.getInstance(RANDOM_SOURCE)) - .setProvider(PROVIDER_NAME)); - checkArgument(!receiverKeys.isEmpty(), "Must give at least one receiver key"); - receiverKeys.forEach( - key -> encryptor.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(key))); - return encryptor.open(os, new byte[BUFFER_SIZE]); - } catch (NoSuchAlgorithmException e) { - throw new ProviderException(e); - } catch (IOException | PGPException e) { - throw new RuntimeException(e); - } - } -} diff --git a/javatests/google/registry/rde/GhostrydeTest.java b/javatests/google/registry/rde/GhostrydeTest.java index aed8374cd..5a3b50633 100644 --- a/javatests/google/registry/rde/GhostrydeTest.java +++ b/javatests/google/registry/rde/GhostrydeTest.java @@ -161,13 +161,15 @@ public class GhostrydeTest { korruption(ciphertext, ciphertext.length / 2); ByteArrayInputStream bsIn = new ByteArrayInputStream(ciphertext); - assertThrows( - PGPException.class, - () -> { - try (InputStream decoder = Ghostryde.decoder(bsIn, privateKey)) { - ByteStreams.copy(decoder, ByteStreams.nullOutputStream()); - } - }); + RuntimeException thrown = + assertThrows( + RuntimeException.class, + () -> { + try (InputStream decoder = Ghostryde.decoder(bsIn, privateKey)) { + ByteStreams.copy(decoder, ByteStreams.nullOutputStream()); + } + }); + assertThat(thrown).hasCauseThat().isInstanceOf(PGPException.class); } @Test @@ -219,15 +221,17 @@ public class GhostrydeTest { } ByteArrayInputStream bsIn = new ByteArrayInputStream(bsOut.toByteArray()); - PGPException thrown = + RuntimeException thrown = assertThrows( - PGPException.class, + RuntimeException.class, () -> { try (InputStream decoder = Ghostryde.decoder(bsIn, privateKey)) { ByteStreams.copy(decoder, ByteStreams.nullOutputStream()); } }); + assertThat(thrown).hasCauseThat().isInstanceOf(PGPException.class); assertThat(thrown) + .hasCauseThat() .hasMessageThat() .contains( "Message was encrypted for keyids [a59c132f3589a1d5] but ours is c9598c84ec70b9fd"); diff --git a/javatests/google/registry/rde/RydeEncryptionTest.java b/javatests/google/registry/rde/RydeEncryptionTest.java new file mode 100644 index 000000000..7a960ed14 --- /dev/null +++ b/javatests/google/registry/rde/RydeEncryptionTest.java @@ -0,0 +1,155 @@ +// 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 google.registry.keyring.api.PgpHelper.KeyRequirement.ENCRYPT; +import static google.registry.testing.JUnitBackports.assertThrows; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteStreams; +import google.registry.testing.BouncyCastleProviderRule; +import google.registry.testing.FakeKeyringModule; +import google.registry.testing.ShardableTestCase; +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.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class RydeEncryptionTest extends ShardableTestCase { + + @Rule public final BouncyCastleProviderRule bouncy = new BouncyCastleProviderRule(); + + @Test + public void testSuccess_oneReceiver_decryptWithCorrectKey() throws Exception { + FakeKeyringModule keyringModule = new FakeKeyringModule(); + PGPKeyPair key = keyringModule.get("rde-unittest@registry.test", ENCRYPT); + byte[] expected = "Testing 1, 2, 3".getBytes(UTF_8); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (OutputStream encryptor = + RydeEncryption.openEncryptor(output, false, ImmutableList.of(key.getPublicKey()))) { + encryptor.write(expected); + } + byte[] encryptedData = output.toByteArray(); + + ByteArrayInputStream input = new ByteArrayInputStream(encryptedData); + try (InputStream decryptor = RydeEncryption.openDecryptor(input, false, key.getPrivateKey())) { + assertThat(ByteStreams.toByteArray(decryptor)).isEqualTo(expected); + } + } + + @Test + public void testFail_oneReceiver_decryptWithWrongKey() throws Exception { + FakeKeyringModule keyringModule = new FakeKeyringModule(); + PGPKeyPair key = keyringModule.get("rde-unittest@registry.test", ENCRYPT); + PGPKeyPair wrongKey = keyringModule.get("rde-unittest-dsa@registry.test", ENCRYPT); + assertThat(key.getKeyID()).isNotEqualTo(wrongKey.getKeyID()); + byte[] expected = "Testing 1, 2, 3".getBytes(UTF_8); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (OutputStream encryptor = + RydeEncryption.openEncryptor(output, false, ImmutableList.of(key.getPublicKey()))) { + encryptor.write(expected); + } + byte[] encryptedData = output.toByteArray(); + + ByteArrayInputStream input = new ByteArrayInputStream(encryptedData); + RuntimeException thrown = + assertThrows( + RuntimeException.class, + () -> { + RydeEncryption.openDecryptor(input, false, wrongKey.getPrivateKey()).read(); + }); + + assertThat(thrown).hasCauseThat().isInstanceOf(PGPException.class); + } + + @Test + public void testSuccess_twoReceivers() throws Exception { + FakeKeyringModule keyringModule = new FakeKeyringModule(); + PGPKeyPair key1 = keyringModule.get("rde-unittest@registry.test", ENCRYPT); + PGPKeyPair key2 = keyringModule.get("rde-unittest-dsa@registry.test", ENCRYPT); + assertThat(key1.getKeyID()).isNotEqualTo(key2.getKeyID()); + byte[] expected = "Testing 1, 2, 3".getBytes(UTF_8); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (OutputStream encryptor = + RydeEncryption.openEncryptor( + output, false, ImmutableList.of(key1.getPublicKey(), key2.getPublicKey()))) { + encryptor.write(expected); + } + byte[] encryptedData = output.toByteArray(); + + ByteArrayInputStream input = new ByteArrayInputStream(encryptedData); + try (InputStream decryptor = RydeEncryption.openDecryptor(input, false, key1.getPrivateKey())) { + assertThat(ByteStreams.toByteArray(decryptor)).isEqualTo(expected); + } + + input.reset(); + try (InputStream decryptor = RydeEncryption.openDecryptor(input, false, key2.getPrivateKey())) { + assertThat(ByteStreams.toByteArray(decryptor)).isEqualTo(expected); + } + } + + @Test + public void testSuccess_decryptHasntChanged() throws Exception { + FakeKeyringModule keyringModule = new FakeKeyringModule(); + PGPKeyPair key = keyringModule.get("rde-unittest@registry.test", ENCRYPT); + byte[] expected = "Testing 1, 2, 3".getBytes(UTF_8); + byte[] encryptedData = + Base64.getMimeDecoder() + .decode( + "hQEMA6WcEy81iaHVAQf+I14Ewo1Fr6epwqtUoMSuy3qtobayZI54u/ohyMBgnpfts8B15320x4eO" + + "ElbaMKLJFZzOI8IsJRlX9mpSMp+qALdhOjXfM4q9wHNPKTRXqkhhblyTt7r4MTRp1w8lTA8R5hGO" + + "MCoxYwicK7DYrqL728FCeA2UBaQVXB6FZIIjujwNRzghvyqGDLLF6LxnR8ovB2PqT4Ho0wTmHWNy" + + "CZWyR5y9TBgTZWpIoNFuHQGe8egz/rTR+ixp1Ru3lxib7xuJVQyjbiGMO+lk4ffeEg4KpwEFblMx" + + "s17nxCrT5E30qktKjRQopvGICSrxyMGrbyUu5HdASZDj4jyqgP152KxJ18khC05Kf6zT4ouLoJHB" + + "XENDmLN3Onf6IwR043Lk0KISKi6z"); + + ByteArrayInputStream input = new ByteArrayInputStream(encryptedData); + try (InputStream decryptor = RydeEncryption.openDecryptor(input, false, key.getPrivateKey())) { + assertThat(ByteStreams.toByteArray(decryptor)).isEqualTo(expected); + } + } + + @Test + public void testSuccess_oneReceiver_withIntegrityPacket() throws Exception { + FakeKeyringModule keyringModule = new FakeKeyringModule(); + PGPKeyPair key = keyringModule.get("rde-unittest@registry.test", ENCRYPT); + byte[] expected = "Testing 1, 2, 3".getBytes(UTF_8); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (OutputStream encryptor = + RydeEncryption.openEncryptor(output, true, ImmutableList.of(key.getPublicKey()))) { + encryptor.write(expected); + } + byte[] encryptedData = output.toByteArray(); + + ByteArrayInputStream input = new ByteArrayInputStream(encryptedData); + try (InputStream decryptor = RydeEncryption.openDecryptor(input, true, key.getPrivateKey())) { + assertThat(ByteStreams.toByteArray(decryptor)).isEqualTo(expected); + } + } +}