Skip to content

Define the MultiKeyring #148

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions src/main/java/com/amazonaws/encryptionsdk/keyrings/MultiKeyring.java
Original file line number Diff line number Diff line change
@@ -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<Keyring> childrenKeyrings;

MultiKeyring(Keyring generatorKeyring, List<Keyring> 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<? extends EncryptedDataKey> encryptedDataKeys) {
requireNonNull(decryptionMaterials, "decryptionMaterials are required");
requireNonNull(encryptedDataKeys, "encryptedDataKeys are required");

if (decryptionMaterials.hasPlaintextDataKey()) {
return;
}
Comment on lines +65 to +70
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are all important for every keyring.
The fact that this code is here in the MultiKeyring implies to me that it will need to be in every keyring?
While not the end of the world,
this does mean that if we have people write their own keyring
they will need to remember to include this.


final List<Keyring> keyringsToDecryptWith = new ArrayList<>();

if (generatorKeyring != null) {
keyringsToDecryptWith.add(generatorKeyring);
}

keyringsToDecryptWith.addAll(childrenKeyrings);

final List<Exception> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<Keyring> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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<EncryptedDataKey> encryptedDataKeys;
final List<Keyring> 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);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Enables mocking final types
mock-maker-inline