Skip to content

Commit b75027d

Browse files
Define the MultiKeyring (#148)
* Define the MultiKeyring * Making defensive copy of child keyrings and adding convienance factory method
1 parent 36958b8 commit b75027d

File tree

4 files changed

+316
-0
lines changed

4 files changed

+316
-0
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except
5+
* in compliance with the License. A copy of the License is located at
6+
*
7+
* http://aws.amazon.com/apache2.0
8+
*
9+
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
14+
package com.amazonaws.encryptionsdk.keyrings;
15+
16+
import com.amazonaws.encryptionsdk.EncryptedDataKey;
17+
import com.amazonaws.encryptionsdk.exception.AwsCryptoException;
18+
import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException;
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
23+
import static java.util.Collections.emptyList;
24+
import static java.util.Collections.unmodifiableList;
25+
import static java.util.Objects.requireNonNull;
26+
import static org.apache.commons.lang3.Validate.isTrue;
27+
28+
/**
29+
* A keyring which combines other keyrings, allowing one OnEncrypt or OnDecrypt call to
30+
* modify the encryption or decryption materials using more than one keyring.
31+
*/
32+
class MultiKeyring implements Keyring {
33+
34+
final Keyring generatorKeyring;
35+
final List<Keyring> childrenKeyrings;
36+
37+
MultiKeyring(Keyring generatorKeyring, List<Keyring> childrenKeyrings) {
38+
this.generatorKeyring = generatorKeyring;
39+
this.childrenKeyrings = childrenKeyrings == null ? emptyList() : unmodifiableList(new ArrayList<>(childrenKeyrings));
40+
41+
isTrue(this.generatorKeyring != null || !this.childrenKeyrings.isEmpty(),
42+
"At least a generator keyring or children keyrings must be defined");
43+
}
44+
45+
@Override
46+
public void onEncrypt(EncryptionMaterials encryptionMaterials) {
47+
requireNonNull(encryptionMaterials, "encryptionMaterials are required");
48+
49+
if (generatorKeyring != null) {
50+
generatorKeyring.onEncrypt(encryptionMaterials);
51+
}
52+
53+
if (!encryptionMaterials.hasPlaintextDataKey()) {
54+
throw new AwsCryptoException("Either a generator keyring must be supplied that produces a plaintext " +
55+
"data key or a plaintext data key must already be present in the encryption materials.");
56+
}
57+
58+
for (Keyring keyring : childrenKeyrings) {
59+
keyring.onEncrypt(encryptionMaterials);
60+
}
61+
}
62+
63+
@Override
64+
public void onDecrypt(DecryptionMaterials decryptionMaterials, List<? extends EncryptedDataKey> encryptedDataKeys) {
65+
requireNonNull(decryptionMaterials, "decryptionMaterials are required");
66+
requireNonNull(encryptedDataKeys, "encryptedDataKeys are required");
67+
68+
if (decryptionMaterials.hasPlaintextDataKey()) {
69+
return;
70+
}
71+
72+
final List<Keyring> keyringsToDecryptWith = new ArrayList<>();
73+
74+
if (generatorKeyring != null) {
75+
keyringsToDecryptWith.add(generatorKeyring);
76+
}
77+
78+
keyringsToDecryptWith.addAll(childrenKeyrings);
79+
80+
final List<Exception> exceptions = new ArrayList<>();
81+
82+
for (Keyring keyring : keyringsToDecryptWith) {
83+
try {
84+
keyring.onDecrypt(decryptionMaterials, encryptedDataKeys);
85+
86+
if (decryptionMaterials.hasPlaintextDataKey()) {
87+
// Decryption succeeded, return immediately
88+
return;
89+
}
90+
} catch (Exception e) {
91+
exceptions.add(e);
92+
}
93+
}
94+
95+
if (!exceptions.isEmpty()) {
96+
final AwsCryptoException exception = new CannotUnwrapDataKeyException(
97+
"Unable to decrypt data key and one or more child keyrings had an error.", exceptions.get(0));
98+
exceptions.forEach(exception::addSuppressed);
99+
throw exception;
100+
}
101+
}
102+
}

src/main/java/com/amazonaws/encryptionsdk/keyrings/StandardKeyrings.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import javax.crypto.SecretKey;
1717
import java.security.PrivateKey;
1818
import java.security.PublicKey;
19+
import java.util.Arrays;
20+
import java.util.List;
1921

2022
/**
2123
* 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
5355
public static Keyring rawRsa(String keyNamespace, String keyName, PublicKey publicKey, PrivateKey privateKey, String wrappingAlgorithm) {
5456
return new RawRsaKeyring(keyNamespace, keyName, publicKey, privateKey, wrappingAlgorithm);
5557
}
58+
59+
/**
60+
* Constructs a {@code Keyring} which combines other keyrings, allowing one OnEncrypt or OnDecrypt call
61+
* to modify the encryption or decryption materials using more than one keyring.
62+
*
63+
* @param generatorKeyring A keyring that can generate data keys. Required if childrenKeyrings is empty.
64+
* @param childrenKeyrings A list of keyrings to be used to modify the encryption or decryption materials.
65+
* At least one is required if generatorKeyring is null.
66+
* @return The {@link Keyring}
67+
*/
68+
public static Keyring multi(Keyring generatorKeyring, List<Keyring> childrenKeyrings) {
69+
return new MultiKeyring(generatorKeyring, childrenKeyrings);
70+
}
71+
72+
/**
73+
* Constructs a {@code Keyring} which combines other keyrings, allowing one OnEncrypt or OnDecrypt call
74+
* to modify the encryption or decryption materials using more than one keyring.
75+
*
76+
* @param generatorKeyring A keyring that can generate data keys. Required if childrenKeyrings is empty.
77+
* @param childrenKeyrings Keyrings to be used to modify the encryption or decryption materials.
78+
* At least one is required if generatorKeyring is null.
79+
* @return The {@link Keyring}
80+
*/
81+
public static Keyring multi(Keyring generatorKeyring, Keyring... childrenKeyrings) {
82+
return new MultiKeyring(generatorKeyring, Arrays.asList(childrenKeyrings));
83+
}
5684
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except
5+
* in compliance with the License. A copy of the License is located at
6+
*
7+
* http://aws.amazon.com/apache2.0
8+
*
9+
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
14+
package com.amazonaws.encryptionsdk.keyrings;
15+
16+
import com.amazonaws.encryptionsdk.EncryptedDataKey;
17+
import com.amazonaws.encryptionsdk.exception.AwsCryptoException;
18+
import org.junit.jupiter.api.BeforeEach;
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.extension.ExtendWith;
21+
import org.mockito.InOrder;
22+
import org.mockito.Mock;
23+
import org.mockito.junit.jupiter.MockitoExtension;
24+
25+
import java.util.ArrayList;
26+
import java.util.Collections;
27+
import java.util.List;
28+
29+
import static org.junit.jupiter.api.Assertions.assertEquals;
30+
import static org.junit.jupiter.api.Assertions.assertThrows;
31+
import static org.junit.jupiter.api.Assertions.fail;
32+
import static org.mockito.Mockito.doThrow;
33+
import static org.mockito.Mockito.inOrder;
34+
import static org.mockito.Mockito.verify;
35+
import static org.mockito.Mockito.verifyNoInteractions;
36+
import static org.mockito.Mockito.when;
37+
38+
@ExtendWith(MockitoExtension.class)
39+
class MultiKeyringTest {
40+
41+
@Mock Keyring generatorKeyring;
42+
@Mock Keyring keyring1;
43+
@Mock Keyring keyring2;
44+
@Mock EncryptionMaterials encryptionMaterials;
45+
@Mock DecryptionMaterials decryptionMaterials;
46+
@Mock List<EncryptedDataKey> encryptedDataKeys;
47+
final List<Keyring> childrenKeyrings = new ArrayList<>();
48+
49+
@BeforeEach
50+
void setup() {
51+
childrenKeyrings.add(keyring1);
52+
childrenKeyrings.add(keyring2);
53+
}
54+
55+
@Test
56+
void testConstructor() {
57+
assertThrows(IllegalArgumentException.class, () -> new MultiKeyring(null, null));
58+
assertThrows(IllegalArgumentException.class, () -> new MultiKeyring(null, Collections.emptyList()));
59+
new MultiKeyring(generatorKeyring, null);
60+
new MultiKeyring(null, Collections.singletonList(keyring1));
61+
}
62+
63+
@Test
64+
void testOnEncryptWithGenerator() {
65+
MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings);
66+
when(encryptionMaterials.hasPlaintextDataKey()).thenReturn(true);
67+
68+
keyring.onEncrypt(encryptionMaterials);
69+
70+
verify(generatorKeyring).onEncrypt(encryptionMaterials);
71+
verify(keyring1).onEncrypt(encryptionMaterials);
72+
verify(keyring2).onEncrypt(encryptionMaterials);
73+
}
74+
75+
@Test
76+
void testOnEncryptWithoutGenerator() {
77+
MultiKeyring keyring = new MultiKeyring(null, childrenKeyrings);
78+
when(encryptionMaterials.hasPlaintextDataKey()).thenReturn(true);
79+
80+
keyring.onEncrypt(encryptionMaterials);
81+
82+
verifyNoInteractions(generatorKeyring);
83+
verify(keyring1).onEncrypt(encryptionMaterials);
84+
verify(keyring2).onEncrypt(encryptionMaterials);
85+
}
86+
87+
@Test
88+
void testOnEncryptNoPlaintextDataKey() {
89+
MultiKeyring keyring = new MultiKeyring(null, childrenKeyrings);
90+
when(encryptionMaterials.hasPlaintextDataKey()).thenReturn(false);
91+
92+
assertThrows(AwsCryptoException.class, () -> keyring.onEncrypt(encryptionMaterials));
93+
}
94+
95+
@Test
96+
void testOnDecryptWithPlaintextDataKey() {
97+
MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings);
98+
99+
when(decryptionMaterials.hasPlaintextDataKey()).thenReturn(true);
100+
keyring.onDecrypt(decryptionMaterials, encryptedDataKeys);
101+
102+
verifyNoInteractions(generatorKeyring, keyring1, keyring2);
103+
}
104+
105+
@Test
106+
void testOnDecryptWithGenerator() {
107+
MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings);
108+
109+
when(decryptionMaterials.hasPlaintextDataKey()).thenReturn(false).thenReturn(false).thenReturn(true);
110+
keyring.onDecrypt(decryptionMaterials, encryptedDataKeys);
111+
112+
InOrder inOrder = inOrder(generatorKeyring, keyring1);
113+
inOrder.verify(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys);
114+
inOrder.verify(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys);
115+
verifyNoInteractions(keyring2);
116+
}
117+
118+
@Test
119+
void testOnDecryptWithoutGenerator() {
120+
MultiKeyring keyring = new MultiKeyring(null, childrenKeyrings);
121+
122+
when(decryptionMaterials.hasPlaintextDataKey()).thenReturn(false).thenReturn(false).thenReturn(true);
123+
keyring.onDecrypt(decryptionMaterials, encryptedDataKeys);
124+
125+
InOrder inOrder = inOrder(keyring1, keyring2);
126+
inOrder.verify(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys);
127+
inOrder.verify(keyring2).onDecrypt(decryptionMaterials, encryptedDataKeys);
128+
verifyNoInteractions(generatorKeyring);
129+
}
130+
131+
@Test
132+
void testOnDecryptFailureThenSuccess() {
133+
MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings);
134+
135+
when(decryptionMaterials.hasPlaintextDataKey()).thenReturn(false).thenReturn(true);
136+
doThrow(new IllegalStateException()).when(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys);
137+
138+
keyring.onDecrypt(decryptionMaterials, encryptedDataKeys);
139+
140+
InOrder inOrder = inOrder(generatorKeyring, keyring1);
141+
inOrder.verify(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys);
142+
inOrder.verify(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys);
143+
verifyNoInteractions(keyring2);
144+
}
145+
146+
@Test
147+
void testOnDecryptFailure() {
148+
MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings);
149+
150+
when(decryptionMaterials.hasPlaintextDataKey()).thenReturn(false);
151+
doThrow(new AwsCryptoException()).when(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys);
152+
doThrow(new IllegalStateException()).when(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys);
153+
doThrow(new IllegalArgumentException()).when(keyring2).onDecrypt(decryptionMaterials, encryptedDataKeys);
154+
155+
AwsCryptoException exception = null;
156+
try {
157+
keyring.onDecrypt(decryptionMaterials, encryptedDataKeys);
158+
fail();
159+
} catch (AwsCryptoException e) {
160+
exception = e;
161+
}
162+
163+
assertEquals(3, exception.getSuppressed().length);
164+
165+
InOrder inOrder = inOrder(generatorKeyring, keyring1, keyring2);
166+
inOrder.verify(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys);
167+
inOrder.verify(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys);
168+
inOrder.verify(keyring2).onDecrypt(decryptionMaterials, encryptedDataKeys);
169+
}
170+
171+
@Test
172+
void testOnDecryptNoFailuresNoPlaintextDataKeys() {
173+
MultiKeyring keyring = new MultiKeyring(generatorKeyring, childrenKeyrings);
174+
175+
when(decryptionMaterials.hasPlaintextDataKey()).thenReturn(false, false, false, false);
176+
keyring.onDecrypt(decryptionMaterials, encryptedDataKeys);
177+
178+
InOrder inOrder = inOrder(generatorKeyring, keyring1, keyring2);
179+
inOrder.verify(generatorKeyring).onDecrypt(decryptionMaterials, encryptedDataKeys);
180+
inOrder.verify(keyring1).onDecrypt(decryptionMaterials, encryptedDataKeys);
181+
inOrder.verify(keyring2).onDecrypt(decryptionMaterials, encryptedDataKeys);
182+
}
183+
184+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Enables mocking final types
2+
mock-maker-inline

0 commit comments

Comments
 (0)