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 | http://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.nio.ByteBuffer; | |
22 | import java.security.SecureRandom; | |
23 | import java.util.ArrayList; | |
24 | import java.util.List; | |
25 | ||
26 | import com.amazonaws.services.kms.AWSKMS; | |
27 | import com.amazonaws.services.kms.AWSKMSClientBuilder; | |
28 | import com.amazonaws.services.secretsmanager.AWSSecretsManagerClientBuilder; | |
29 | import com.macasaet.fernet.Key; | |
30 | ||
31 | /** | |
32 | * <p>This rotator can be used when an array of Fernet keys is stored in AWSCURRENT.</p> | |
33 | * | |
34 | * <p>Grant AWS Secrets Manager permission to execute the Lambda using this:</p> | |
35 | * <pre>aws lambda add-permission --function-name arn:aws:lambda:{region}:{accountId}:function:{functionName} --principal secretsmanager.amazonaws.com --action lambda:InvokeFunction --statement-id SecretsManagerAccess</pre> | |
36 | * | |
37 | * <p>Copyright © 2018 Carlos Macasaet.</p> | |
38 | * @author Carlos Macasaet | |
39 | */ | |
40 | @SuppressWarnings("PMD.LawOfDemeter") | |
41 | public class MultiFernetKeyRotator extends AbstractFernetKeyRotator { | |
42 | ||
43 | private static final int fernetKeySize = 32; | |
44 | private int maxActiveKeys = 3; | |
45 | ||
46 | /** | |
47 | * @param secretsManager a utility for interacting with AWS Secrets Manager | |
48 | * @param kms a KMS client for seeding the random number generator | |
49 | * @param random an entropy source | |
50 | */ | |
51 | protected MultiFernetKeyRotator(final SecretsManager secretsManager, final AWSKMS kms, final SecureRandom random) { | |
52 | super(secretsManager, kms, random); | |
53 | final String maxActiveKeysString = System.getenv("MAX_ACTIVE_KEYS"); | |
54 |
2
1. <init> : negated conditional → NO_COVERAGE 2. <init> : negated conditional → KILLED |
if (maxActiveKeysString != null && !"".equals(maxActiveKeysString)) { |
55 |
1
1. <init> : removed call to com/macasaet/fernet/aws/secretsmanager/rotation/MultiFernetKeyRotator::setMaxActiveKeys → NO_COVERAGE |
setMaxActiveKeys(Integer.parseInt(maxActiveKeysString)); |
56 | } | |
57 | } | |
58 | ||
59 | /** | |
60 | * @param random an entropy source | |
61 | */ | |
62 | protected MultiFernetKeyRotator(final SecureRandom random) { | |
63 | this(new SecretsManager(AWSSecretsManagerClientBuilder.standard() | |
64 | .withRequestHandlers(new MemoryOverwritingRequestHandler(random)).build()), | |
65 | AWSKMSClientBuilder.defaultClient(), random); | |
66 | } | |
67 | ||
68 | public MultiFernetKeyRotator() { | |
69 | this(new SecureRandom()); | |
70 | } | |
71 | ||
72 | @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") | |
73 | protected void createSecret(final String secretId, final String clientRequestToken) { | |
74 | final ByteBuffer currentSecret = getSecretsManager().getSecretStage(secretId, CURRENT); | |
75 | try { | |
76 |
2
1. createSecret : Replaced integer modulus with multiplication → KILLED 2. createSecret : negated conditional → KILLED |
if (currentSecret.remaining() % fernetKeySize != 0) { |
77 | throw new IllegalStateException("There must be a multiple of 32 bytes."); | |
78 | } | |
79 |
1
1. createSecret : Replaced integer division with multiplication → SURVIVED |
final int numKeys = currentSecret.remaining() / fernetKeySize; |
80 |
1
1. createSecret : Replaced integer addition with subtraction → SURVIVED |
List<Key> keys = new ArrayList<>(numKeys + 1); |
81 |
1
1. createSecret : negated conditional → KILLED |
while (currentSecret.hasRemaining()) { |
82 | final byte[] signingKey = new byte[16]; | |
83 | currentSecret.get(signingKey); | |
84 | final byte[] encryptionKey = new byte[16]; | |
85 | currentSecret.get(encryptionKey); | |
86 | final Key key = new Key(signingKey, encryptionKey); | |
87 | keys.add(key); | |
88 |
1
1. createSecret : removed call to com/macasaet/fernet/aws/secretsmanager/rotation/MultiFernetKeyRotator::wipe → SURVIVED |
wipe(signingKey); |
89 |
1
1. createSecret : removed call to com/macasaet/fernet/aws/secretsmanager/rotation/MultiFernetKeyRotator::wipe → SURVIVED |
wipe(encryptionKey); |
90 | } | |
91 | final Key keyToStage = Key.generateKey(getRandom()); | |
92 |
1
1. createSecret : removed call to java/util/List::add → KILLED |
keys.add(0, keyToStage); |
93 |
1
1. createSecret : Replaced integer addition with subtraction → KILLED |
final int desiredSize = getMaxActiveKeys() + 1; // max active keys + one pending |
94 |
2
1. createSecret : changed conditional boundary → SURVIVED 2. createSecret : negated conditional → KILLED |
if (keys.size() > desiredSize) { |
95 | keys = keys.subList(0, desiredSize); | |
96 | } | |
97 | ||
98 |
1
1. createSecret : removed call to com/macasaet/fernet/aws/secretsmanager/rotation/SecretsManager::putSecretValue → KILLED |
getSecretsManager().putSecretValue(secretId, clientRequestToken, keys, PENDING); |
99 | } finally { | |
100 |
1
1. createSecret : removed call to com/macasaet/fernet/aws/secretsmanager/rotation/MultiFernetKeyRotator::wipe → KILLED |
wipe(currentSecret); |
101 | } | |
102 | getLogger().info("createSecret: Successfully put secret for ARN {} and version {}.", secretId, | |
103 | clientRequestToken); | |
104 | } | |
105 | ||
106 | protected void testSecret(final String secretId, final String clientRequestToken) { | |
107 | final ByteBuffer currentSecret = getSecretsManager().getSecretVersion(secretId, | |
108 | clientRequestToken); | |
109 | try { | |
110 |
2
1. testSecret : Replaced integer modulus with multiplication → KILLED 2. testSecret : negated conditional → KILLED |
if (currentSecret.remaining() % fernetKeySize != 0) { |
111 | throw new IllegalStateException("There must be a multiple of " + fernetKeySize + " bytes."); | |
112 | } | |
113 | // first key will become the staged key | |
114 | final byte[] signingKey = new byte[16]; | |
115 | currentSecret.get(signingKey); | |
116 | final byte[] encryptionKey = new byte[16]; | |
117 | currentSecret.get(encryptionKey); | |
118 | new Key(signingKey, encryptionKey); | |
119 |
1
1. testSecret : removed call to com/macasaet/fernet/aws/secretsmanager/rotation/MultiFernetKeyRotator::wipe → SURVIVED |
wipe(signingKey); |
120 |
1
1. testSecret : removed call to com/macasaet/fernet/aws/secretsmanager/rotation/MultiFernetKeyRotator::wipe → SURVIVED |
wipe(encryptionKey); |
121 | } finally { | |
122 |
1
1. testSecret : removed call to com/macasaet/fernet/aws/secretsmanager/rotation/MultiFernetKeyRotator::wipe → KILLED |
wipe(currentSecret); |
123 | } | |
124 | } | |
125 | ||
126 | /** | |
127 | * @return the total number of keys that can be used for decryption. The actual number of keys stored will be this value plus one. | |
128 | */ | |
129 | protected int getMaxActiveKeys() { | |
130 |
1
1. getMaxActiveKeys : replaced return of integer sized value with (x == 0 ? 1 : 0) → KILLED |
return maxActiveKeys; |
131 | } | |
132 | ||
133 | /** | |
134 | * @param maxActiveKeys the total number of keys that can be used for decryption. The actual number of keys stored will be this value plus one. | |
135 | */ | |
136 | @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") | |
137 | protected void setMaxActiveKeys(final int maxActiveKeys) { | |
138 | getLogger().info("Setting the maximum number of active keys to: {}.", maxActiveKeys); | |
139 |
2
1. setMaxActiveKeys : changed conditional boundary → SURVIVED 2. setMaxActiveKeys : negated conditional → KILLED |
if (maxActiveKeys < 1) { |
140 | throw new IllegalArgumentException("The maximum number of active keys must be at least 1."); | |
141 | } | |
142 | this.maxActiveKeys = maxActiveKeys; | |
143 | } | |
144 | ||
145 | } | |
Mutations | ||
54 |
1.1 2.2 |
|
55 |
1.1 |
|
76 |
1.1 2.2 |
|
79 |
1.1 |
|
80 |
1.1 |
|
81 |
1.1 |
|
88 |
1.1 |
|
89 |
1.1 |
|
92 |
1.1 |
|
93 |
1.1 |
|
94 |
1.1 2.2 |
|
98 |
1.1 |
|
100 |
1.1 |
|
110 |
1.1 2.2 |
|
119 |
1.1 |
|
120 |
1.1 |
|
122 |
1.1 |
|
130 |
1.1 |
|
139 |
1.1 2.2 |