diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/MultiKeyring.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/MultiKeyring.java new file mode 100644 index 000000000..7d957b281 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/MultiKeyring.java @@ -0,0 +1,102 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. 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. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.Validate.isTrue; + +/** + * A keyring which combines other keyrings, allowing one OnEncrypt or OnDecrypt call to + * modify the encryption or decryption materials using more than one keyring. + */ +class MultiKeyring implements Keyring { + + final Keyring generatorKeyring; + final List childrenKeyrings; + + MultiKeyring(Keyring generatorKeyring, List childrenKeyrings) { + this.generatorKeyring = generatorKeyring; + this.childrenKeyrings = childrenKeyrings == null ? emptyList() : unmodifiableList(new ArrayList<>(childrenKeyrings)); + + isTrue(this.generatorKeyring != null || !this.childrenKeyrings.isEmpty(), + "At least a generator keyring or children keyrings must be defined"); + } + + @Override + public void onEncrypt(EncryptionMaterials encryptionMaterials) { + requireNonNull(encryptionMaterials, "encryptionMaterials are required"); + + if (generatorKeyring != null) { + generatorKeyring.onEncrypt(encryptionMaterials); + } + + if (!encryptionMaterials.hasPlaintextDataKey()) { + throw new AwsCryptoException("Either a generator keyring must be supplied that produces a plaintext " + + "data key or a plaintext data key must already be present in the encryption materials."); + } + + for (Keyring keyring : childrenKeyrings) { + keyring.onEncrypt(encryptionMaterials); + } + } + + @Override + public void onDecrypt(DecryptionMaterials decryptionMaterials, List encryptedDataKeys) { + requireNonNull(decryptionMaterials, "decryptionMaterials are required"); + requireNonNull(encryptedDataKeys, "encryptedDataKeys are required"); + + if (decryptionMaterials.hasPlaintextDataKey()) { + return; + } + + final List keyringsToDecryptWith = new ArrayList<>(); + + if (generatorKeyring != null) { + keyringsToDecryptWith.add(generatorKeyring); + } + + keyringsToDecryptWith.addAll(childrenKeyrings); + + final List exceptions = new ArrayList<>(); + + for (Keyring keyring : keyringsToDecryptWith) { + try { + keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + + if (decryptionMaterials.hasPlaintextDataKey()) { + // Decryption succeeded, return immediately + return; + } + } catch (Exception e) { + exceptions.add(e); + } + } + + if (!exceptions.isEmpty()) { + final AwsCryptoException exception = new CannotUnwrapDataKeyException( + "Unable to decrypt data key and one or more child keyrings had an error.", exceptions.get(0)); + exceptions.forEach(exception::addSuppressed); + throw exception; + } + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/keyrings/StandardKeyrings.java b/src/main/java/com/amazonaws/encryptionsdk/keyrings/StandardKeyrings.java index d36dae07e..34b7e22f4 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/keyrings/StandardKeyrings.java +++ b/src/main/java/com/amazonaws/encryptionsdk/keyrings/StandardKeyrings.java @@ -16,6 +16,8 @@ import javax.crypto.SecretKey; import java.security.PrivateKey; import java.security.PublicKey; +import java.util.Arrays; +import java.util.List; /** * Factory methods for instantiating the standard {@code Keyring}s provided by the AWS Encryption SDK. @@ -53,4 +55,30 @@ public static Keyring rawAes(String keyNamespace, String keyName, SecretKey wrap public static Keyring rawRsa(String keyNamespace, String keyName, PublicKey publicKey, PrivateKey privateKey, String wrappingAlgorithm) { return new RawRsaKeyring(keyNamespace, keyName, publicKey, privateKey, wrappingAlgorithm); } + + /** + * Constructs a {@code Keyring} which combines other keyrings, allowing one OnEncrypt or OnDecrypt call + * to modify the encryption or decryption materials using more than one keyring. + * + * @param generatorKeyring A keyring that can generate data keys. Required if childrenKeyrings is empty. + * @param childrenKeyrings A list of keyrings to be used to modify the encryption or decryption materials. + * At least one is required if generatorKeyring is null. + * @return The {@link Keyring} + */ + public static Keyring multi(Keyring generatorKeyring, List childrenKeyrings) { + return new MultiKeyring(generatorKeyring, childrenKeyrings); + } + + /** + * Constructs a {@code Keyring} which combines other keyrings, allowing one OnEncrypt or OnDecrypt call + * to modify the encryption or decryption materials using more than one keyring. + * + * @param generatorKeyring A keyring that can generate data keys. Required if childrenKeyrings is empty. + * @param childrenKeyrings Keyrings to be used to modify the encryption or decryption materials. + * At least one is required if generatorKeyring is null. + * @return The {@link Keyring} + */ + public static Keyring multi(Keyring generatorKeyring, Keyring... childrenKeyrings) { + return new MultiKeyring(generatorKeyring, Arrays.asList(childrenKeyrings)); + } } diff --git a/src/test/java/com/amazonaws/encryptionsdk/keyrings/MultiKeyringTest.java b/src/test/java/com/amazonaws/encryptionsdk/keyrings/MultiKeyringTest.java new file mode 100644 index 000000000..75e43a676 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/keyrings/MultiKeyringTest.java @@ -0,0 +1,184 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. 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. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.encryptionsdk.keyrings; + +import com.amazonaws.encryptionsdk.EncryptedDataKey; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class MultiKeyringTest { + + @Mock Keyring generatorKeyring; + @Mock Keyring keyring1; + @Mock Keyring keyring2; + @Mock EncryptionMaterials encryptionMaterials; + @Mock DecryptionMaterials decryptionMaterials; + @Mock List encryptedDataKeys; + final List childrenKeyrings = new ArrayList<>(); + + @BeforeEach + void setup() { + childrenKeyrings.add(keyring1); + childrenKeyrings.add(keyring2); + } + + @Test + void testConstructor() { + assertThrows(IllegalArgumentException.class, () -> new MultiKeyring(null, null)); + assertThrows(IllegalArgumentException.class, () -> new MultiKeyring(null, Collections.emptyList())); + new MultiKeyring(generatorKeyring, null); + new MultiKeyring(null, Collections.singletonList(keyring1)); + } + + @Test + void testOnEncryptWithGenerator() { + MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings); + when(encryptionMaterials.hasPlaintextDataKey()).thenReturn(true); + + keyring.onEncrypt(encryptionMaterials); + + verify(generatorKeyring).onEncrypt(encryptionMaterials); + verify(keyring1).onEncrypt(encryptionMaterials); + verify(keyring2).onEncrypt(encryptionMaterials); + } + + @Test + void testOnEncryptWithoutGenerator() { + MultiKeyring keyring = new MultiKeyring(null, childrenKeyrings); + when(encryptionMaterials.hasPlaintextDataKey()).thenReturn(true); + + keyring.onEncrypt(encryptionMaterials); + + verifyNoInteractions(generatorKeyring); + verify(keyring1).onEncrypt(encryptionMaterials); + verify(keyring2).onEncrypt(encryptionMaterials); + } + + @Test + void testOnEncryptNoPlaintextDataKey() { + MultiKeyring keyring = new MultiKeyring(null, childrenKeyrings); + when(encryptionMaterials.hasPlaintextDataKey()).thenReturn(false); + + assertThrows(AwsCryptoException.class, () -> keyring.onEncrypt(encryptionMaterials)); + } + + @Test + void testOnDecryptWithPlaintextDataKey() { + MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings); + + when(decryptionMaterials.hasPlaintextDataKey()).thenReturn(true); + keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + + verifyNoInteractions(generatorKeyring, keyring1, keyring2); + } + + @Test + void testOnDecryptWithGenerator() { + MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings); + + when(decryptionMaterials.hasPlaintextDataKey()).thenReturn(false).thenReturn(false).thenReturn(true); + keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + + InOrder inOrder = inOrder(generatorKeyring, keyring1); + inOrder.verify(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys); + inOrder.verify(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys); + verifyNoInteractions(keyring2); + } + + @Test + void testOnDecryptWithoutGenerator() { + MultiKeyring keyring = new MultiKeyring(null, childrenKeyrings); + + when(decryptionMaterials.hasPlaintextDataKey()).thenReturn(false).thenReturn(false).thenReturn(true); + keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + + InOrder inOrder = inOrder(keyring1, keyring2); + inOrder.verify(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys); + inOrder.verify(keyring2).onDecrypt(decryptionMaterials, encryptedDataKeys); + verifyNoInteractions(generatorKeyring); + } + + @Test + void testOnDecryptFailureThenSuccess() { + MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings); + + when(decryptionMaterials.hasPlaintextDataKey()).thenReturn(false).thenReturn(true); + doThrow(new IllegalStateException()).when(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys); + + keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + + InOrder inOrder = inOrder(generatorKeyring, keyring1); + inOrder.verify(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys); + inOrder.verify(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys); + verifyNoInteractions(keyring2); + } + + @Test + void testOnDecryptFailure() { + MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings); + + when(decryptionMaterials.hasPlaintextDataKey()).thenReturn(false); + doThrow(new AwsCryptoException()).when(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys); + doThrow(new IllegalStateException()).when(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys); + doThrow(new IllegalArgumentException()).when(keyring2).onDecrypt(decryptionMaterials, encryptedDataKeys); + + AwsCryptoException exception = null; + try { + keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + fail(); + } catch (AwsCryptoException e) { + exception = e; + } + + assertEquals(3, exception.getSuppressed().length); + + InOrder inOrder = inOrder(generatorKeyring, keyring1, keyring2); + inOrder.verify(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys); + inOrder.verify(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys); + inOrder.verify(keyring2).onDecrypt(decryptionMaterials, encryptedDataKeys); + } + + @Test + void testOnDecryptNoFailuresNoPlaintextDataKeys() { + MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings); + + when(decryptionMaterials.hasPlaintextDataKey()).thenReturn(false, false, false, false); + keyring.onDecrypt(decryptionMaterials, encryptedDataKeys); + + InOrder inOrder = inOrder(generatorKeyring, keyring1, keyring2); + inOrder.verify(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys); + inOrder.verify(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys); + inOrder.verify(keyring2).onDecrypt(decryptionMaterials, encryptedDataKeys); + } + +} diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..d41cd3cca --- /dev/null +++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1,2 @@ +# Enables mocking final types +mock-maker-inline