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 | 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; | |
17 | ||
18 | import static com.macasaet.fernet.Constants.charset; | |
19 | import static com.macasaet.fernet.Constants.cipherTextBlockSize; | |
20 | import static com.macasaet.fernet.Constants.decoder; | |
21 | import static com.macasaet.fernet.Constants.encoder; | |
22 | import static com.macasaet.fernet.Constants.initializationVectorBytes; | |
23 | import static com.macasaet.fernet.Constants.minimumTokenBytes; | |
24 | import static com.macasaet.fernet.Constants.signatureBytes; | |
25 | import static com.macasaet.fernet.Constants.supportedVersion; | |
26 | import static com.macasaet.fernet.Constants.tokenStaticBytes; | |
27 | ||
28 | import java.io.ByteArrayInputStream; | |
29 | import java.io.ByteArrayOutputStream; | |
30 | import java.io.DataInputStream; | |
31 | import java.io.DataOutputStream; | |
32 | import java.io.IOException; | |
33 | import java.io.OutputStream; | |
34 | import java.math.BigInteger; | |
35 | import java.security.MessageDigest; | |
36 | import java.security.SecureRandom; | |
37 | import java.time.Instant; | |
38 | import java.util.Base64.Encoder; | |
39 | import java.util.Collection; | |
40 | ||
41 | import javax.crypto.spec.IvParameterSpec; | |
42 | ||
43 | /** | |
44 | * A Fernet token. | |
45 | * | |
46 | * <p>Copyright © 2017 Carlos Macasaet.</p> | |
47 | * | |
48 | * @author Carlos Macasaet | |
49 | */ | |
50 | @SuppressWarnings({"PMD.TooManyMethods", "PMD.AvoidDuplicateLiterals"}) | |
51 | /* | |
52 | * TooManyMethods can be avoided by making the following API-breaking changes: | |
53 | * * remove the static `generate` methods and introduce a `TokenFactory` or `TokenBuilder` | |
54 | * * remove the public `validateAndDecrypt` methods since they are already available in the `Validator` interface | |
55 | * | |
56 | * AvoidDuplicateLiterals is from the method-level @SuppressWarnings annotations | |
57 | */ | |
58 | public class Token { | |
59 | ||
60 | private final byte version; | |
61 | private final Instant timestamp; | |
62 | private final IvParameterSpec initializationVector; | |
63 | private final byte[] cipherText; | |
64 | private final byte[] hmac; | |
65 | ||
66 | /** | |
67 | * <p>Initialise a new Token from raw components. No validation of the signature is performed. However, the other | |
68 | * fields are validated to ensure they conform to the Fernet specification.</p> | |
69 | * | |
70 | * <p>Warning: Subsequent modifications to the input arrays will write through to this object.</p> | |
71 | * | |
72 | * @param version | |
73 | * The version of the Fernet token specification. Currently, only 0x80 is supported. | |
74 | * @param timestamp | |
75 | * the time the token was generated | |
76 | * @param initializationVector | |
77 | * the randomly-generated bytes used to initialise the encryption cipher | |
78 | * @param cipherText | |
79 | * the encrypted the encrypted payload | |
80 | * @param hmac | |
81 | * the signature of the token | |
82 | */ | |
83 | @SuppressWarnings({"PMD.ArrayIsStoredDirectly", "PMD.CyclomaticComplexity"}) | |
84 | protected Token(final byte version, final Instant timestamp, final IvParameterSpec initializationVector, | |
85 | final byte[] cipherText, final byte[] hmac) { | |
86 |
1
1. <init> : negated conditional → KILLED |
if (version != supportedVersion) { |
87 | throw new IllegalTokenException("Unsupported version: " + version); | |
88 | } | |
89 |
1
1. <init> : negated conditional → KILLED |
if (timestamp == null) { |
90 | throw new IllegalTokenException("timestamp cannot be null"); | |
91 | } | |
92 |
2
1. <init> : negated conditional → KILLED 2. <init> : negated conditional → KILLED |
if (initializationVector == null || initializationVector.getIV().length != initializationVectorBytes) { |
93 | throw new IllegalTokenException("Initialization Vector must be 128 bits"); | |
94 | } | |
95 |
3
1. <init> : Replaced integer modulus with multiplication → KILLED 2. <init> : negated conditional → KILLED 3. <init> : negated conditional → KILLED |
if (cipherText == null || cipherText.length % cipherTextBlockSize != 0) { |
96 | throw new IllegalTokenException("Ciphertext must be a multiple of 128 bits"); | |
97 | } | |
98 |
2
1. <init> : negated conditional → KILLED 2. <init> : negated conditional → KILLED |
if (hmac == null || hmac.length != signatureBytes) { |
99 | throw new IllegalTokenException("hmac must be 256 bits"); | |
100 | } | |
101 | this.version = version; | |
102 | this.timestamp = timestamp; | |
103 | this.initializationVector = initializationVector; | |
104 | this.cipherText = cipherText; | |
105 | this.hmac = hmac; | |
106 | } | |
107 | ||
108 | /** | |
109 | * Read a Token from bytes. This does NOT validate that the token was | |
110 | * generated using a valid {@link Key}. | |
111 | * | |
112 | * @param bytes a Fernet token in the form Version | Timestamp | IV | | |
113 | * Ciphertext | HMAC | |
114 | * @return a new Token | |
115 | * @throws IllegalTokenException if the input string cannot be a valid | |
116 | * token irrespective of key or timestamp. | |
117 | */ | |
118 | @SuppressWarnings({"PMD.PrematureDeclaration", "PMD.DataflowAnomalyAnalysis"}) | |
119 | public static Token fromBytes(final byte[] bytes) { | |
120 |
2
1. fromBytes : changed conditional boundary → KILLED 2. fromBytes : negated conditional → KILLED |
if (bytes.length < minimumTokenBytes) { |
121 | throw new IllegalTokenException("Not enough bits to generate a Token"); | |
122 | } | |
123 |
2
1. fromBytes : removed call to java/io/ByteArrayInputStream::close → NO_COVERAGE 2. fromBytes : removed call to java/lang/Throwable::addSuppressed → NO_COVERAGE |
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes)) { |
124 |
2
1. fromBytes : removed call to java/io/DataInputStream::close → NO_COVERAGE 2. fromBytes : removed call to java/lang/Throwable::addSuppressed → NO_COVERAGE |
try (DataInputStream dataStream = new DataInputStream(inputStream)) { |
125 | final byte version = dataStream.readByte(); | |
126 | final long timestampSeconds = dataStream.readLong(); | |
127 | ||
128 | final byte[] initializationVector = read(dataStream, initializationVectorBytes); | |
129 |
1
1. fromBytes : Replaced integer subtraction with addition → KILLED |
final byte[] cipherText = read(dataStream, bytes.length - tokenStaticBytes); |
130 | final byte[] hmac = read(dataStream, signatureBytes); | |
131 | ||
132 |
1
1. fromBytes : negated conditional → KILLED |
if (dataStream.read() != -1) { |
133 | throw new IllegalTokenException("more bits found"); | |
134 | } | |
135 | ||
136 |
1
1. fromBytes : mutated return of Object value for com/macasaet/fernet/Token::fromBytes to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return new Token(version, Instant.ofEpochSecond(timestampSeconds), |
137 | new IvParameterSpec(initializationVector), cipherText, hmac); | |
138 |
1
1. fromBytes : removed call to java/io/DataInputStream::close → SURVIVED |
} |
139 |
1
1. fromBytes : removed call to java/io/ByteArrayInputStream::close → SURVIVED |
} catch (final IOException ioe) { |
140 | // this should not happen as I/O is from memory and stream | |
141 | // length is verified ahead of time | |
142 | throw new IllegalStateException(ioe.getMessage(), ioe); | |
143 | } | |
144 | } | |
145 | ||
146 | protected static byte[] read(final DataInputStream stream, final int numBytes) throws IOException { | |
147 | final byte[] retval = new byte[numBytes]; | |
148 | final int bytesRead = stream.read(retval); | |
149 |
2
1. read : changed conditional boundary → KILLED 2. read : negated conditional → KILLED |
if (bytesRead < numBytes) { |
150 | throw new IllegalTokenException("Not enough bits to generate a Token"); | |
151 | } | |
152 |
1
1. read : mutated return of Object value for com/macasaet/fernet/Token::read to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return retval; |
153 | } | |
154 | ||
155 | /** | |
156 | * Deserialise a Base64 URL Fernet token string. This does NOT validate that the token was generated using a valid {@link Key}. | |
157 | * | |
158 | * @param string | |
159 | * the Base 64 URL encoding of a token in the form Version | Timestamp | IV | Ciphertext | HMAC | |
160 | * @return a new Token | |
161 | * @throws IllegalTokenException | |
162 | * if the input string cannot be a valid token irrespective of key or timestamp | |
163 | */ | |
164 | public static Token fromString(final String string) { | |
165 |
1
1. fromString : mutated return of Object value for com/macasaet/fernet/Token::fromString to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return fromBytes(decoder.decode(string)); |
166 | } | |
167 | ||
168 | /** | |
169 | * Convenience method to generate a new Fernet token with a string payload. | |
170 | * | |
171 | * @param key the secret key for encrypting <em>plainText</em> and signing the token | |
172 | * @param plainText the payload to embed in the token | |
173 | * @return a unique Fernet token | |
174 | */ | |
175 | public static Token generate(final Key key, final String plainText) { | |
176 | return generate(new SecureRandom(), key, plainText); | |
177 | } | |
178 | ||
179 | /** | |
180 | * Convenience method to generate a new Fernet token with a string payload. | |
181 | * | |
182 | * @param random a source of entropy for your application | |
183 | * @param key the secret key for encrypting <em>plainText</em> and signing the token | |
184 | * @param plainText the payload to embed in the token | |
185 | * @return a unique Fernet token | |
186 | */ | |
187 | public static Token generate(final SecureRandom random, final Key key, final String plainText) { | |
188 |
1
1. generate : mutated return of Object value for com/macasaet/fernet/Token::generate to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return generate(random, key, plainText.getBytes(charset)); |
189 | } | |
190 | ||
191 | /** | |
192 | * Convenience method to generate a new Fernet token. | |
193 | * | |
194 | * @param key the secret key for encrypting <em>payload</em> and signing the token | |
195 | * @param payload the unencrypted data to embed in the token | |
196 | * @return a unique Fernet token | |
197 | */ | |
198 | public static Token generate(final Key key, final byte[] payload) { | |
199 | return generate(new SecureRandom(), key, payload); | |
200 | } | |
201 | ||
202 | /** | |
203 | * Generate a new Fernet token. | |
204 | * | |
205 | * @param random a source of entropy for your application | |
206 | * @param key the secret key for encrypting <em>payload</em> and signing the token | |
207 | * @param payload the unencrypted data to embed in the token | |
208 | * @return a unique Fernet token | |
209 | */ | |
210 | public static Token generate(final SecureRandom random, final Key key, final byte[] payload) { | |
211 | final IvParameterSpec initializationVector = generateInitializationVector(random); | |
212 | final byte[] cipherText = key.encrypt(payload, initializationVector); | |
213 | final Instant timestamp = Instant.now(); | |
214 | final byte[] hmac = key.sign(supportedVersion, timestamp, initializationVector, cipherText); | |
215 |
1
1. generate : mutated return of Object value for com/macasaet/fernet/Token::generate to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return new Token(supportedVersion, timestamp, initializationVector, cipherText, hmac); |
216 | } | |
217 | ||
218 | /** | |
219 | * Check the validity of this token. | |
220 | * | |
221 | * @param key the secret key against which to validate the token | |
222 | * @param validator an object that encapsulates the validation parameters (e.g. TTL) | |
223 | * @return the decrypted, deserialised payload of this token | |
224 | * @throws TokenValidationException if <em>key</em> was NOT used to generate this token | |
225 | */ | |
226 | @SuppressWarnings("PMD.LawOfDemeter") | |
227 | public <T> T validateAndDecrypt(final Key key, final Validator<T> validator) { | |
228 |
1
1. validateAndDecrypt : mutated return of Object value for com/macasaet/fernet/Token::validateAndDecrypt to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return validator.validateAndDecrypt(key, this); |
229 | } | |
230 | ||
231 | /** | |
232 | * Check the validity of this token against a collection of keys. Use this if you have implemented key rotation. | |
233 | * | |
234 | * @param keys the active keys which may have been used to generate token | |
235 | * @param validator an object that encapsulates the validation parameters (e.g. TTL) | |
236 | * @return the decrypted, deserialised payload of this token | |
237 | * @throws TokenValidationException if none of the keys were used to generate this token | |
238 | */ | |
239 | @SuppressWarnings("PMD.LawOfDemeter") | |
240 | public <T> T validateAndDecrypt(final Collection<? extends Key> keys, final Validator<T> validator) { | |
241 |
1
1. validateAndDecrypt : mutated return of Object value for com/macasaet/fernet/Token::validateAndDecrypt to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return validator.validateAndDecrypt(keys, this); |
242 | } | |
243 | ||
244 | @SuppressWarnings({"PMD.ConfusingTernary", "PMD.LawOfDemeter"}) | |
245 | protected byte[] validateAndDecrypt(final Key key, final Instant earliestValidInstant, | |
246 | final Instant latestValidInstant) { | |
247 |
1
1. validateAndDecrypt : negated conditional → KILLED |
if (getVersion() != (byte) 0x80) { |
248 | throw new TokenValidationException("Invalid version"); | |
249 |
1
1. validateAndDecrypt : negated conditional → KILLED |
} else if (!getTimestamp().isAfter(earliestValidInstant)) { |
250 | throw new TokenExpiredException("Token is expired"); | |
251 |
1
1. validateAndDecrypt : negated conditional → KILLED |
} else if (!getTimestamp().isBefore(latestValidInstant)) { |
252 | throw new TokenValidationException("Token timestamp is in the future (clock skew)."); | |
253 |
1
1. validateAndDecrypt : negated conditional → KILLED |
} else if (!isValidSignature(key)) { |
254 | throw new TokenValidationException("Signature does not match."); | |
255 | } | |
256 |
1
1. validateAndDecrypt : mutated return of Object value for com/macasaet/fernet/Token::validateAndDecrypt to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return key.decrypt(getCipherText(), getInitializationVector()); |
257 | } | |
258 | ||
259 | /** | |
260 | * @return the Base 64 URL encoding of this token in the form Version | Timestamp | IV | Ciphertext | HMAC | |
261 | */ | |
262 | @SuppressWarnings("PMD.LawOfDemeter") | |
263 | public String serialise() { | |
264 |
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( |
265 |
1
1. serialise : Replaced integer addition with subtraction → KILLED |
tokenStaticBytes + getCipherText().length)) { |
266 |
1
1. serialise : removed call to com/macasaet/fernet/Token::writeTo → KILLED |
writeTo(byteStream); |
267 |
1
1. serialise : mutated return of Object value for com/macasaet/fernet/Token::serialise to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return getEncoder().encodeToString(byteStream.toByteArray()); |
268 |
1
1. serialise : removed call to java/io/ByteArrayOutputStream::close → SURVIVED |
} catch (final IOException e) { |
269 | // this should not happen as IO is to memory only | |
270 | throw new IllegalStateException(e.getMessage(), e); | |
271 | } | |
272 | } | |
273 | ||
274 | /** | |
275 | * Write the raw bytes of this token to the specified output stream. | |
276 | * | |
277 | * @param outputStream | |
278 | * the target | |
279 | * @throws IOException | |
280 | * if data cannot be written to the underlying stream | |
281 | */ | |
282 | @SuppressWarnings("PMD.LawOfDemeter") | |
283 | public void writeTo(final OutputStream outputStream) throws IOException { | |
284 |
2
1. writeTo : removed call to java/io/DataOutputStream::close → NO_COVERAGE 2. writeTo : removed call to java/lang/Throwable::addSuppressed → NO_COVERAGE |
try (DataOutputStream dataStream = new DataOutputStream(outputStream)) { |
285 |
1
1. writeTo : removed call to java/io/DataOutputStream::writeByte → KILLED |
dataStream.writeByte(getVersion()); |
286 |
1
1. writeTo : removed call to java/io/DataOutputStream::writeLong → KILLED |
dataStream.writeLong(getTimestamp().getEpochSecond()); |
287 |
1
1. writeTo : removed call to java/io/DataOutputStream::write → KILLED |
dataStream.write(getInitializationVector().getIV()); |
288 |
1
1. writeTo : removed call to java/io/DataOutputStream::write → KILLED |
dataStream.write(getCipherText()); |
289 |
1
1. writeTo : removed call to java/io/DataOutputStream::write → KILLED |
dataStream.write(getHmac()); |
290 |
1
1. writeTo : removed call to java/io/DataOutputStream::close → SURVIVED |
} |
291 | } | |
292 | ||
293 | /** | |
294 | * @return the Fernet specification version of this token | |
295 | */ | |
296 | public byte getVersion() { | |
297 |
1
1. getVersion : replaced return of integer sized value with (x == 0 ? 1 : 0) → KILLED |
return version; |
298 | } | |
299 | ||
300 | /** | |
301 | * @return the time that this token was generated | |
302 | */ | |
303 | public Instant getTimestamp() { | |
304 |
1
1. getTimestamp : mutated return of Object value for com/macasaet/fernet/Token::getTimestamp to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return timestamp; |
305 | } | |
306 | ||
307 | /** | |
308 | * @return the initialisation vector used to encrypt the token contents | |
309 | */ | |
310 | public IvParameterSpec getInitializationVector() { | |
311 |
1
1. getInitializationVector : mutated return of Object value for com/macasaet/fernet/Token::getInitializationVector to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return initializationVector; |
312 | } | |
313 | ||
314 | public String toString() { | |
315 | final StringBuilder builder = new StringBuilder(107); | |
316 | builder.append("Token [version=").append(String.format("0x%x", new BigInteger(1, new byte[] {getVersion()}))) | |
317 | .append(", timestamp=").append(getTimestamp()) | |
318 | .append(", hmac=").append(encoder.encodeToString(getHmac())).append(']'); | |
319 |
1
1. toString : mutated return of Object value for com/macasaet/fernet/Token::toString to ( if (x != null) null else throw new RuntimeException ) → SURVIVED |
return builder.toString(); |
320 | } | |
321 | ||
322 | protected static IvParameterSpec generateInitializationVector(final SecureRandom random) { | |
323 |
1
1. generateInitializationVector : mutated return of Object value for com/macasaet/fernet/Token::generateInitializationVector to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return new IvParameterSpec(generateInitializationVectorBytes(random)); |
324 | } | |
325 | ||
326 | protected static byte[] generateInitializationVectorBytes(final SecureRandom random) { | |
327 | final byte[] retval = new byte[initializationVectorBytes]; | |
328 | random.nextBytes(retval); | |
329 |
1
1. generateInitializationVectorBytes : mutated return of Object value for com/macasaet/fernet/Token::generateInitializationVectorBytes to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return retval; |
330 | } | |
331 | ||
332 | /** | |
333 | * Recompute the HMAC signature of the token with the stored shared secret key. | |
334 | * | |
335 | * @param key | |
336 | * the shared secret key against which to validate the token | |
337 | * @return true if and only if the signature on the token was generated using the supplied key | |
338 | */ | |
339 | public boolean isValidSignature(final Key key) { | |
340 | final byte[] computedHmac = key.sign(getVersion(), getTimestamp(), getInitializationVector(), | |
341 | getCipherText()); | |
342 |
1
1. isValidSignature : replaced return of integer sized value with (x == 0 ? 1 : 0) → KILLED |
return MessageDigest.isEqual(getHmac(), computedHmac); |
343 | } | |
344 | ||
345 | protected Encoder getEncoder() { | |
346 |
1
1. getEncoder : mutated return of Object value for com/macasaet/fernet/Token::getEncoder to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return encoder; |
347 | } | |
348 | ||
349 | /** | |
350 | * Warning: modifications to the returned array will write through to this object. | |
351 | * | |
352 | * @return the raw encrypted payload bytes | |
353 | */ | |
354 | @SuppressWarnings("PMD.MethodReturnsInternalArray") | |
355 | protected byte[] getCipherText() { | |
356 |
1
1. getCipherText : mutated return of Object value for com/macasaet/fernet/Token::getCipherText to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return cipherText; |
357 | } | |
358 | ||
359 | /** | |
360 | * Warning: modifications to the returned array will write through to this object. | |
361 | * | |
362 | * @return the HMAC 256 signature of this token | |
363 | */ | |
364 | @SuppressWarnings("PMD.MethodReturnsInternalArray") | |
365 | protected byte[] getHmac() { | |
366 |
1
1. getHmac : mutated return of Object value for com/macasaet/fernet/Token::getHmac to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return hmac; |
367 | } | |
368 | ||
369 | } | |
Mutations | ||
86 |
1.1 |
|
89 |
1.1 |
|
92 |
1.1 2.2 |
|
95 |
1.1 2.2 3.3 |
|
98 |
1.1 2.2 |
|
120 |
1.1 2.2 |
|
123 |
1.1 2.2 |
|
124 |
1.1 2.2 |
|
129 |
1.1 |
|
132 |
1.1 |
|
136 |
1.1 |
|
138 |
1.1 |
|
139 |
1.1 |
|
149 |
1.1 2.2 |
|
152 |
1.1 |
|
165 |
1.1 |
|
188 |
1.1 |
|
215 |
1.1 |
|
228 |
1.1 |
|
241 |
1.1 |
|
247 |
1.1 |
|
249 |
1.1 |
|
251 |
1.1 |
|
253 |
1.1 |
|
256 |
1.1 |
|
264 |
1.1 2.2 |
|
265 |
1.1 |
|
266 |
1.1 |
|
267 |
1.1 |
|
268 |
1.1 |
|
284 |
1.1 2.2 |
|
285 |
1.1 |
|
286 |
1.1 |
|
287 |
1.1 |
|
288 |
1.1 |
|
289 |
1.1 |
|
290 |
1.1 |
|
297 |
1.1 |
|
304 |
1.1 |
|
311 |
1.1 |
|
319 |
1.1 |
|
323 |
1.1 |
|
329 |
1.1 |
|
342 |
1.1 |
|
346 |
1.1 |
|
356 |
1.1 |
|
366 |
1.1 |