Copyright © 2017 Carlos Macasaet.
+ * @author Carlos Macasaet + */ +class Constants { + + static final Charset charset = UTF_8; + /** + * The algorithm used to encrypt the token contents. + */ + static final String encryptionAlgorithm = "AES"; + /** + * The algorithm used to sign the token. + */ + static final String signingAlgorithm = "HmacSHA256"; + /** + * The number of bytes used to store the encryption initialisation vector + */ + static final int initializationVectorBytes = 16; + /** + * The number of bytes used to store the timestamp of a Fernet token. + */ + static final int timestampBytes = 8; + /** + * The number of bytes used to indicate the version of a Fernet token. + */ + static final int versionBytes = 1; + /** + * The number of bytes before the cipher text portion of a Fernet token. + */ + static final int tokenPrefixBytes = versionBytes + timestampBytes + initializationVectorBytes; + /** + * The number of bytes in a valid signing key. + */ + static final int signingKeyBytes = 16; + /** + * The number of bytes in a valid encryption key. + */ + static final int encryptionKeyBytes = 16; + /** + * The total number of bytes in a valid Fernet key. + */ + static final int fernetKeyBytes = signingKeyBytes + encryptionKeyBytes; + /** + * The AES block size used by the cipher. + */ + static final int cipherTextBlockSize = 16; + /** + * The transformation (algorithm, mode, and padding) used by the cipher. + * + * @see Cipher#getInstance(String) + */ + static final String cipherTransformation = encryptionAlgorithm + "/CBC/PKCS5Padding"; + /** + * The number of bytes for the HMAC signature. + */ + static final int signatureBytes = 32; + /** + * The Fernet token version supported by this library. + */ + static final byte supportedVersion = (byte) 0x80; + /** + * The number of bytes in the static portion of the token (excludes cipher text). + */ + static final int tokenStaticBytes = versionBytes + timestampBytes + initializationVectorBytes + signatureBytes; + /** + * The minimum number of bytes in a token (i.e. with an empty plaintext). + */ + static final int minimumTokenBytes = tokenStaticBytes + cipherTextBlockSize; + +} \ No newline at end of file diff --git a/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/IllegalTokenException.java b/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/IllegalTokenException.java new file mode 100644 index 00000000..58896c21 --- /dev/null +++ b/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/IllegalTokenException.java @@ -0,0 +1,37 @@ +/** + Copyright 2017 Carlos Macasaet + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +package com.hiddenservices.genesissearchengine.production.libs.fernet; + +/** + * This exception indicates that a Fernet token could not be created because one or more of the parameters was invalid. + * + *Copyright © 2017 Carlos Macasaet.
+ * + * @author Carlos Macasaet + */ +public class IllegalTokenException extends IllegalArgumentException { + + private static final long serialVersionUID = -1794971941479648725L; + + public IllegalTokenException(final String message) { + super(message); + } + + public IllegalTokenException(final String message, final Throwable cause) { + super(message, cause); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/Key.java b/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/Key.java new file mode 100644 index 00000000..8bba8494 --- /dev/null +++ b/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/Key.java @@ -0,0 +1,343 @@ +/** + Copyright 2017 Carlos Macasaet + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +package com.hiddenservices.genesissearchengine.production.libs.fernet; + +import static com.hiddenservices.genesissearchengine.production.libs.fernet.Constants.cipherTransformation; +import static com.hiddenservices.genesissearchengine.production.libs.fernet.Constants.encryptionAlgorithm; +import static com.hiddenservices.genesissearchengine.production.libs.fernet.Constants.encryptionKeyBytes; +import static com.hiddenservices.genesissearchengine.production.libs.fernet.Constants.fernetKeyBytes; +import static com.hiddenservices.genesissearchengine.production.libs.fernet.Constants.signingAlgorithm; +import static com.hiddenservices.genesissearchengine.production.libs.fernet.Constants.signingKeyBytes; +import static com.hiddenservices.genesissearchengine.production.libs.fernet.Constants.tokenPrefixBytes; +import static java.util.Arrays.copyOf; +import static java.util.Arrays.copyOfRange; +import static javax.crypto.Cipher.DECRYPT_MODE; +import static javax.crypto.Cipher.ENCRYPT_MODE; + +import android.util.Base64; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.time.Instant; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * A Fernet shared secret key. + * + *Copyright © 2017 Carlos Macasaet.
+ * + * @author Carlos Macasaet + */ +@SuppressWarnings({"PMD.AvoidDuplicateLiterals", "PMD.TooManyMethods"}) +public class Key { + + private byte[] signingKey; + private byte[] encryptionKey; + + /** + * Create a Key from individual components. + * + * @param signingKey + * a 128-bit (16 byte) key for signing tokens. + * @param encryptionKey + * a 128-bit (16 byte) key for encrypting and decrypting token contents. + */ + public Key(final byte[] signingKey, final byte[] encryptionKey) { + if (signingKey == null || signingKey.length != signingKeyBytes) { + throw new IllegalArgumentException("Signing key must be 128 bits"); + } + if (encryptionKey == null || encryptionKey.length != encryptionKeyBytes) { + throw new IllegalArgumentException("Encryption key must be 128 bits"); + } + this.signingKey = copyOf(signingKey, signingKeyBytes); + this.encryptionKey = copyOf(encryptionKey, encryptionKeyBytes); + } + + /** + * Create a Key from a payload containing the signing and encryption + * key. + * + * @param concatenatedKeys an array of 32 bytes of which the first 16 is + * the signing key and the last 16 is the + * encryption/decryption key + */ + public Key(final byte[] concatenatedKeys) { + this(copyOfRange(concatenatedKeys, 0, signingKeyBytes), + copyOfRange(concatenatedKeys, signingKeyBytes, fernetKeyBytes)); + } + + /** + * @param string + * a Base 64 URL string in the format Signing-key (128 bits) || Encryption-key (128 bits) + */ + public Key(final String string) { + this(android.util.Base64.decode(string, Base64.DEFAULT)); + } + + /** + * Generate a random key + * + * @return a new shared secret key + */ + public static Key generateKey() { + return generateKey(new SecureRandom()); + } + + /** + * Generate a random key + * + * @param random + * source of entropy + * @return a new shared secret key + */ + public static Key generateKey(final SecureRandom random) { + final byte[] signingKey = new byte[signingKeyBytes]; + random.nextBytes(signingKey); + final byte[] encryptionKey = new byte[encryptionKeyBytes]; + random.nextBytes(encryptionKey); + return new Key(signingKey, encryptionKey); + } + + /** + * Generate an HMAC SHA-256 signature from the components of a Fernet token. + * + * @param version + * the Fernet version number + * @param timestamp + * the seconds after the epoch that the token was generated + * @param initializationVector + * the encryption and decryption initialization vector + * @param cipherText + * the encrypted content of the token + * @return the HMAC signature + */ + public byte[] sign(final byte version, final Instant timestamp, final IvParameterSpec initializationVector, + final byte[] cipherText) { + try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream( + getTokenPrefixBytes() + cipherText.length)) { + return sign(version, timestamp, initializationVector, cipherText, byteStream); + } catch (final IOException e) { + // this should not happen as I/O is to memory only + throw new IllegalStateException(e.getMessage(), e); + } + } + + /** + * Encrypt a payload to embed in a Fernet token + * + * @param payload the raw bytes of the data to store in a token + * @param initializationVector random bytes from a high-entropy source to initialise the AES cipher + * @return the AES-encrypted payload. The length will always be a multiple of 16 (128 bits). + * @see #decrypt(byte[], IvParameterSpec) + */ + @SuppressWarnings("PMD.LawOfDemeter") + public byte[] encrypt(final byte[] payload, final IvParameterSpec initializationVector) { + final SecretKeySpec encryptionKeySpec = getEncryptionKeySpec(); + try { + final Cipher cipher = Cipher.getInstance(cipherTransformation); + cipher.init(ENCRYPT_MODE, encryptionKeySpec, initializationVector); + return cipher.doFinal(payload); + } catch (final NoSuchAlgorithmException | NoSuchPaddingException e) { + // these should not happen as we use an algorithm (AES) and padding (PKCS5) that are guaranteed to exist + throw new IllegalStateException("Unable to access cipher " + cipherTransformation + ": " + e.getMessage(), e); + } catch (final InvalidKeyException | InvalidAlgorithmParameterException e) { + // this should not happen as the key is validated ahead of time and + // we use an algorithm guaranteed to exist + throw new IllegalStateException( + "Unable to initialise encryption cipher with algorithm " + encryptionKeySpec.getAlgorithm() + + " and format " + encryptionKeySpec.getFormat() + ": " + e.getMessage(), + e); + } catch (final IllegalBlockSizeException | BadPaddingException e) { + // these should not happen as we control the block size and padding + throw new IllegalStateException("Unable to encrypt data: " + e.getMessage(), e); + } + } + + /** + *Decrypt the payload of a Fernet token.
+ * + *Warning: Do not call this unless the cipher text has first been verified. Attempting to decrypt a cipher text + * that has been tampered with will leak whether or not the padding is correct and this can be used to decrypt + * stolen cipher text.
+ * + * @param cipherText + * the verified padded encrypted payload of a token. The length must be a multiple of 16 (128 + * bits). + * @param initializationVector + * the random bytes used in the AES encryption of the token + * @return the decrypted payload + * @see Key#encrypt(byte[], IvParameterSpec) + */ + @SuppressWarnings("PMD.LawOfDemeter") + protected byte[] decrypt(final byte[] cipherText, final IvParameterSpec initializationVector) { + try { + final Cipher cipher = Cipher.getInstance(getCipherTransformation()); + cipher.init(DECRYPT_MODE, getEncryptionKeySpec(), initializationVector); + return cipher.doFinal(cipherText); + } catch (final NoSuchAlgorithmException | NoSuchPaddingException + | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) { + // this should not happen as we use an algorithm (AES) and padding + // (PKCS5) that are guaranteed to exist. + // in addition, we validate the encryption key and initialization vector up front + throw new IllegalStateException(e.getMessage(), e); + } catch (final BadPaddingException bpe) { + throw new TokenValidationException("Invalid padding in token: " + bpe.getMessage(), bpe); + } + } + + /** + * @return the Base 64 URL representation of this Fernet key + */ + @SuppressWarnings("PMD.LawOfDemeter") + public String serialise() { + try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(fernetKeyBytes)) { + writeTo(byteStream); + return android.util.Base64.encodeToString(byteStream.toByteArray(), Base64.DEFAULT); + } catch (final IOException ioe) { + // this should not happen as I/O is to memory + throw new IllegalStateException(ioe.getMessage(), ioe); + } + } + + /** + * Write the raw bytes of this key to the specified output stream. + * + * @param outputStream + * the target + * @throws IOException + * if the underlying I/O device cannot be written to + */ + public void writeTo(final OutputStream outputStream) throws IOException { + outputStream.write(getSigningKey()); + outputStream.write(getEncryptionKey()); + } + + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(getSigningKey()); + result = prime * result + Arrays.hashCode(getEncryptionKey()); + return result; + } + + @SuppressWarnings("PMD.LawOfDemeter") + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Key)) { + return false; + } + final Key other = (Key) obj; + + return MessageDigest.isEqual(getSigningKey(), other.getSigningKey()) + && MessageDigest.isEqual(getEncryptionKey(), other.getEncryptionKey()); + } + + @SuppressWarnings("PMD.LawOfDemeter") + protected byte[] sign(final byte version, final Instant timestamp, final IvParameterSpec initializationVector, + final byte[] cipherText, final ByteArrayOutputStream byteStream) + throws IOException { + try (DataOutputStream dataStream = new DataOutputStream(byteStream)) { + long mTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()); + dataStream.writeByte(version); + dataStream.writeLong(mTime); + dataStream.write(initializationVector.getIV()); + dataStream.write(cipherText); + + try { + final Mac mac = Mac.getInstance(getSigningAlgorithm()); + mac.init(getSigningKeySpec()); + return mac.doFinal(byteStream.toByteArray()); + } catch (final InvalidKeyException ike) { + // this should not happen because we control the signing key + // algorithm and pre-validate the length + throw new IllegalStateException("Unable to initialise HMAC with shared secret: " + ike.getMessage(), + ike); + } catch (final NoSuchAlgorithmException nsae) { + // this should not happen as implementors are required to + // provide the HmacSHA256 algorithm. + throw new IllegalStateException(nsae.getMessage(), nsae); + } + } + } + + /** + * @return an HMAC SHA-256 key for signing the token + */ + protected java.security.Key getSigningKeySpec() { + return new SecretKeySpec(getSigningKey(), getSigningAlgorithm()); + } + + /** + * @return the AES key for encrypting and decrypting the token payload + */ + protected SecretKeySpec getEncryptionKeySpec() { + return new SecretKeySpec(getEncryptionKey(), getEncryptionAlgorithm()); + } + + /** + * Warning: Modifying the returned byte array will write through to this object. + * + * @return the raw underlying signing key bytes + */ + @SuppressWarnings("PMD.MethodReturnsInternalArray") + protected byte[] getSigningKey() { + return signingKey; + } + + /** + * Warning: Modifying the returned byte array will write through to this object. + * + * @return the raw underlying encryption key bytes + */ + @SuppressWarnings("PMD.MethodReturnsInternalArray") + protected byte[] getEncryptionKey() { + return encryptionKey; + } + + protected int getTokenPrefixBytes() { + return tokenPrefixBytes; + } + + protected String getSigningAlgorithm() { + return signingAlgorithm; + } + + protected String getEncryptionAlgorithm() { + return encryptionAlgorithm; + } + + protected String getCipherTransformation() { + return cipherTransformation; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/PayloadValidationException.java b/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/PayloadValidationException.java new file mode 100644 index 00000000..ddafbdad --- /dev/null +++ b/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/PayloadValidationException.java @@ -0,0 +1,43 @@ +/** + Copyright 2018 Carlos Macasaet + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +package com.hiddenservices.genesissearchengine.production.libs.fernet; + +import com.hiddenservices.genesissearchengine.production.libs.fernet.TokenValidationException; + +/** + * This exception indicates that a Fernet token is valid, but the payload inside fails business logic validation. + * + *Copyright © 2018 Carlos Macasaet.
+ * + * @author Carlos Macasaet + */ +public class PayloadValidationException extends TokenValidationException { + + private static final long serialVersionUID = -2067765218609208844L; + + public PayloadValidationException(final String message) { + super(message); + } + + public PayloadValidationException(final Throwable cause) { + super(cause.getMessage(), cause); + } + + public PayloadValidationException(final String message, final Throwable cause) { + super(message, cause); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/StringObjectValidator.java b/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/StringObjectValidator.java new file mode 100644 index 00000000..9545cc97 --- /dev/null +++ b/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/StringObjectValidator.java @@ -0,0 +1,50 @@ +/** + Copyright 2017 Carlos Macasaet + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +package com.hiddenservices.genesissearchengine.production.libs.fernet; + + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.hiddenservices.genesissearchengine.production.libs.fernet.Validator; + +import java.nio.charset.Charset; +import java.util.function.Function; + +public interface StringObjectValidatorCopyright © 2017 Carlos Macasaet.
+ * + * @author Carlos Macasaet + */ +public interface StringValidator extends ValidatorCopyright © 2017 Carlos Macasaet.
+ * + * @author Carlos Macasaet + */ +@SuppressWarnings({"PMD.TooManyMethods", "PMD.AvoidDuplicateLiterals"}) +/* + * TooManyMethods can be avoided by making the following API-breaking changes: + * * remove the static `generate` methods and introduce a `TokenFactory` or `TokenBuilder` + * * remove the public `validateAndDecrypt` methods since they are already available in the `Validator` interface + * + * AvoidDuplicateLiterals is from the method-level @SuppressWarnings annotations + */ +public class Token { + + private final byte version; + private final Instant timestamp; + private final IvParameterSpec initializationVector; + private final byte[] cipherText; + private final byte[] hmac; + + /** + *Initialise a new Token from raw components. No validation of the signature is performed. However, the other + * fields are validated to ensure they conform to the Fernet specification.
+ * + *Warning: Subsequent modifications to the input arrays will write through to this object.
+ * + * @param version + * The version of the Fernet token specification. Currently, only 0x80 is supported. + * @param timestamp + * the time the token was generated + * @param initializationVector + * the randomly-generated bytes used to initialise the encryption cipher + * @param cipherText + * the encrypted the encrypted payload + * @param hmac + * the signature of the token + */ + @SuppressWarnings({"PMD.ArrayIsStoredDirectly", "PMD.CyclomaticComplexity"}) + protected Token(final byte version, final Instant timestamp, final IvParameterSpec initializationVector, + final byte[] cipherText, final byte[] hmac) { + if (version != supportedVersion) { + throw new IllegalTokenException("Unsupported version: " + version); + } + if (initializationVector == null || initializationVector.getIV().length != initializationVectorBytes) { + throw new IllegalTokenException("Initialization Vector must be 128 bits"); + } + if (cipherText == null || cipherText.length % cipherTextBlockSize != 0) { + throw new IllegalTokenException("Ciphertext must be a multiple of 128 bits"); + } + if (hmac == null || hmac.length != signatureBytes) { + throw new IllegalTokenException("hmac must be 256 bits"); + } + this.version = version; + this.timestamp = timestamp; + this.initializationVector = initializationVector; + this.cipherText = cipherText; + this.hmac = hmac; + } + @SuppressWarnings({"PMD.PrematureDeclaration", "PMD.DataflowAnomalyAnalysis"}) + public static Token fromBytes(final byte[] bytes) { + if (bytes.length < minimumTokenBytes) { + throw new IllegalTokenException("Not enough bits to generate a Token"); + } + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes)) { + try (DataInputStream dataStream = new DataInputStream(inputStream)) { + final byte version = dataStream.readByte(); + final long timestampSeconds = dataStream.readLong(); + + final byte[] initializationVector = read(dataStream, initializationVectorBytes); + final byte[] cipherText = read(dataStream, bytes.length - tokenStaticBytes); + final byte[] hmac = read(dataStream, signatureBytes); + + if (dataStream.read() != -1) { + throw new IllegalTokenException("more bits found"); + } + + return new Token(version, null, + new IvParameterSpec(initializationVector), cipherText, hmac); + } + } catch (final IOException ioe) { + // this should not happen as I/O is from memory and stream + // length is verified ahead of time + throw new IllegalStateException(ioe.getMessage(), ioe); + } + } + + protected static byte[] read(final DataInputStream stream, final int numBytes) throws IOException { + final byte[] retval = new byte[numBytes]; + final int bytesRead = stream.read(retval); + if (bytesRead < numBytes) { + throw new IllegalTokenException("Not enough bits to generate a Token"); + } + return retval; + } + + public static Token fromString(final String string) { + return fromBytes(android.util.Base64.decode(string, Base64.DEFAULT)); + } + + /** + * Convenience method to generate a new Fernet token with a string payload. + * + * @param key the secret key for encrypting plainText and signing the token + * @param plainText the payload to embed in the token + * @return a unique Fernet token + */ + public static Token generate(final com.hiddenservices.genesissearchengine.production.libs.fernet.Key key, final String plainText) { + return generate(new SecureRandom(), key, plainText); + } + + /** + * Convenience method to generate a new Fernet token with a string payload. + * + * @param random a source of entropy for your application + * @param key the secret key for encrypting plainText and signing the token + * @param plainText the payload to embed in the token + * @return a unique Fernet token + */ + public static Token generate(final SecureRandom random, final com.hiddenservices.genesissearchengine.production.libs.fernet.Key key, final String plainText) { + return generate(random, key, plainText.getBytes(charset)); + } + + /** + * Convenience method to generate a new Fernet token. + * + * @param key the secret key for encrypting payload and signing the token + * @param payload the unencrypted data to embed in the token + * @return a unique Fernet token + */ + public static Token generate(final com.hiddenservices.genesissearchengine.production.libs.fernet.Key key, final byte[] payload) { + return generate(new SecureRandom(), key, payload); + } + + /** + * Generate a new Fernet token. + * + * @param random a source of entropy for your application + * @param key the secret key for encrypting payload and signing the token + * @param payload the unencrypted data to embed in the token + * @return a unique Fernet token + */ + public static Token generate(final SecureRandom random, final com.hiddenservices.genesissearchengine.production.libs.fernet.Key key, final byte[] payload) { + final IvParameterSpec initializationVector = generateInitializationVector(random); + final byte[] cipherText = key.encrypt(payload, initializationVector); + final Instant timestamp = null; + final byte[] hmac = key.sign(supportedVersion, timestamp, initializationVector, cipherText); + return new Token(supportedVersion, timestamp, initializationVector, cipherText, hmac); + } + + /** + * Check the validity of this token. + * + * @param key the secret key against which to validate the token + * @param validator an object that encapsulates the validation parameters (e.g. TTL) + * @return the decrypted, deserialised payload of this token + * @throws TokenValidationException if key was NOT used to generate this token + */ + + /** + * Check the validity of this token against a collection of keys. Use this if you have implemented key rotation. + * + * @param keys the active keys which may have been used to generate token + * @param validator an object that encapsulates the validation parameters (e.g. TTL) + * @return the decrypted, deserialised payload of this token + * @throws TokenValidationException if none of the keys were used to generate this token + */ + + /** + * @return the Base 64 URL encoding of this token in the form Version | Timestamp | IV | Ciphertext | HMAC + */ + @SuppressWarnings("PMD.LawOfDemeter") + public String serialise() { + try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream( + tokenStaticBytes + getCipherText().length)) { + writeTo(byteStream); + return android.util.Base64.encodeToString(byteStream.toByteArray(), Base64.DEFAULT); + } catch (final IOException e) { + // this should not happen as IO is to memory only + throw new IllegalStateException(e.getMessage(), e); + } + } + + /** + * Write the raw bytes of this token to the specified output stream. + * + * @param outputStream + * the target + * @throws IOException + * if data cannot be written to the underlying stream + */ + @SuppressWarnings("PMD.LawOfDemeter") + public void writeTo(final OutputStream outputStream) throws IOException { + try (DataOutputStream dataStream = new DataOutputStream(outputStream)) { + long mTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()); + dataStream.writeByte(getVersion()); + dataStream.writeLong(mTime); + dataStream.write(getInitializationVector().getIV()); + dataStream.write(getCipherText()); + dataStream.write(getHmac()); + } + } + + /** + * @return the Fernet specification version of this token + */ + public byte getVersion() { + return version; + } + + /** + * @return the time that this token was generated + */ + public Instant getTimestamp() { + return timestamp; + } + + /** + * @return the initialisation vector used to encrypt the token contents + */ + public IvParameterSpec getInitializationVector() { + return initializationVector; + } + + public String toString() { + final StringBuilder builder = new StringBuilder(107); + builder.append("Token [version=").append(String.format("0x%x", new BigInteger(1, new byte[] {getVersion()}))) + .append(", timestamp=").append(getTimestamp()) + .append(", hmac=").append(android.util.Base64.encodeToString(getHmac(), Base64.DEFAULT)).append(']'); + return builder.toString(); + } + + protected static IvParameterSpec generateInitializationVector(final SecureRandom random) { + return new IvParameterSpec(generateInitializationVectorBytes(random)); + } + + protected static byte[] generateInitializationVectorBytes(final SecureRandom random) { + final byte[] retval = new byte[initializationVectorBytes]; + random.nextBytes(retval); + return retval; + } + + /** + * Recompute the HMAC signature of the token with the stored shared secret key. + * + * @param key + * the shared secret key against which to validate the token + * @return true if and only if the signature on the token was generated using the supplied key + */ + public boolean isValidSignature(final Key key) { + final byte[] computedHmac = key.sign(getVersion(), getTimestamp(), getInitializationVector(), + getCipherText()); + return MessageDigest.isEqual(getHmac(), computedHmac); + } + + /** + * Warning: modifications to the returned array will write through to this object. + * + * @return the raw encrypted payload bytes + */ + @SuppressWarnings("PMD.MethodReturnsInternalArray") + protected byte[] getCipherText() { + return cipherText; + } + + /** + * Warning: modifications to the returned array will write through to this object. + * + * @return the HMAC 256 signature of this token + */ + @SuppressWarnings("PMD.MethodReturnsInternalArray") + protected byte[] getHmac() { + return hmac; + } + + public byte[] validateAndDecrypt(final com.hiddenservices.genesissearchengine.production.libs.fernet.Key key) { + return key.decrypt(getCipherText(), getInitializationVector()); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/TokenExpiredException.java b/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/TokenExpiredException.java new file mode 100644 index 00000000..c4573bc9 --- /dev/null +++ b/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/TokenExpiredException.java @@ -0,0 +1,45 @@ +/** + Copyright 2017 Carlos Macasaet + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +package com.hiddenservices.genesissearchengine.production.libs.fernet; + +import com.hiddenservices.genesissearchengine.production.libs.fernet.TokenValidationException; + +/** + * This is a special case of the {@link TokenValidationException} that indicates that the Fernet token is invalid + * because the application-defined time-to-live has elapsed. Applications can use this to communicate to the client that + * a new Fernet must be generated, possibly by re-authenticating. + * + *Copyright © 2017 Carlos Macasaet.
+ * + * @author Carlos Macasaet + */ +public class TokenExpiredException extends TokenValidationException { + + private static final long serialVersionUID = -8250681539503776783L; + + public TokenExpiredException(final String message) { + super(message); + } + + public TokenExpiredException(final Throwable cause) { + this(cause.getMessage(), cause); + } + + public TokenExpiredException(final String message, final Throwable cause) { + super(message, cause); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/TokenValidationException.java b/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/TokenValidationException.java new file mode 100644 index 00000000..38e3cf97 --- /dev/null +++ b/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/TokenValidationException.java @@ -0,0 +1,42 @@ +/** + Copyright 2017 Carlos Macasaet + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +package com.hiddenservices.genesissearchengine.production.libs.fernet; + +/** + * This exception indicates that an operation (e.g. payload decryption) was + * attempted on an invalid Fernet token. + * + *Copyright © 2017 Carlos Macasaet.
+ * + * @author Carlos Macasaet + */ +public class TokenValidationException extends RuntimeException { + + private static final long serialVersionUID = 5175834607547919885L; + + public TokenValidationException(final String message) { + super(message); + } + + public TokenValidationException(final Throwable cause) { + this(cause.getMessage(), cause); + } + + public TokenValidationException(final String message, final Throwable cause) { + super(message, cause); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/Validator.java b/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/Validator.java new file mode 100644 index 00000000..24648843 --- /dev/null +++ b/app/src/main/java/com/hiddenservices/genesissearchengine.production/libs/fernet/Validator.java @@ -0,0 +1,37 @@ +/** + Copyright 2017 Carlos Macasaet + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +package com.hiddenservices.genesissearchengine.production.libs.fernet; + + +/** + * This class validates a token according to the Fernet specification. It may be extended to provide domain-specific + * validation of the decrypted content of the token. If you use a dependency injection / inversion of control framework, + * it would be appropriate for a subclass to be a singleton which accesses a data store. + * + *Copyright © 2017 Carlos Macasaet.
+ * + * @param