Skip to content

Commit 78d1071

Browse files
Return AccessDenied Error Code when failing to decrypt credentials (#396)
1 parent 1f831de commit 78d1071

File tree

4 files changed

+134
-43
lines changed

4 files changed

+134
-43
lines changed

src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import software.amazon.cloudformation.encryption.Cipher;
3939
import software.amazon.cloudformation.encryption.KMSCipher;
4040
import software.amazon.cloudformation.exceptions.BaseHandlerException;
41+
import software.amazon.cloudformation.exceptions.EncryptionException;
4142
import software.amazon.cloudformation.exceptions.FileScrubberException;
4243
import software.amazon.cloudformation.exceptions.TerminalException;
4344
import software.amazon.cloudformation.injection.CloudWatchLogsProvider;
@@ -233,33 +234,41 @@ private ProgressEvent<TargetT, CallbackT> processInvocation(final JSONObject raw
233234

234235
// TODO: Include hook schema validation here after schema is finalized
235236

236-
// initialise dependencies with platform credentials
237-
initialiseRuntime(request.getHookTypeName(), request.getRequestData().getProviderCredentials(),
238-
request.getRequestData().getProviderLogGroupName(), request.getAwsAccountId(),
239-
request.getRequestData().getHookEncryptionKeyArn(), request.getRequestData().getHookEncryptionKeyRole());
237+
try {
238+
// initialise dependencies with platform credentials
239+
initialiseRuntime(request.getHookTypeName(), request.getRequestData().getProviderCredentials(),
240+
request.getRequestData().getProviderLogGroupName(), request.getAwsAccountId(),
241+
request.getRequestData().getHookEncryptionKeyArn(), request.getRequestData().getHookEncryptionKeyRole());
240242

241-
// transform the request object to pass to caller
242-
HookHandlerRequest hookHandlerRequest = transform(request);
243-
ConfigurationT typeConfiguration = request.getHookModel();
243+
// transform the request object to pass to caller
244+
HookHandlerRequest hookHandlerRequest = transform(request);
245+
ConfigurationT typeConfiguration = request.getHookModel();
244246

245-
HookRequestContext<CallbackT> requestContext = request.getRequestContext();
247+
HookRequestContext<CallbackT> requestContext = request.getRequestContext();
246248

247-
this.metricsPublisherProxy.publishInvocationMetric(Instant.now(), request.getActionInvocationPoint());
249+
this.metricsPublisherProxy.publishInvocationMetric(Instant.now(), request.getActionInvocationPoint());
248250

249-
// last mile proxy creation with passed-in credentials (unless we are operating
250-
// in a non-AWS model)
251-
AmazonWebServicesClientProxy awsClientProxy = null;
252-
Credentials processedCallerCredentials = processCredentials(request.getRequestData().getCallerCredentials());
253-
if (processedCallerCredentials != null) {
254-
awsClientProxy = new AmazonWebServicesClientProxy(this.loggerProxy, processedCallerCredentials,
255-
DelayFactory.CONSTANT_DEFAULT_DELAY_FACTORY,
256-
WaitStrategy.scheduleForCallbackStrategy());
251+
// last mile proxy creation with passed-in credentials (unless we are operating
252+
// in a non-AWS model)
253+
AmazonWebServicesClientProxy awsClientProxy = null;
254+
Credentials processedCallerCredentials = processCredentials(request.getRequestData().getCallerCredentials());
255+
if (processedCallerCredentials != null) {
256+
awsClientProxy = new AmazonWebServicesClientProxy(this.loggerProxy, processedCallerCredentials,
257+
DelayFactory.CONSTANT_DEFAULT_DELAY_FACTORY,
258+
WaitStrategy.scheduleForCallbackStrategy());
257259

258-
}
260+
}
259261

260-
CallbackT callbackContext = (requestContext != null) ? requestContext.getCallbackContext() : null;
262+
CallbackT callbackContext = (requestContext != null) ? requestContext.getCallbackContext() : null;
261263

262-
return wrapInvocationAndHandleErrors(awsClientProxy, hookHandlerRequest, request, callbackContext, typeConfiguration);
264+
return wrapInvocationAndHandleErrors(awsClientProxy, hookHandlerRequest, request, callbackContext, typeConfiguration);
265+
266+
} catch (EncryptionException e) {
267+
publishExceptionMetric(request.getActionInvocationPoint(), e, HandlerErrorCode.AccessDenied);
268+
logUnhandledError("An encryption error occurred while processing request", request, e);
269+
270+
return ProgressEvent.defaultFailureHandler(e, HandlerErrorCode.AccessDenied);
271+
}
263272
}
264273

265274
private void logUnhandledError(final String errorDescription,

src/main/java/software/amazon/cloudformation/encryption/KMSCipher.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,13 @@ public Credentials decryptCredentials(final String encryptedCredentials) {
8787
try {
8888
final CryptoResult<byte[],
8989
KmsMasterKey> result = cryptoHelper.decryptData(kmsKeyProvider, Base64.decode(encryptedCredentials));
90-
return serializer.deserialize(new String(result.getResult(), StandardCharsets.UTF_8), this.credentialsTypeReference);
90+
final Credentials credentials = serializer.deserialize(new String(result.getResult(), StandardCharsets.UTF_8),
91+
this.credentialsTypeReference);
92+
if (credentials == null) {
93+
throw new EncryptionException("Failed to decrypt credentials. Decrypted credentials are 'null'.");
94+
}
95+
96+
return credentials;
9197
} catch (final IOException | AwsCryptoException e) {
9298
throw new EncryptionException("Failed to decrypt credentials.", e);
9399
}

src/test/java/software/amazon/cloudformation/HookWrapperTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import software.amazon.awssdk.http.HttpStatusCode;
4949
import software.amazon.awssdk.http.SdkHttpClient;
5050
import software.amazon.cloudformation.encryption.KMSCipher;
51+
import software.amazon.cloudformation.exceptions.EncryptionException;
5152
import software.amazon.cloudformation.exceptions.ResourceNotFoundException;
5253
import software.amazon.cloudformation.exceptions.TerminalException;
5354
import software.amazon.cloudformation.injection.CredentialsProvider;
@@ -743,6 +744,34 @@ public void invokeHandler_throwsHookNotFoundException_returnsNotFound() throws I
743744
}
744745
}
745746

747+
@Test
748+
public void invokeHandler_throwsEncryptionException_returnsAccessDenied() throws IOException {
749+
wrapper.setTransformResponse(hookHandlerRequest);
750+
751+
lenient().when(cipher.decryptCredentials(any())).thenThrow(new EncryptionException("Failed to decrypt credentials."));
752+
753+
try (final InputStream in = loadRequestStream("preCreate.request.json");
754+
final OutputStream out = new ByteArrayOutputStream()) {
755+
756+
wrapper.processRequest(in, out);
757+
758+
// all metrics should be published, once for a single invocation
759+
verify(providerMetricsPublisher, times(0)).publishInvocationMetric(any(Instant.class),
760+
eq(HookInvocationPoint.CREATE_PRE_PROVISION));
761+
verify(providerMetricsPublisher, times(0)).publishDurationMetric(any(Instant.class),
762+
eq(HookInvocationPoint.CREATE_PRE_PROVISION), anyLong());
763+
764+
// failure metric should be published
765+
verify(providerMetricsPublisher, times(0)).publishExceptionMetric(any(Instant.class), any(HookInvocationPoint.class),
766+
any(ResourceNotFoundException.class), any(HandlerErrorCode.class));
767+
768+
// verify output response
769+
verifyHandlerResponse(out,
770+
HookProgressEvent.<TestContext>builder().clientRequestToken("123456").errorCode(HandlerErrorCode.AccessDenied)
771+
.hookStatus(HookStatus.FAILED).message("Failed to decrypt credentials.").build());
772+
}
773+
}
774+
746775
@Test
747776
public void invokeHandler_metricPublisherThrowable_returnsFailureResponse() throws IOException {
748777
// simulate runtime Errors in the metrics publisher (such as dependency

src/test/java/software/amazon/cloudformation/encryption/KMSCipherTest.java

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,20 @@
1515
package software.amazon.cloudformation.encryption;
1616

1717
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1819
import static org.mockito.ArgumentMatchers.any;
1920
import static org.mockito.Mockito.lenient;
2021
import com.amazonaws.encryptionsdk.AwsCrypto;
2122
import com.amazonaws.encryptionsdk.CryptoResult;
23+
import com.amazonaws.encryptionsdk.exception.AwsCryptoException;
2224
import com.amazonaws.encryptionsdk.kms.KmsMasterKey;
2325
import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider;
26+
import java.io.IOException;
2427
import org.junit.jupiter.api.Test;
2528
import org.junit.jupiter.api.extension.ExtendWith;
2629
import org.mockito.Mock;
2730
import org.mockito.junit.jupiter.MockitoExtension;
31+
import software.amazon.cloudformation.exceptions.EncryptionException;
2832
import software.amazon.cloudformation.proxy.Credentials;
2933

3034
@ExtendWith(MockitoExtension.class)
@@ -53,33 +57,76 @@ public void constructKMSCipher_constructSuccess() {
5357
public void decryptCredentials_decryptSuccess() {
5458
cipher = new KMSCipher(cryptoHelper, kmsKeyProvider);
5559
lenient().when(cryptoHelper.decryptData(any(KmsMasterKeyProvider.class), any(byte[].class))).thenReturn(result);
56-
lenient().when(result.getResult()).thenReturn("{\"test\":\"test\"}".getBytes());
57-
58-
try {
59-
Credentials decryptedCredentials = cipher
60-
.decryptCredentials("ewogICAgICAgICAgICAiYWNjZXNzS2V5SWQiOiAiSUFTQVlLODM1R0FJRkhBSEVJMjMiLAogICAg\n"
61-
+ "ICAgICAgICAic2VjcmV0QWNjZXNzS2V5IjogIjY2aU9HUE41TG5wWm9yY0xyOEtoMjV1OEFiakhW\n"
62-
+ "bGx2NS9wb2gyTzAiLAogICAgICAgICAgICAic2Vzc2lvblRva2VuIjogImxhbWVIUzJ2UU9rblNI\n"
63-
+ "V2hkRllUeG0yZUpjMUpNbjlZQk5JNG5WNG1YdWU5NDVLUEw2REhmVzhFc1VRVDV6d3NzWUVDMU52\n"
64-
+ "WVA5eUQ2WTVzNWxLUjNjaGZsT0hQRnNJZTZlcWciCiAgICAgICAgfQ==");
65-
assertThat(decryptedCredentials).isNotNull();
66-
} catch (final Exception ex) {
67-
}
60+
lenient().when(result.getResult()).thenReturn(
61+
"{\"accessKeyId\":\"testAccessKeyId\", \"secretAccessKey\": \"testSecretAccessKey\", \"sessionToken\": \"testToken\"}"
62+
.getBytes());
6863

64+
Credentials decryptedCredentials = cipher
65+
.decryptCredentials("ewogICAgICAgICAgICAiYWNjZXNzS2V5SWQiOiAiSUFTQVlLODM1R0FJRkhBSEVJMjMiLAogICAg\n"
66+
+ "ICAgICAgICAic2VjcmV0QWNjZXNzS2V5IjogIjY2aU9HUE41TG5wWm9yY0xyOEtoMjV1OEFiakhW\n"
67+
+ "bGx2NS9wb2gyTzAiLAogICAgICAgICAgICAic2Vzc2lvblRva2VuIjogImxhbWVIUzJ2UU9rblNI\n"
68+
+ "V2hkRllUeG0yZUpjMUpNbjlZQk5JNG5WNG1YdWU5NDVLUEw2REhmVzhFc1VRVDV6d3NzWUVDMU52\n"
69+
+ "WVA5eUQ2WTVzNWxLUjNjaGZsT0hQRnNJZTZlcWciCiAgICAgICAgfQ==");
70+
assertThat(decryptedCredentials).isNotNull();
71+
assertThat(decryptedCredentials.getAccessKeyId()).isEqualTo("testAccessKeyId");
72+
assertThat(decryptedCredentials.getSecretAccessKey()).isEqualTo("testSecretAccessKey");
73+
assertThat(decryptedCredentials.getSessionToken()).isEqualTo("testToken");
6974
}
7075

7176
@Test
7277
public void decryptCredentials_decryptFailure() {
7378
cipher = new KMSCipher("encryptionKeyArn", "encryptionKeyRole");
74-
try {
75-
Credentials decryptedCredentials = cipher
76-
.decryptCredentials("ewogICAgICAgICAgICAiYWNjZXNzS2V5SWQiOiAiSUFTQVlLODM1R0FJRkhBSEVJMjMiLAogICAg\n"
77-
+ "ICAgICAgICAic2VjcmV0QWNjZXNzS2V5IjogIjY2aU9HUE41TG5wWm9yY0xyOEtoMjV1OEFiakhW\n"
78-
+ "bGx2NS9wb2gyTzAiLAogICAgICAgICAgICAic2Vzc2lvblRva2VuIjogImxhbWVIUzJ2UU9rblNI\n"
79-
+ "V2hkRllUeG0yZUpjMUpNbjlZQk5JNG5WNG1YdWU5NDVLUEw2REhmVzhFc1VRVDV6d3NzWUVDMU52\n"
80-
+ "WVA5eUQ2WTVzNWxLUjNjaGZsT0hQRnNJZTZlcWciCiAgICAgICAgfQ==");
81-
assertThat(decryptedCredentials).isNotNull();
82-
} catch (final Exception ex) {
83-
}
79+
assertThatThrownBy(
80+
() -> cipher.decryptCredentials("ewogICAgICAgICAgICAiYWNjZXNzS2V5SWQiOiAiSUFTQVlLODM1R0FJRkhBSEVJMjMiLAogICAg\n"
81+
+ "ICAgICAgICAic2VjcmV0QWNjZXNzS2V5IjogIjY2aU9HUE41TG5wWm9yY0xyOEtoMjV1OEFiakhW\n"
82+
+ "bGx2NS9wb2gyTzAiLAogICAgICAgICAgICAic2Vzc2lvblRva2VuIjogImxhbWVIUzJ2UU9rblNI\n"
83+
+ "V2hkRllUeG0yZUpjMUpNbjlZQk5JNG5WNG1YdWU5NDVLUEw2REhmVzhFc1VRVDV6d3NzWUVDMU52\n"
84+
+ "WVA5eUQ2WTVzNWxLUjNjaGZsT0hQRnNJZTZlcWciCiAgICAgICAgfQ==")).isInstanceOf(EncryptionException.class)
85+
.hasMessageContaining("Failed to decrypt credentials");
86+
}
87+
88+
@Test
89+
public void decryptCredentials_returnsNullCredentials_decryptFailure() {
90+
cipher = new KMSCipher(cryptoHelper, kmsKeyProvider);
91+
lenient().when(cryptoHelper.decryptData(any(KmsMasterKeyProvider.class), any(byte[].class))).thenReturn(result);
92+
lenient().when(result.getResult()).thenReturn("null".getBytes());
93+
94+
assertThatThrownBy(
95+
() -> cipher.decryptCredentials("ewogICAgICAgICAgICAiYWNjZXNzS2V5SWQiOiAiSUFTQVlLODM1R0FJRkhBSEVJMjMiLAogICAg\n"
96+
+ "ICAgICAgICAic2VjcmV0QWNjZXNzS2V5IjogIjY2aU9HUE41TG5wWm9yY0xyOEtoMjV1OEFiakhW\n"
97+
+ "bGx2NS9wb2gyTzAiLAogICAgICAgICAgICAic2Vzc2lvblRva2VuIjogImxhbWVIUzJ2UU9rblNI\n"
98+
+ "V2hkRllUeG0yZUpjMUpNbjlZQk5JNG5WNG1YdWU5NDVLUEw2REhmVzhFc1VRVDV6d3NzWUVDMU52\n"
99+
+ "WVA5eUQ2WTVzNWxLUjNjaGZsT0hQRnNJZTZlcWciCiAgICAgICAgfQ==")).isInstanceOf(EncryptionException.class)
100+
.hasMessageContaining("Failed to decrypt credentials. Decrypted credentials are 'null'");
101+
}
102+
103+
@Test
104+
public void decryptCredentials_encryptionSDKError_decryptFailure() {
105+
cipher = new KMSCipher(cryptoHelper, kmsKeyProvider);
106+
lenient().when(cryptoHelper.decryptData(any(KmsMasterKeyProvider.class), any(byte[].class)))
107+
.thenThrow(new AwsCryptoException());
108+
109+
assertThatThrownBy(
110+
() -> cipher.decryptCredentials("ewogICAgICAgICAgICAiYWNjZXNzS2V5SWQiOiAiSUFTQVlLODM1R0FJRkhBSEVJMjMiLAogICAg\n"
111+
+ "ICAgICAgICAic2VjcmV0QWNjZXNzS2V5IjogIjY2aU9HUE41TG5wWm9yY0xyOEtoMjV1OEFiakhW\n"
112+
+ "bGx2NS9wb2gyTzAiLAogICAgICAgICAgICAic2Vzc2lvblRva2VuIjogImxhbWVIUzJ2UU9rblNI\n"
113+
+ "V2hkRllUeG0yZUpjMUpNbjlZQk5JNG5WNG1YdWU5NDVLUEw2REhmVzhFc1VRVDV6d3NzWUVDMU52\n"
114+
+ "WVA5eUQ2WTVzNWxLUjNjaGZsT0hQRnNJZTZlcWciCiAgICAgICAgfQ==")).isInstanceOf(EncryptionException.class)
115+
.hasCauseInstanceOf(AwsCryptoException.class).hasMessageContaining("Failed to decrypt credentials");
116+
}
117+
118+
@Test
119+
public void decryptCredentials_deserializationError_decryptFailure() {
120+
cipher = new KMSCipher(cryptoHelper, kmsKeyProvider);
121+
lenient().when(cryptoHelper.decryptData(any(KmsMasterKeyProvider.class), any(byte[].class))).thenReturn(result);
122+
lenient().when(result.getResult()).thenReturn("{test: test\"".getBytes());
123+
124+
assertThatThrownBy(
125+
() -> cipher.decryptCredentials("ewogICAgICAgICAgICAiYWNjZXNzS2V5SWQiOiAiSUFTQVlLODM1R0FJRkhBSEVJMjMiLAogICAg\n"
126+
+ "ICAgICAgICAic2VjcmV0QWNjZXNzS2V5IjogIjY2aU9HUE41TG5wWm9yY0xyOEtoMjV1OEFiakhW\n"
127+
+ "bGx2NS9wb2gyTzAiLAogICAgICAgICAgICAic2Vzc2lvblRva2VuIjogImxhbWVIUzJ2UU9rblNI\n"
128+
+ "V2hkRllUeG0yZUpjMUpNbjlZQk5JNG5WNG1YdWU5NDVLUEw2REhmVzhFc1VRVDV6d3NzWUVDMU52\n"
129+
+ "WVA5eUQ2WTVzNWxLUjNjaGZsT0hQRnNJZTZlcWciCiAgICAgICAgfQ==")).isInstanceOf(EncryptionException.class)
130+
.hasCauseInstanceOf(IOException.class).hasMessageContaining("Failed to decrypt credentials");
84131
}
85132
}

0 commit comments

Comments
 (0)