1 | /** | |
2 | Copyright 2018 Carlos Macasaet | |
3 | ||
4 | Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | you may not use this file except in compliance with the License. | |
6 | You may obtain a copy of the License at | |
7 | ||
8 | https://www.apache.org/licenses/LICENSE-2.0 | |
9 | ||
10 | Unless required by applicable law or agreed to in writing, software | |
11 | distributed under the License is distributed on an "AS IS" BASIS, | |
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | See the License for the specific language governing permissions and | |
14 | limitations under the License. | |
15 | */ | |
16 | package com.macasaet.fernet.aws.secretsmanager.rotation; | |
17 | ||
18 | import static com.macasaet.fernet.aws.secretsmanager.rotation.Stage.CURRENT; | |
19 | import static com.macasaet.fernet.aws.secretsmanager.rotation.Stage.PENDING; | |
20 | ||
21 | import java.io.IOException; | |
22 | import java.io.InputStream; | |
23 | import java.io.OutputStream; | |
24 | import java.nio.Buffer; | |
25 | import java.nio.ByteBuffer; | |
26 | import java.security.SecureRandom; | |
27 | import java.util.Collection; | |
28 | import java.util.List; | |
29 | import java.util.Map; | |
30 | import java.util.Map.Entry; | |
31 | import java.util.concurrent.atomic.AtomicBoolean; | |
32 | ||
33 | import org.apache.logging.log4j.LogManager; | |
34 | import org.apache.logging.log4j.Logger; | |
35 | ||
36 | import com.amazonaws.services.kms.AWSKMS; | |
37 | import com.amazonaws.services.kms.model.GenerateRandomRequest; | |
38 | import com.amazonaws.services.kms.model.GenerateRandomResult; | |
39 | import com.amazonaws.services.lambda.runtime.Context; | |
40 | import com.amazonaws.services.lambda.runtime.RequestStreamHandler; | |
41 | import com.amazonaws.services.secretsmanager.model.DescribeSecretResult; | |
42 | import com.amazonaws.services.secretsmanager.model.ResourceNotFoundException; | |
43 | import com.fasterxml.jackson.databind.ObjectMapper; | |
44 | import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule; | |
45 | ||
46 | /** | |
47 | * This is an AWS Lambda {@link RequestStreamHandler} that rotates a Fernet key. | |
48 | * | |
49 | * <p>Copyright © 2018 Carlos Macasaet.</p> | |
50 | * @author Carlos Macasaet | |
51 | */ | |
52 | @SuppressWarnings({"PMD.LawOfDemeter", "PMD.BeanMembersShouldSerialize"}) | |
53 | abstract class AbstractFernetKeyRotator implements RequestStreamHandler { | |
54 | ||
55 | private final Logger logger = LogManager.getLogger(getClass()); | |
56 | ||
57 | private final ObjectMapper mapper; | |
58 | private final SecretsManager secretsManager; | |
59 | private final AWSKMS kms; | |
60 | private final SecureRandom random; | |
61 | ||
62 | private final AtomicBoolean seeded = new AtomicBoolean(false); | |
63 | ||
64 | protected AbstractFernetKeyRotator(final SecretsManager secretsManager, final AWSKMS kms, | |
65 | final SecureRandom random) { | |
66 | this(new ObjectMapper().registerModule(new JaxbAnnotationModule()), secretsManager, kms, random); | |
67 | } | |
68 | ||
69 | protected AbstractFernetKeyRotator(final ObjectMapper mapper, final SecretsManager secretsManager, final AWSKMS kms, | |
70 | final SecureRandom random) { | |
71 |
1
1. <init> : negated conditional → KILLED |
if (mapper == null) { |
72 | throw new IllegalArgumentException("mapper cannot be null"); | |
73 | } | |
74 |
1
1. <init> : negated conditional → KILLED |
if (secretsManager == null) { |
75 | throw new IllegalArgumentException("secretsManager cannot be null"); | |
76 | } | |
77 |
1
1. <init> : negated conditional → KILLED |
if (kms == null) { |
78 | throw new IllegalArgumentException("kms cannot be null"); | |
79 | } | |
80 |
1
1. <init> : negated conditional → KILLED |
if (random == null) { |
81 | throw new IllegalArgumentException("random cannot be null"); | |
82 | } | |
83 | this.mapper = mapper; | |
84 | this.secretsManager = secretsManager; | |
85 | this.kms = kms; | |
86 | this.random = random; | |
87 | } | |
88 | ||
89 | public void handleRequest(final InputStream input, final OutputStream output, final Context context) throws IOException { | |
90 | final RotationRequest request = getMapper().readValue(input, RotationRequest.class); | |
91 | getLogger().debug("Processing request: {}", request); | |
92 | ||
93 |
1
1. handleRequest : removed call to com/macasaet/fernet/aws/secretsmanager/rotation/AbstractFernetKeyRotator::handleRotationRequest → KILLED |
handleRotationRequest(request); |
94 | } | |
95 | ||
96 | protected void finalize() throws Throwable { | |
97 |
1
1. finalize : removed call to com/amazonaws/services/kms/AWSKMS::shutdown → NO_COVERAGE |
getKms().shutdown(); |
98 |
1
1. finalize : removed call to com/macasaet/fernet/aws/secretsmanager/rotation/SecretsManager::shutdown → NO_COVERAGE |
getSecretsManager().shutdown(); |
99 | ||
100 |
1
1. finalize : removed call to java/lang/Object::finalize → NO_COVERAGE |
super.finalize(); |
101 | } | |
102 | ||
103 | protected void handleRotationRequest(final RotationRequest request) { | |
104 | final String secretId = request.getSecretId(); | |
105 | final Map<String, List<String>> versions = getAndValidateVersions(secretId); | |
106 | final String clientRequestToken = getAndValidateClientRequestToken(request, secretId, versions); | |
107 | ||
108 | final List<String> stages = versions.get(clientRequestToken); | |
109 |
1
1. handleRotationRequest : negated conditional → KILLED |
if (stages.contains(CURRENT.getAwsName())) { |
110 | getLogger().warn("Secret version {} already set as AWSCURRENT for secret {}. Doing nothing.", | |
111 | clientRequestToken, secretId); | |
112 | return; | |
113 |
1
1. handleRotationRequest : negated conditional → KILLED |
} else if (!stages.contains(PENDING.getAwsName())) { |
114 | throw new IllegalArgumentException("Secret version " + clientRequestToken | |
115 | + " not set as AWSPENDING for rotation of secret " + secretId + "."); | |
116 | } | |
117 | switch (request.getStep()) { | |
118 | case CREATE_SECRET: | |
119 |
1
1. handleRotationRequest : removed call to com/macasaet/fernet/aws/secretsmanager/rotation/AbstractFernetKeyRotator::conditionallyCreateSecret → KILLED |
conditionallyCreateSecret(secretId, clientRequestToken); |
120 | return; | |
121 | case FINISH_SECRET: | |
122 |
1
1. handleRotationRequest : removed call to com/macasaet/fernet/aws/secretsmanager/rotation/AbstractFernetKeyRotator::finishSecret → NO_COVERAGE |
finishSecret(secretId, clientRequestToken, versions); |
123 | return; | |
124 | case SET_SECRET: | |
125 | // not applicable | |
126 | return; | |
127 | case TEST_SECRET: | |
128 |
1
1. handleRotationRequest : removed call to com/macasaet/fernet/aws/secretsmanager/rotation/AbstractFernetKeyRotator::testSecret → KILLED |
testSecret(secretId, clientRequestToken); |
129 | return; | |
130 | default: | |
131 | throw new IllegalArgumentException("Missing or invalid step provided"); | |
132 | } | |
133 | } | |
134 | ||
135 | protected Map<String, List<String>> getAndValidateVersions(final String secretId) { | |
136 | final DescribeSecretResult secretMetadata = getSecretsManager().describeSecret(secretId); | |
137 |
2
1. getAndValidateVersions : negated conditional → KILLED 2. getAndValidateVersions : negated conditional → KILLED |
if (secretMetadata.isRotationEnabled() == null || !secretMetadata.isRotationEnabled()) { |
138 | throw new IllegalArgumentException("Secret " + secretId + " is not enabled for rotation."); | |
139 | } | |
140 |
1
1. getAndValidateVersions : mutated return of Object value for com/macasaet/fernet/aws/secretsmanager/rotation/AbstractFernetKeyRotator::getAndValidateVersions to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return secretMetadata.getVersionIdsToStages(); |
141 | } | |
142 | ||
143 | protected String getAndValidateClientRequestToken(final RotationRequest request, final String secretId, | |
144 | final Map<String, List<String>> versions) { | |
145 | final String retval = request.getClientRequestToken(); | |
146 |
1
1. getAndValidateClientRequestToken : negated conditional → KILLED |
if (!versions.containsKey(retval)) { |
147 | throw new IllegalArgumentException("Secret version " + retval | |
148 | + " has no stage for rotation of secret " + secretId + "."); | |
149 | } | |
150 |
1
1. getAndValidateClientRequestToken : mutated return of Object value for com/macasaet/fernet/aws/secretsmanager/rotation/AbstractFernetKeyRotator::getAndValidateClientRequestToken to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return retval; |
151 | } | |
152 | ||
153 | protected void conditionallyCreateSecret(final String secretId, final String clientRequestToken) { | |
154 |
1
1. conditionallyCreateSecret : removed call to com/macasaet/fernet/aws/secretsmanager/rotation/SecretsManager::assertCurrentStageExists → SURVIVED |
getSecretsManager().assertCurrentStageExists(secretId); |
155 | try { | |
156 | getSecretsManager().getSecretVersion(secretId, clientRequestToken); | |
157 | getLogger().warn("createSecret: Successfully retrieved secret for {}. Doing nothing.", secretId); | |
158 | } catch (final ResourceNotFoundException rnfe) { | |
159 |
1
1. conditionallyCreateSecret : removed call to com/macasaet/fernet/aws/secretsmanager/rotation/AbstractFernetKeyRotator::createSecret → KILLED |
createSecret(secretId, clientRequestToken); |
160 | } | |
161 | } | |
162 | ||
163 | /** | |
164 | * Create a Fernet key secret and store it in AWS Secrets Manager with the stage "AWSPENDING". If there is already an | |
165 | * "AWSPENDING" secret, then do nothing. | |
166 | * | |
167 | * @param secretId the ARN of the secret. e.g. arn:aws:secretsmanager:{region}:{account}:secret:{secret-name} | |
168 | * @param clientRequestToken a unique identifier for this rotation request | |
169 | */ | |
170 | protected abstract void createSecret(String secretId, String clientRequestToken); | |
171 | ||
172 | /** | |
173 | * Validate the Fernet key secret generated by {@link #createSecret(String, String)}. Throw an exception if there is | |
174 | * a problem with the secret that would make it unusable. | |
175 | * | |
176 | * @param secretId | |
177 | * the ARN of the secret. e.g. arn:aws:secretsmanager:{region}:{account}:secret:{secret-name} | |
178 | * @param clientRequestToken | |
179 | * a unique identifier for this rotation request | |
180 | */ | |
181 | protected abstract void testSecret(String secretId, String clientRequestToken); | |
182 | ||
183 | @SuppressWarnings("PMD.DataflowAnomalyAnalysis") | |
184 | protected void finishSecret(final String secretId, final String clientRequestToken, | |
185 | final Map<String, List<String>> versions) { | |
186 | final Entry<? extends String, ?> currentEntry = versions.entrySet().stream().filter(entry -> { | |
187 | final Collection<? extends String> versionStages = entry.getValue(); | |
188 |
1
1. lambda$finishSecret$0 : replaced return of integer sized value with (x == 0 ? 1 : 0) → KILLED |
return versionStages.contains(CURRENT.getAwsName() ); |
189 |
1
1. lambda$finishSecret$1 : mutated return of Object value for com/macasaet/fernet/aws/secretsmanager/rotation/AbstractFernetKeyRotator::lambda$finishSecret$1 to ( if (x != null) null else throw new RuntimeException ) → SURVIVED |
}).findFirst().orElseThrow(() -> new IllegalStateException("No AWSCURRENT secret set for " + secretId + ".")); |
190 | final String currentVersion = currentEntry.getKey(); | |
191 |
1
1. finishSecret : negated conditional → KILLED |
if (currentVersion.equalsIgnoreCase(clientRequestToken)) { |
192 | // The correct version is already marked as current, return | |
193 | getLogger().warn("finishSecret: Version {} already marked as AWSCURRENT for {}", currentVersion, | |
194 | secretId); | |
195 | return; | |
196 | } | |
197 | ||
198 |
1
1. finishSecret : removed call to com/macasaet/fernet/aws/secretsmanager/rotation/SecretsManager::rotateSecret → KILLED |
getSecretsManager().rotateSecret(secretId, clientRequestToken, currentVersion); |
199 | getLogger().info("finishSecret: Successfully set AWSCURRENT stage to version {} for secret {}.", | |
200 | clientRequestToken, secretId); | |
201 | } | |
202 | ||
203 | /** | |
204 | * This seeds the random number generator using KMS if and only it hasn't already been seeded. | |
205 | * | |
206 | * This requires the permission: <code>kms:GenerateRandom</code> | |
207 | */ | |
208 | protected void seed() { | |
209 |
1
1. seed : negated conditional → KILLED |
if (!seeded.get()) { |
210 | synchronized (random) { | |
211 |
1
1. seed : negated conditional → KILLED |
if (!seeded.get()) { |
212 | getLogger().debug("Seeding random number generator"); | |
213 | final GenerateRandomRequest request = new GenerateRandomRequest(); | |
214 |
1
1. seed : removed call to com/amazonaws/services/kms/model/GenerateRandomRequest::setNumberOfBytes → KILLED |
request.setNumberOfBytes(512); |
215 | final GenerateRandomResult result = getKms().generateRandom(request); | |
216 | final ByteBuffer randomBytes = result.getPlaintext(); | |
217 | final byte[] bytes = new byte[randomBytes.remaining()]; | |
218 | randomBytes.get(bytes); | |
219 | random.setSeed(bytes); | |
220 |
1
1. seed : removed call to java/util/concurrent/atomic/AtomicBoolean::set → SURVIVED |
seeded.set(true); |
221 | getLogger().debug("Seeded random number generator"); | |
222 | } | |
223 | } | |
224 | } | |
225 | } | |
226 | ||
227 | /** | |
228 | * Overwrite the data in the byte array prior to returning memory to the system. | |
229 | * | |
230 | * @param secretBytes secret data that is no longer needed | |
231 | */ | |
232 | protected void wipe(final byte[] secretBytes) { | |
233 | getRandom().nextBytes(secretBytes); | |
234 | } | |
235 | ||
236 | /** | |
237 | * Overwrite the data in the byte buffer prior to returning memory to the system. | |
238 | * | |
239 | * @param secret secret data that is no longer needed | |
240 | */ | |
241 | protected void wipe(final ByteBuffer secret) { | |
242 | ((Buffer)secret).clear(); | |
243 | final byte[] random = new byte[secret.capacity()]; | |
244 | getRandom().nextBytes(random); | |
245 | secret.put(random); | |
246 | } | |
247 | ||
248 | protected SecretsManager getSecretsManager() { | |
249 |
1
1. getSecretsManager : mutated return of Object value for com/macasaet/fernet/aws/secretsmanager/rotation/AbstractFernetKeyRotator::getSecretsManager to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return secretsManager; |
250 | } | |
251 | ||
252 | protected AWSKMS getKms() { | |
253 |
1
1. getKms : mutated return of Object value for com/macasaet/fernet/aws/secretsmanager/rotation/AbstractFernetKeyRotator::getKms to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return kms; |
254 | } | |
255 | ||
256 | protected SecureRandom getRandom() { | |
257 |
1
1. getRandom : removed call to com/macasaet/fernet/aws/secretsmanager/rotation/AbstractFernetKeyRotator::seed → SURVIVED |
seed(); |
258 |
1
1. getRandom : mutated return of Object value for com/macasaet/fernet/aws/secretsmanager/rotation/AbstractFernetKeyRotator::getRandom to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return random; |
259 | } | |
260 | ||
261 | protected Logger getLogger() { | |
262 |
1
1. getLogger : mutated return of Object value for com/macasaet/fernet/aws/secretsmanager/rotation/AbstractFernetKeyRotator::getLogger to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return logger; |
263 | } | |
264 | ||
265 | protected ObjectMapper getMapper() { | |
266 |
1
1. getMapper : mutated return of Object value for com/macasaet/fernet/aws/secretsmanager/rotation/AbstractFernetKeyRotator::getMapper to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return mapper; |
267 | } | |
268 | ||
269 | } | |
Mutations | ||
71 |
1.1 |
|
74 |
1.1 |
|
77 |
1.1 |
|
80 |
1.1 |
|
93 |
1.1 |
|
97 |
1.1 |
|
98 |
1.1 |
|
100 |
1.1 |
|
109 |
1.1 |
|
113 |
1.1 |
|
119 |
1.1 |
|
122 |
1.1 |
|
128 |
1.1 |
|
137 |
1.1 2.2 |
|
140 |
1.1 |
|
146 |
1.1 |
|
150 |
1.1 |
|
154 |
1.1 |
|
159 |
1.1 |
|
188 |
1.1 |
|
189 |
1.1 |
|
191 |
1.1 |
|
198 |
1.1 |
|
209 |
1.1 |
|
211 |
1.1 |
|
214 |
1.1 |
|
220 |
1.1 |
|
249 |
1.1 |
|
253 |
1.1 |
|
257 |
1.1 |
|
258 |
1.1 |
|
262 |
1.1 |
|
266 |
1.1 |