| 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 |