EncryptionAlgorithm changes, class cleanup, test coverage, etc. Still a work in progress, but getting close to be finished with AES encryption.

This commit is contained in:
Les Hazlewood 2016-04-20 22:24:05 -07:00
parent 8ea397b609
commit d111dc8b22
38 changed files with 1582 additions and 37 deletions

View File

@ -0,0 +1,17 @@
package io.jsonwebtoken;
import java.security.Key;
/**
* @since 0.7.0
*/
public interface DecryptionKeyResolver {
/**
* Returns the decryption key that should be used to decrypt a corresponding JWE's Ciphertext (payload).
*
* @param header the JWE header to inspect to determine which decryption key should be used
* @return the decryption key that should be used to decrypt a corresponding JWE's Ciphertext (payload).
*/
Key resolveDecryptionKey(JweHeader header);
}

View File

@ -0,0 +1,74 @@
package io.jsonwebtoken;
import io.jsonwebtoken.impl.crypto.CryptoException;
public enum EncryptionAlgorithmName {
A128CBC_HS256("A128CBC-HS256", "AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.3", "AES/CBC/PKCS5Padding"),
A192CBC_HS384("A192CBC-HS384", "AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.4", "AES/CBC/PKCS5Padding"),
A256CBC_HS512("A256CBC-HS512", "AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.5", "AES/CBC/PKCS5Padding"),
A128GCM("A128GCM", "AES GCM using 128-bit key", "AES/GCM/NoPadding"),
A192GCM("A192GCM", "AES GCM using 192-bit key", "AES/GCM/NoPadding"),
A256GCM("A256GCM", "AES GCM using 256-bit key", "AES/GCM/NoPadding");
private final String name;
private final String description;
private final String jcaName;
EncryptionAlgorithmName(String name, String description, String jcaName) {
this.name = name;
this.description = description;
this.jcaName = jcaName;
}
/**
* Returns the JWA algorithm name constant.
*
* @return the JWA algorithm name constant.
*/
public String getValue() {
return name;
}
/**
* Returns the JWA algorithm description.
*
* @return the JWA algorithm description.
*/
public String getDescription() {
return description;
}
/**
* Returns the name of the JCA algorithm used to encrypt or decrypt JWE content.
*
* @return the name of the JCA algorithm used to encrypt or decrypt JWE content.
*/
public String getJcaName() {
return jcaName;
}
/**
* Returns the corresponding {@code EncryptionAlgorithmName} enum instance based on a
* case-<em>insensitive</em> name comparison of the specified JWE <code>enc</code> value.
*
* @param name the case-insensitive JWE <code>enc</code> header value.
* @return Returns the corresponding {@code EncryptionAlgorithmName} enum instance based on a
* case-<em>insensitive</em> name comparison of the specified JWE <code>enc</code> value.
* @throws CryptoException if the specified value does not match any JWE {@code EncryptionAlgorithmName} value.
*/
public static EncryptionAlgorithmName forName(String name) throws CryptoException {
for (EncryptionAlgorithmName enc : values()) {
if (enc.getValue().equalsIgnoreCase(name)) {
return enc;
}
}
throw new CryptoException("Unsupported JWE Content Encryption Algorithm name: " + name);
}
@Override
public String toString() {
return name;
}
}

View File

@ -0,0 +1,32 @@
package io.jsonwebtoken;
import io.jsonwebtoken.impl.crypto.AesEncryptionAlgorithm;
import io.jsonwebtoken.impl.crypto.GcmAesEncryptionAlgorithm;
import io.jsonwebtoken.impl.crypto.HmacAesEncryptionAlgorithm;
import io.jsonwebtoken.lang.Collections;
import java.util.List;
public final class EncryptionAlgorithms {
public static final HmacAesEncryptionAlgorithm A128CBC_HS256 =
new HmacAesEncryptionAlgorithm(EncryptionAlgorithmName.A128CBC_HS256.getValue(), SignatureAlgorithm.HS256);
public static final HmacAesEncryptionAlgorithm A192CBC_HS384 =
new HmacAesEncryptionAlgorithm(EncryptionAlgorithmName.A192CBC_HS384.getValue(), SignatureAlgorithm.HS384);
public static final HmacAesEncryptionAlgorithm A256CBC_HS512 =
new HmacAesEncryptionAlgorithm(EncryptionAlgorithmName.A256CBC_HS512.getValue(), SignatureAlgorithm.HS512);
public static final GcmAesEncryptionAlgorithm A128GCM =
new GcmAesEncryptionAlgorithm(EncryptionAlgorithmName.A128GCM.getValue(), 16);
public static final GcmAesEncryptionAlgorithm A192GCM =
new GcmAesEncryptionAlgorithm(EncryptionAlgorithmName.A192GCM.getValue(), 24);
public static final GcmAesEncryptionAlgorithm A256GCM =
new GcmAesEncryptionAlgorithm(EncryptionAlgorithmName.A256GCM.getValue(), 32);
public static List<? extends AesEncryptionAlgorithm> VALUES =
Collections.of(A128CBC_HS256, A192CBC_HS384, A256CBC_HS512, A128GCM, A192GCM, A256GCM);
}

View File

@ -0,0 +1,8 @@
package io.jsonwebtoken;
public interface Jwe<B> extends Jwt<JweHeader,B> {
byte[] getInitializationVector();
byte[] getAadTag();
}

View File

@ -63,4 +63,30 @@ public interface JweHeader<T extends JweHeader<T>> extends Header<T> {
*/
public static final String CRITICAL = "crit";
/**
* Returns the JWE Key Management
* <a href="https://tools.ietf.org/html/rfc7516#section-4.1.1"><code>alg</code></a> (Algorithm) header value or {@code null} if not present.
* <p>For a JWE, the algorithm header parameter identifies the cryptographic algorithm used to encrypt or
* determine the value of the Content Encryption Key (CEK). The encrypted content is not usable if the
* <code>alg</code> value does not represent a supported algorithm, or if the recipient does not have a key
* that can be used with that algorithm.</p>
*
* @return the JWE Key Management Algorithm header value or {@code null} if not present. This will always be
* {@code non-null} on validly constructed JWE instances, but could be {@code null} during construction.
*/
KeyManagementAlgorithm getKeyManagementAlgorithm();
/**
* Sets the JWE Key Management
* <a href="https://tools.ietf.org/html/rfc7516#section-4.1.1"><code>alg</code></a> (Algorithm) header value.
* A {@code null} value will remove the property from the JSON map.
* <p>For a JWE, the algorithm header parameter identifies the cryptographic algorithm used to encrypt or
* determine the value of the Content Encryption Key (CEK). The encrypted content is not usable if the
* <code>alg</code> value does not represent a supported algorithm, or if the recipient does not have a key
* that can be used with that algorithm.</p>
*
* @return the JWE Key Management Algorithm header value or {@code null} if not present. This will always be
* {@code non-null} on validly constructed JWE instances, but could be {@code null} during construction.
*/
T setKeyManagementAlgorithm(KeyManagementAlgorithm alg);
}

View File

@ -0,0 +1,107 @@
package io.jsonwebtoken;
import io.jsonwebtoken.impl.crypto.CryptoException;
import io.jsonwebtoken.lang.Collections;
import java.util.List;
/**
* Type-safe representation of standard JWE encryption key management algorithm names as defined in the
* <a href="https://tools.ietf.org/html/rfc7518">JSON Web Algorithms</a> specification.
*
* @since 0.7.0
*/
public enum KeyManagementAlgorithm {
RSA1_5("RSA1_5", "RSAES-PKCS1-v1_5", Collections.<String>emptyList(), "RSA/ECB/PKCS1Padding"),
RSA_OAEP("RSA-OAEP", "RSAES OAEP using default parameters", Collections.<String>emptyList(), "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"),
RSA_OAEP_256("RSA-OAEP-256", "RSAES OAEP using SHA-256 and MGF1 with SHA-256", Collections.<String>emptyList(), "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"), // & MGF1ParameterSpec.SHA256
A128KW("A128KW", "AES Key Wrap with default initial value using 128-bit key", Collections.<String>emptyList(), "AESWrap"),
A192KW("A192KW", "AES Key Wrap with default initial value using 192-bit key", Collections.<String>emptyList(), "AESWrap"),
A256KW("A256KW", "AES Key Wrap with default initial value using 256-bit key", Collections.<String>emptyList(), "AESWrap"),
dir("dir", "Direct use of a shared symmetric key as the CEK", Collections.<String>emptyList(), "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"),
ECDH_ES("ECDH-ES", "Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF", Collections.of("epk", "apu", "apv"), "ECDH"),
ECDH_ES_A128KW("ECDH-ES+A128KW", "ECDH-ES using Concat KDF and CEK wrapped with \"A128KW\"", Collections.of("epk", "apu", "apv"), "ECDH???"),
ECDH_ES_A192KW("ECDH-ES+A192KW", "ECDH-ES using Concat KDF and CEK wrapped with \"A192KW\"", Collections.of("epk", "apu", "apv"), "ECDH???"),
ECDH_ES_A256KW("ECDH-ES+A256KW", "ECDH-ES using Concat KDF and CEK wrapped with \"A256KW\"", Collections.of("epk", "apu", "apv"), "ECDH???"),
A128GCMKW("A128GCMKW", "Key wrapping with AES GCM using 128-bit key", Collections.of("iv", "tag"), "???"),
A192GCMKW("A192GCMKW", "Key wrapping with AES GCM using 192-bit key", Collections.of("iv", "tag"), "???"),
A256GCMKW("A256GCMKW", "Key wrapping with AES GCM using 256-bit key", Collections.of("iv", "tag"), "???"),
PBES2_HS256_A128KW("PBES2-HS256+A128KW", "PBES2 with HMAC SHA-256 and \"A128KW\" wrapping", Collections.of("p2s", "p2c"), "???"),
PBES2_HS384_A192KW("PBES2-HS384+A192KW", "PBES2 with HMAC SHA-384 and \"A192KW\" wrapping", Collections.of("p2s", "p2c"), "???"),
PBES2_HS512_A256KW("PBES2-HS512+A256KW", "PBES2 with HMAC SHA-512 and \"A256KW\" wrapping", Collections.of("p2s", "p2c"), "???");
private final String value;
private final String description;
private final List<String> moreHeaderParams;
private final String jcaName;
KeyManagementAlgorithm(String value, String description, List<String> moreHeaderParams, String jcaName) {
this.value = value;
this.description = description;
this.moreHeaderParams = moreHeaderParams;
this.jcaName = jcaName;
}
/**
* Returns the JWA algorithm name constant.
*
* @return the JWA algorithm name constant.
*/
public String getValue() {
return value;
}
/**
* Returns the JWA algorithm description.
*
* @return the JWA algorithm description.
*/
public String getDescription() {
return description;
}
/**
* Returns a list of header parameters that must exist in the JWE header when evaluating the key management
* algorithm. The list will be empty for algorithms that do not require additional header parameters.
*
* @return a list of header parameters that must exist in the JWE header when evaluating the key management
* algorithm.
*/
public List<String> getMoreHeaderParams() {
return moreHeaderParams;
}
/**
* Returns the name of the JCA algorithm used to create or validate the Content Encryption Key (CEK).
*
* @return the name of the JCA algorithm used to create or validate the Content Encryption Key (CEK).
*/
public String getJcaName() {
return jcaName;
}
/**
* Returns the corresponding {@code KeyManagementAlgorithm} enum instance based on a
* case-<em>insensitive</em> name comparison of the specified JWE <code>alg</code> value.
*
* @param name the case-insensitive JWE <code>alg</code> header value.
* @return Returns the corresponding {@code KeyManagementAlgorithm} enum instance based on a
* case-<em>insensitive</em> name comparison of the specified JWE <code>alg</code> value.
* @throws CryptoException if the specified value does not match any JWE {@code KeyManagementAlgorithm} value.
*/
public static KeyManagementAlgorithm forName(String name) throws CryptoException {
for (KeyManagementAlgorithm alg : values()) {
if (alg.getValue().equalsIgnoreCase(name)) {
return alg;
}
}
throw new CryptoException("Unsupported JWE Key Management Algorithm name: " + name);
}
@Override
public String toString() {
return value;
}
}

View File

@ -0,0 +1,11 @@
package io.jsonwebtoken;
public interface Named {
/**
* Returns the string name of the associated object.
*
* @return the string name of the associated object.
*/
String getName();
}

View File

@ -19,7 +19,7 @@ import io.jsonwebtoken.lang.RuntimeEnvironment;
/**
* Type-safe representation of standard JWT signature algorithm names as defined in the
* <a href="https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-31">JSON Web Algorithms</a> specification.
* <a href="https://tools.ietf.org/html/rfc7518">JSON Web Algorithms</a> specification.
*
* @since 0.1
*/
@ -268,4 +268,10 @@ public enum SignatureAlgorithm {
throw new SignatureException("Unsupported signature algorithm '" + value + "'");
}
@Override
public String toString() {
return value;
}
}

View File

@ -0,0 +1,119 @@
package io.jsonwebtoken.impl;
import io.jsonwebtoken.DecryptionKeyResolver;
import io.jsonwebtoken.EncryptionAlgorithms;
import io.jsonwebtoken.Jwe;
import io.jsonwebtoken.impl.crypto.DisabledDecryptionKeyResolver;
import io.jsonwebtoken.impl.crypto.EncryptionAlgorithm;
import io.jsonwebtoken.impl.serialization.JacksonSerializationCodec;
import io.jsonwebtoken.impl.serialization.SerializationCodec;
import io.jsonwebtoken.lang.Assert;
import java.util.Map;
public class DefaultJweFactory {
private final TextCodec base64UrlCodec;
private final SerializationCodec serializationCodec;
private final EncryptionAlgorithm encryptionAlgorithm;
private final DecryptionKeyResolver decryptionKeyResolver;
public DefaultJweFactory() {
this(TextCodec.BASE64URL, new JacksonSerializationCodec(), EncryptionAlgorithms.A256GCM, new DisabledDecryptionKeyResolver());
}
public DefaultJweFactory(TextCodec base64UrlCodec, SerializationCodec serializationCodec,
EncryptionAlgorithm encryptionAlgorithm, DecryptionKeyResolver decryptionKeyResolver) {
Assert.notNull(base64UrlCodec, "Base64Url TextCodec cannot be null.");
Assert.notNull(serializationCodec, "SerializationCodec cannot be null.");
Assert.notNull(encryptionAlgorithm, "EncryptionService cannot be null.");
Assert.notNull(decryptionKeyResolver, "DecryptionKeyResolver cannot be null.");
this.serializationCodec = serializationCodec;
this.encryptionAlgorithm = encryptionAlgorithm;
this.base64UrlCodec = base64UrlCodec;
this.decryptionKeyResolver = decryptionKeyResolver;
}
public Jwe createJwe(String base64UrlProtectedHeader, String base64UrlEncryptedKey, String base64UrlIv,
String base64UrlCiphertext, String base64UrlAuthenticationTag) {
// ====================================================================
// https://tools.ietf.org/html/rfc7516#section-5.2 #2
// ====================================================================
final byte[] headerBytes = base64UrlDecode(base64UrlProtectedHeader, "Protected Header");
// encrypted key can be null with Direct Key or Direct Key Agreement
// https://tools.ietf.org/html/rfc7516#section-5.2
// so we use a 'null safe' variant:
final byte[] encryptedKeyBytes = nullSafeBase64UrlDecode(base64UrlEncryptedKey, "Encrypted Key");
final byte[] iv = base64UrlDecode(base64UrlIv, "Initialization Vector");
final byte[] ciphertext = base64UrlDecode(base64UrlCiphertext, "Ciphertext");
final byte[] authcTag = base64UrlDecode(base64UrlAuthenticationTag, "Authentication Tag");
// ====================================================================
// https://tools.ietf.org/html/rfc7516#section-5.2 #3
// ====================================================================
Map<String, Object> protectedHeader;
try {
protectedHeader = parseJson(headerBytes);
} catch (Exception e) {
String msg = "JWE Protected Header must be a valid JSON object.";
throw new IllegalArgumentException(msg, e);
}
Assert.notEmpty(protectedHeader, "JWE Protected Header cannot be a null or empty JSON object.");
DefaultJweHeader header = new DefaultJweHeader(protectedHeader);
// ====================================================================
// https://tools.ietf.org/html/rfc7516#section-5.2 #4
// ====================================================================
// we currently don't support JSON serialization (just compact), so we can skip #4
// ====================================================================
// https://tools.ietf.org/html/rfc7516#section-5.2 #11 and #12
// ====================================================================
throw new UnsupportedOperationException("Not yet finished.");
}
protected byte[] nullSafeBase64UrlDecode(String base64UrlEncoded, String jweName) {
if (base64UrlEncoded == null) {
return null;
}
return base64UrlDecode(base64UrlEncoded, jweName);
}
protected byte[] base64UrlDecode(String base64UrlEncoded, String jweName) {
if (base64UrlEncoded == null) {
String msg = "Invalid compact JWE: base64url JWE " + jweName + " is missing.";
throw new IllegalArgumentException(msg);
}
try {
return base64UrlCodec.decode(base64UrlEncoded);
} catch (Exception e) {
String msg = "Invalid compact JWE: JWE " + jweName +
" fragment is invalid and cannot be Base64Url-decoded: " + base64UrlEncoded;
throw new IllegalArgumentException(msg, e);
}
}
@SuppressWarnings("unchecked")
protected Map<String, Object> parseJson(byte[] json) {
return serializationCodec.deserialize(json, Map.class);
}
}

View File

@ -0,0 +1,32 @@
package io.jsonwebtoken.impl;
import io.jsonwebtoken.JweHeader;
import io.jsonwebtoken.KeyManagementAlgorithm;
import java.util.Map;
public class DefaultJweHeader extends DefaultHeader implements JweHeader {
public DefaultJweHeader() {
super();
}
public DefaultJweHeader(Map<String, Object> map) {
super(map);
}
@Override
public KeyManagementAlgorithm getKeyManagementAlgorithm() {
String value = getString(JweHeader.ALGORITHM);
if (value != null) {
return KeyManagementAlgorithm.forName(value);
}
return null;
}
@Override
public JweHeader setKeyManagementAlgorithm(KeyManagementAlgorithm alg) {
setValue(ALGORITHM, alg.getValue());
return this;
}
}

View File

@ -0,0 +1,144 @@
package io.jsonwebtoken.impl;
import io.jsonwebtoken.impl.crypto.*;
import io.jsonwebtoken.lang.Assert;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
public class DispatchingParser {
static final char DELIMITER = '.';
public void parse(String compactJwe) {
//parse the constituent parts of the compact JWE:
String base64UrlEncodedHeader = null; //JWT, JWS or JWE
String base64UrlEncodedCek = null; //JWE only
String base64UrlEncodedPayload = null; //JWT or JWS
String base64UrlEncodedIv = null; //JWE only
String base64UrlEncodedCiphertext = null; //JWE only
String base64UrlEncodedTag = null; //JWE only
String base64UrlencodedDigest = null; //JWS only
StringBuilder sb = new StringBuilder();
char[] chars = compactJwe.toCharArray();
int tokenIndex = 0;
for (char c : chars) {
Assert.isTrue(!Character.isWhitespace(c), "Compact JWT strings cannot contain whitespace.");
if (c == DELIMITER) {
String value = sb.length() > 0 ? sb.toString() : null;
switch (tokenIndex) {
case 0:
base64UrlEncodedHeader = value;
break;
case 1:
//we'll figure out if we have a compact JWE or JWS after finishing inspecting the char array:
base64UrlEncodedCek = value;
base64UrlEncodedPayload = value;
case 2:
base64UrlEncodedIv = value;
break;
case 3:
base64UrlEncodedCiphertext = value;
break;
}
sb = new StringBuilder();
tokenIndex++;
} else {
sb.append(c);
}
}
boolean jwe = false;
if (tokenIndex == 2) { // JWT or JWS
jwe = false;
} else if (tokenIndex == 4) { // JWE
jwe = true;
} else {
String msg = "Invalid compact JWT string - invalid number of period character delimiters: " + tokenIndex +
". JWTs and JWSs must have exactly 2 periods, JWEs must have exactly 4 periods.";
throw new IllegalArgumentException(msg);
}
if (sb.length() > 0) {
String value = sb.toString();
if (jwe) {
base64UrlEncodedTag = value;
} else {
base64UrlencodedDigest = value;
}
}
throw new UnsupportedOperationException("Not yet implemented.");
/*
base64UrlEncodedTag = sb.toString();
Assert.notNull(base64UrlEncodedHeader, "Invalid compact JWE: base64Url JWE Protected Header is missing.");
Assert.notNull(base64UrlEncodedIv, "Invalid compact JWE: base64Url JWE Initialization Vector is missing.");
Assert.notNull(base64UrlEncodedCiphertext, "Invalid compact JWE: base64Url JWE Ciphertext is missing.");
Assert.notNull(base64UrlEncodedTag, "Invalid compact JWE: base64Url JWE Authentication Tag is missing.");
//find which encryption key was used so we can decrypt:
final byte[] headerBytes = base64UrlDecode(base64UrlEncodedHeader);
final DefaultHeaders headers = serializationCodec.deserialize(headerBytes, DefaultHeaders.class);
SecretKey secretKey = secretKeyResolver.getSecretKey(headers);
if (secretKey == null) {
String msg = "SecretKeyResolver did not return a secret key for headers " + headers +
". This is required for message decryption.";
throw new CryptoException(msg);
}
byte[] aad = base64UrlEncodedHeader.getBytes(StandardCharsets.US_ASCII);
byte[] iv = base64UrlDecode(base64UrlEncodedIv);
byte[] ciphertext = base64UrlDecode(base64UrlEncodedCiphertext);
byte[] tag = base64UrlDecode(base64UrlEncodedTag);
DecryptionRequest dreq = DecryptionRequests.builder()
.setKey(secretKey.getEncoded())
.setAdditionalAuthenticatedData(aad)
.setInitializationVector(iv)
.setCiphertext(ciphertext)
.setAuthenticationTag(tag)
.build();
byte[] plaintext = encryptionService.decrypt(dreq);
CompressionAlgorithm calg = headers.getCompressionAlgorithm();
if (calg != null) {
plaintext = calg.getCodec().decompress(plaintext);
}
Object body = null;
val = headers.get(JAVA_TYPE_HEADER_NAME);
if (val != null) {
String jtyp = val.toString();
if (jtyp != null) {
Class bodyType = ClassUtils.forName(jtyp);
body = serializationCodec.deserialize(plaintext, bodyType);
}
}
message.getHeaders().putAll(headers);
message.setBody(body);
*/
}
}

View File

@ -15,16 +15,25 @@
*/
package io.jsonwebtoken.impl.crypto;
import java.security.SecureRandom;
public abstract class AbstractCryptoRequest implements CryptoRequest {
private final SecureRandom random;
private final byte[] key;
private final byte[] iv;
public AbstractCryptoRequest(byte[] key, byte[] iv) {
public AbstractCryptoRequest(SecureRandom random, byte[] key, byte[] iv) {
this.random = random;
this.key = key;
this.iv = iv;
}
@Override
public SecureRandom getSecureRandom() {
return this.random;
}
@Override
public byte[] getKey() {
return this.key;

View File

@ -0,0 +1,167 @@
package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.lang.Assert;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import java.security.Key;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import static io.jsonwebtoken.lang.Arrays.length;
@SuppressWarnings("Duplicates")
public abstract class AesEncryptionAlgorithm implements EncryptionAlgorithm {
public static final SecureRandom DEFAULT_RANDOM = new SecureRandom();
protected static final int AES_BLOCK_SIZE = 16;
public static final String INVALID_GENERATED_IV_LENGTH =
"generatedIvLength must be a positive number <= " + AES_BLOCK_SIZE;
protected static final String DECRYPT_NO_IV = "This EncryptionAlgorithm implementation rejects decryption " +
"requests that do not include initialization vectors. AES ciphertext without an IV is weak and should " +
"never be used.";
private final String name;
private final String transformationString;
private final int generatedIvLength;
private final int requiredKeyLength;
public AesEncryptionAlgorithm(String name, String transformationString, int generatedIvLength, int requiredKeyLength) {
Assert.hasText(name, "Name cannot be null or empty.");
this.name = name;
this.transformationString = transformationString;
Assert.isTrue(generatedIvLength > 0 && generatedIvLength <= AES_BLOCK_SIZE, INVALID_GENERATED_IV_LENGTH);
this.generatedIvLength = generatedIvLength;
Assert.isTrue(requiredKeyLength > 0, "requiredKeyLength must be greater than zero.");
this.requiredKeyLength = requiredKeyLength;
}
public int getRequiredKeyLength() {
return this.requiredKeyLength;
}
public SecretKey generateKey() {
try {
return doGenerateKey();
} catch (Exception e) {
throw new CryptoException("Unable to obtain AES KeyGenerator: " + e.getMessage(), e);
}
}
protected SecretKey doGenerateKey() throws Exception {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
int generatedKeyLength = getRequiredKeyLength();
keyGenerator.init(generatedKeyLength * Byte.SIZE);
return keyGenerator.generateKey();
}
@Override
public String getName() {
return this.name;
}
protected Cipher createCipher(int mode, Key key, byte[] iv) throws Exception {
Cipher cipher = Cipher.getInstance(this.transformationString);
AlgorithmParameterSpec spec = createAlgorithmParameterSpec(iv);
cipher.init(mode, key, spec);
return cipher;
}
protected AlgorithmParameterSpec createAlgorithmParameterSpec(byte[] iv) {
return new IvParameterSpec(iv);
}
@Override
public EncryptionResult encrypt(EncryptionRequest req) throws CryptoException {
try {
Assert.notNull(req, "EncryptionRequest cannot be null.");
return doEncrypt(req);
} catch (Exception e) {
String msg = "Unable to perform encryption: " + e.getMessage();
throw new CryptoException(msg, e);
}
}
protected byte[] generateInitializationVector(SecureRandom random) {
byte[] iv = new byte[this.generatedIvLength];
random.nextBytes(iv);
return iv;
}
protected SecureRandom getSecureRandom(CryptoRequest request) {
SecureRandom random = request.getSecureRandom();
return random != null ? random : DEFAULT_RANDOM;
}
protected byte[] assertKey(CryptoRequest request) {
byte[] key = request.getKey();
return assertKeyLength(key);
}
protected byte[] assertKeyLength(byte[] key) {
int length = length(key);
if (length != requiredKeyLength) {
throw new CryptoException("The " + getName() + " algorithm requires that keys have a key length of " +
"(preferrably secure-random) " + requiredKeyLength + " bytes (" +
requiredKeyLength * Byte.SIZE + " bits). The provided key has a length of " +
length + " bytes (" + length * Byte.SIZE + " bits).");
}
return key;
}
protected byte[] ensureEncryptionIv(EncryptionRequest req) {
final SecureRandom random = getSecureRandom(req);
byte[] iv = req.getInitializationVector();
int ivLength = length(iv);
if (ivLength == 0) {
iv = generateInitializationVector(random);
}
return iv;
}
protected byte[] assertDecryptionIv(DecryptionRequest req) throws IllegalArgumentException {
byte[] iv = req.getInitializationVector();
Assert.notEmpty(iv, DECRYPT_NO_IV);
return iv;
}
protected byte[] getAAD(CryptoRequest request) {
if (request instanceof AssociatedDataSource) {
byte[] aad = ((AssociatedDataSource) request).getAssociatedData();
return io.jsonwebtoken.lang.Arrays.clean(aad);
}
return null;
}
protected abstract EncryptionResult doEncrypt(EncryptionRequest req) throws Exception;
@Override
public byte[] decrypt(DecryptionRequest req) throws CryptoException {
try {
Assert.notNull(req, "DecryptionRequest cannot be null.");
return doDecrypt(req);
} catch (Exception e) {
String msg = "Unable to perform decryption: " + e.getMessage();
throw new CryptoException(msg, e);
}
}
protected abstract byte[] doDecrypt(DecryptionRequest req) throws Exception;
}

View File

@ -15,7 +15,13 @@
*/
package io.jsonwebtoken.impl.crypto;
public class CryptoException extends RuntimeException {
import io.jsonwebtoken.JwtException;
public class CryptoException extends JwtException {
public CryptoException(String message) {
super(message);
}
public CryptoException(String message, Throwable cause) {
super(message, cause);

View File

@ -15,10 +15,41 @@
*/
package io.jsonwebtoken.impl.crypto;
import java.security.SecureRandom;
/**
* @since 0.7.0
*/
public interface CryptoRequest {
/**
* Returns the {@code SecureRandom} to use when performing cryptographic operations when processing the request, or
* {@code null} if a default {@link SecureRandom} should be used.
*
* @return the {@code SecureRandom} to use when performing cryptographic operations when processing the request, or
* {@code null} if a default {@link SecureRandom} should be used.
*/
SecureRandom getSecureRandom();
/**
* Returns the encryption key to use for encryption or decryption depending on the type of request.
*
* @return the encryption key to use for encryption or decryption depending on the type of request.
*/
byte[] getKey();
/**
* Returns the initialization vector to use during encryption or decryption depending on the type of request.
* <p>
* <p>If this value is {@code null} on an {@link EncryptionRequest}, a default initialization vector will be
* auto-generated, as it is never safe to use most cryptographic algorithms without initialization vectors
* (such as AES).</p>
* <p>
* <p>This implies that all decryption requests must always supply an initialization vector since encryption
* will always have one.</p>
*
* @return the initialization vector to use during encryption or decryption depending on the type of request.
*/
byte[] getInitializationVector();
}

View File

@ -15,8 +15,12 @@
*/
package io.jsonwebtoken.impl.crypto;
import java.security.SecureRandom;
public interface DecryptionRequestBuilder {
DecryptionRequestBuilder setSecureRandom(SecureRandom secureRandom);
DecryptionRequestBuilder setInitializationVector(byte[] iv);
DecryptionRequestBuilder setKey(byte[] key);

View File

@ -15,7 +15,6 @@
*/
package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert;
import javax.crypto.*;
@ -31,6 +30,7 @@ import static io.jsonwebtoken.lang.Arrays.length;
/**
* Default {@link EncryptionService} implementation that uses AES in GCM mode.
*/
@SuppressWarnings("Duplicates")
public class DefaultAesEncryptionService implements EncryptionService {
private static final int GCM_TAG_SIZE = 16; //number of bytes, not bits. Highest for GCM is 128 bits and recommended
@ -41,10 +41,22 @@ public class DefaultAesEncryptionService implements EncryptionService {
"requests that do not include initialization vectors. AES " +
"ciphertext without an IV is weak and should never be used.";
private static final byte[] RANDOM_KEY; //TODO: remove this concept
static {
byte[] key = new byte[32];
DEFAULT_RANDOM.nextBytes(key);
RANDOM_KEY = key;
}
private final SecretKey key;
private final SecureRandom random;
public DefaultAesEncryptionService() {
this(RANDOM_KEY, DEFAULT_RANDOM);
}
public DefaultAesEncryptionService(byte[] key) {
this(key, DEFAULT_RANDOM);
}

View File

@ -17,6 +17,8 @@ package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.lang.Assert;
import java.security.SecureRandom;
public class DefaultAuthenticatedDecryptionRequest extends DefaultDecryptionRequest
implements AuthenticatedDecryptionRequest {
@ -24,9 +26,8 @@ public class DefaultAuthenticatedDecryptionRequest extends DefaultDecryptionRequ
private final byte[] tag;
public DefaultAuthenticatedDecryptionRequest(byte[] key, byte[] iv, byte[] ciphertext, byte[] aad, byte[] tag) {
super(key, iv, ciphertext);
Assert.notEmpty(aad, "Additional Authenticated Data cannot be null or empty.");
public DefaultAuthenticatedDecryptionRequest(SecureRandom secureRandom, byte[] key, byte[] iv, byte[] ciphertext, byte[] aad, byte[] tag) {
super(secureRandom, key, iv, ciphertext);
Assert.notEmpty(tag, "Authentication tag cannot be null or empty.");
this.aad = aad;
this.tag = tag;

View File

@ -17,13 +17,15 @@ package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.lang.Assert;
import java.security.SecureRandom;
public class DefaultAuthenticatedEncryptionRequest extends DefaultEncryptionRequest
implements AuthenticatedEncryptionRequest {
private final byte[] aad;
public DefaultAuthenticatedEncryptionRequest(byte[] key, byte[] iv, byte[] plaintext, byte[] aad) {
super(key, iv, plaintext);
public DefaultAuthenticatedEncryptionRequest(SecureRandom secureRandom, byte[] key, byte[] iv, byte[] plaintext, byte[] aad) {
super(secureRandom, key, iv, plaintext);
Assert.notEmpty(aad, "Additional Authenticated Data cannot be null or empty.");
this.aad = aad;
}

View File

@ -17,12 +17,14 @@ package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.lang.Assert;
import java.security.SecureRandom;
public class DefaultDecryptionRequest extends AbstractCryptoRequest implements DecryptionRequest {
private final byte[] ciphertext;
public DefaultDecryptionRequest(byte[] key, byte[] iv, byte[] ciphertext) {
super(key, iv);
public DefaultDecryptionRequest(SecureRandom secureRandom, byte[] key, byte[] iv, byte[] ciphertext) {
super(secureRandom, key, iv);
Assert.notEmpty(ciphertext, "ciphertext cannot be null or empty.");
this.ciphertext = ciphertext;
}

View File

@ -17,6 +17,8 @@ package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.lang.Assert;
import java.security.SecureRandom;
import static io.jsonwebtoken.lang.Arrays.clean;
public class DefaultDecryptionRequestBuilder implements DecryptionRequestBuilder {
@ -25,15 +27,19 @@ public class DefaultDecryptionRequestBuilder implements DecryptionRequestBuilder
"decryption, you must also specify the authentication tag " +
"computed during encryption.";
public static final String TAG_NEEDS_AAD_MSG = "If you specify an authentication tag during decryption, you must " +
"also specify the additional authenticated data used " +
"during encryption.";
private SecureRandom secureRandom;
private byte[] iv;
private byte[] key;
private byte[] ciphertext;
private byte[] aad;
private byte[] tag;
@Override
public DecryptionRequestBuilder setSecureRandom(SecureRandom secureRandom) {
this.secureRandom = secureRandom;
return this;
}
@Override
public DecryptionRequestBuilder setInitializationVector(byte[] iv) {
this.iv = clean(iv);
@ -73,16 +79,11 @@ public class DefaultDecryptionRequestBuilder implements DecryptionRequestBuilder
throw new IllegalArgumentException(msg);
}
if (tag != null && aad == null) {
String msg = TAG_NEEDS_AAD_MSG;
throw new IllegalArgumentException(msg);
if (aad != null || tag != null) {
return new DefaultAuthenticatedDecryptionRequest(secureRandom, key, iv, ciphertext, aad, tag);
}
if (aad != null) {
return new DefaultAuthenticatedDecryptionRequest(key, iv, ciphertext, aad, tag);
}
return new DefaultDecryptionRequest(key, iv, ciphertext);
return new DefaultDecryptionRequest(secureRandom, key, iv, ciphertext);
}
}

View File

@ -17,12 +17,14 @@ package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.lang.Assert;
import java.security.SecureRandom;
public class DefaultEncryptionRequest extends AbstractCryptoRequest implements EncryptionRequest {
private final byte[] plaintext;
public DefaultEncryptionRequest(byte[] key, byte[] iv, byte[] plaintext) {
super(key, iv);
public DefaultEncryptionRequest(SecureRandom secureRandom, byte[] key, byte[] iv, byte[] plaintext) {
super(secureRandom, key, iv);
Assert.notEmpty(plaintext, "plaintext cannot be null or empty.");
this.plaintext = plaintext;
}

View File

@ -17,15 +17,24 @@ package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.lang.Assert;
import java.security.SecureRandom;
import static io.jsonwebtoken.lang.Arrays.clean;
public class DefaultEncryptionRequestBuilder implements EncryptionRequestBuilder {
private SecureRandom secureRandom;
private byte[] iv;
private byte[] key;
private byte[] plaintext;
private byte[] aad;
@Override
public EncryptionRequestBuilder setSecureRandom(SecureRandom secureRandom) {
this.secureRandom = secureRandom;
return this;
}
@Override
public EncryptionRequestBuilder setInitializationVector(byte[] iv) {
this.iv = clean(iv);
@ -56,9 +65,9 @@ public class DefaultEncryptionRequestBuilder implements EncryptionRequestBuilder
Assert.notEmpty(plaintext, "Plaintext cannot be null or empty.");
if (aad != null) {
return new DefaultAuthenticatedEncryptionRequest(key, iv, plaintext, aad);
return new DefaultAuthenticatedEncryptionRequest(secureRandom, key, iv, plaintext, aad);
}
return new DefaultEncryptionRequest(key, iv, plaintext);
return new DefaultEncryptionRequest(secureRandom, key, iv, plaintext);
}
}

View File

@ -0,0 +1,17 @@
package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.DecryptionKeyResolver;
import io.jsonwebtoken.JweHeader;
import java.security.Key;
/**
* @since 0.7.0
*/
public class DisabledDecryptionKeyResolver implements DecryptionKeyResolver {
@Override
public Key resolveDecryptionKey(JweHeader header) {
return null;
}
}

View File

@ -0,0 +1,10 @@
package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.Named;
public interface EncryptionAlgorithm extends Named {
EncryptionResult encrypt(EncryptionRequest request) throws CryptoException;
byte[] decrypt(DecryptionRequest request) throws CryptoException;
}

View File

@ -15,8 +15,12 @@
*/
package io.jsonwebtoken.impl.crypto;
import java.security.SecureRandom;
public interface EncryptionRequestBuilder {
EncryptionRequestBuilder setSecureRandom(SecureRandom secureRandom);
EncryptionRequestBuilder setInitializationVector(byte[] iv);
EncryptionRequestBuilder setKey(byte[] key);

View File

@ -0,0 +1,101 @@
package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.lang.Assert;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.spec.AlgorithmParameterSpec;
public class GcmAesEncryptionAlgorithm extends AesEncryptionAlgorithm {
private static final int GCM_IV_SIZE = 12; //number of bytes, not bits. 12 is recommended for GCM for efficiency
private static final String TRANSFORMATION_STRING = "AES/GCM/NoPadding";
public GcmAesEncryptionAlgorithm(String name, int requiredKeyLength) {
super(name, TRANSFORMATION_STRING, GCM_IV_SIZE, requiredKeyLength);
//Standard AES only supports 128, 192, and 256 key lengths, respectively:
Assert.isTrue(requiredKeyLength == 16 || requiredKeyLength == 24 || requiredKeyLength == 32, "Invalid AES Key length.");
}
@Override
protected AlgorithmParameterSpec createAlgorithmParameterSpec(byte[] iv) {
return new GCMParameterSpec(AES_BLOCK_SIZE * Byte.SIZE, iv);
}
@Override
protected EncryptionResult doEncrypt(EncryptionRequest req) throws Exception {
//Ensure IV:
byte[] iv = ensureEncryptionIv(req);
//Ensure Key:
byte[] keyBytes = assertKey(req);
//See if there is any AAD:
byte[] aad = getAAD(req); //can be null if request associated data does not exist or is empty
final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = createCipher(Cipher.ENCRYPT_MODE, encryptionKey, iv);
if (aad != null) {
cipher.updateAAD(aad);
}
byte[] plaintext = req.getPlaintext();
byte[] ciphertext = cipher.doFinal(plaintext);
// When using GCM mode, the JDK actually appends the authentication tag to the ciphertext, so let's
// represent this appropriately:
byte[] taggedCiphertext = ciphertext;
// Now separate the tag from the ciphertext (tag has a length of AES_BLOCK_SIZE):
int ciphertextLength = taggedCiphertext.length - AES_BLOCK_SIZE;
ciphertext = new byte[ciphertextLength];
System.arraycopy(taggedCiphertext, 0, ciphertext, 0, ciphertextLength);
byte[] tag = new byte[AES_BLOCK_SIZE];
System.arraycopy(taggedCiphertext, ciphertextLength, tag, 0, AES_BLOCK_SIZE);
return new DefaultAuthenticatedEncryptionResult(iv, ciphertext, tag);
}
@Override
protected byte[] doDecrypt(DecryptionRequest dreq) throws Exception {
Assert.isInstanceOf(AuthenticatedDecryptionRequest.class, dreq,
"AES GCM encryption always authenticates and therefore requires that DecryptionRequests are " +
"AuthenticatedDecryptionRequest instances.");
AuthenticatedDecryptionRequest req = (AuthenticatedDecryptionRequest) dreq;
byte[] tag = req.getAuthenticationTag();
Assert.notEmpty(tag, "AuthenticatedDecryptionRequests must include a non-empty authentication tag.");
byte[] iv = assertDecryptionIv(req);
//Ensure Key:
byte[] keyBytes = assertKey(req);
//See if there is any AAD:
byte[] aad = getAAD(req); //can be null if request associated data does not exist or is empty
final SecretKey key = new SecretKeySpec(keyBytes, "AES");
final byte[] ciphertext = req.getCiphertext();
Cipher cipher = createCipher(Cipher.DECRYPT_MODE, key, iv);
if (aad != null) {
cipher.updateAAD(aad);
}
//for tagged GCM, the JVM spec requires that the tag be appended to the end of the ciphertext
//byte array. So we'll append it here:
byte[] taggedCiphertext = new byte[ciphertext.length + tag.length];
System.arraycopy(ciphertext, 0, taggedCiphertext, 0, ciphertext.length);
System.arraycopy(tag, 0, taggedCiphertext, ciphertext.length, tag.length);
return cipher.doFinal(taggedCiphertext);
}
}

View File

@ -0,0 +1,180 @@
package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.lang.Assert;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Arrays;
import static io.jsonwebtoken.lang.Arrays.length;
public class HmacAesEncryptionAlgorithm extends AesEncryptionAlgorithm {
protected static final String TRANSFORMATION_STRING = "AES/CBC/PKCS5Padding";
protected static int getRequiredKeyLength(SignatureAlgorithm alg) {
Assert.notNull(alg, "SignatureAlgorithm is required.");
switch (alg) {
case HS256:
return 32;
case HS384:
return 48;
case HS512:
return 64;
default:
String msg = "The JWT AES_CBC_HMAC_SHA2 family of algorithms only supports the " +
SignatureAlgorithm.HS256.getValue() + ", " +
SignatureAlgorithm.HS384.getValue() + ", and " +
SignatureAlgorithm.HS512.getValue() + " SignatureAlgorithms. See " +
"https://tools.ietf.org/html/rfc7518#section-5.2.2.1 for more information.";
throw new IllegalArgumentException(msg);
}
}
private final SignatureAlgorithm SIGALG;
public HmacAesEncryptionAlgorithm(String name, SignatureAlgorithm sigAlg) {
super(name, TRANSFORMATION_STRING, AES_BLOCK_SIZE, getRequiredKeyLength(sigAlg));
this.SIGALG = sigAlg;
}
@Override
protected SecretKey doGenerateKey() throws Exception {
int subKeyLength = getRequiredKeyLength() / 2;
SecretKey macKey = MacProvider.generateKey(SIGALG);
byte[] macKeyBytes = macKey.getEncoded();
if (macKeyBytes.length > subKeyLength) {
byte[] subKeyBytes = new byte[subKeyLength];
System.arraycopy(macKeyBytes, 0, subKeyBytes, 0, subKeyLength);
macKeyBytes = subKeyBytes;
}
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(subKeyLength * Byte.SIZE);
SecretKey encKey = keyGenerator.generateKey();
byte[] encKeyBytes = encKey.getEncoded();
//return as one single key per https://tools.ietf.org/html/rfc7518#section-5.2.2.1
byte[] combinedKeyBytes = new byte[macKeyBytes.length + encKeyBytes.length];
System.arraycopy(macKeyBytes, 0, combinedKeyBytes, 0, macKeyBytes.length);
System.arraycopy(encKeyBytes, 0, combinedKeyBytes, macKeyBytes.length, encKeyBytes.length);
return new SecretKeySpec(combinedKeyBytes, getName());
}
@Override
protected EncryptionResult doEncrypt(EncryptionRequest req) throws Exception {
//Ensure IV:
byte[] iv = ensureEncryptionIv(req);
//Ensure Key:
byte[] keyBytes = assertKey(req);
//See if there is any AAD:
byte[] aad = getAAD(req); //can be null if request associated data does not exist or is empty
int halfCount = keyBytes.length / 2; // https://tools.ietf.org/html/rfc7518#section-5.2
byte[] macKeyBytes = Arrays.copyOfRange(keyBytes, 0, halfCount);
keyBytes = Arrays.copyOfRange(keyBytes, halfCount, keyBytes.length);
final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = createCipher(Cipher.ENCRYPT_MODE, encryptionKey, iv);
byte[] plaintext = req.getPlaintext();
byte[] ciphertext = cipher.doFinal(plaintext);
byte[] tag = sign(aad, iv, ciphertext, macKeyBytes);
return new DefaultAuthenticatedEncryptionResult(iv, ciphertext, tag);
}
private byte[] sign(byte[] aad, byte[] iv, byte[] ciphertext, byte[] macKeyBytes) {
long aadLength = length(aad);
long aadLengthInBits = aadLength * Byte.SIZE;
long aadLengthInBitsAsUnsignedInt = aadLengthInBits & 0xffffffffL;
byte[] AL = toBytes(aadLengthInBitsAsUnsignedInt);
byte[] toHash = new byte[(int) aadLength + iv.length + ciphertext.length + AL.length];
if (aad != null) {
System.arraycopy(aad, 0, toHash, 0, aad.length);
System.arraycopy(iv, 0, toHash, aad.length, iv.length);
System.arraycopy(ciphertext, 0, toHash, aad.length + iv.length, ciphertext.length);
System.arraycopy(AL, 0, toHash, aad.length + iv.length + ciphertext.length, AL.length);
} else {
System.arraycopy(iv, 0, toHash, 0, iv.length);
System.arraycopy(ciphertext, 0, toHash, iv.length, ciphertext.length);
System.arraycopy(AL, 0, toHash, iv.length + ciphertext.length, AL.length);
}
MacSigner macSigner = new MacSigner(SIGALG, macKeyBytes);
byte[] digest = macSigner.sign(toHash);
// https://tools.ietf.org/html/rfc7518#section-5.2.2.1 #5 requires truncating the signature
// to be the same length as the macKey/encKey:
return Arrays.copyOfRange(digest, 0, macKeyBytes.length);
}
private static byte[] toBytes(long l) {
byte[] b = new byte[8];
for (int i = 7; i > 0; i--) {
b[i] = (byte) l;
l >>>= 8;
}
b[0] = (byte) l;
return b;
}
@Override
protected byte[] doDecrypt(DecryptionRequest dreq) throws Exception {
Assert.isInstanceOf(AuthenticatedDecryptionRequest.class, dreq,
"AES_CBC_HMAC_SHA2 encryption always authenticates and therefore requires that DecryptionRequests " +
"are AuthenticatedDecryptionRequest instances.");
AuthenticatedDecryptionRequest req = (AuthenticatedDecryptionRequest) dreq;
byte[] tag = req.getAuthenticationTag();
Assert.notEmpty(tag, "AuthenticatedDecryptionRequests must include a non-empty authentication tag.");
byte[] iv = assertDecryptionIv(req);
//Ensure Key:
byte[] keyBytes = assertKey(req);
//See if there is any AAD:
byte[] aad = getAAD(req); //can be null if request associated data does not exist or is empty
int halfCount = keyBytes.length / 2; // https://tools.ietf.org/html/rfc7518#section-5.2
byte[] macKeyBytes = Arrays.copyOfRange(keyBytes, 0, halfCount);
keyBytes = Arrays.copyOfRange(keyBytes, halfCount, keyBytes.length);
final SecretKey key = new SecretKeySpec(keyBytes, "AES");
final byte[] ciphertext = req.getCiphertext();
Cipher cipher = createCipher(Cipher.DECRYPT_MODE, key, iv);
// Assert that the aad + iv + ciphertext provided, when signed, equals the tag provided,
// thereby indicating none of it has been tampered with:
byte[] digest = sign(aad, iv, ciphertext, macKeyBytes);
if (!Arrays.equals(digest, tag)) {
String msg = "Ciphertext decryption failed: HMAC digest verification failed.";
throw new CryptoException(msg);
}
return cipher.doFinal(ciphertext);
}
}

View File

@ -0,0 +1,37 @@
package io.jsonwebtoken.impl.serialization;
import io.jsonwebtoken.lang.Assert;
import java.io.IOException;
public abstract class AbstractSerializationCodec implements SerializationCodec {
protected static final String SERIALIZING_ERROR = "Exception occurred while serializing %s to byte[].";
protected static final String DESERIALIZING_ERROR = "Exception occurred when deserializing byte[] to %s.";
@Override
public final <T> byte[] serialize(T object) {
Assert.notNull(object, "object cannot be null.");
try {
return doSerialize(object);
} catch (IOException e) {
throw new SerializationException(String.format(SERIALIZING_ERROR, object.getClass().getSimpleName()), e);
}
}
protected abstract <T> byte[] doSerialize(T object) throws IOException;
@Override
public final <T> T deserialize(byte[] bytes, Class<T> targetClass) {
Assert.notNull(bytes, "bytes cannot be null.");
Assert.notNull(targetClass, "targetClass cannot be null.");
try {
return doDeserialize(bytes, targetClass);
} catch (IOException e) {
throw new SerializationException(String.format(DESERIALIZING_ERROR, targetClass.getSimpleName()), e);
}
}
protected abstract <T> T doDeserialize(byte[] bytes, Class<T> targetClass) throws IOException;
}

View File

@ -0,0 +1,36 @@
package io.jsonwebtoken.impl.serialization;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.lang.Assert;
import java.io.IOException;
/**
* Implementation of the {@link SerializationCodec} that relies on Jackson's {@link ObjectMapper objectMapper} to
* serialize/deserialize objects.
*
* @since 0.7.0
*/
public class JacksonSerializationCodec extends AbstractSerializationCodec {
private final ObjectMapper objectMapper;
public JacksonSerializationCodec() {
this(new ObjectMapper());
}
public JacksonSerializationCodec(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "objectMapper cannot be null");
this.objectMapper = objectMapper;
}
@Override
protected <T> T doDeserialize(byte[] bytes, Class<T> typeClass) throws IOException {
return objectMapper.readValue(bytes, typeClass);
}
@Override
protected <T> byte[] doSerialize(T object) throws IOException {
return objectMapper.writeValueAsBytes(object);
}
}

View File

@ -0,0 +1,33 @@
package io.jsonwebtoken.impl.serialization;
/**
* Serializes an {@code object} to a {@code byte[]} and deserialize a {@code byte[]} to an {@code object}.
*
* @since 0.7.0
*/
public interface SerializationCodec {
/**
* Serializes an {@code object} to a {@code byte[]}.
*
* @param object
* @param <T> the type of object to serialize.
* @return the serialized object as {@code byte[]}.
*/
<T> byte[] serialize(T object) throws SerializationException;
/**
* Deserialize a {@code byte[]} to an {@code object} of an specific {@code type}
* <pre>
* bytes[] serialized = ...
* Map instance = serializationCodec.deserialize(serialized, Map.class);
* <pre>
*
* @param bytes of the serialized object.
* @param targetClass of the instance to return.
* @param <T> the specific type of object instance to return.
* @return A deserialized instance of type {@code T}.
*/
<T> T deserialize(byte[] bytes, Class<T> targetClass) throws SerializationException;
}

View File

@ -0,0 +1,11 @@
package io.jsonwebtoken.impl.serialization;
import io.jsonwebtoken.JwtException;
public class SerializationException extends JwtException {
public SerializationException(String message, Throwable e) {
super(message, e);
}
}

View File

@ -31,6 +31,17 @@ public final class Collections {
private Collections(){}
public static <T> List<T> emptyList() {
return java.util.Collections.emptyList();
}
public static <T> List<T> of(T... elements) {
if (elements == null || elements.length == 0) {
return java.util.Collections.emptyList();
}
return java.util.Collections.unmodifiableList(Arrays.asList(elements));
}
/**
* Return <code>true</code> if the supplied Collection is <code>null</code>
* or empty. Otherwise, return <code>false</code>.

View File

@ -0,0 +1,103 @@
package io.jsonwebtoken.impl.crypto
import io.jsonwebtoken.EncryptionAlgorithmName
import io.jsonwebtoken.EncryptionAlgorithms
import org.junit.Test
import static org.junit.Assert.assertArrayEquals
import static org.junit.Assert.assertTrue
/**
* Test case defined in https://tools.ietf.org/html/rfc7518#appendix-B.1
*/
class Aes128CbcHmacSha256Test {
final byte[] K =
[0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f] as byte[]
final byte[] MAC_KEY =
[0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f] as byte[]
final byte[] ENC_KEY =
[0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f] as byte[]
final byte[] P =
[0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20,
0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75,
0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65,
0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62,
0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69,
0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66,
0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f,
0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65] as byte[]
final byte[] IV =
[0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04] as byte[]
final byte[] A =
[0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63,
0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20,
0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73] as byte[]
final byte[] AL = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x50] as byte[]
final byte[] E =
[0xc8, 0x0e, 0xdf, 0xa3, 0x2d, 0xdf, 0x39, 0xd5, 0xef, 0x00, 0xc0, 0xb4, 0x68, 0x83, 0x42, 0x79,
0xa2, 0xe4, 0x6a, 0x1b, 0x80, 0x49, 0xf7, 0x92, 0xf7, 0x6b, 0xfe, 0x54, 0xb9, 0x03, 0xa9, 0xc9,
0xa9, 0x4a, 0xc9, 0xb4, 0x7a, 0xd2, 0x65, 0x5c, 0x5f, 0x10, 0xf9, 0xae, 0xf7, 0x14, 0x27, 0xe2,
0xfc, 0x6f, 0x9b, 0x3f, 0x39, 0x9a, 0x22, 0x14, 0x89, 0xf1, 0x63, 0x62, 0xc7, 0x03, 0x23, 0x36,
0x09, 0xd4, 0x5a, 0xc6, 0x98, 0x64, 0xe3, 0x32, 0x1c, 0xf8, 0x29, 0x35, 0xac, 0x40, 0x96, 0xc8,
0x6e, 0x13, 0x33, 0x14, 0xc5, 0x40, 0x19, 0xe8, 0xca, 0x79, 0x80, 0xdf, 0xa4, 0xb9, 0xcf, 0x1b,
0x38, 0x4c, 0x48, 0x6f, 0x3a, 0x54, 0xc5, 0x10, 0x78, 0x15, 0x8e, 0xe5, 0xd7, 0x9d, 0xe5, 0x9f,
0xbd, 0x34, 0xd8, 0x48, 0xb3, 0xd6, 0x95, 0x50, 0xa6, 0x76, 0x46, 0x34, 0x44, 0x27, 0xad, 0xe5,
0x4b, 0x88, 0x51, 0xff, 0xb5, 0x98, 0xf7, 0xf8, 0x00, 0x74, 0xb9, 0x47, 0x3c, 0x82, 0xe2, 0xdb] as byte[]
final byte[] M =
[0x65, 0x2c, 0x3f, 0xa3, 0x6b, 0x0a, 0x7c, 0x5b, 0x32, 0x19, 0xfa, 0xb3, 0xa3, 0x0b, 0xc1, 0xc4,
0xe6, 0xe5, 0x45, 0x82, 0x47, 0x65, 0x15, 0xf0, 0xad, 0x9f, 0x75, 0xa2, 0xb7, 0x1c, 0x73, 0xef] as byte[]
final byte[] T =
[0x65, 0x2c, 0x3f, 0xa3, 0x6b, 0x0a, 0x7c, 0x5b, 0x32, 0x19, 0xfa, 0xb3, 0xa3, 0x0b, 0xc1, 0xc4] as byte[]
@Test
public void test() {
def alg = EncryptionAlgorithms.A128CBC_HS256
EncryptionRequest request = EncryptionRequests.builder()
.setAdditionalAuthenticatedData(A)
.setInitializationVector(IV)
.setKey(K)
.setPlaintext(P)
.build();
def r = alg.encrypt(request);
assertTrue r instanceof AuthenticatedEncryptionResult
AuthenticatedEncryptionResult result = r as AuthenticatedEncryptionResult;
byte[] resultCiphertext = result.getCiphertext()
byte[] resultTag = result.getAuthenticationTag();
byte[] resultIv = result.getInitializationVector();
assertArrayEquals E, resultCiphertext
assertArrayEquals T, resultTag
assertArrayEquals IV, resultIv //shouldn't have been altered
// now test decryption:
AuthenticatedDecryptionRequest decryptionRequest = DecryptionRequests.builder()
.setAdditionalAuthenticatedData(A)
.setCiphertext(resultCiphertext)
.setInitializationVector(resultIv)
.setKey(K)
.setAuthenticationTag(resultTag)
.build();
byte[] decryptionResult = alg.decrypt(decryptionRequest)
assertArrayEquals(P, decryptionResult);
}
}

View File

@ -258,6 +258,11 @@ class DefaultAesEncryptionServiceTest {
byte[] plaintext;
@Override
SecureRandom getSecureRandom() {
return null;
}
@Override
byte[] getAssociatedData() {
return new byte[0]

View File

@ -54,17 +54,6 @@ class DefaultDecryptionRequestBuilderTest {
}
}
@Test
void testTagWithoutAad() {
try {
new DefaultDecryptionRequestBuilder().setCiphertext(generateData())
.setAuthenticationTag(generateData()).build()
fail()
} catch (IllegalArgumentException expected) {
assertEquals(DefaultDecryptionRequestBuilder.TAG_NEEDS_AAD_MSG, expected.getMessage())
}
}
@Test
void testSetInitializationVectorWithEmptyArray() {
def b = new DefaultDecryptionRequestBuilder().setInitializationVector(new byte[0])

View File

@ -0,0 +1,105 @@
package io.jsonwebtoken.impl.crypto
import io.jsonwebtoken.EncryptionAlgorithms
import org.junit.Test
import static org.junit.Assert.assertArrayEquals
import static org.junit.Assert.assertEquals
class EncryptionAlgorithmsTest {
private static final String PLAINTEXT =
'''Bacon ipsum dolor amet venison beef pork chop, doner jowl pastrami ground round alcatra.
Beef leberkas filet mignon ball tip pork spare ribs kevin short loin ribeye ground round
biltong jerky short ribs corned beef. Strip steak turducken meatball porchetta beef ribs
shoulder pork belly doner salami corned beef kielbasa cow filet mignon drumstick. Bacon
tenderloin pancetta flank frankfurter ham kevin leberkas meatball turducken beef ribs.
Cupim short loin short ribs shankle tenderloin. Ham ribeye hamburger flank tenderloin
cupim t-bone, shank tri-tip venison salami sausage pancetta. Pork belly chuck salami
alcatra sirloin.
,
,
,
,
, '''
private static final byte[] PLAINTEXT_BYTES = PLAINTEXT.getBytes("UTF-8")
private static final String AAD = 'You can get with this, or you can get with that'
private static final byte[] AAD_BYTES = AAD.getBytes("UTF-8")
@Test
void testWithoutAad() {
for (AesEncryptionAlgorithm alg : EncryptionAlgorithms.VALUES) {
def skey = alg.generateKey()
def key = skey.getEncoded()
def request = EncryptionRequests.builder().setKey(key).setPlaintext(PLAINTEXT_BYTES).build()
def result = alg.encrypt(request);
assert result instanceof AuthenticatedEncryptionResult
byte[] ciphertext = result.getCiphertext()
boolean gcm = alg instanceof GcmAesEncryptionAlgorithm
if (gcm) { //AES GCM always results in ciphertext the same length as the plaintext:
assertEquals(ciphertext.length, PLAINTEXT_BYTES.length)
}
def dreq = DecryptionRequests.builder()
.setKey(key)
.setInitializationVector(result.getInitializationVector())
.setAuthenticationTag(result.getAuthenticationTag())
.setCiphertext(result.getCiphertext())
.build()
byte[] decryptedPlaintextBytes = alg.decrypt(dreq)
assertArrayEquals(PLAINTEXT_BYTES, decryptedPlaintextBytes);
}
}
@Test
void testWithAad() {
for (AesEncryptionAlgorithm alg : EncryptionAlgorithms.VALUES) {
def skey = alg.generateKey()
def key = skey.getEncoded()
def request = EncryptionRequests.builder()
.setAdditionalAuthenticatedData(AAD_BYTES)
.setKey(key)
.setPlaintext(PLAINTEXT_BYTES)
.build()
def result = alg.encrypt(request);
assert result instanceof AuthenticatedEncryptionResult
byte[] ciphertext = result.getCiphertext()
boolean gcm = alg instanceof GcmAesEncryptionAlgorithm
if (gcm) {
assertEquals(ciphertext.length, PLAINTEXT_BYTES.length)
}
def dreq = DecryptionRequests.builder()
.setAdditionalAuthenticatedData(AAD_BYTES)
.setKey(key)
.setInitializationVector(result.getInitializationVector())
.setAuthenticationTag(result.getAuthenticationTag())
.setCiphertext(result.getCiphertext())
.build()
byte[] decryptedPlaintextBytes = alg.decrypt(dreq)
assertArrayEquals(PLAINTEXT_BYTES, decryptedPlaintextBytes);
}
}
}

View File

@ -0,0 +1,81 @@
package io.jsonwebtoken.impl.crypto
import io.jsonwebtoken.EncryptionAlgorithms
import org.junit.Test
import static org.junit.Assert.assertArrayEquals
import static org.junit.Assert.assertTrue
/**
* Test defined in https://tools.ietf.org/html/rfc7516#appendix-A.1
*/
class GcmAesEncryptionServiceTest {
final byte[] K =
[0xb1, 0xa1, 0xf4, 0x80, 0x54, 0x8f, 0xe1, 0x73, 0x3f, 0xb4, 0x3, 0xff, 0x6b, 0x9a, 0xd4, 0xf6,
0x8a, 0x7, 0x6e, 0x5b, 0x70, 0x2e, 0x22, 0x69, 0x2f, 0x82, 0xcb, 0x2e, 0x7a, 0xea, 0x40, 0xfc] as byte[]
final byte[] P = "The true sign of intelligence is not knowledge but imagination.".getBytes("UTF-8")
final byte[] IV = [0xe3, 0xc5, 0x75, 0xfc, 0x2, 0xdb, 0xe9, 0x44, 0xb4, 0xe1, 0x4d, 0xdb] as byte[]
final byte[] AAD =
[0x65, 0x79, 0x4a, 0x68, 0x62, 0x47, 0x63, 0x69, 0x4f, 0x69, 0x4a, 0x53, 0x55, 0x30, 0x45, 0x74,
0x54, 0x30, 0x46, 0x46, 0x55, 0x43, 0x49, 0x73, 0x49, 0x6d, 0x56, 0x75, 0x59, 0x79, 0x49, 0x36,
0x49, 0x6b, 0x45, 0x79, 0x4e, 0x54, 0x5a, 0x48, 0x51, 0x30, 0x30, 0x69, 0x66, 0x51] as byte[]
final byte[] E =
[0xe5, 0xec, 0xa6, 0xf1, 0x35, 0xbf, 0x73, 0xc4, 0xae, 0x2b, 0x49, 0x6d, 0x27, 0x7a, 0xe9, 0x60,
0x8c, 0xce, 0x78, 0x34, 0x33, 0xed, 0x30, 0xb, 0xbe, 0xdb, 0xba, 0x50, 0x6f, 0x68, 0x32, 0x8e,
0x2f, 0xa7, 0x3b, 0x3d, 0xb5, 0x7f, 0xc4, 0x15, 0x28, 0x52, 0xf2, 0x20, 0x7b, 0x8f, 0xa8, 0xe2,
0x49, 0xd8, 0xb0, 0x90, 0x8a, 0xf7, 0x6a, 0x3c, 0x10, 0xcd, 0xa0, 0x6d, 0x40, 0x3f, 0xc0] as byte[]
final byte[] T =
[0x5c, 0x50, 0x68, 0x31, 0x85, 0x19, 0xa1, 0xd7, 0xad, 0x65, 0xdb, 0xd3, 0x88, 0x5b, 0xd2, 0x91] as byte[]
@Test
void test() {
def alg = EncryptionAlgorithms.A256GCM
EncryptionRequest request = EncryptionRequests.builder()
.setAdditionalAuthenticatedData(AAD)
.setInitializationVector(IV)
.setKey(K)
.setPlaintext(P)
.build();
def r = alg.encrypt(request);
assertTrue r instanceof AuthenticatedEncryptionResult
AuthenticatedEncryptionResult result = r as AuthenticatedEncryptionResult;
byte[] resultCiphertext = result.getCiphertext()
byte[] resultTag = result.getAuthenticationTag();
byte[] resultIv = result.getInitializationVector();
assertArrayEquals E, resultCiphertext
assertArrayEquals T, resultTag
assertArrayEquals IV, resultIv //shouldn't have been altered
// now test decryption:
AuthenticatedDecryptionRequest decryptionRequest = DecryptionRequests.builder()
.setAdditionalAuthenticatedData(AAD)
.setCiphertext(resultCiphertext)
.setInitializationVector(resultIv)
.setKey(K)
.setAuthenticationTag(resultTag)
.build();
byte[] decryptionResult = alg.decrypt(decryptionRequest)
assertArrayEquals(P, decryptionResult);
/*
def c = array.collect { '0x' + Integer.toHexString(it) }
println '[' + c.join(', ') + ']' */
}
}