1 | /** | |
2 | Copyright 2017 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; | |
17 | ||
18 | import static com.macasaet.fernet.Constants.cipherTransformation; | |
19 | import static com.macasaet.fernet.Constants.decoder; | |
20 | import static com.macasaet.fernet.Constants.encoder; | |
21 | import static com.macasaet.fernet.Constants.encryptionAlgorithm; | |
22 | import static com.macasaet.fernet.Constants.encryptionKeyBytes; | |
23 | import static com.macasaet.fernet.Constants.fernetKeyBytes; | |
24 | import static com.macasaet.fernet.Constants.signingAlgorithm; | |
25 | import static com.macasaet.fernet.Constants.signingKeyBytes; | |
26 | import static com.macasaet.fernet.Constants.tokenPrefixBytes; | |
27 | import static java.util.Arrays.copyOf; | |
28 | import static java.util.Arrays.copyOfRange; | |
29 | import static javax.crypto.Cipher.DECRYPT_MODE; | |
30 | import static javax.crypto.Cipher.ENCRYPT_MODE; | |
31 | ||
32 | import java.io.ByteArrayOutputStream; | |
33 | import java.io.DataOutputStream; | |
34 | import java.io.IOException; | |
35 | import java.io.OutputStream; | |
36 | import java.security.InvalidAlgorithmParameterException; | |
37 | import java.security.InvalidKeyException; | |
38 | import java.security.MessageDigest; | |
39 | import java.security.NoSuchAlgorithmException; | |
40 | import java.security.SecureRandom; | |
41 | import java.time.Instant; | |
42 | import java.util.Arrays; | |
43 | import java.util.Base64.Encoder; | |
44 | ||
45 | import javax.crypto.BadPaddingException; | |
46 | import javax.crypto.Cipher; | |
47 | import javax.crypto.IllegalBlockSizeException; | |
48 | import javax.crypto.Mac; | |
49 | import javax.crypto.NoSuchPaddingException; | |
50 | import javax.crypto.spec.IvParameterSpec; | |
51 | import javax.crypto.spec.SecretKeySpec; | |
52 | ||
53 | /** | |
54 | * A Fernet shared secret key. | |
55 | * | |
56 | * <p>Copyright © 2017 Carlos Macasaet.</p> | |
57 | * | |
58 | * @author Carlos Macasaet | |
59 | */ | |
60 | @SuppressWarnings({"PMD.AvoidDuplicateLiterals", "PMD.TooManyMethods"}) | |
61 | public class Key { | |
62 | ||
63 | private final byte[] signingKey; | |
64 | private final byte[] encryptionKey; | |
65 | ||
66 | /** | |
67 | * Create a Key from individual components. | |
68 | * | |
69 | * @param signingKey | |
70 | * a 128-bit (16 byte) key for signing tokens. | |
71 | * @param encryptionKey | |
72 | * a 128-bit (16 byte) key for encrypting and decrypting token contents. | |
73 | */ | |
74 | public Key(final byte[] signingKey, final byte[] encryptionKey) { | |
75 |
2
1. <init> : negated conditional → KILLED 2. <init> : negated conditional → KILLED |
if (signingKey == null || signingKey.length != signingKeyBytes) { |
76 | throw new IllegalArgumentException("Signing key must be 128 bits"); | |
77 | } | |
78 |
2
1. <init> : negated conditional → KILLED 2. <init> : negated conditional → KILLED |
if (encryptionKey == null || encryptionKey.length != encryptionKeyBytes) { |
79 | throw new IllegalArgumentException("Encryption key must be 128 bits"); | |
80 | } | |
81 | this.signingKey = copyOf(signingKey, signingKeyBytes); | |
82 | this.encryptionKey = copyOf(encryptionKey, encryptionKeyBytes); | |
83 | } | |
84 | ||
85 | /** | |
86 | * Create a Key from a payload containing the signing and encryption | |
87 | * key. | |
88 | * | |
89 | * @param concatenatedKeys an array of 32 bytes of which the first 16 is | |
90 | * the signing key and the last 16 is the | |
91 | * encryption/decryption key | |
92 | */ | |
93 | public Key(final byte[] concatenatedKeys) { | |
94 | this(copyOfRange(concatenatedKeys, 0, signingKeyBytes), | |
95 | copyOfRange(concatenatedKeys, signingKeyBytes, fernetKeyBytes)); | |
96 | } | |
97 | ||
98 | /** | |
99 | * @param string | |
100 | * a Base 64 URL string in the format Signing-key (128 bits) || Encryption-key (128 bits) | |
101 | */ | |
102 | public Key(final String string) { | |
103 | this(decoder.decode(string)); | |
104 | } | |
105 | ||
106 | /** | |
107 | * Generate a random key | |
108 | * | |
109 | * @return a new shared secret key | |
110 | */ | |
111 | public static Key generateKey() { | |
112 | return generateKey(new SecureRandom()); | |
113 | } | |
114 | ||
115 | /** | |
116 | * Generate a random key | |
117 | * | |
118 | * @param random | |
119 | * source of entropy | |
120 | * @return a new shared secret key | |
121 | */ | |
122 | public static Key generateKey(final SecureRandom random) { | |
123 | final byte[] signingKey = new byte[signingKeyBytes]; | |
124 | random.nextBytes(signingKey); | |
125 | final byte[] encryptionKey = new byte[encryptionKeyBytes]; | |
126 | random.nextBytes(encryptionKey); | |
127 |
1
1. generateKey : mutated return of Object value for com/macasaet/fernet/Key::generateKey to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return new Key(signingKey, encryptionKey); |
128 | } | |
129 | ||
130 | /** | |
131 | * Generate an HMAC SHA-256 signature from the components of a Fernet token. | |
132 | * | |
133 | * @param version | |
134 | * the Fernet version number | |
135 | * @param timestamp | |
136 | * the seconds after the epoch that the token was generated | |
137 | * @param initializationVector | |
138 | * the encryption and decryption initialization vector | |
139 | * @param cipherText | |
140 | * the encrypted content of the token | |
141 | * @return the HMAC signature | |
142 | */ | |
143 | public byte[] sign(final byte version, final Instant timestamp, final IvParameterSpec initializationVector, | |
144 | final byte[] cipherText) { | |
145 |
2
1. sign : removed call to java/io/ByteArrayOutputStream::close → NO_COVERAGE 2. sign : removed call to java/lang/Throwable::addSuppressed → NO_COVERAGE |
try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream( |
146 |
1
1. sign : Replaced integer addition with subtraction → KILLED |
getTokenPrefixBytes() + cipherText.length)) { |
147 |
1
1. sign : mutated return of Object value for com/macasaet/fernet/Key::sign to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return sign(version, timestamp, initializationVector, cipherText, byteStream); |
148 |
1
1. sign : removed call to java/io/ByteArrayOutputStream::close → SURVIVED |
} catch (final IOException e) { |
149 | // this should not happen as I/O is to memory only | |
150 | throw new IllegalStateException(e.getMessage(), e); | |
151 | } | |
152 | } | |
153 | ||
154 | /** | |
155 | * Encrypt a payload to embed in a Fernet token | |
156 | * | |
157 | * @param payload the raw bytes of the data to store in a token | |
158 | * @param initializationVector random bytes from a high-entropy source to initialise the AES cipher | |
159 | * @return the AES-encrypted payload. The length will always be a multiple of 16 (128 bits). | |
160 | * @see #decrypt(byte[], IvParameterSpec) | |
161 | */ | |
162 | @SuppressWarnings("PMD.LawOfDemeter") | |
163 | public byte[] encrypt(final byte[] payload, final IvParameterSpec initializationVector) { | |
164 | final SecretKeySpec encryptionKeySpec = getEncryptionKeySpec(); | |
165 | try { | |
166 | final Cipher cipher = Cipher.getInstance(cipherTransformation); | |
167 |
1
1. encrypt : removed call to javax/crypto/Cipher::init → KILLED |
cipher.init(ENCRYPT_MODE, encryptionKeySpec, initializationVector); |
168 |
1
1. encrypt : mutated return of Object value for com/macasaet/fernet/Key::encrypt to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return cipher.doFinal(payload); |
169 | } catch (final NoSuchAlgorithmException | NoSuchPaddingException e) { | |
170 | // these should not happen as we use an algorithm (AES) and padding (PKCS5) that are guaranteed to exist | |
171 | throw new IllegalStateException("Unable to access cipher " + cipherTransformation + ": " + e.getMessage(), e); | |
172 | } catch (final InvalidKeyException | InvalidAlgorithmParameterException e) { | |
173 | // this should not happen as the key is validated ahead of time and | |
174 | // we use an algorithm guaranteed to exist | |
175 | throw new IllegalStateException( | |
176 | "Unable to initialise encryption cipher with algorithm " + encryptionKeySpec.getAlgorithm() | |
177 | + " and format " + encryptionKeySpec.getFormat() + ": " + e.getMessage(), | |
178 | e); | |
179 | } catch (final IllegalBlockSizeException | BadPaddingException e) { | |
180 | // these should not happen as we control the block size and padding | |
181 | throw new IllegalStateException("Unable to encrypt data: " + e.getMessage(), e); | |
182 | } | |
183 | } | |
184 | ||
185 | /** | |
186 | * <p>Decrypt the payload of a Fernet token.</p> | |
187 | * | |
188 | * <p>Warning: Do not call this unless the cipher text has first been verified. Attempting to decrypt a cipher text | |
189 | * that has been tampered with will leak whether or not the padding is correct and this can be used to decrypt | |
190 | * stolen cipher text.</p> | |
191 | * | |
192 | * @param cipherText | |
193 | * the verified padded encrypted payload of a token. The length <em>must</em> be a multiple of 16 (128 | |
194 | * bits). | |
195 | * @param initializationVector | |
196 | * the random bytes used in the AES encryption of the token | |
197 | * @return the decrypted payload | |
198 | * @see Key#encrypt(byte[], IvParameterSpec) | |
199 | */ | |
200 | @SuppressWarnings("PMD.LawOfDemeter") | |
201 | protected byte[] decrypt(final byte[] cipherText, final IvParameterSpec initializationVector) { | |
202 | try { | |
203 | final Cipher cipher = Cipher.getInstance(getCipherTransformation()); | |
204 |
1
1. decrypt : removed call to javax/crypto/Cipher::init → KILLED |
cipher.init(DECRYPT_MODE, getEncryptionKeySpec(), initializationVector); |
205 |
1
1. decrypt : mutated return of Object value for com/macasaet/fernet/Key::decrypt to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return cipher.doFinal(cipherText); |
206 | } catch (final NoSuchAlgorithmException | NoSuchPaddingException | |
207 | | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) { | |
208 | // this should not happen as we use an algorithm (AES) and padding | |
209 | // (PKCS5) that are guaranteed to exist. | |
210 | // in addition, we validate the encryption key and initialization vector up front | |
211 | throw new IllegalStateException(e.getMessage(), e); | |
212 | } catch (final BadPaddingException bpe) { | |
213 | throw new TokenValidationException("Invalid padding in token: " + bpe.getMessage(), bpe); | |
214 | } | |
215 | } | |
216 | ||
217 | /** | |
218 | * @return the Base 64 URL representation of this Fernet key | |
219 | */ | |
220 | @SuppressWarnings("PMD.LawOfDemeter") | |
221 | public String serialise() { | |
222 |
2
1. serialise : removed call to java/io/ByteArrayOutputStream::close → NO_COVERAGE 2. serialise : removed call to java/lang/Throwable::addSuppressed → NO_COVERAGE |
try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(fernetKeyBytes)) { |
223 |
1
1. serialise : removed call to com/macasaet/fernet/Key::writeTo → KILLED |
writeTo(byteStream); |
224 |
1
1. serialise : mutated return of Object value for com/macasaet/fernet/Key::serialise to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return getEncoder().encodeToString(byteStream.toByteArray()); |
225 |
1
1. serialise : removed call to java/io/ByteArrayOutputStream::close → SURVIVED |
} catch (final IOException ioe) { |
226 | // this should not happen as I/O is to memory | |
227 | throw new IllegalStateException(ioe.getMessage(), ioe); | |
228 | } | |
229 | } | |
230 | ||
231 | /** | |
232 | * Write the raw bytes of this key to the specified output stream. | |
233 | * | |
234 | * @param outputStream | |
235 | * the target | |
236 | * @throws IOException | |
237 | * if the underlying I/O device cannot be written to | |
238 | */ | |
239 | public void writeTo(final OutputStream outputStream) throws IOException { | |
240 |
1
1. writeTo : removed call to java/io/OutputStream::write → KILLED |
outputStream.write(getSigningKey()); |
241 |
1
1. writeTo : removed call to java/io/OutputStream::write → KILLED |
outputStream.write(getEncryptionKey()); |
242 | } | |
243 | ||
244 | public int hashCode() { | |
245 | final int prime = 31; | |
246 | int result = 1; | |
247 |
2
1. hashCode : Replaced integer multiplication with division → SURVIVED 2. hashCode : Replaced integer addition with subtraction → SURVIVED |
result = prime * result + Arrays.hashCode(getSigningKey()); |
248 |
2
1. hashCode : Replaced integer addition with subtraction → SURVIVED 2. hashCode : Replaced integer multiplication with division → KILLED |
result = prime * result + Arrays.hashCode(getEncryptionKey()); |
249 |
1
1. hashCode : replaced return of integer sized value with (x == 0 ? 1 : 0) → KILLED |
return result; |
250 | } | |
251 | ||
252 | @SuppressWarnings("PMD.LawOfDemeter") | |
253 | public boolean equals(final Object obj) { | |
254 |
1
1. equals : negated conditional → KILLED |
if (this == obj) { |
255 |
1
1. equals : replaced return of integer sized value with (x == 0 ? 1 : 0) → KILLED |
return true; |
256 | } | |
257 |
1
1. equals : negated conditional → KILLED |
if (!(obj instanceof Key)) { |
258 |
1
1. equals : replaced return of integer sized value with (x == 0 ? 1 : 0) → KILLED |
return false; |
259 | } | |
260 | final Key other = (Key) obj; | |
261 | ||
262 |
2
1. equals : negated conditional → KILLED 2. equals : replaced return of integer sized value with (x == 0 ? 1 : 0) → KILLED |
return MessageDigest.isEqual(getSigningKey(), other.getSigningKey()) |
263 |
1
1. equals : negated conditional → KILLED |
&& MessageDigest.isEqual(getEncryptionKey(), other.getEncryptionKey()); |
264 | } | |
265 | ||
266 | @SuppressWarnings("PMD.LawOfDemeter") | |
267 | protected byte[] sign(final byte version, final Instant timestamp, final IvParameterSpec initializationVector, | |
268 | final byte[] cipherText, final ByteArrayOutputStream byteStream) | |
269 | throws IOException { | |
270 |
2
1. sign : removed call to java/io/DataOutputStream::close → NO_COVERAGE 2. sign : removed call to java/lang/Throwable::addSuppressed → NO_COVERAGE |
try (DataOutputStream dataStream = new DataOutputStream(byteStream)) { |
271 |
1
1. sign : removed call to java/io/DataOutputStream::writeByte → KILLED |
dataStream.writeByte(version); |
272 |
1
1. sign : removed call to java/io/DataOutputStream::writeLong → KILLED |
dataStream.writeLong(timestamp.getEpochSecond()); |
273 |
1
1. sign : removed call to java/io/DataOutputStream::write → KILLED |
dataStream.write(initializationVector.getIV()); |
274 |
1
1. sign : removed call to java/io/DataOutputStream::write → KILLED |
dataStream.write(cipherText); |
275 | ||
276 | try { | |
277 | final Mac mac = Mac.getInstance(getSigningAlgorithm()); | |
278 |
1
1. sign : removed call to javax/crypto/Mac::init → KILLED |
mac.init(getSigningKeySpec()); |
279 |
1
1. sign : mutated return of Object value for com/macasaet/fernet/Key::sign to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return mac.doFinal(byteStream.toByteArray()); |
280 | } catch (final InvalidKeyException ike) { | |
281 | // this should not happen because we control the signing key | |
282 | // algorithm and pre-validate the length | |
283 | throw new IllegalStateException("Unable to initialise HMAC with shared secret: " + ike.getMessage(), | |
284 | ike); | |
285 | } catch (final NoSuchAlgorithmException nsae) { | |
286 | // this should not happen as implementors are required to | |
287 | // provide the HmacSHA256 algorithm. | |
288 | throw new IllegalStateException(nsae.getMessage(), nsae); | |
289 | } | |
290 |
1
1. sign : removed call to java/io/DataOutputStream::close → SURVIVED |
} |
291 | } | |
292 | ||
293 | /** | |
294 | * @return an HMAC SHA-256 key for signing the token | |
295 | */ | |
296 | protected java.security.Key getSigningKeySpec() { | |
297 |
1
1. getSigningKeySpec : mutated return of Object value for com/macasaet/fernet/Key::getSigningKeySpec to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return new SecretKeySpec(getSigningKey(), getSigningAlgorithm()); |
298 | } | |
299 | ||
300 | /** | |
301 | * @return the AES key for encrypting and decrypting the token payload | |
302 | */ | |
303 | protected SecretKeySpec getEncryptionKeySpec() { | |
304 |
1
1. getEncryptionKeySpec : mutated return of Object value for com/macasaet/fernet/Key::getEncryptionKeySpec to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return new SecretKeySpec(getEncryptionKey(), getEncryptionAlgorithm()); |
305 | } | |
306 | ||
307 | /** | |
308 | * Warning: Modifying the returned byte array will write through to this object. | |
309 | * | |
310 | * @return the raw underlying signing key bytes | |
311 | */ | |
312 | @SuppressWarnings("PMD.MethodReturnsInternalArray") | |
313 | protected byte[] getSigningKey() { | |
314 |
1
1. getSigningKey : mutated return of Object value for com/macasaet/fernet/Key::getSigningKey to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return signingKey; |
315 | } | |
316 | ||
317 | /** | |
318 | * Warning: Modifying the returned byte array will write through to this object. | |
319 | * | |
320 | * @return the raw underlying encryption key bytes | |
321 | */ | |
322 | @SuppressWarnings("PMD.MethodReturnsInternalArray") | |
323 | protected byte[] getEncryptionKey() { | |
324 |
1
1. getEncryptionKey : mutated return of Object value for com/macasaet/fernet/Key::getEncryptionKey to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return encryptionKey; |
325 | } | |
326 | ||
327 | protected int getTokenPrefixBytes() { | |
328 |
1
1. getTokenPrefixBytes : replaced return of integer sized value with (x == 0 ? 1 : 0) → SURVIVED |
return tokenPrefixBytes; |
329 | } | |
330 | ||
331 | protected String getSigningAlgorithm() { | |
332 |
1
1. getSigningAlgorithm : mutated return of Object value for com/macasaet/fernet/Key::getSigningAlgorithm to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return signingAlgorithm; |
333 | } | |
334 | ||
335 | protected String getEncryptionAlgorithm() { | |
336 |
1
1. getEncryptionAlgorithm : mutated return of Object value for com/macasaet/fernet/Key::getEncryptionAlgorithm to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return encryptionAlgorithm; |
337 | } | |
338 | ||
339 | protected Encoder getEncoder() { | |
340 |
1
1. getEncoder : mutated return of Object value for com/macasaet/fernet/Key::getEncoder to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return encoder; |
341 | } | |
342 | ||
343 | protected String getCipherTransformation() { | |
344 |
1
1. getCipherTransformation : mutated return of Object value for com/macasaet/fernet/Key::getCipherTransformation to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return cipherTransformation; |
345 | } | |
346 | ||
347 | } | |
Mutations | ||
75 |
1.1 2.2 |
|
78 |
1.1 2.2 |
|
127 |
1.1 |
|
145 |
1.1 2.2 |
|
146 |
1.1 |
|
147 |
1.1 |
|
148 |
1.1 |
|
167 |
1.1 |
|
168 |
1.1 |
|
204 |
1.1 |
|
205 |
1.1 |
|
222 |
1.1 2.2 |
|
223 |
1.1 |
|
224 |
1.1 |
|
225 |
1.1 |
|
240 |
1.1 |
|
241 |
1.1 |
|
247 |
1.1 2.2 |
|
248 |
1.1 2.2 |
|
249 |
1.1 |
|
254 |
1.1 |
|
255 |
1.1 |
|
257 |
1.1 |
|
258 |
1.1 |
|
262 |
1.1 2.2 |
|
263 |
1.1 |
|
270 |
1.1 2.2 |
|
271 |
1.1 |
|
272 |
1.1 |
|
273 |
1.1 |
|
274 |
1.1 |
|
278 |
1.1 |
|
279 |
1.1 |
|
290 |
1.1 |
|
297 |
1.1 |
|
304 |
1.1 |
|
314 |
1.1 |
|
324 |
1.1 |
|
328 |
1.1 |
|
332 |
1.1 |
|
336 |
1.1 |
|
340 |
1.1 |
|
344 |
1.1 |