mirror of
https://github.com/jwtk/jjwt.git
synced 2025-02-18 02:35:11 +00:00
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:
parent
8ea397b609
commit
d111dc8b22
17
src/main/java/io/jsonwebtoken/DecryptionKeyResolver.java
Normal file
17
src/main/java/io/jsonwebtoken/DecryptionKeyResolver.java
Normal 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);
|
||||
}
|
74
src/main/java/io/jsonwebtoken/EncryptionAlgorithmName.java
Normal file
74
src/main/java/io/jsonwebtoken/EncryptionAlgorithmName.java
Normal 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;
|
||||
}
|
||||
}
|
32
src/main/java/io/jsonwebtoken/EncryptionAlgorithms.java
Normal file
32
src/main/java/io/jsonwebtoken/EncryptionAlgorithms.java
Normal 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);
|
||||
}
|
8
src/main/java/io/jsonwebtoken/Jwe.java
Normal file
8
src/main/java/io/jsonwebtoken/Jwe.java
Normal file
@ -0,0 +1,8 @@
|
||||
package io.jsonwebtoken;
|
||||
|
||||
public interface Jwe<B> extends Jwt<JweHeader,B> {
|
||||
|
||||
byte[] getInitializationVector();
|
||||
|
||||
byte[] getAadTag();
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
107
src/main/java/io/jsonwebtoken/KeyManagementAlgorithm.java
Normal file
107
src/main/java/io/jsonwebtoken/KeyManagementAlgorithm.java
Normal 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;
|
||||
}
|
||||
}
|
11
src/main/java/io/jsonwebtoken/Named.java
Normal file
11
src/main/java/io/jsonwebtoken/Named.java
Normal 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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
119
src/main/java/io/jsonwebtoken/impl/DefaultJweFactory.java
Normal file
119
src/main/java/io/jsonwebtoken/impl/DefaultJweFactory.java
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
32
src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java
Normal file
32
src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java
Normal 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;
|
||||
}
|
||||
}
|
144
src/main/java/io/jsonwebtoken/impl/DispatchingParser.java
Normal file
144
src/main/java/io/jsonwebtoken/impl/DispatchingParser.java
Normal 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);
|
||||
|
||||
*/
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
@ -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);
|
||||
|
@ -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();
|
||||
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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>.
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -258,6 +258,11 @@ class DefaultAesEncryptionServiceTest {
|
||||
|
||||
byte[] plaintext;
|
||||
|
||||
@Override
|
||||
SecureRandom getSecureRandom() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
byte[] getAssociatedData() {
|
||||
return new byte[0]
|
||||
|
@ -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])
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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(', ') + ']' */
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user