password hashing / iteration estimate algorithm / impl checkpoint

This commit is contained in:
Les Hazlewood 2021-10-11 11:16:19 -07:00
parent aa9af6859e
commit 23ef0333a3
91 changed files with 1272 additions and 918 deletions

View File

@ -1,6 +1,5 @@
package io.jsonwebtoken;
import io.jsonwebtoken.security.EncryptedKeyAlgorithm;
import io.jsonwebtoken.security.KeyAlgorithm;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
@ -17,6 +16,4 @@ public interface JweBuilder extends JwtBuilder<JweBuilder> {
JweBuilder withKey(SecretKey key);
<K extends Key> JweBuilder withKeyFrom(K key, KeyAlgorithm<K, ?> alg);
JweBuilder withKeyFrom(char[] password, int iterations, EncryptedKeyAlgorithm<SecretKey,SecretKey> alg);
}

View File

@ -74,7 +74,7 @@ public interface JwtBuilder<T extends JwtBuilder<T>> extends ClaimsMutator<T> {
* @param header the header to set (and potentially replace any existing header).
* @return the builder for method chaining.
*/
T setHeader(Map<String, Object> header);
T setHeader(Map<String, ?> header);
/**
* Applies the specified name/value pairs to the header. If a header does not yet exist at the time this method
@ -83,7 +83,7 @@ public interface JwtBuilder<T extends JwtBuilder<T>> extends ClaimsMutator<T> {
* @param params the header name/value pairs to append to the header.
* @return the builder for method chaining.
*/
T setHeaderParams(Map<String, Object> params);
T setHeaderParams(Map<String, ?> params);
//sets the specified header parameter, overwriting any previous value under the same name.
@ -141,7 +141,7 @@ public interface JwtBuilder<T extends JwtBuilder<T>> extends ClaimsMutator<T> {
* @return the builder for method chaining.
* @since 0.8
*/
T addClaims(Map<String, Object> claims);
T addClaims(Map<String, ?> claims);
/**
* Sets the JWT Claims <a href="https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-4.1.1">

View File

@ -293,7 +293,7 @@ public interface JwtParserBuilder {
* .parseClaimsJws(compact);
* </pre>
* <p>
* <p>A {@code KeyResolver} is invoked once during parsing before performing decryption or signature verification.</p>
* <p>A Key {@code Locator} is invoked once during parsing before performing decryption or signature verification.</p>
*
* @param keyLocator the locator used to retrieve decryption or signature verification keys.
* @return the parser builder for method chaining.

View File

@ -3,5 +3,5 @@ package io.jsonwebtoken.security;
/**
* @since JJWT_RELEASE_VERSION
*/
public interface AeadResult extends PayloadSupplier<byte[]>, AuthenticationTagSource, InitializationVectorSource {
public interface AeadResult extends PayloadSupplier<byte[]>, DigestSupplier, InitializationVectorSupplier {
}

View File

@ -18,7 +18,7 @@ package io.jsonwebtoken.security;
/**
* @since JJWT_RELEASE_VERSION
*/
public interface AssociatedDataSource {
public interface AssociatedDataSupplier {
byte[] getAssociatedData();

View File

@ -18,8 +18,8 @@ package io.jsonwebtoken.security;
/**
* @since JJWT_RELEASE_VERSION
*/
public interface AuthenticationTagSource {
public interface DigestSupplier {
byte[] getAuthenticationTag();
byte[] getDigest();
}

View File

@ -0,0 +1,8 @@
package io.jsonwebtoken.security;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.ECKey;
public interface EcKeyAlgorithm<E extends ECKey & PublicKey, D extends ECKey & PrivateKey> extends KeyAlgorithm<E, D> {
}

View File

@ -1,17 +0,0 @@
package io.jsonwebtoken.security;
import java.security.Key;
/**
* A {@link KeyAlgorithm} that produces an encrypted key value. {@code EncryptedKeyAlgorithm}s will be supplied
* a secure-randomly-generated Content Encryption Key in the request's {@link CryptoRequest#getPayload() getData()} method.
*
* <p>
* <b>A {@code KeyAlgorithm} that does not produce an encrypted key value (or produces an empty key byte array) should
* not implement this interface, and instead implement the {@code KeyAlgorithm} parent interface directly.</p>
*
* @since JJWT_RELEASE_VERSION
*/
public interface EncryptedKeyAlgorithm<E extends Key, D extends Key> extends KeyAlgorithm<E, D> {
}

View File

@ -1,11 +0,0 @@
package io.jsonwebtoken.security;
import io.jsonwebtoken.JweHeader;
/**
* @since JJWT_RELEASE_VERSION
*/
public interface EncryptionAlgorithmLocator {
SymmetricAeadAlgorithm getEncryptionAlgorithm(JweHeader jweHeader);
}

View File

@ -1,6 +0,0 @@
package io.jsonwebtoken.security;
import io.jsonwebtoken.Identifiable;
public interface HashAlgorithm extends Identifiable {
}

View File

@ -1,20 +0,0 @@
package io.jsonwebtoken.security;
public final class HashAlgorithms {
//prevent instantiation
private HashAlgorithms() {}
private static HashAlgorithm forJcaName(final String jcaName) {
//TODO: IMPLEMENT ME
return new HashAlgorithm() {
@Override
public String getId() {
return jcaName;
}
};
}
public static final HashAlgorithm MD5 = forJcaName("MD5");
public static final HashAlgorithm SHA_1 = forJcaName("SHA-1");
public static final HashAlgorithm SHA_256 = forJcaName("SHA-256");
}

View File

@ -3,7 +3,7 @@ package io.jsonwebtoken.security;
/**
* @since JJWT_RELEASE_VERSION
*/
public interface InitializationVectorSource {
public interface InitializationVectorSupplier {
/**
* Returns the secure-random initialization vector used during encryption that must be presented in order

View File

@ -6,21 +6,15 @@ import javax.crypto.SecretKey;
import java.security.Key;
/**
* A <code><a href="https://tools.ietf.org/html/rfc7516#section-2">Key Management Algorithm</a></code> is an algorithm that
* produces a {@link SecretKey} used to encrypt or decrypt a JWE. The Key Management Algorithm used for a particular
* JWE is {@link #getId() identified} in the
* <a href="https://tools.ietf.org/html/rfc7516#section-4.1.1">JWE's {@code alg} header</a>.
* <h4>Key Management Mode</h4>
* The JWE specification indicates that all {@code Key Management Algorithm}s utilize what is called a
* {@code Key Management Mode} to indicate if the encryption key will be supplied to a JWE recipient within the
* JWE as an encrypted value. <b>This interface does not indicate which {@code Key Management Mode} is used:</b>
* the {@link #getEncryptionKey(KeyRequest) key result} may contain either an empty or populated encrypted key via
* {@link KeyResult#getPayload() result.getPayload()}.
* <p>Therefore, <b>algorithms that produce encrypted keys <em>MUST</em> implement the
* {@link EncryptedKeyAlgorithm} interface instead of this one.</b></p>
* A {@code KeyAlgorithm} produces the {@link SecretKey} used to encrypt or decrypt a JWE. The {@code KeyAlgorithm}
* used for a particular JWE is {@link #getId() identified} in the JWE's
* <a href="https://tools.ietf.org/html/rfc7516#section-4.1.1">{@code alg} header</a>.
* <p/>
* <p>The {@code KeyAlgorithm} interface is JJWT's idiomatic approach to the JWE specification's
* <code><a href="https://tools.ietf.org/html/rfc7516#section-2">{@code Key Management Mode}</a></code> concept.</p>
*
* @since JJWT_RELEASE_VERSION
* @see EncryptedKeyAlgorithm
* @see <a href="https://tools.ietf.org/html/rfc7516#section-2">RFC 7561, Section 2: JWE Key (Management) Algorithms</a>
*/
public interface KeyAlgorithm<E extends Key, D extends Key> extends Identifiable {

View File

@ -4,8 +4,6 @@ import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import javax.crypto.SecretKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Collection;
/**
@ -19,8 +17,9 @@ public final class KeyAlgorithms {
private static final String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.KeyAlgorithmsBridge";
private static final Class<?>[] ID_ARG_TYPES = new Class[]{String.class};
private static final Class<?>[] ESTIMATE_ITERATIONS_ARG_TYPES = new Class[]{KeyAlgorithm.class, long.class};
public static Collection<SymmetricAeadAlgorithm> values() {
public static Collection<KeyAlgorithm<?, ?>> values() {
return Classes.invokeStatic(BRIDGE_CLASSNAME, "values", null, (Object[]) null);
}
@ -49,16 +48,23 @@ public final class KeyAlgorithms {
}
public static final KeyAlgorithm<SecretKey, SecretKey> DIRECT = forId0("dir");
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> A128KW = forId0("A128KW");
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> A192KW = forId0("A192KW");
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> A256KW = forId0("A256KW");
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> A128GCMKW = forId0("A128GCMKW");
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> A192GCMKW = forId0("A192GCMKW");
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> A256GCMKW = forId0("A256GCMKW");
public static final EncryptedKeyAlgorithm<RSAPublicKey, RSAPrivateKey> RSA1_5 = forId0("RSA1_5");
public static final EncryptedKeyAlgorithm<RSAPublicKey, RSAPrivateKey> RSA_OAEP = forId0("RSA-OAEP");
public static final EncryptedKeyAlgorithm<RSAPublicKey, RSAPrivateKey> RSA_OAEP_256 = forId0("RSA-OAEP-256");
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> PBES2_HS256_A128KW = forId0("PBES2-HS256+A128KW");
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> PBES2_HS384_A192KW = forId0("PBES2-HS384+A192KW");
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> PBES2_HS512_A256KW = forId0("PBES2-HS512+A256KW");
public static final KeyAlgorithm<SecretKey, SecretKey> A128KW = forId0("A128KW");
public static final KeyAlgorithm<SecretKey, SecretKey> A192KW = forId0("A192KW");
public static final KeyAlgorithm<SecretKey, SecretKey> A256KW = forId0("A256KW");
public static final KeyAlgorithm<SecretKey, SecretKey> A128GCMKW = forId0("A128GCMKW");
public static final KeyAlgorithm<SecretKey, SecretKey> A192GCMKW = forId0("A192GCMKW");
public static final KeyAlgorithm<SecretKey, SecretKey> A256GCMKW = forId0("A256GCMKW");
@SuppressWarnings("rawtypes")
public static final RsaKeyAlgorithm RSA1_5 = forId0("RSA1_5");
@SuppressWarnings("rawtypes")
public static final RsaKeyAlgorithm RSA_OAEP = forId0("RSA-OAEP");
@SuppressWarnings("rawtypes")
public static final RsaKeyAlgorithm RSA_OAEP_256 = forId0("RSA-OAEP-256");
public static final KeyAlgorithm<PbeKey, SecretKey> PBES2_HS256_A128KW = forId0("PBES2-HS256+A128KW");
public static final KeyAlgorithm<PbeKey, SecretKey> PBES2_HS384_A192KW = forId0("PBES2-HS384+A192KW");
public static final KeyAlgorithm<PbeKey, SecretKey> PBES2_HS512_A256KW = forId0("PBES2-HS512+A256KW");
public static int estimateIterations(KeyAlgorithm<PbeKey, SecretKey> alg, long desiredMillis) {
return Classes.invokeStatic(BRIDGE_CLASSNAME, "estimateIterations", ESTIMATE_ITERATIONS_ARG_TYPES, alg, desiredMillis);
}
}

View File

@ -9,5 +9,7 @@ import java.security.Key;
*/
public interface KeyRequest<T, K extends Key> extends CryptoRequest<T, K> {
SymmetricAeadAlgorithm getEncryptionAlgorithm();
JweHeader getHeader();
}

View File

@ -1,13 +0,0 @@
package io.jsonwebtoken.security;
import io.jsonwebtoken.Header;
import java.security.Key;
/**
* @since JJWT_RELEASE_VERSION
*/
public interface KeyResolver {
Key resolveKey(Header<?> header);
}

View File

@ -1,12 +1,9 @@
package io.jsonwebtoken.security;
import javax.crypto.SecretKey;
import java.util.Map;
/**
* @since JJWT_RELEASE_VERSION
*/
public interface KeyResult extends PayloadSupplier<byte[]>, KeySupplier<SecretKey> {
Map<String,?> getHeaderParams();
}

View File

@ -16,8 +16,10 @@
package io.jsonwebtoken.security;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import javax.crypto.SecretKey;
import javax.crypto.interfaces.PBEKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.KeyPair;
@ -28,6 +30,8 @@ import java.security.KeyPair;
*/
public final class Keys {
private static final String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.KeysBridge";
//prevent instantiation
private Keys() {
}
@ -115,7 +119,7 @@ public final class Keys {
@Deprecated
public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) throws IllegalArgumentException {
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
SignatureAlgorithm<?,?> salg = SignatureAlgorithms.forId(alg.name());
SignatureAlgorithm<?, ?> salg = SignatureAlgorithms.forId(alg.name());
if (!(salg instanceof SecretKeySignatureAlgorithm)) {
String msg = "The " + alg.name() + " algorithm does not support shared secret keys.";
throw new IllegalArgumentException(msg);
@ -210,12 +214,20 @@ public final class Keys {
@Deprecated
public static KeyPair keyPairFor(io.jsonwebtoken.SignatureAlgorithm alg) throws IllegalArgumentException {
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
SignatureAlgorithm<?,?> salg = SignatureAlgorithms.forId(alg.name());
SignatureAlgorithm<?, ?> salg = SignatureAlgorithms.forId(alg.name());
if (!(salg instanceof AsymmetricKeySignatureAlgorithm)) {
String msg = "The " + alg.name() + " algorithm does not support Key Pairs.";
throw new IllegalArgumentException(msg);
}
AsymmetricKeySignatureAlgorithm<?,?> asalg = ((AsymmetricKeySignatureAlgorithm<?,?>) salg);
AsymmetricKeySignatureAlgorithm<?, ?> asalg = ((AsymmetricKeySignatureAlgorithm<?, ?>) salg);
return asalg.generateKeyPair();
}
public static PbeKey toPbeKey(PBEKey key) {
return forPbe().forKey(key).build();
}
public static PbeKeyBuilder<PbeKey> forPbe() {
return Classes.invokeStatic(BRIDGE_CLASSNAME, "forPbe", null, (Object[]) null);
}
}

View File

@ -15,9 +15,9 @@ public abstract class LocatorAdapter<H extends Header<H>, R> implements Locator<
return locate((JwsHeader) header);
} else if (header instanceof JweHeader) {
return locate((JweHeader) header);
} else {
return doLocate(header);
}
String msg = "Unrecognized header type: " + header.getClass().getName();
throw new IllegalStateException(msg);
}
protected R locate(JweHeader header) {
@ -27,4 +27,8 @@ public abstract class LocatorAdapter<H extends Header<H>, R> implements Locator<
protected R locate(JwsHeader header) {
return null;
}
protected R doLocate(Header<?> header) {
return null;
}
}

View File

@ -0,0 +1,11 @@
package io.jsonwebtoken.security;
import javax.crypto.SecretKey;
public interface PbeKey extends SecretKey {
char[] getPassword();
int getWorkFactor();
}

View File

@ -0,0 +1,16 @@
package io.jsonwebtoken.security;
import javax.crypto.interfaces.PBEKey;
public interface PbeKeyBuilder<K extends PbeKey> {
PbeKeyBuilder<K> forKey(PBEKey jcaKey);
PbeKeyBuilder<K> setPassword(String password);
PbeKeyBuilder<K> setPassword(char[] password);
PbeKeyBuilder<K> setWorkFactor(int workFactor);
K build();
}

View File

@ -0,0 +1,8 @@
package io.jsonwebtoken.security;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAKey;
public interface RsaKeyAlgorithm<EK extends RSAKey & PublicKey, DK extends RSAKey & PrivateKey> extends KeyAlgorithm<EK, DK> {
}

View File

@ -7,7 +7,7 @@ import io.jsonwebtoken.Identifiable;
*/
public interface SymmetricAeadAlgorithm extends Identifiable, SecretKeyGenerator {
AeadResult encrypt(SymmetricAeadRequest request) throws CryptoException, KeyException;
AeadResult encrypt(SymmetricAeadRequest request) throws SecurityException;
PayloadSupplier<byte[]> decrypt(SymmetricAeadDecryptionRequest request) throws CryptoException, KeyException;
PayloadSupplier<byte[]> decrypt(SymmetricAeadDecryptionRequest request) throws SecurityException;
}

View File

@ -3,5 +3,5 @@ package io.jsonwebtoken.security;
/**
* @since JJWT_RELEASE_VERSION
*/
public interface SymmetricAeadDecryptionRequest extends SymmetricAeadRequest, InitializationVectorSource, AuthenticationTagSource {
public interface SymmetricAeadDecryptionRequest extends SymmetricAeadRequest, InitializationVectorSupplier, DigestSupplier {
}

View File

@ -5,5 +5,5 @@ import javax.crypto.SecretKey;
/**
* @since JJWT_RELEASE_VERSION
*/
public interface SymmetricAeadRequest extends CryptoRequest<byte[], SecretKey>, AssociatedDataSource {
public interface SymmetricAeadRequest extends CryptoRequest<byte[], SecretKey>, AssociatedDataSupplier {
}

View File

@ -5,7 +5,5 @@ import java.security.Key;
/**
* @since JJWT_RELEASE_VERSION
*/
public interface VerifySignatureRequest<K extends Key> extends SignatureRequest<K> {
byte[] getSignature();
public interface VerifySignatureRequest<K extends Key> extends SignatureRequest<K>, DigestSupplier {
}

View File

@ -7,7 +7,6 @@ import io.jsonwebtoken.impl.lang.Function;
import io.jsonwebtoken.impl.lang.PropagatingExceptionFunction;
import io.jsonwebtoken.impl.lang.Services;
import io.jsonwebtoken.impl.security.DefaultKeyRequest;
import io.jsonwebtoken.impl.security.DefaultPBEKey;
import io.jsonwebtoken.impl.security.DefaultSymmetricAeadRequest;
import io.jsonwebtoken.io.SerializationException;
import io.jsonwebtoken.io.Serializer;
@ -16,25 +15,24 @@ import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.lang.Strings;
import io.jsonwebtoken.security.AeadResult;
import io.jsonwebtoken.security.EncryptedKeyAlgorithm;
import io.jsonwebtoken.security.KeyAlgorithm;
import io.jsonwebtoken.security.KeyAlgorithms;
import io.jsonwebtoken.security.KeyRequest;
import io.jsonwebtoken.security.KeyResult;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.PbeKey;
import io.jsonwebtoken.security.SecurityException;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
import io.jsonwebtoken.security.SymmetricAeadRequest;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.interfaces.PBEKey;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Map;
public class DefaultJweBuilder extends DefaultJwtBuilder<JweBuilder> implements JweBuilder {
private static final SecretKey EMPTY_SECRET_KEY = new SecretKeySpec("NONE".getBytes(StandardCharsets.UTF_8), "NONE");
private SymmetricAeadAlgorithm enc; // MUST be Symmetric AEAD per https://tools.ietf.org/html/rfc7516#section-4.1.2
private Function<SymmetricAeadRequest, AeadResult> encFunction;
@ -82,6 +80,12 @@ public class DefaultJweBuilder extends DefaultJwtBuilder<JweBuilder> implements
@Override
public JweBuilder withKey(SecretKey key) {
if (key instanceof PBEKey) {
key = Keys.toPbeKey((PBEKey) key);
}
if (key instanceof PbeKey) {
return withKeyFrom((PbeKey) key, KeyAlgorithms.PBES2_HS512_A256KW);
}
return withKeyFrom(key, KeyAlgorithms.DIRECT);
}
@ -104,11 +108,6 @@ public class DefaultJweBuilder extends DefaultJwtBuilder<JweBuilder> implements
return this;
}
@Override
public JweBuilder withKeyFrom(char[] password, int iterations, EncryptedKeyAlgorithm<SecretKey, SecretKey> alg) {
return withKeyFrom(new DefaultPBEKey(password, iterations, alg.getId()), alg);
}
@Override
public String compact() {
@ -146,15 +145,13 @@ public class DefaultJweBuilder extends DefaultJwtBuilder<JweBuilder> implements
jweHeader.setCompressionAlgorithm(compressionCodec.getAlgorithmName());
}
SecretKey cek = alg instanceof EncryptedKeyAlgorithm ? enc.generateKey() : EMPTY_SECRET_KEY; //for algorithms that don't need one
KeyRequest<SecretKey, Key> keyRequest = new DefaultKeyRequest<>(this.provider, this.secureRandom, cek, this.key, jweHeader);
KeyRequest<SecretKey, Key> keyRequest = new DefaultKeyRequest<>(this.provider, this.secureRandom, null, this.key, jweHeader, enc);
KeyResult keyResult = algFunction.apply(keyRequest);
Assert.state(keyResult != null, "KeyAlgorithm must return a KeyResult.");
cek = Assert.notNull(keyResult.getKey(), "KeyResult must return a content encryption key.");
SecretKey cek = Assert.notNull(keyResult.getKey(), "KeyResult must return a content encryption key.");
byte[] encryptedCek = Assert.notNull(keyResult.getPayload(), "KeyResult must return an encrypted key byte array, even if empty.");
jweHeader.putAll(keyResult.getHeaderParams());
jweHeader.setAlgorithm(alg.getId());
jweHeader.setEncryptionAlgorithm(enc.getId());
@ -167,7 +164,7 @@ public class DefaultJweBuilder extends DefaultJwtBuilder<JweBuilder> implements
byte[] iv = Assert.notEmpty(encResult.getInitializationVector(), "Encryption result must have a non-empty initialization vector.");
byte[] ciphertext = Assert.notEmpty(encResult.getPayload(), "Encryption result must have non-empty ciphertext (result.getData()).");
byte[] tag = Assert.notEmpty(encResult.getAuthenticationTag(), "Encryption result must have a non-empty authentication tag.");
byte[] tag = Assert.notEmpty(encResult.getDigest(), "Encryption result must have a non-empty authentication tag.");
String base64UrlEncodedEncryptedCek = base64UrlEncoder.encode(encryptedCek);
String base64UrlEncodedIv = base64UrlEncoder.encode(iv);

View File

@ -122,13 +122,13 @@ public class DefaultJwtBuilder<T extends JwtBuilder<T>> implements JwtBuilder<T>
}
@Override
public T setHeader(Map<String, Object> header) {
public T setHeader(Map<String, ?> header) {
this.header = new DefaultHeader<>(header);
return (T)this;
}
@Override
public T setHeaderParams(Map<String, Object> params) {
public T setHeaderParams(Map<String, ?> params) {
if (!Collections.isEmpty(params)) {
Header<?> header = ensureHeader();
header.putAll(params);
@ -237,7 +237,7 @@ public class DefaultJwtBuilder<T extends JwtBuilder<T>> implements JwtBuilder<T>
}
@Override
public T addClaims(Map<String, Object> claims) {
public T addClaims(Map<String, ?> claims) {
ensureClaims().putAll(claims);
return (T)this;
}

View File

@ -455,7 +455,7 @@ public class DefaultJwtParser implements JwtParser {
throw new UnsupportedJwtException(msg);
}
KeyRequest<byte[], ?> request = new DefaultKeyRequest<>(this.provider, null, cekBytes, key, jweHeader);
KeyRequest<byte[], ?> request = new DefaultKeyRequest<>(this.provider, null, cekBytes, key, jweHeader, encAlg);
final SecretKey cek = keyAlg.getDecryptionKey(request);
SymmetricAeadDecryptionRequest decryptRequest =

View File

@ -216,7 +216,7 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
@Override
public JwtParserBuilder addEncryptionAlgorithms(Collection<SymmetricAeadAlgorithm> encAlgs) {
Assert.notEmpty(encAlgs, "Additional EncryptionAlgorithm collection cannot be null or empty.");
Assert.notEmpty(encAlgs, "Additional SymmetricAeadAlgorithm collection cannot be null or empty.");
this.extraEncryptionAlgorithms.addAll(encAlgs);
return this;
}

View File

@ -66,7 +66,7 @@ public final class Bytes {
return ints;
}
public static byte[] plus(byte[]... arrays) {
public static byte[] concat(byte[]... arrays) {
int len = 0;
int count = Arrays.length(arrays);
for(int i = 0; i < count; i++) {
@ -90,6 +90,14 @@ public final class Bytes {
return bytes == null ? 0 : bytes.length * (long)Byte.SIZE;
}
public static String bitsMsg(long bitLength) {
return bitLength + " bits (" + bitLength / Byte.SIZE + " bytes)";
}
public static String bytesMsg(int byteArrayLength) {
return bitsMsg((long)byteArrayLength * Byte.SIZE);
}
public static void increment(byte[] a) {
for (int i = a.length - 1; i >= 0; --i) {
if (++a[i] != 0) {

View File

@ -1,6 +0,0 @@
package io.jsonwebtoken.impl.lang;
public interface Supplier<T> {
T get();
}

View File

@ -0,0 +1,18 @@
package io.jsonwebtoken.impl.lang;
import java.math.BigInteger;
public interface ValueGetter {
String getRequiredString(String key);
int getRequiredInteger(String key);
int getRequiredPositiveInteger(String key);
byte[] getRequiredBytes(String key);
byte[] getRequiredBytes(String key, int requiredByteLength);
BigInteger getRequiredBigInt(String key, boolean sensitive);
}

View File

@ -96,8 +96,9 @@ abstract class AbstractEcJwkFactory<K extends Key & ECKey, J extends Jwk<K>> ext
}
/**
* Returns {@code true} if a given elliptic curve contains the specified {@code point}, {@code false} otherwise.
* Assumes elliptic curves over finite fields adhering to the reduced (a.k.a short or narrow) Weierstrass form:
* Returns {@code true} if a given elliptic {@code curve} contains the specified {@code point}, {@code false}
* otherwise. Assumes elliptic curves over finite fields adhering to the reduced (a.k.a short or narrow)
* Weierstrass form:
* <p>
* <code>y<sup>2</sup> = x<sup>3</sup> + ax + b</code>
* </p>
@ -125,24 +126,6 @@ abstract class AbstractEcJwkFactory<K extends Key & ECKey, J extends Jwk<K>> ext
return lhs.equals(rhs);
}
protected ECPublicKey derivePublic(final JwkContext<ECPrivateKey> ctx) {
final ECPrivateKey key = ctx.getKey();
final ECParameterSpec params = key.getParams();
final ECPoint w = multiply(params.getGenerator(), key.getS(), params);
final ECPublicKeySpec spec = new ECPublicKeySpec(w, params);
return generateKey(ctx, ECPublicKey.class, new CheckedFunction<KeyFactory, ECPublicKey>() {
@Override
public ECPublicKey apply(KeyFactory kf) {
try {
return (ECPublicKey) kf.generatePublic(spec);
} catch (Exception e) {
String msg = "Unable to derive ECPublicKey from ECPrivateKey {" + ctx + "}.";
throw new UnsupportedKeyException(msg);
}
}
});
}
/**
* Multiply a point {@code p} by scalar {@code s} on the curve identified by {@code spec}.
*
@ -217,4 +200,22 @@ abstract class AbstractEcJwkFactory<K extends Key & ECKey, J extends Jwk<K>> ext
AbstractEcJwkFactory(Class<K> keyType) {
super(DefaultEcPublicJwk.TYPE_VALUE, keyType);
}
protected ECPublicKey derivePublic(final JwkContext<ECPrivateKey> ctx) {
final ECPrivateKey key = ctx.getKey();
final ECParameterSpec params = key.getParams();
final ECPoint w = multiply(params.getGenerator(), key.getS(), params);
final ECPublicKeySpec spec = new ECPublicKeySpec(w, params);
return generateKey(ctx, ECPublicKey.class, new CheckedFunction<KeyFactory, ECPublicKey>() {
@Override
public ECPublicKey apply(KeyFactory kf) {
try {
return (ECPublicKey) kf.generatePublic(spec);
} catch (Exception e) {
String msg = "Unable to derive ECPublicKey from ECPrivateKey {" + ctx + "}.";
throw new UnsupportedKeyException(msg);
}
}
});
}
}

View File

@ -1,13 +1,10 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Strings;
import io.jsonwebtoken.security.InvalidKeyException;
import io.jsonwebtoken.security.Jwk;
import io.jsonwebtoken.security.MalformedKeyException;
import java.math.BigInteger;
import java.security.Key;
@ -17,35 +14,6 @@ import java.security.spec.InvalidKeySpecException;
abstract class AbstractFamilyJwkFactory<K extends Key, J extends Jwk<K>> implements FamilyJwkFactory<K, J> {
static void malformed(String msg) {
throw new MalformedKeyException(msg);
}
static String getRequiredString(JwkContext<?> ctx, String name) {
Assert.notNull(ctx, "JWK map cannot be null or empty.");
Object value = ctx.get(name);
if (value == null) {
malformed("JWK is missing required case-sensitive '" + name + "' member.");
}
String s = String.valueOf(value);
if (!Strings.hasText(s)) {
malformed("JWK '" + name + "' member cannot be null or empty.");
}
return s;
}
static BigInteger getRequiredBigInt(JwkContext<?> ctx, String name, boolean sensitive) {
String s = getRequiredString(ctx, name);
try {
byte[] bytes = Decoders.BASE64URL.decode(s);
return new BigInteger(1, bytes);
} catch (Exception e) {
String val = sensitive ? AbstractJwk.REDACTED_VALUE : s;
String msg = "Unable to decode JWK member '" + name + "' to BigInteger from value: " + val;
throw new MalformedKeyException(msg, e);
}
}
// Copied from Apache Commons Codec 1.14:
// https://github.com/apache/commons-codec/blob/af7b94750e2178b8437d9812b28e36ac87a455f2/src/main/java/org/apache/commons/codec/binary/Base64.java#L746-L775
static byte[] toUnsignedBytes(BigInteger bigInt) {
@ -109,9 +77,9 @@ abstract class AbstractFamilyJwkFactory<K extends Key, J extends Jwk<K>> impleme
}
protected <T extends Key> T generateKey(final JwkContext<?> ctx, final Class<T> type, final CheckedFunction<KeyFactory, T> fn) {
return new JcaTemplate(getId(), ctx.getProvider()).execute(KeyFactory.class, new InstanceCallback<KeyFactory, T>() {
return new JcaTemplate(getId(), ctx.getProvider()).execute(KeyFactory.class, new CheckedFunction<KeyFactory, T>() {
@Override
public T doWithInstance(KeyFactory instance) throws Exception {
public T apply(KeyFactory instance) throws Exception {
try {
return fn.apply(instance);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {

View File

@ -62,10 +62,7 @@ abstract class AbstractJwk<K extends Key> implements Jwk<K> {
@Override
public boolean containsKey(Object key) {
if (key instanceof String) {
return this.context.containsKey((String) key);
}
return false;
return this.context.containsKey(key);
}
@Override
@ -75,10 +72,7 @@ abstract class AbstractJwk<K extends Key> implements Jwk<K> {
@Override
public Object get(Object key) {
if (key instanceof String) {
return this.context.get((String) key);
}
return null;
return this.context.get(key);
}
@Override
@ -132,9 +126,8 @@ abstract class AbstractJwk<K extends Key> implements Jwk<K> {
@Override
public boolean equals(Object obj) {
if (obj instanceof AbstractJwk) {
AbstractJwk<?> other = (AbstractJwk<?>) obj;
return this.context.equals(other.context);
if (obj instanceof Map) {
return this.context.equals(obj);
}
return false;
}

View File

@ -0,0 +1,27 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.security.SecurityRequest;
import java.security.Provider;
import java.security.SecureRandom;
abstract class AbstractSecurityRequest implements SecurityRequest {
private final Provider provider;
private final SecureRandom secureRandom;
public AbstractSecurityRequest(Provider provider, SecureRandom secureRandom) {
this.provider = provider;
this.secureRandom = secureRandom;
}
@Override
public Provider getProvider() {
return this.provider;
}
@Override
public SecureRandom getSecureRandom() {
return this.secureRandom;
}
}

View File

@ -45,7 +45,7 @@ abstract class AbstractSignatureAlgorithm<SK extends Key, VK extends Key> extend
public boolean verify(VerifySignatureRequest<VK> request) throws SecurityException {
final VK key = Assert.notNull(request.getKey(), "Request key cannot be null.");
Assert.notEmpty(request.getPayload(), "Request payload cannot be null or empty.");
Assert.notEmpty(request.getSignature(), "Request signature byte array cannot be null or empty.");
Assert.notEmpty(request.getDigest(), "Request signature byte array cannot be null or empty.");
try {
validateKey(key, false);
return doVerify(request);
@ -59,7 +59,7 @@ abstract class AbstractSignatureAlgorithm<SK extends Key, VK extends Key> extend
}
protected boolean doVerify(VerifySignatureRequest<VK> request) throws Exception {
byte[] providedSignature = request.getSignature();
byte[] providedSignature = request.getDigest();
Assert.notEmpty(providedSignature, "Request signature byte array cannot be null or empty.");
@SuppressWarnings("unchecked") byte[] computedSignature = sign((SignatureRequest<SK>)request);
return MessageDigest.isEqual(providedSignature, computedSignature);

View File

@ -1,12 +1,13 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.AssociatedDataSource;
import io.jsonwebtoken.security.CryptoRequest;
import io.jsonwebtoken.security.InitializationVectorSource;
import io.jsonwebtoken.security.SecurityRequest;
import io.jsonwebtoken.security.AssociatedDataSupplier;
import io.jsonwebtoken.security.InitializationVectorSupplier;
import io.jsonwebtoken.security.KeySupplier;
import io.jsonwebtoken.security.SecretKeyGenerator;
import io.jsonwebtoken.security.SecurityRequest;
import io.jsonwebtoken.security.WeakKeyException;
import javax.crypto.SecretKey;
@ -29,41 +30,41 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerato
"requests that do not include initialization vectors. AES ciphertext without an IV is weak and " +
"susceptible to attack.";
protected final int keyLength;
protected final int ivLength;
protected final int tagLength;
protected final int keyBitLength;
protected final int ivBitLength;
protected final int tagBitLength;
protected final boolean gcm;
AesAlgorithm(String id, String jcaTransformation, int keyLength) {
AesAlgorithm(String id, String jcaTransformation, int keyBitLength) {
super(id, jcaTransformation);
Assert.isTrue(keyLength == 128 || keyLength == 192 || keyLength == 256, "Invalid AES key length: it must equal 128, 192, or 256.");
this.keyLength = keyLength;
Assert.isTrue(keyBitLength == 128 || keyBitLength == 192 || keyBitLength == 256, "Invalid AES key length: it must equal 128, 192, or 256.");
this.keyBitLength = keyBitLength;
this.gcm = jcaTransformation.startsWith("AES/GCM");
this.ivLength = jcaTransformation.equals("AESWrap") ? 0 : (this.gcm ? GCM_IV_SIZE : BLOCK_SIZE);
// https://tools.ietf.org/html/rfc7518#section-5.2.3 through ttps://tools.ietf.org/html/rfc7518#section-5.3 :
this.tagLength = this.gcm ? BLOCK_SIZE : this.keyLength;
this.ivBitLength = jcaTransformation.equals("AESWrap") ? 0 : (this.gcm ? GCM_IV_SIZE : BLOCK_SIZE);
// https://tools.ietf.org/html/rfc7518#section-5.2.3 through https://tools.ietf.org/html/rfc7518#section-5.3 :
this.tagBitLength = this.gcm ? BLOCK_SIZE : this.keyBitLength;
}
@Override
public SecretKey generateKey() {
return new JcaTemplate(KEY_ALG_NAME, null).generateSecretKey(this.keyLength);
return new JcaTemplate(KEY_ALG_NAME, null).generateSecretKey(this.keyBitLength);
//TODO: assert generated key length?
}
protected SecretKey assertKey(CryptoRequest<?,SecretKey> request) {
protected SecretKey assertKey(KeySupplier<? extends SecretKey> request) {
SecretKey key = Assert.notNull(request.getKey(), "Request key cannot be null.");
validateLengthIfPossible(key);
return key;
}
private void validateLengthIfPossible(SecretKey key) {
validateLength(key, this.keyLength, false);
validateLength(key, this.keyBitLength, false);
}
protected static String lengthMsg(String id, String type, int requiredLengthInBits, int actualLengthInBits) {
return "The '" + id + "' algorithm requires " + type + " with a length of " + requiredLengthInBits +
" bits (" + (requiredLengthInBits / Byte.SIZE) + " bytes). The provided key has a length of " +
actualLengthInBits + " bits (" + actualLengthInBits / Byte.SIZE + " bytes).";
return "The '" + id + "' algorithm requires " + type + " with a length of " +
Bytes.bitsMsg(requiredLengthInBits) + ". The provided key has a length of " +
Bytes.bitsMsg(actualLengthInBits) + ".";
}
protected byte[] validateLength(SecretKey key, int requiredBitLength, boolean propagate) {
@ -88,8 +89,8 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerato
byte[] assertIvLength(final byte[] iv) {
int length = length(iv);
if ((this.ivLength / Byte.SIZE) != length) {
String msg = lengthMsg(getId(), "initialization vectors", this.ivLength, length * Byte.SIZE);
if ((this.ivBitLength / Byte.SIZE) != length) {
String msg = lengthMsg(getId(), "initialization vectors", this.ivBitLength, length * Byte.SIZE);
throw new IllegalArgumentException(msg);
}
return iv;
@ -97,14 +98,14 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerato
byte[] assertTag(byte[] tag) {
int len = Arrays.length(tag) * Byte.SIZE;
if (this.tagLength != len) {
String msg = lengthMsg(getId(), "authentication tags", this.tagLength, len);
if (this.tagBitLength != len) {
String msg = lengthMsg(getId(), "authentication tags", this.tagBitLength, len);
throw new IllegalArgumentException(msg);
}
return tag;
}
byte[] assertDecryptionIv(InitializationVectorSource src) throws IllegalArgumentException {
byte[] assertDecryptionIv(InitializationVectorSupplier src) throws IllegalArgumentException {
byte[] iv = src.getInitializationVector();
Assert.notEmpty(iv, DECRYPT_NO_IV);
return assertIvLength(iv);
@ -112,10 +113,10 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerato
protected byte[] ensureInitializationVector(SecurityRequest request) {
byte[] iv = null;
if (request instanceof InitializationVectorSource) {
iv = Arrays.clean(((InitializationVectorSource) request).getInitializationVector());
if (request instanceof InitializationVectorSupplier) {
iv = Arrays.clean(((InitializationVectorSupplier) request).getInitializationVector());
}
int ivByteLength = this.ivLength / Byte.SIZE;
int ivByteLength = this.ivBitLength / Byte.SIZE;
if (iv == null || iv.length == 0) {
iv = new byte[ivByteLength];
SecureRandom random = ensureSecureRandom(request);
@ -135,16 +136,9 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerato
protected byte[] getAAD(SecurityRequest request) {
byte[] aad = null;
if (request instanceof AssociatedDataSource) {
aad = Arrays.clean(((AssociatedDataSource) request).getAssociatedData());
if (request instanceof AssociatedDataSupplier) {
aad = Arrays.clean(((AssociatedDataSupplier) request).getAssociatedData());
}
return aad;
}
protected byte[] plus(byte[] a, byte[] b) {
byte[] c = new byte[length(a) + length(b)];
System.arraycopy(a, 0, c, 0, a.length);
System.arraycopy(b, 0, c, a.length, b.length);
return c;
}
}

View File

@ -1,27 +1,26 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.JweHeader;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.impl.lang.ValueGetter;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Maps;
import io.jsonwebtoken.security.EncryptedKeyAlgorithm;
import io.jsonwebtoken.security.KeyAlgorithm;
import io.jsonwebtoken.security.KeyRequest;
import io.jsonwebtoken.security.KeyResult;
import io.jsonwebtoken.security.SecurityException;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import java.security.Key;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Map;
/**
* @since JJWT_RELEASE_VERSION
*/
public class AesGcmKeyAlgorithm extends AesAlgorithm implements EncryptedKeyAlgorithm<SecretKey, SecretKey> {
public class AesGcmKeyAlgorithm extends AesAlgorithm implements KeyAlgorithm<SecretKey, SecretKey> {
public static final String TRANSFORMATION = "AES/GCM/NoPadding";
@ -34,19 +33,20 @@ public class AesGcmKeyAlgorithm extends AesAlgorithm implements EncryptedKeyAlgo
Assert.notNull(request, "request cannot be null.");
final SecretKey kek = assertKey(request);
final SecretKey cek = Assert.notNull(request.getPayload(), "Request content encryption key (request.getPayload()) cannot be null.");
SymmetricAeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null.");
final SecretKey cek = Assert.notNull(enc.generateKey(), "Request encryption algorithm cannot generate a null key.");
final byte[] iv = ensureInitializationVector(request);
final AlgorithmParameterSpec ivSpec = getIvSpec(iv);
byte[] taggedCiphertext = execute(request, Cipher.class, new InstanceCallback<Cipher, byte[]>() {
byte[] taggedCiphertext = execute(request, Cipher.class, new CheckedFunction<Cipher, byte[]>() {
@Override
public byte[] doWithInstance(Cipher cipher) throws Exception {
public byte[] apply(Cipher cipher) throws Exception {
cipher.init(Cipher.WRAP_MODE, kek, ivSpec);
return cipher.wrap(cek);
}
});
int tagByteLength = this.tagLength / Byte.SIZE;
int tagByteLength = this.tagBitLength / Byte.SIZE;
// When using GCM mode, the JDK appends the authentication tag to the ciphertext, so let's extract it:
int ciphertextLength = taggedCiphertext.length - tagByteLength;
byte[] ciphertext = new byte[ciphertextLength];
@ -56,9 +56,10 @@ public class AesGcmKeyAlgorithm extends AesAlgorithm implements EncryptedKeyAlgo
String encodedIv = Encoders.BASE64URL.encode(iv);
String encodedTag = Encoders.BASE64URL.encode(tag);
Map<String,String> extraParams = Maps.of("iv", encodedIv).and("tag", encodedTag).build();
request.getHeader().put("iv", encodedIv);
request.getHeader().put("tag", encodedTag);
return new DefaultKeyResult(ciphertext, cek, extraParams);
return new DefaultKeyResult(cek, ciphertext);
}
@Override
@ -67,53 +68,22 @@ public class AesGcmKeyAlgorithm extends AesAlgorithm implements EncryptedKeyAlgo
final SecretKey kek = assertKey(request);
final byte[] cekBytes = Assert.notEmpty(request.getPayload(), "Decryption request payload (ciphertext) cannot be null or empty.");
final JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null.");
final byte[] tag = getHeaderByteArray(header, "tag", this.tagLength / Byte.SIZE);
final byte[] iv = getHeaderByteArray(header, "iv", this.ivLength / Byte.SIZE);
final ValueGetter getter = new DefaultValueGetter(header);
final byte[] tag = getter.getRequiredBytes("tag", this.tagBitLength / Byte.SIZE);
final byte[] iv = getter.getRequiredBytes("iv", this.ivBitLength / Byte.SIZE);
final AlgorithmParameterSpec ivSpec = getIvSpec(iv);
//for tagged GCM, the JCA spec requires that the tag be appended to the end of the ciphertext byte array:
final byte[] taggedCiphertext = plus(cekBytes, tag);
final byte[] taggedCiphertext = Bytes.concat(cekBytes, tag);
return execute(request, Cipher.class, new InstanceCallback<Cipher, SecretKey>() {
return execute(request, Cipher.class, new CheckedFunction<Cipher, SecretKey>() {
@Override
public SecretKey doWithInstance(Cipher cipher) throws Exception {
public SecretKey apply(Cipher cipher) throws Exception {
cipher.init(Cipher.UNWRAP_MODE, kek, ivSpec);
Key key = cipher.unwrap(taggedCiphertext, KEY_ALG_NAME, Cipher.SECRET_KEY);
Assert.state(key instanceof SecretKey, "cipher.unwrap must produce a SecretKey instance.");
return (SecretKey)key;
return (SecretKey) key;
}
});
}
private byte[] getHeaderByteArray(JweHeader header, String name, int requiredByteLength) {
Object value = header.get(name);
if (value == null) {
String msg = "The " + getId() + " Key Management Algorithm requires a JweHeader '" + name + "' value.";
throw new MalformedJwtException(msg);
}
if (!(value instanceof String)) {
String msg = "The " + getId() + " Key Management Algorithm requires the JweHeader '" + name + "' value to be a Base64URL-encoded String. Actual type: " + value.getClass().getName();
throw new MalformedJwtException(msg);
}
String encoded = (String)value;
byte[] decoded;
try {
decoded = Decoders.BASE64URL.decode(encoded);
} catch (Exception e) {
String msg = "JweHeader '" + name + "' value '" + encoded +
"' does not appear to be a valid Base64URL String: " + e.getMessage();
throw new MalformedJwtException(msg, e);
}
int len = Arrays.length(decoded);
if (len != requiredByteLength) {
String msg = "The '" + getId() + "' key management algorithm requires the JweHeader '" + name +
"' value to be " + (requiredByteLength * Byte.SIZE) + " bits (" + requiredByteLength +
" bytes) in length. Actual length: " + (len * Byte.SIZE) + " bits (" + len + " bytes).";
throw new MalformedJwtException(msg);
}
return decoded;
}
}

View File

@ -1,10 +1,12 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.EncryptedKeyAlgorithm;
import io.jsonwebtoken.security.KeyAlgorithm;
import io.jsonwebtoken.security.KeyRequest;
import io.jsonwebtoken.security.KeyResult;
import io.jsonwebtoken.security.SecurityException;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
@ -13,7 +15,7 @@ import java.security.Key;
/**
* @since JJWT_RELEASE_VERSION
*/
public class AesWrapKeyAlgorithm extends AesAlgorithm implements EncryptedKeyAlgorithm<SecretKey, SecretKey> {
public class AesWrapKeyAlgorithm extends AesAlgorithm implements KeyAlgorithm<SecretKey, SecretKey> {
private static final String TRANSFORMATION = "AESWrap";
@ -25,17 +27,19 @@ public class AesWrapKeyAlgorithm extends AesAlgorithm implements EncryptedKeyAlg
public KeyResult getEncryptionKey(KeyRequest<SecretKey, SecretKey> request) throws SecurityException {
Assert.notNull(request, "request cannot be null.");
final SecretKey kek = assertKey(request);
final SecretKey cek = Assert.notNull(request.getPayload(), "Request content encryption key (request.getPayload()) cannot be null.");
SymmetricAeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null.");
final SecretKey cek = enc.generateKey();
Assert.notNull(cek, "Request encryption algorithm cannot generate a null key.");
byte[] ciphertext = execute(request, Cipher.class, new InstanceCallback<Cipher, byte[]>() {
byte[] ciphertext = execute(request, Cipher.class, new CheckedFunction<Cipher, byte[]>() {
@Override
public byte[] doWithInstance(Cipher cipher) throws Exception {
public byte[] apply(Cipher cipher) throws Exception {
cipher.init(Cipher.WRAP_MODE, kek);
return cipher.wrap(cek);
}
});
return new DefaultKeyResult(ciphertext, cek);
return new DefaultKeyResult(cek, ciphertext);
}
@Override
@ -44,9 +48,9 @@ public class AesWrapKeyAlgorithm extends AesAlgorithm implements EncryptedKeyAlg
final SecretKey kek = assertKey(request);
final byte[] cekBytes = Assert.notEmpty(request.getPayload(), "Request encrypted key (request.getPayload()) cannot be null or empty.");
return execute(request, Cipher.class, new InstanceCallback<Cipher, SecretKey>() {
return execute(request, Cipher.class, new CheckedFunction<Cipher, SecretKey>() {
@Override
public SecretKey doWithInstance(Cipher cipher) throws Exception {
public SecretKey apply(Cipher cipher) throws Exception {
cipher.init(Cipher.UNWRAP_MODE, kek);
Key key = cipher.unwrap(cekBytes, KEY_ALG_NAME, Cipher.SECRET_KEY);
Assert.state(key instanceof SecretKey, "Cipher unwrap must return a SecretKey instance.");

View File

@ -1,5 +1,6 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.CryptoException;
@ -29,9 +30,9 @@ final class ConcatKDF extends CryptoAlgorithm {
ConcatKDF(String jcaName) {
super("ConcatKDF", jcaName);
int hashByteLength = new JcaTemplate(jcaName, null).execute(MessageDigest.class, new InstanceCallback<MessageDigest, Integer>() {
int hashByteLength = execute(MessageDigest.class, new CheckedFunction<MessageDigest, Integer>() {
@Override
public Integer doWithInstance(MessageDigest instance) {
public Integer apply(MessageDigest instance) {
return instance.getDigestLength();
}
});
@ -74,9 +75,8 @@ final class ConcatKDF extends CryptoAlgorithm {
}
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf, Section 5.8.1.1, Input requirement #2:
if (derivedKeyBitLength > MAX_DERIVED_KEY_BIT_LENGTH) {
String msg = "derivedKeyBitLength for " + getJcaName() + " derived keys may not exceed " +
MAX_DERIVED_KEY_BIT_LENGTH + " bits (" + MAX_DERIVED_KEY_BIT_LENGTH / Byte.SIZE + " bytes). " +
"Specified size: " + derivedKeyBitLength + " bits (" + derivedKeyBitLength / Byte.SIZE + " bytes).";
String msg = "derivedKeyBitLength for " + getJcaName() + "-derived keys may not exceed " +
bitsMsg(MAX_DERIVED_KEY_BIT_LENGTH) + ". Specified size: " + bitsMsg(derivedKeyBitLength) + ".";
throw new IllegalArgumentException(msg);
}
@ -94,11 +94,11 @@ final class ConcatKDF extends CryptoAlgorithm {
long inputBitLength = bitLength(counter) + bitLength(Z) + bitLength(OtherInfo);
assert inputBitLength <= MAX_HASH_INPUT_BIT_LENGTH : "Hash input is too large.";
byte[] derivedKeyBytes = new JcaTemplate(getJcaName(), null).execute(MessageDigest.class, new InstanceCallback<MessageDigest, byte[]>() {
byte[] derivedKeyBytes = new JcaTemplate(getJcaName(), null).execute(MessageDigest.class, new CheckedFunction<MessageDigest, byte[]>() {
@Override
public byte[] doWithInstance(MessageDigest md) throws Exception {
public byte[] apply(MessageDigest md) throws Exception {
final ByteArrayOutputStream stream = new ByteArrayOutputStream((int)derivedKeyByteLength);
final ByteArrayOutputStream stream = new ByteArrayOutputStream((int) derivedKeyByteLength);
long kLastIndex = reps - 1;
// Section 5.8.1.1, Process step #5:
@ -118,7 +118,7 @@ final class ConcatKDF extends CryptoAlgorithm {
// Section 5.8.1.1, Process step #6:
if (i == kLastIndex && repsd != (double) reps) { //repsd calculation above didn't result in a whole number:
long leftmostBitLength = derivedKeyBitLength % hashBitLength;
int leftmostByteLength = (int)(leftmostBitLength / Byte.SIZE);
int leftmostByteLength = (int) (leftmostBitLength / Byte.SIZE);
byte[] kLast = new byte[leftmostByteLength];
System.arraycopy(Ki, 0, kLast, 0, kLast.length);
Ki = kLast;

View File

@ -1,6 +1,7 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.Identifiable;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.SecurityRequest;
@ -30,17 +31,20 @@ abstract class CryptoAlgorithm implements Identifiable {
}
SecureRandom ensureSecureRandom(SecurityRequest request) {
Assert.notNull(request, "request cannot be null.");
SecureRandom random = request.getSecureRandom();
SecureRandom random = request != null ? request.getSecureRandom() : null;
return random != null ? random : Randoms.secureRandom();
}
protected <I, T> T execute(SecurityRequest request, Class<I> clazz, InstanceCallback<I, T> callback) {
protected <T, R> R execute(Class<T> clazz, CheckedFunction<T, R> fn) {
return new JcaTemplate(getJcaName(), null).execute(clazz, fn);
}
protected <I, T> T execute(SecurityRequest request, Class<I> clazz, CheckedFunction<I, T> fn) {
Assert.notNull(request, "request cannot be null.");
Provider provider = request.getProvider();
SecureRandom random = ensureSecureRandom(request);
JcaTemplate template = new JcaTemplate(getJcaName(), provider, random);
return template.execute(clazz, callback);
return template.execute(clazz, fn);
}
@Override

View File

@ -1,8 +1,8 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.SymmetricAeadDecryptionRequest;
import io.jsonwebtoken.security.AeadResult;
import io.jsonwebtoken.security.SymmetricAeadDecryptionRequest;
import javax.crypto.SecretKey;
import java.security.Provider;
@ -19,7 +19,7 @@ public class DefaultAeadResult extends DefaultSymmetricAeadRequest implements Ae
}
@Override
public byte[] getAuthenticationTag() {
public byte[] getDigest() {
return this.TAG;
}
}

View File

@ -13,8 +13,8 @@ public class DefaultCryptoRequest<T, K extends Key> extends DefaultPayloadSuppli
private final SecureRandom secureRandom;
private final K key;
public DefaultCryptoRequest(Provider provider, SecureRandom secureRandom, T data, K key) {
super(data);
public DefaultCryptoRequest(Provider provider, SecureRandom secureRandom, T payload, K key) {
super(payload);
this.provider = provider;
this.secureRandom = secureRandom;
this.key = Assert.notNull(key, "key cannot be null.");

View File

@ -1,6 +1,7 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.EllipticCurveSignatureAlgorithm;
import io.jsonwebtoken.security.InvalidKeyException;
@ -55,9 +56,9 @@ public class DefaultEllipticCurveSignatureAlgorithm<SK extends ECKey & PrivateKe
public KeyPair generateKeyPair() {
final ECGenParameterSpec spec = new ECGenParameterSpec(this.curveName);
JcaTemplate template = new JcaTemplate("EC", null);
return template.execute(KeyPairGenerator.class, new InstanceCallback<KeyPairGenerator, KeyPair>() {
return template.execute(KeyPairGenerator.class, new CheckedFunction<KeyPairGenerator, KeyPair>() {
@Override
public KeyPair doWithInstance(KeyPairGenerator generator) throws Exception {
public KeyPair apply(KeyPairGenerator generator) throws Exception {
generator.initialize(spec, Randoms.secureRandom());
return generator.generateKeyPair();
}
@ -104,9 +105,9 @@ public class DefaultEllipticCurveSignatureAlgorithm<SK extends ECKey & PrivateKe
@Override
protected byte[] doSign(final SignatureRequest<SK> request) {
return execute(request, Signature.class, new InstanceCallback<Signature, byte[]>() {
return execute(request, Signature.class, new CheckedFunction<Signature, byte[]>() {
@Override
public byte[] doWithInstance(Signature sig) throws Exception {
public byte[] apply(Signature sig) throws Exception {
sig.initSign(request.getKey());
sig.update(request.getPayload());
byte[] signature = sig.sign();
@ -117,10 +118,10 @@ public class DefaultEllipticCurveSignatureAlgorithm<SK extends ECKey & PrivateKe
@Override
protected boolean doVerify(final VerifySignatureRequest<VK> request) {
return execute(request, Signature.class, new InstanceCallback<Signature, Boolean>() {
return execute(request, Signature.class, new CheckedFunction<Signature, Boolean>() {
@Override
public Boolean doWithInstance(Signature sig) throws Exception {
byte[] signature = request.getSignature();
public Boolean apply(Signature sig) throws Exception {
byte[] signature = request.getDigest();
/*
* If the expected size is not valid for JOSE, fall back to ASN.1 DER signature.
* This fallback is for backwards compatibility ONLY (to support tokens generated by previous versions of jjwt)

View File

@ -1,123 +0,0 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.Services;
import io.jsonwebtoken.io.Decoder;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.EncryptionAlgorithms;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
import java.util.Map;
/**
* @since JJWT_RELEASE_VERSION
*/
public class DefaultJweFactory {
/*private final Decoder<String, byte[]> base64UrlDecoder;
private final Deserializer<Map<String, ?>> deserializer;
private final SymmetricAeadAlgorithm encryptionAlgorithm;
private static Deserializer<Map<String, ?>> loadDeserializer() {
Deserializer deserializer = Services.loadFirst(Deserializer.class);
//noinspection unchecked
return deserializer;
}
public DefaultJweFactory() {
//this(Decoders.BASE64URL, loadDeserializer(), EncryptionAlgorithms.A256GCM);
}
public DefaultJweFactory(Decoder<String, byte[]> base64UrlDecoder,
Deserializer<Map<String, Object>> deserializer,
EncryptionAlgorithm encryptionAlgorithm) {
this.base64UrlDecoder = Assert.notNull(base64UrlDecoder, "Base64Url TextCodec cannot be null.");
this.deserializer = Assert.notNull(deserializer, "Deserializer cannot be null.");
this.encryptionAlgorithm = Assert.notNull(encryptionAlgorithm, "EncryptionAlgorithm cannot be null.");
}
/*
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 base64UrlDecoder.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 deserializer.deserialize(json);
}
*/
}

View File

@ -66,10 +66,10 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
SETTERS = java.util.Collections.unmodifiableMap(s);
}
private final Map<String, Object> values;
private final Map<String, Object> canonicalValues;
private final Map<String, Object> redactedValues;
private final Set<String> privateMemberNames;
private final Map<String, Object> values; // canonical values formatted per RFC requirements
private final Map<String, Object> idiomaticValues; // the values map with any string/encoded values converted to Java type-safe values where possible
private final Map<String, Object> redactedValues; // the values map with any sensitive/secret values redacted. Used in the toString implementation.
private final Set<String> privateMemberNames; // names of values that should be redacted for toString output
private K key;
private PublicKey publicKey;
private Provider provider;
@ -84,7 +84,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
public DefaultJwkContext(Set<String> privateMemberNames) {
this.privateMemberNames = Assert.notEmpty(privateMemberNames, "privateMemberNames cannot be null or empty.");
this.values = new LinkedHashMap<>();
this.canonicalValues = new LinkedHashMap<>();
this.idiomaticValues = new LinkedHashMap<>();
this.redactedValues = new LinkedHashMap<>();
}
@ -111,7 +111,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
DefaultJwkContext<?> src = (DefaultJwkContext<?>) other;
this.provider = other.getProvider();
this.values = new LinkedHashMap<>(src.values);
this.canonicalValues = new LinkedHashMap<>(src.canonicalValues);
this.idiomaticValues = new LinkedHashMap<>(src.idiomaticValues);
this.redactedValues = new LinkedHashMap<>(src.redactedValues);
if (removePrivate) {
for (String name : this.privateMemberNames) {
@ -126,7 +126,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
} else {
Object redactedValue = this.privateMemberNames.contains(name) ? AbstractJwk.REDACTED_VALUE : value;
this.redactedValues.put(name, redactedValue);
this.canonicalValues.put(name, value);
this.idiomaticValues.put(name, value);
return this.values.put(name, value);
}
}
@ -139,13 +139,15 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
} else if (Objects.isArray(value) && !value.getClass().getComponentType().isPrimitive()) {
value = Collections.arrayToList(value);
}
return doPut(name, value);
return idiomaticPut(name, value);
}
private Object doPut(String name, Object value) {
// ensures that if a property name matches an RFC-specified name, that value can be represented
// as an idiomatic type-safe Java value in addition to the canonical RFC/encoded value.
private Object idiomaticPut(String name, Object value) {
assert name != null; //asserted by caller.
Canonicalizer<?> fn = SETTERS.get(name);
if (fn != null) { //Setting a JWA-standard property - let's ensure we can represent it canonically:
if (fn != null) { //Setting a JWA-standard property - let's ensure we can represent it idiomatically:
return fn.apply(this, value);
} else { //non-standard/custom property:
return nullSafePut(name, value);
@ -153,17 +155,17 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
}
@Override
public JwkContext<K> putAll(Map<? extends String, ?> m) {
public void putAll(Map<? extends String, ?> m) {
Assert.notEmpty(m, "JWK values cannot be null or empty.");
for (Map.Entry<? extends String, ?> entry : m.entrySet()) {
put(entry.getKey(), entry.getValue());
}
return this;
}
private Object remove(String key) {
@Override
public Object remove(Object key) {
this.redactedValues.remove(key);
this.canonicalValues.remove(key);
this.idiomaticValues.remove(key);
return this.values.remove(key);
}
@ -178,7 +180,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
}
@Override
public boolean containsKey(String key) {
public boolean containsKey(Object key) {
return this.values.containsKey(key);
}
@ -188,10 +190,15 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
}
@Override
public Object get(String key) {
public Object get(Object key) {
return this.values.get(key);
}
@Override
public void clear() {
throw new UnsupportedOperationException("Cannot clear JwkContext objects.");
}
@Override
public Set<String> keySet() {
return this.values.keySet();
@ -207,14 +214,9 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
return this.values.entrySet();
}
@Override
public Map<String, Object> getValues() {
return this.values;
}
@Override
public String getAlgorithm() {
return (String) this.canonicalValues.get(AbstractJwk.ALGORITHM);
return (String) this.values.get(AbstractJwk.ALGORITHM);
}
@Override
@ -225,7 +227,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override
public String getId() {
return (String) this.canonicalValues.get(AbstractJwk.ID);
return (String) this.values.get(AbstractJwk.ID);
}
@Override
@ -237,7 +239,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override
public Set<String> getOperations() {
//noinspection unchecked
return (Set<String>) this.canonicalValues.get(AbstractJwk.OPERATIONS);
return (Set<String>) this.idiomaticValues.get(AbstractJwk.OPERATIONS);
}
@Override
@ -248,7 +250,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override
public String getType() {
return (String) this.canonicalValues.get(AbstractJwk.TYPE);
return (String) this.values.get(AbstractJwk.TYPE);
}
@Override
@ -259,7 +261,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override
public String getPublicKeyUse() {
return (String) this.canonicalValues.get(AbstractAsymmetricJwk.PUBLIC_KEY_USE);
return (String) this.values.get(AbstractAsymmetricJwk.PUBLIC_KEY_USE);
}
@Override
@ -271,7 +273,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override
public List<X509Certificate> getX509CertificateChain() {
//noinspection unchecked
return (List<X509Certificate>) this.canonicalValues.get(AbstractAsymmetricJwk.X509_CERT_CHAIN);
return (List<X509Certificate>) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_CERT_CHAIN);
}
@Override
@ -282,7 +284,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override
public byte[] getX509CertificateSha1Thumbprint() {
return (byte[]) this.canonicalValues.get(AbstractAsymmetricJwk.X509_SHA1_THUMBPRINT);
return (byte[]) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_SHA1_THUMBPRINT);
}
@Override
@ -293,7 +295,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override
public byte[] getX509CertificateSha256Thumbprint() {
return (byte[]) this.canonicalValues.get(AbstractAsymmetricJwk.X509_SHA256_THUMBPRINT);
return (byte[]) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_SHA256_THUMBPRINT);
}
@Override
@ -304,7 +306,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override
public URI getX509Url() {
return (URI) this.canonicalValues.get(AbstractAsymmetricJwk.X509_URL);
return (URI) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_URL);
}
@Override
@ -353,18 +355,13 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override
public int hashCode() {
int hash = 7;
hash = hash * 31 + Objects.nullSafeHashCode(this.key);
hash = hash * 31 + Objects.nullSafeHashCode(this.values);
return hash;
return this.values.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof DefaultJwkContext) {
DefaultJwkContext<?> c = (DefaultJwkContext<?>) obj;
return Objects.nullSafeEquals(this.key, c.key) &&
Objects.nullSafeEquals(this.values, c.values);
if (obj instanceof Map) {
return this.values.equals(obj);
}
return false;
}
@ -403,23 +400,23 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
public T apply(DefaultJwkContext<?> ctx, Object rawValue) {
if (JwtMap.isReduceableToNull(rawValue)) {
//noinspection unchecked
return (T) ctx.remove(id);
ctx.remove(id);
return null;
}
T canonicalValue;
Object encodedValue;
T idiomaticValue; // preferred Java format
Object canonicalValue; //as required by the RFC
try {
canonicalValue = converter.applyFrom(rawValue);
encodedValue = converter.applyTo(canonicalValue);
idiomaticValue = converter.applyFrom(rawValue);
canonicalValue = converter.applyTo(idiomaticValue);
} catch (Exception e) {
String msg = "Invalid JWK " + title + "('" + id + "') value [" + rawValue + "]: " + e.getMessage();
String msg = "Invalid JWK '" + id + "' (" + title + ") value [" + rawValue + "]: " + e.getMessage();
throw new MalformedKeyException(msg, e);
}
ctx.nullSafePut(id, encodedValue);
ctx.canonicalValues.put(id, canonicalValue);
ctx.nullSafePut(id, canonicalValue);
ctx.idiomaticValues.put(id, idiomaticValue);
//noinspection unchecked
return (T) encodedValue;
return (T) canonicalValue;
}
}
}

View File

@ -3,6 +3,7 @@ package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.JweHeader;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.KeyRequest;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
import java.security.Key;
import java.security.Provider;
@ -11,14 +12,29 @@ import java.security.SecureRandom;
public class DefaultKeyRequest<T, K extends Key> extends DefaultCryptoRequest<T, K> implements KeyRequest<T, K> {
private final JweHeader header;
private final SymmetricAeadAlgorithm encryptionAlgorithm;
public DefaultKeyRequest(Provider provider, SecureRandom secureRandom, T data, K key, JweHeader header) {
super(provider, secureRandom, data, key);
public DefaultKeyRequest(Provider provider, SecureRandom secureRandom, T payload, K key, JweHeader header, SymmetricAeadAlgorithm encryptionAlgorithm) {
super(provider, secureRandom, payload, key);
this.header = Assert.notNull(header, "JweHeader cannot be null.");
this.encryptionAlgorithm = Assert.notNull(encryptionAlgorithm, "SymmetricAeadAlgorithm argument cannot be null.");
}
@Override
protected T assertValidPayload(T payload) throws IllegalArgumentException {
if (payload != null) {
return super.assertValidPayload(payload);
}
return null;
}
@Override
public JweHeader getHeader() {
return this.header;
}
@Override
public SymmetricAeadAlgorithm getEncryptionAlgorithm() {
return this.encryptionAlgorithm;
}
}

View File

@ -1,34 +1,23 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.KeyResult;
import javax.crypto.SecretKey;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
public class DefaultKeyResult implements KeyResult {
private static final byte[] EMPTY_BYTES = new byte[0];
private final byte[] payload;
private final SecretKey key;
private final Map<String, ?> headerParams;
public DefaultKeyResult(SecretKey key) {
this(EMPTY_BYTES, key);
this(key, Bytes.EMPTY);
}
public DefaultKeyResult(byte[] encryptedKey, SecretKey key) {
this(encryptedKey, key, Collections.<String, Object>emptyMap());
}
public DefaultKeyResult(byte[] encryptedKey, SecretKey key, Map<String, ?> headerParams) {
public DefaultKeyResult(SecretKey key, byte[] encryptedKey) {
this.payload = Assert.notNull(encryptedKey, "encryptedKey cannot be null (but can be empty).");
this.key = Assert.notNull(key, "Key argument cannot be null.");
Assert.notNull(headerParams, "headerParams cannot be null.");
this.headerParams = Collections.unmodifiableMap(new LinkedHashMap<>(headerParams));
}
@Override
@ -40,9 +29,4 @@ public class DefaultKeyResult implements KeyResult {
public SecretKey getKey() {
return this.key;
}
@Override
public Map<String, ?> getHeaderParams() {
return this.headerParams;
}
}

View File

@ -1,91 +0,0 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Objects;
import javax.crypto.interfaces.PBEKey;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
public final class DefaultPBEKey implements PBEKey {
private static final String RAW_FORMAT = "RAW";
private volatile boolean destroyed = false;
private final char[] chars;
private final byte[] bytes;
private final int iterations;
private final String algorithm;
private static byte[] toBytes(char[] chars) {
ByteBuffer buf = StandardCharsets.UTF_8.encode(CharBuffer.wrap(chars));
byte[] bytes = new byte[buf.limit()];
buf.get(bytes);
return bytes;
}
public DefaultPBEKey(char[] password, int iterations, String algorithm) {
boolean empty = Objects.isEmpty(password);
this.chars = empty ? new char[0] : password.clone();
this.bytes = empty ? new byte[0] : toBytes(this.chars);
if (iterations <= 0) {
String msg = "Iterations must be an integer greater than zero. Value: " + iterations;
throw new IllegalArgumentException(msg);
}
this.iterations = iterations;
this.algorithm = Assert.hasText(algorithm, "Algorithm string cannot be null or empty.");
}
private void assertActive() {
if (destroyed) {
String msg = "PBKey has been destroyed. Password characters or bytes may not be obtained.";
throw new IllegalStateException(msg);
}
}
@Override
public char[] getPassword() {
assertActive();
return this.chars.clone();
}
@Override
public byte[] getSalt() {
return null;
}
@Override
public int getIterationCount() {
return this.iterations;
}
@Override
public String getAlgorithm() {
return this.algorithm;
}
@Override
public String getFormat() {
return RAW_FORMAT;
}
@Override
public byte[] getEncoded() {
assertActive();
return this.bytes.clone();
}
@Override
public void destroy() {
if (destroyed) return;
java.util.Arrays.fill(bytes, (byte) 0);
java.util.Arrays.fill(chars, '\u0000');
this.destroyed = true;
}
@Override
public boolean isDestroyed() {
return destroyed;
}
}

View File

@ -10,11 +10,18 @@ class DefaultPayloadSupplier<T> implements PayloadSupplier<T> {
private final T payload;
DefaultPayloadSupplier(T payload) {
this.payload = Assert.notNull(payload, "payload cannot be null.");
Assert.isTrue(payload instanceof byte[] || payload instanceof Key, "Payload argument must be either a byte array or a java.security.Key.");
if (payload instanceof byte[] && ((byte[]) payload).length == 0) {
throw new IllegalArgumentException("Payload byte array cannot be empty.");
this.payload = assertValidPayload(payload);
}
protected T assertValidPayload(T payload) throws IllegalArgumentException {
Assert.notNull(payload, "payload cannot be null.");
if (payload instanceof byte[]) {
Assert.notEmpty((byte[])payload, "payload byte array cannot be empty.");
} else if (!(payload instanceof Key)) {
String msg = "payload must be either a byte array or a java.security.Key instance.";
throw new IllegalArgumentException(msg);
}
return payload;
}
@Override

View File

@ -0,0 +1,104 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.lang.Objects;
import io.jsonwebtoken.security.PbeKey;
public class DefaultPbeKey implements PbeKey {
private static final String RAW_FORMAT = "RAW";
private static final String NONE_ALGORITHM = "NONE";
private volatile boolean destroyed = false;
private final char[] chars;
//private final byte[] bytes;
private final int workFactor;
// private static byte[] toBytes(char[] chars) {
// ByteBuffer buf = StandardCharsets.UTF_8.encode(CharBuffer.wrap(chars));
// byte[] bytes = new byte[buf.limit()];
// buf.get(bytes);
// return bytes;
// }
public DefaultPbeKey(char[] password, int workFactor) {
boolean empty = Objects.isEmpty(password);
this.chars = empty ? new char[0] : password.clone();
//this.bytes = empty ? new byte[0] : toBytes(this.chars);
if (workFactor < 0) {
String msg = "workFactor cannot be negative. Value: " + workFactor;
throw new IllegalArgumentException(msg);
}
this.workFactor = workFactor;
}
private void assertActive() {
if (destroyed) {
String msg = "PBKey has been destroyed. Password characters or bytes may not be obtained.";
throw new IllegalStateException(msg);
}
}
@Override
public char[] getPassword() {
assertActive();
return this.chars.clone();
}
@Override
public int getWorkFactor() {
return this.workFactor;
}
@Override
public String getAlgorithm() {
return NONE_ALGORITHM;
}
@Override
public String getFormat() {
return RAW_FORMAT;
}
@Override
public byte[] getEncoded() {
throw new UnsupportedOperationException("getEncoded is not supported for PbeKey instances.");
//assertActive();
//return this.bytes.clone();
}
@Override
public void destroy() {
// if (bytes != null) {
// java.util.Arrays.fill(bytes, (byte) 0);
// }
if (chars != null) {
java.util.Arrays.fill(chars, '\u0000');
}
this.destroyed = true;
}
@Override
public boolean isDestroyed() {
return destroyed;
}
@Override
public int hashCode() {
return Objects.nullSafeHashCode(this.chars);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof DefaultPbeKey) {
DefaultPbeKey other = (DefaultPbeKey) obj;
return this.workFactor == other.workFactor &&
Objects.nullSafeEquals(this.chars, other.chars);
}
return false;
}
@Override
public String toString() {
return "password=<redacted>, workFactor=" + this.workFactor;
}
}

View File

@ -0,0 +1,96 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.PbeKey;
import io.jsonwebtoken.security.PbeKeyBuilder;
import javax.crypto.interfaces.PBEKey;
import javax.security.auth.Destroyable;
//
// MAINTAINER NOTE:
//
// If editing/modifying this class, DO NOT attempt to call jcaKey.getPassword(): doing so creates a clone of that
// character array. There is no need to create copies of sensitive data (that we would be responsible for cleaning up)
// since the JcaPbeKey implementation will just delegate to the jcaKey as needed.
//
public class DefaultPbeKeyBuilder<K extends PbeKey> implements PbeKeyBuilder<K>, Destroyable {
private char[] password;
private int workFactor;
private PBEKey jcaKey;
private volatile boolean destroyed;
private static char[] assertPassword(char[] password) {
Assert.notEmpty(password, "Password cannot be null or empty.");
return password;
}
private static int assertWorkFactor(int workFactor) {
if (workFactor < 0) {
String msg = "workFactor cannot be negative.";
throw new IllegalArgumentException(msg);
}
return workFactor;
}
@Override
public PbeKeyBuilder<K> forKey(PBEKey jcaKey) {
this.jcaKey = Assert.notNull(jcaKey, "PBEKey cannot be null.");
return this;
}
@Override
public PbeKeyBuilder<K> setPassword(String password) {
return setPassword(Assert.notNull(password, "password cannot be null.").toCharArray());
}
@Override
public DefaultPbeKeyBuilder<K> setPassword(char[] password) {
this.password = password;
return this;
}
@Override
public DefaultPbeKeyBuilder<K> setWorkFactor(int workFactor) {
this.workFactor = workFactor;
return this;
}
@Override
public void destroy() {
if (this.password != null) {
destroyed = true;
java.util.Arrays.fill(this.password, '\u0000');
}
}
@Override
public boolean isDestroyed() {
return destroyed;
}
private void assertActive() {
if (destroyed) {
String msg = "This PbeKeyBuilder has been destroyed in order to clean/zero-out internal password " +
"arrays for safety. Please use a new builder for each PbeKey instance you need to create.";
throw new IllegalStateException(msg);
}
}
@SuppressWarnings("unchecked")
@Override
public K build() {
try {
if (this.jcaKey != null) {
return (K) new JcaPbeKey(this.jcaKey);
}
assertActive();
assertPassword(this.password);
assertWorkFactor(this.workFactor);
return (K) new DefaultPbeKey(this.password, this.workFactor);
} finally {
destroy();
}
}
}

View File

@ -1,10 +1,12 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.EncryptedKeyAlgorithm;
import io.jsonwebtoken.security.KeyRequest;
import io.jsonwebtoken.security.KeyResult;
import io.jsonwebtoken.security.RsaKeyAlgorithm;
import io.jsonwebtoken.security.SecurityException;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
@ -14,8 +16,8 @@ import java.security.PublicKey;
import java.security.interfaces.RSAKey;
import java.security.spec.AlgorithmParameterSpec;
public class DefaultRsaKeyAlgorithm<EK extends RSAKey & PublicKey, DK extends RSAKey & PrivateKey> extends CryptoAlgorithm
implements EncryptedKeyAlgorithm<EK, DK> {
public class DefaultRsaKeyAlgorithm<E extends RSAKey & PublicKey, D extends RSAKey & PrivateKey> extends CryptoAlgorithm
implements RsaKeyAlgorithm<E, D> {
private final AlgorithmParameterSpec SPEC; //can be null
@ -29,14 +31,15 @@ public class DefaultRsaKeyAlgorithm<EK extends RSAKey & PublicKey, DK extends RS
}
@Override
public KeyResult getEncryptionKey(final KeyRequest<SecretKey, EK> request) throws SecurityException {
public KeyResult getEncryptionKey(final KeyRequest<SecretKey, E> request) throws SecurityException {
Assert.notNull(request, "Request cannot be null.");
final EK kek = Assert.notNull(request.getKey(), "Request key encryption key cannot be null.");
final SecretKey cek = Assert.notNull(request.getPayload(), "Request content encryption key (request.getPayload() cannot be null.");
final E kek = Assert.notNull(request.getKey(), "Request key encryption key cannot be null.");
SymmetricAeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null.");
final SecretKey cek = Assert.notNull(enc.generateKey(), "Request encryption algorithm cannot generate a null key.");
byte[] ciphertext = execute(request, Cipher.class, new InstanceCallback<Cipher, byte[]>() {
byte[] ciphertext = execute(request, Cipher.class, new CheckedFunction<Cipher, byte[]>() {
@Override
public byte[] doWithInstance(Cipher cipher) throws Exception {
public byte[] apply(Cipher cipher) throws Exception {
if (SPEC == null) {
cipher.init(Cipher.WRAP_MODE, kek, ensureSecureRandom(request));
} else {
@ -46,18 +49,18 @@ public class DefaultRsaKeyAlgorithm<EK extends RSAKey & PublicKey, DK extends RS
}
});
return new DefaultKeyResult(ciphertext, cek);
return new DefaultKeyResult(cek, ciphertext);
}
@Override
public SecretKey getDecryptionKey(KeyRequest<byte[], DK> request) throws SecurityException {
public SecretKey getDecryptionKey(KeyRequest<byte[], D> request) throws SecurityException {
Assert.notNull(request, "request cannot be null.");
final DK kek = Assert.notNull(request.getKey(), "Request key decryption key cannot be null.");
final D kek = Assert.notNull(request.getKey(), "Request key decryption key cannot be null.");
final byte[] cekBytes = Assert.notEmpty(request.getPayload(), "Request encrypted key (request.getPayload()) cannot be null or empty.");
return execute(request, Cipher.class, new InstanceCallback<Cipher, SecretKey>() {
return execute(request, Cipher.class, new CheckedFunction<Cipher, SecretKey>() {
@Override
public SecretKey doWithInstance(Cipher cipher) throws Exception {
public SecretKey apply(Cipher cipher) throws Exception {
if (SPEC == null) {
cipher.init(Cipher.UNWRAP_MODE, kek);
} else {

View File

@ -1,5 +1,6 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.RuntimeEnvironment;
import io.jsonwebtoken.security.InvalidKeyException;
import io.jsonwebtoken.security.RsaSignatureAlgorithm;
@ -102,10 +103,10 @@ public class DefaultRsaSignatureAlgorithm<SK extends RSAKey & PrivateKey, VK ext
}
@Override
protected byte[] doSign(final SignatureRequest<SK> request) throws Exception {
return execute(request, Signature.class, new InstanceCallback<Signature, byte[]>() {
protected byte[] doSign(final SignatureRequest<SK> request) {
return execute(request, Signature.class, new CheckedFunction<Signature, byte[]>() {
@Override
public byte[] doWithInstance(Signature sig) throws Exception {
public byte[] apply(Signature sig) throws Exception {
if (algorithmParameterSpec != null) {
sig.setParameter(algorithmParameterSpec);
}
@ -122,15 +123,15 @@ public class DefaultRsaSignatureAlgorithm<SK extends RSAKey & PrivateKey, VK ext
if (key instanceof PrivateKey) { //legacy support only
return super.doVerify(request);
}
return execute(request, Signature.class, new InstanceCallback<Signature, Boolean>() {
return execute(request, Signature.class, new CheckedFunction<Signature, Boolean>() {
@Override
public Boolean doWithInstance(Signature sig) throws Exception {
public Boolean apply(Signature sig) throws Exception {
if (algorithmParameterSpec != null) {
sig.setParameter(algorithmParameterSpec);
}
sig.initVerify(request.getKey());
sig.update(request.getPayload());
return sig.verify(request.getSignature());
return sig.verify(request.getDigest());
}
});
}

View File

@ -0,0 +1,27 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.security.SecurityRequest;
import java.security.Provider;
import java.security.SecureRandom;
public class DefaultSecurityRequest implements SecurityRequest {
private final Provider provider;
private final SecureRandom secureRandom;
public DefaultSecurityRequest(Provider provider, SecureRandom secureRandom) {
this.provider = provider;
this.secureRandom = secureRandom;
}
@Override
public Provider getProvider() {
return this.provider;
}
@Override
public SecureRandom getSecureRandom() {
return this.secureRandom;
}
}

View File

@ -1,13 +1,13 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.security.InitializationVectorSource;
import io.jsonwebtoken.security.InitializationVectorSupplier;
import io.jsonwebtoken.security.SymmetricAeadRequest;
import javax.crypto.SecretKey;
import java.security.Provider;
import java.security.SecureRandom;
public class DefaultSymmetricAeadRequest extends DefaultCryptoRequest<byte[], SecretKey> implements SymmetricAeadRequest, InitializationVectorSource {
public class DefaultSymmetricAeadRequest extends DefaultCryptoRequest<byte[], SecretKey> implements SymmetricAeadRequest, InitializationVectorSupplier {
private final byte[] IV;

View File

@ -0,0 +1,149 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.JweHeader;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.impl.lang.ValueGetter;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Strings;
import io.jsonwebtoken.security.Jwk;
import io.jsonwebtoken.security.MalformedKeyException;
import java.math.BigInteger;
import java.util.Map;
/**
* Allows use af shared assertions across codebase, regardless of inheritance hierarchy.
*/
public class DefaultValueGetter implements ValueGetter {
private final Map<String, ?> values;
public DefaultValueGetter(Map<String, ?> values) {
this.values = Assert.notEmpty(values, "Values cannot be null or empty.");
}
private String name() {
if (values instanceof JweHeader) {
return "JWE header";
} else if (values instanceof JwsHeader) {
return "JWS header";
} else if (values instanceof Header) {
return "JWT header";
} else if (values instanceof Jwk || values instanceof JwkContext) {
Object value = values.get(AbstractJwk.TYPE);
if (DefaultSecretJwk.TYPE_VALUE.equals(value)) {
value = "Secret";
}
return value instanceof String ? value + " JWK" : "JWK";
} else {
return "Map";
}
}
private JwtException malformed(String msg) {
if (values instanceof JwkContext || values instanceof Jwk) {
return new MalformedKeyException(msg);
} else {
return new MalformedJwtException(msg);
}
}
protected Object getRequiredValue(String key) {
Object value = this.values.get(key);
if (value == null) {
String msg = name() + " is missing required '" + key + "' value.";
throw malformed(msg);
}
return value;
}
@Override
public String getRequiredString(String key) {
Object value = getRequiredValue(key);
if (!(value instanceof String)) {
String msg = name() + " '" + key + "' value must be a String. Actual type: " + value.getClass().getName();
throw malformed(msg);
}
String sval = Strings.clean((String) value);
if (!Strings.hasText(sval)) {
String msg = name() + " '" + key + "' string value cannot be null or empty.";
throw malformed(msg);
}
return (String) value;
}
@Override
public int getRequiredInteger(String key) {
Object value = getRequiredValue(key);
if (!(value instanceof Integer)) {
String msg = name() + " '" + key + "' value must be an Integer. Actual type: " + value.getClass().getName();
throw malformed(msg);
}
return (Integer) value;
}
@Override
public int getRequiredPositiveInteger(String key) {
int value = getRequiredInteger(key);
if (value <= 0) {
String msg = name() + " '" + key + "' value must be a positive Integer. Value: " + value;
throw malformed(msg);
}
return value;
}
@Override
public byte[] getRequiredBytes(String key) {
String encoded = getRequiredString(key);
byte[] decoded;
try {
decoded = Decoders.BASE64URL.decode(encoded);
} catch (Exception e) {
String msg = name() + " '" + key + "' value is not a valid Base64URL String: " + e.getMessage();
throw malformed(msg);
}
if (Arrays.length(decoded) == 0) {
String msg = name() + " '" + key + "' decoded byte array cannot be empty.";
throw malformed(msg);
}
return decoded;
}
@Override
public byte[] getRequiredBytes(String key, int requiredByteLength) {
byte[] decoded = getRequiredBytes(key);
int len = Arrays.length(decoded);
if (len != requiredByteLength) {
String msg = name() + " '" + key + "' decoded byte array must be " + Bytes.bytesMsg(requiredByteLength) +
" long. Actual length: " + Bytes.bytesMsg(len) + ".";
throw malformed(msg);
}
return decoded;
}
@Override
public BigInteger getRequiredBigInt(String key, boolean sensitive) {
String s = getRequiredString(key);
try {
byte[] bytes = Decoders.BASE64URL.decode(s);
return new BigInteger(1, bytes);
} catch (Exception e) {
String msg = "Unable to decode " + name() + " '" + key + "' value";
if (!sensitive) {
msg += " '" + s + "'";
}
msg += " to BigInteger: " + e.getMessage();
throw malformed(msg);
}
}
}

View File

@ -17,7 +17,7 @@ public class DefaultVerifySignatureRequest<K extends Key> extends DefaultSignatu
}
@Override
public byte[] getSignature() {
public byte[] getDigest() {
return this.signature;
}
}

View File

@ -1,6 +1,7 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.impl.lang.ValueGetter;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.EcPrivateJwk;
import io.jsonwebtoken.security.EcPublicJwk;
@ -55,8 +56,9 @@ class EcPrivateJwkFactory extends AbstractEcJwkFactory<ECPrivateKey, EcPrivateJw
@Override
protected EcPrivateJwk createJwkFromValues(final JwkContext<ECPrivateKey> ctx) {
String curveId = getRequiredString(ctx, DefaultEcPublicJwk.CURVE_ID);
BigInteger d = getRequiredBigInt(ctx, DefaultEcPrivateJwk.D, true);
ValueGetter getter = new DefaultValueGetter(ctx);
String curveId = getter.getRequiredString(DefaultEcPublicJwk.CURVE_ID);
BigInteger d = getter.getRequiredBigInt(DefaultEcPrivateJwk.D, true);
// We don't actually need the public x,y point coordinates for JVM lookup, but the
// [JWA spec](https://tools.ietf.org/html/rfc7518#section-6.2.2)

View File

@ -1,6 +1,7 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.impl.lang.ValueGetter;
import io.jsonwebtoken.security.EcPublicJwk;
import io.jsonwebtoken.security.InvalidKeyException;
@ -45,9 +46,10 @@ class EcPublicJwkFactory extends AbstractEcJwkFactory<ECPublicKey, EcPublicJwk>
@Override
protected EcPublicJwk createJwkFromValues(final JwkContext<ECPublicKey> ctx) {
String curveId = getRequiredString(ctx, DefaultEcPublicJwk.CURVE_ID);
BigInteger x = getRequiredBigInt(ctx, DefaultEcPublicJwk.X, false);
BigInteger y = getRequiredBigInt(ctx, DefaultEcPublicJwk.Y, false);
ValueGetter getter = new DefaultValueGetter(ctx);
String curveId = getter.getRequiredString(DefaultEcPublicJwk.CURVE_ID);
BigInteger x = getter.getRequiredBigInt(DefaultEcPublicJwk.X, false);
BigInteger y = getter.getRequiredBigInt(DefaultEcPublicJwk.Y, false);
ECParameterSpec spec = getCurveByJwaId(curveId);
ECPoint point = new ECPoint(x, y);

View File

@ -1,22 +0,0 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.Identifiable;
import io.jsonwebtoken.security.CryptoException;
import io.jsonwebtoken.security.CryptoRequest;
import io.jsonwebtoken.security.KeyException;
import io.jsonwebtoken.security.PayloadSupplier;
import java.security.Key;
/**
* @since JJWT_RELEASE_VERSION
*/
public interface EncryptionAlgorithm<T,
EK extends Key, DK extends Key,
EReq extends CryptoRequest<T, EK>, ERes extends PayloadSupplier<byte[]>,
DReq extends CryptoRequest<byte[], DK>, DRes extends PayloadSupplier<T>> extends Identifiable {
ERes encrypt(EReq request) throws CryptoException, KeyException;
DRes decrypt(DReq request) throws CryptoException, KeyException;
}

View File

@ -40,7 +40,7 @@ public class EncryptionAlgorithmsBridge {
public static SymmetricAeadAlgorithm forId(String id) {
SymmetricAeadAlgorithm alg = findById(id);
if (alg == null) {
String msg = "Unrecognized JWA EncryptionAlgorithm identifier: " + id;
String msg = "Unrecognized JWA SymmetricAeadAlgorithm identifier: " + id;
throw new UnsupportedJwtException(msg);
}
return alg;

View File

@ -1,14 +1,16 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.RuntimeEnvironment;
import io.jsonwebtoken.security.AeadResult;
import io.jsonwebtoken.security.CryptoException;
import io.jsonwebtoken.security.KeyException;
import io.jsonwebtoken.security.PayloadSupplier;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
import io.jsonwebtoken.security.SymmetricAeadDecryptionRequest;
import io.jsonwebtoken.security.AeadResult;
import io.jsonwebtoken.security.SymmetricAeadRequest;
import javax.crypto.Cipher;
@ -42,9 +44,9 @@ public class GcmAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadAl
final byte[] iv = ensureInitializationVector(req);
final AlgorithmParameterSpec ivSpec = getIvSpec(iv);
byte[] taggedCiphertext = execute(req, Cipher.class, new InstanceCallback<Cipher, byte[]>() {
byte[] taggedCiphertext = execute(req, Cipher.class, new CheckedFunction<Cipher, byte[]>() {
@Override
public byte[] doWithInstance(Cipher cipher) throws Exception {
public byte[] apply(Cipher cipher) throws Exception {
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
if (Arrays.length(aad) > 0) {
cipher.updateAAD(aad);
@ -61,7 +63,7 @@ public class GcmAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadAl
byte[] tag = new byte[BLOCK_BYTE_SIZE];
System.arraycopy(taggedCiphertext, ciphertextLength, tag, 0, BLOCK_BYTE_SIZE);
return new DefaultAeadResult(req.getProvider(), req.getSecureRandom(),ciphertext, key, aad, tag, iv);
return new DefaultAeadResult(req.getProvider(), req.getSecureRandom(), ciphertext, key, aad, tag, iv);
}
@Override
@ -71,16 +73,16 @@ public class GcmAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadAl
final SecretKey key = assertKey(req);
final byte[] ciphertext = Assert.notEmpty(req.getPayload(), "Decryption request payload (ciphertext) cannot be null or empty.");
final byte[] aad = getAAD(req);
final byte[] tag = Assert.notEmpty(req.getAuthenticationTag(), "Decryption request authentication tag cannot be null or empty.");
final byte[] tag = Assert.notEmpty(req.getDigest(), "Decryption request authentication tag cannot be null or empty.");
final byte[] iv = assertDecryptionIv(req);
final AlgorithmParameterSpec ivSpec = getIvSpec(iv);
//for tagged GCM, the JCA spec requires that the tag be appended to the end of the ciphertext byte array:
final byte[] taggedCiphertext = plus(ciphertext, tag);
final byte[] taggedCiphertext = Bytes.concat(ciphertext, tag);
byte[] plaintext = execute(req, Cipher.class, new InstanceCallback<Cipher, byte[]>() {
byte[] plaintext = execute(req, Cipher.class, new CheckedFunction<Cipher, byte[]>() {
@Override
public byte[] doWithInstance(Cipher cipher) throws Exception {
public byte[] apply(Cipher cipher) throws Exception {
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
if (Arrays.length(aad) > 0) {
cipher.updateAAD(aad);

View File

@ -1,5 +1,6 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.AeadResult;
import io.jsonwebtoken.security.CryptoRequest;
@ -46,12 +47,12 @@ public class HmacAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadA
@Override
public SecretKey generateKey() {
return new JcaTemplate("AES", null).generateSecretKey(this.keyLength * 2);
return new JcaTemplate("AES", null).generateSecretKey(this.keyBitLength * 2);
}
byte[] assertKeyBytes(CryptoRequest<?, SecretKey> request) {
SecretKey key = Assert.notNull(request.getKey(), "Request key cannot be null.");
return validateLength(key, this.keyLength * 2, true);
return validateLength(key, this.keyBitLength * 2, true);
}
@Override
@ -70,9 +71,9 @@ public class HmacAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadA
final byte[] iv = ensureInitializationVector(req);
final AlgorithmParameterSpec ivSpec = getIvSpec(iv);
final byte[] ciphertext = execute(req, Cipher.class, new InstanceCallback<Cipher, byte[]>() {
final byte[] ciphertext = execute(req, Cipher.class, new CheckedFunction<Cipher, byte[]>() {
@Override
public byte[] doWithInstance(Cipher cipher) throws Exception {
public byte[] apply(Cipher cipher) throws Exception {
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, ivSpec);
return cipher.doFinal(plaintext);
}
@ -135,7 +136,7 @@ public class HmacAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadA
final byte[] ciphertext = Assert.notEmpty(req.getPayload(), "Decryption request payload (ciphertext) cannot be null or empty.");
final byte[] aad = getAAD(req);
final byte[] tag = assertTag(req.getAuthenticationTag());
final byte[] tag = assertTag(req.getDigest());
final byte[] iv = assertDecryptionIv(req);
final AlgorithmParameterSpec ivSpec = getIvSpec(iv);
@ -147,9 +148,9 @@ public class HmacAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadA
throw new SignatureException(msg);
}
byte[] plaintext = execute(req, Cipher.class, new InstanceCallback<Cipher, byte[]>() {
byte[] plaintext = execute(req, Cipher.class, new CheckedFunction<Cipher, byte[]>() {
@Override
public byte[] doWithInstance(Cipher cipher) throws Exception {
public byte[] apply(Cipher cipher) throws Exception {
cipher.init(Cipher.DECRYPT_MODE, decryptionKey, ivSpec);
return cipher.doFinal(ciphertext);
}

View File

@ -1,6 +0,0 @@
package io.jsonwebtoken.impl.security;
public interface InstanceCallback<I,O> {
O doWithInstance(I instance) throws Exception;
}

View File

@ -0,0 +1,51 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.PbeKey;
import javax.crypto.interfaces.PBEKey;
import javax.security.auth.DestroyFailedException;
public class JcaPbeKey implements PbeKey {
private final PBEKey jcaKey;
public JcaPbeKey(PBEKey jcaKey) {
this.jcaKey = Assert.notNull(jcaKey, "PBEKey cannot be null.");
}
@Override
public char[] getPassword() {
return this.jcaKey.getPassword();
}
@Override
public int getWorkFactor() {
return this.jcaKey.getIterationCount();
}
@Override
public String getAlgorithm() {
return this.jcaKey.getAlgorithm();
}
@Override
public String getFormat() {
return this.jcaKey.getFormat();
}
@Override
public byte[] getEncoded() {
return this.jcaKey.getEncoded();
}
@Override
public void destroy() throws DestroyFailedException {
this.jcaKey.destroy();
}
@Override
public boolean isDestroyed() {
return this.jcaKey.isDestroyed();
}
}

View File

@ -1,5 +1,6 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import io.jsonwebtoken.security.CryptoException;
@ -32,34 +33,34 @@ public class JcaTemplate {
this.secureRandom = Assert.notNull(secureRandom, "SecureRandom cannot be null.");
}
public <I, T> T execute(Class<I> clazz, InstanceCallback<I, T> callback) throws CryptoException {
return execute(new JcaInstanceSupplier<>(clazz, this.jcaName, this.provider), callback);
public <T, R> R execute(Class<T> clazz, CheckedFunction<T, R> fn) throws CryptoException {
return execute(new JcaInstanceSupplier<>(clazz, this.jcaName, this.provider), fn);
}
public SecretKey generateSecretKey(final int keyLength) {
return execute(KeyGenerator.class, new InstanceCallback<KeyGenerator, SecretKey>() {
public SecretKey generateSecretKey(final int keyBitLength) {
return execute(KeyGenerator.class, new CheckedFunction<KeyGenerator, SecretKey>() {
@Override
public SecretKey doWithInstance(KeyGenerator generator) {
generator.init(keyLength, secureRandom);
public SecretKey apply(KeyGenerator generator) {
generator.init(keyBitLength, secureRandom);
return generator.generateKey();
}
});
}
public KeyPair generateKeyPair(final int keyLength) {
return execute(KeyPairGenerator.class, new InstanceCallback<KeyPairGenerator, KeyPair>() {
public KeyPair generateKeyPair(final int keyBitLength) {
return execute(KeyPairGenerator.class, new CheckedFunction<KeyPairGenerator, KeyPair>() {
@Override
public KeyPair doWithInstance(KeyPairGenerator generator) {
generator.initialize(keyLength, secureRandom);
public KeyPair apply(KeyPairGenerator generator) {
generator.initialize(keyBitLength, secureRandom);
return generator.generateKeyPair();
}
});
}
private <I, T> T execute(JcaInstanceSupplier<I> supplier, InstanceCallback<I, T> callback) throws CryptoException {
private <T, R> R execute(JcaInstanceSupplier<T> supplier, CheckedFunction<T, R> callback) throws CryptoException {
try {
I instance = supplier.getInstance();
return callback.doWithInstance(instance);
T instance = supplier.getInstance();
return callback.apply(instance);
} catch (SecurityException se) {
throw se; //propagate
} catch (Exception e) {

View File

@ -7,34 +7,11 @@ import java.security.Key;
import java.security.Provider;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
public interface JwkContext<K extends Key> extends Identifiable {
int size();
boolean isEmpty();
boolean containsKey(String key);
boolean containsValue(Object value);
Object get(String key);
Set<String> keySet();
Collection<Object> values();
Set<Map.Entry<String, Object>> entrySet();
Map<String,Object> getValues();
Object put(String name, Object value);
JwkContext<K> putAll(Map<? extends String, ?> m);
public interface JwkContext<K extends Key> extends Identifiable, Map<String,Object> {
JwkContext<K> setId(String id);

View File

@ -1,16 +1,29 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.impl.DefaultJweHeader;
import io.jsonwebtoken.impl.IdRegistry;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.impl.lang.Registry;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.security.EncryptionAlgorithms;
import io.jsonwebtoken.security.KeyAlgorithm;
import io.jsonwebtoken.security.KeyRequest;
import io.jsonwebtoken.security.KeyResult;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.PbeKey;
import io.jsonwebtoken.security.SecurityException;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.PSource;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.MGF1ParameterSpec;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.KeyAlgorithms implementation
public final class KeyAlgorithmsBridge {
@ -65,4 +78,153 @@ public final class KeyAlgorithmsBridge {
}
return instance;
}
private static KeyAlgorithm<PbeKey, SecretKey> lean(final Pbes2HsAkwAlgorithm alg) {
// ensure we use the same key factory over and over so that time spent acquiring one is not repeated:
JcaTemplate template = new JcaTemplate(alg.getJcaName(), null, Randoms.secureRandom());
final SecretKeyFactory factory = template.execute(SecretKeyFactory.class, new CheckedFunction<SecretKeyFactory, SecretKeyFactory>() {
@Override
public SecretKeyFactory apply(SecretKeyFactory secretKeyFactory) {
return secretKeyFactory;
}
});
// pre-compute the salt so we don't spend time doing that on each iteration. Doesn't need to be random for a
// computation-only test:
final byte[] rfcSalt = alg.toRfcSalt(alg.generateInputSalt(null));
// ensure that the bare minimum steps are performed to hash, ensuring our time sampling pertains only to
// hashing and not ancillary steps needed to setup the hashing/derivation
return new KeyAlgorithm<PbeKey, SecretKey>() {
@Override
public KeyResult getEncryptionKey(KeyRequest<SecretKey, PbeKey> request) throws SecurityException {
int iterations = request.getKey().getWorkFactor();
char[] password = request.getKey().getPassword();
try {
alg.deriveKey(factory, password, rfcSalt, iterations);
} catch (Exception e) {
throw new SecurityException("Unable to derive key", e);
}
return null;
}
@Override
public SecretKey getDecryptionKey(KeyRequest<byte[], SecretKey> request) throws SecurityException {
throw new UnsupportedOperationException("Not intended to be called.");
}
@Override
public String getId() {
return alg.getId();
}
};
}
public static int estimateIterations(KeyAlgorithm<PbeKey, SecretKey> alg, long desiredMillis) {
// The number of computational samples that land in our 'sweet spot' timing range matching desiredMillis.
// These samples will be averaged and the final average will be the return value of this method
// representing the number of iterations that should be taken for any given PBE hashing attempt to get
// reasonably close to desiredMillis:
final int NUM_SAMPLES = 30;
final int SKIP = 3;
// This is used by `alg` to generate an encryption key during the PBE attempt. While technically the time to
// generate this key during the alg call is not part of the hashing time and shouldn't be counted towards
// desiredMillis, in practice, this is so fast (about ~ 3 milliseconds total aggregated across all
// NUM_SAMPLES on a developer laptop), it is in practice negligible, so we won't need to adjust our
// timing logic below to account for this.
SymmetricAeadAlgorithm encAlg = EncryptionAlgorithms.A128GCM;
// Strip away all things that cause time during computation except for the actual hashing algorithm:
if (alg instanceof Pbes2HsAkwAlgorithm) {
alg = lean((Pbes2HsAkwAlgorithm) alg); //strip out everything except for the computation we care about
}
int workFactor = 1000; // same as iterations for PBKDF2. Different concept for Bcrypt/Scrypt
int minWorkFactor = workFactor;
List<Point> points = new ArrayList<>(NUM_SAMPLES);
for (int i = 0; points.size() < NUM_SAMPLES; i++) {
PbeKey pbeKey = Keys.forPbe().setPassword("12345678").setWorkFactor(workFactor).build();
KeyRequest<SecretKey, PbeKey> request = new DefaultKeyRequest<>(null, null, null, pbeKey, new DefaultJweHeader(), encAlg);
long start = System.currentTimeMillis();
alg.getEncryptionKey(request); // <-- Computation occurs here. Don't need the result, just need to exec
long end = System.currentTimeMillis();
long duration = end - start;
// Exclude the first SKIP number of attempts from the average due to initial JIT optimization/slowness.
// After a few attempts, the JVM should be relatively optimized and the subsequent
// PBE hashing times are the ones we want to include in our analysis
boolean warmedUp = i >= SKIP;
// how close we were on this hashing attempt to reach our desiredMillis target:
// A number under 1 means we weren't slow enough, a number greater than 1 means we were too slow:
double durationPercentAchieved = (double) duration / (double) desiredMillis;
// we only want to collect timing samples if :
// 1. we're warmed up (to account for JIT optimization)
// 2. The attempt time at least met (>=) the desiredMillis target
boolean collectSample = warmedUp && duration >= desiredMillis;
if (collectSample) {
// For each attempt, the x axis is the workFactor, and the y axis is how long it took to compute:
points.add(new Point(workFactor, duration));
//System.out.println("Collected point: workFactor=" + workFactor + ", duration=" + duration + " ms, %achieved=" + durationPercentAchieved);
} else {
minWorkFactor = Math.max(minWorkFactor, workFactor);
//System.out.println(" Excluding sample: workFactor=" + workFactor + ", duration=" + duration + " ms, %achieved=" + durationPercentAchieved);
}
// amount to increase or decrease the workFactor for the next hashing iteration. We increase if
// we haven't met the desired millisecond time, and decrease if we're over it a little too much, always
// trying to stay in that desired timing sweet spot
double percentAdjust = workFactor * 0.0075; // 3/4ths of a percent
if (durationPercentAchieved < 1d) {
// Under target. Let's increase by the amount that should get right at (or near) 100%:
double ratio = desiredMillis / (double) duration;
if (ratio > 1) {
double result = workFactor * ratio;
workFactor = (int) result;
} else {
double difference = workFactor * (1 - durationPercentAchieved);
workFactor += Math.max(percentAdjust, difference);
}
} else if (durationPercentAchieved > 1.01d) {
// Over target. Let's decrease gently to get closer.
double difference = workFactor * (durationPercentAchieved - 1.01);
difference = Math.min(percentAdjust, difference);
// math.max here because the min allowed is 1000 per the JWA RFC, so we never want to go below that.
workFactor = (int) Math.max(1000, workFactor - difference);
} else {
// we're at our target (desiredMillis); let's increase by a teeny bit to see where we get
// (and the JVM might optimize with the same inputs, so we want to prevent that here)
workFactor += 100;
}
}
// We've collected all of our samples, now let's find the workFactor average number
// That average is the best estimate for ensuring PBE hashes for the specified algorithm meet the
// desiredMillis target on the current JVM/CPU platform:
double sumX = 0;
for (Point p : points) {
sumX += p.x;
}
double average = sumX / points.size();
//ensure our average is at least as much as the smallest work factor that got us closest to desiredMillis:
return (int) Math.max(average, minWorkFactor);
}
private static class Point {
long x;
long y;
double lnY;
public Point(long x, long y) {
this.x = x;
this.y = y;
this.lnY = Math.log((double) y);
}
}
}

View File

@ -0,0 +1,24 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.KeySupplier;
import io.jsonwebtoken.security.SecurityRequest;
import java.security.Key;
import java.security.Provider;
import java.security.SecureRandom;
public class KeyedRequest<K extends Key> extends DefaultSecurityRequest implements SecurityRequest, KeySupplier<K> {
private final K key;
public KeyedRequest(Provider provider, SecureRandom secureRandom, K key) {
super(provider, secureRandom);
this.key = Assert.notNull(key, "Key cannot be null.");
}
@Override
public K getKey() {
return this.key;
}
}

View File

@ -0,0 +1,16 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.security.PbeKey;
import io.jsonwebtoken.security.PbeKeyBuilder;
@SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.Keys implementation
public class KeysBridge {
// prevent instantiation
private KeysBridge() {
}
public static PbeKeyBuilder<PbeKey> forPbe() {
return new DefaultPbeKeyBuilder<>();
}
}

View File

@ -1,5 +1,6 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Collections;
@ -134,9 +135,9 @@ public class MacSignatureAlgorithm extends AbstractSignatureAlgorithm<SecretKey,
@Override
public byte[] doSign(final SignatureRequest<SecretKey> request) throws Exception {
return execute(request, Mac.class, new InstanceCallback<Mac, byte[]>() {
return execute(request, Mac.class, new CheckedFunction<Mac, byte[]>() {
@Override
public byte[] doWithInstance(Mac mac) throws Exception {
public byte[] apply(Mac mac) throws Exception {
mac.init(request.getKey());
return mac.doFinal(request.getPayload());
}

View File

@ -1,17 +1,17 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.JweHeader;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.impl.lang.ValueGetter;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.EncryptedKeyAlgorithm;
import io.jsonwebtoken.security.KeyAlgorithm;
import io.jsonwebtoken.security.KeyRequest;
import io.jsonwebtoken.security.KeyResult;
import io.jsonwebtoken.security.PbeKey;
import io.jsonwebtoken.security.SecurityException;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
@ -21,10 +21,11 @@ import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements EncryptedKeyAlgorithm<SecretKey, SecretKey> {
public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements KeyAlgorithm<PbeKey, SecretKey> {
private static final String SALT_HEADER_NAME = "p2s";
private static final String ITERATION_HEADER_NAME = "p2c"; // iteration count
private static final int MIN_RECOMMENDED_ITERATIONS = 1000; // https://datatracker.ietf.org/doc/html/rfc7518#section-4.8.1.2
private final int HASH_BYTE_LENGTH;
private final int DERIVED_KEY_BIT_LENGTH;
@ -50,6 +51,16 @@ public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements EncryptedKey
return "PBES2-HS" + hashBitLength + "+" + wrapAlg.getId();
}
public static int assertIterations(int iterations) {
if (iterations < MIN_RECOMMENDED_ITERATIONS) {
String msg = "[JWA RFC 7518, Section 4.8.1.2](https://datatracker.ietf.org/doc/html/rfc7518#section-4.8.1.2) " +
"recommends password-based-encryption iterations be greater than or equal to " +
MIN_RECOMMENDED_ITERATIONS + ". Provided: " + iterations;
throw new IllegalArgumentException(msg);
}
return iterations;
}
public Pbes2HsAkwAlgorithm(int keyBitLength) {
this(hashBitLength(keyBitLength), new AesWrapKeyAlgorithm(keyBitLength));
}
@ -70,63 +81,63 @@ public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements EncryptedKey
this.SALT_PREFIX = toRfcSaltPrefix(getId().getBytes(StandardCharsets.UTF_8));
}
private SecretKey deriveKey(final KeyRequest<?, ?> request, final PBEKey pbeKey, final byte[] salt, final int iterations) {
return execute(request, SecretKeyFactory.class, new InstanceCallback<SecretKeyFactory, SecretKey>() {
@Override
public SecretKey doWithInstance(SecretKeyFactory factory) throws Exception {
PBEKeySpec spec = null;
try {
spec = new PBEKeySpec(pbeKey.getPassword(), salt, iterations, DERIVED_KEY_BIT_LENGTH);
return factory.generateSecret(spec);
} finally {
if (spec != null) {
spec.clearPassword();
}
}
// protected visibility for testing
protected SecretKey deriveKey(SecretKeyFactory factory, final char[] password, final byte[] rfcSalt, int iterations) throws Exception {
PBEKeySpec spec = null;
try {
spec = new PBEKeySpec(password, rfcSalt, iterations, DERIVED_KEY_BIT_LENGTH);
return factory.generateSecret(spec);
} finally {
if (spec != null) {
spec.clearPassword();
}
});
}
}
protected byte[] generateInputSalt(KeyRequest<SecretKey, SecretKey> request) {
private SecretKey deriveKey(final KeyRequest<?, ?> request, final char[] password, final byte[] salt, final int iterations) {
try {
return execute(request, SecretKeyFactory.class, new CheckedFunction<SecretKeyFactory, SecretKey>() {
@Override
public SecretKey apply(SecretKeyFactory factory) throws Exception {
return deriveKey(factory, password, salt, iterations);
}
});
} finally {
if (password != null) {
java.util.Arrays.fill(password, '\u0000');
}
}
}
protected byte[] generateInputSalt(KeyRequest<?, ?> request) {
byte[] inputSalt = new byte[this.HASH_BYTE_LENGTH];
ensureSecureRandom(request).nextBytes(inputSalt);
return inputSalt;
}
// protected visibility for testing
protected byte[] toRfcSalt(byte[] inputSalt) {
return Bytes.concat(this.SALT_PREFIX, inputSalt);
}
@Override
public KeyResult getEncryptionKey(KeyRequest<SecretKey, SecretKey> request) throws SecurityException {
public KeyResult getEncryptionKey(KeyRequest<SecretKey, PbeKey> request) throws SecurityException {
Assert.notNull(request, "request cannot be null.");
final SecretKey cek = Assert.notNull(request.getPayload(), "request.getPayload() (content encryption key) cannot be null.");
SecretKey reqKey = request.getKey();
Assert.notNull(reqKey, "request.getKey() cannot be null.");
if (!(reqKey instanceof PBEKey)) {
String msg = "request.getKey() must be a " + PBEKey.class.getName() + " instance. Type found: " +
reqKey.getClass().getName();
throw new IllegalArgumentException(msg);
}
final PBEKey pbeKey = (PBEKey) reqKey;
// we explicitly do not attempt to validate pbeKey.getPassword() at this point because that call will create
// a clone of the char array, and we'd have to guarantee cleanup of that clone if any failure/exception occurs.
// Instead, we access the password in only one place - in the execute* method call below - and guarantee
// cleanup there in a try/finally block
final int iterations = pbeKey.getIterationCount();
if (iterations < 1000) {
String msg = "Password-based encryption password iterations must be >= 1000. Found: " + iterations;
throw new IllegalArgumentException(msg);
}
SymmetricAeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null.");
final SecretKey cek = Assert.notNull(enc.generateKey(), "Request encryption algorithm cannot generate a null key.");
final PbeKey pbeKey = Assert.notNull(request.getKey(), "request.getKey() cannot be null.");
final int iterations = assertIterations(pbeKey.getWorkFactor());
byte[] inputSalt = generateInputSalt(request);
final byte[] rfcSalt = Bytes.plus(this.SALT_PREFIX, inputSalt);
final byte[] rfcSalt = toRfcSalt(inputSalt);
final String p2s = Encoders.BASE64URL.encode(inputSalt);
final SecretKey derivedKek = deriveKey(request, pbeKey, rfcSalt, iterations);
char[] password = pbeKey.getPassword(); // will be safely cleaned/zeroed in deriveKey next:
final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations);
// now encrypt (wrap) the CEK with the PBE-derived key:
DefaultKeyRequest<SecretKey, SecretKey> wrapReq = new DefaultKeyRequest<>(request.getProvider(),
request.getSecureRandom(), cek, derivedKek, request.getHeader());
request.getSecureRandom(), cek, derivedKek, request.getHeader(), request.getEncryptionAlgorithm());
KeyResult result = wrapAlg.getEncryptionKey(wrapReq);
request.getHeader().put(SALT_HEADER_NAME, p2s);
@ -138,30 +149,25 @@ public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements EncryptedKey
private static char[] toChars(byte[] bytes) {
ByteBuffer buf = ByteBuffer.wrap(bytes);
CharBuffer cbuf = StandardCharsets.UTF_8.decode(buf);
char[] chars = new char[cbuf.limit()];
cbuf.get(chars);
char[] chars = cbuf.compact().array();
return chars;
}
private PBEKey toPBEKey(SecretKey key, int iterations) {
private char[] toPasswordChars(SecretKey key) {
if (key instanceof PBEKey) {
return (PBEKey) key;
return ((PBEKey) key).getPassword();
}
if (key instanceof PbeKey) {
return ((PbeKey) key).getPassword();
}
// convert bytes to UTF-8 characters:
byte[] keyBytes = null;
char[] keyChars = null;
try {
keyBytes = key.getEncoded();
keyChars = toChars(keyBytes);
return new DefaultPBEKey(keyChars, iterations, getId());
return toChars(keyBytes);
} finally {
try {
if (keyChars != null) {
java.util.Arrays.fill(keyChars, '\u0000');
}
} finally {
if (keyBytes != null) {
java.util.Arrays.fill(keyBytes, (byte) 0);
}
if (keyBytes != null) {
java.util.Arrays.fill(keyBytes, (byte) 0);
}
}
}
@ -170,48 +176,18 @@ public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements EncryptedKey
public SecretKey getDecryptionKey(KeyRequest<byte[], SecretKey> request) throws SecurityException {
JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null.");
String name = SALT_HEADER_NAME;
final SecretKey key = Assert.notNull(request.getKey(), "Request Key cannot be null.");
Object value = header.get(name);
if (value == null) {
String msg = "The " + getId() + " Key Management Algorithm requires a JweHeader '" + name + "' value.";
throw new MalformedJwtException(msg);
}
if (!(value instanceof String)) {
String msg = "The " + getId() + " Key Management Algorithm requires the JweHeader '" + name + "' value to be a Base64URL-encoded String. Actual type: " + value.getClass().getName();
throw new MalformedJwtException(msg);
}
String encoded = (String) value;
ValueGetter getter = new DefaultValueGetter(header);
final byte[] inputSalt = getter.getRequiredBytes(SALT_HEADER_NAME);
final byte[] rfcSalt = Bytes.concat(SALT_PREFIX, inputSalt);
final int iterations = getter.getRequiredPositiveInteger(ITERATION_HEADER_NAME);
final char[] password = toPasswordChars(key); // will be safely cleaned/zeroed in deriveKey next:
final byte[] inputSalt = Decoders.BASE64URL.decode(encoded);
if (Arrays.length(inputSalt) == 0) {
String msg = "The " + getId() + " Key Management Algorithm does not support empty JweHeader '" + name + "' values.";
throw new MalformedJwtException(msg);
}
final byte[] rfcSalt = Bytes.plus(SALT_PREFIX, inputSalt);
name = ITERATION_HEADER_NAME;
value = header.get(name);
if (value == null) {
String msg = "The " + getId() + " Key Management Algorithm requires a JweHeader '" + name + "' value.";
throw new MalformedJwtException(msg);
}
if (!(value instanceof Integer)) {
String msg = "The " + getId() + " Key Management Algorithm requires the JweHeader '" + name + "' value to be an integer. Actual type: " + value.getClass().getName();
throw new MalformedJwtException(msg);
}
final int iterations = (Integer) value;
if (iterations <= 0) {
String msg = "The " + getId() + " Key Management Algorithm requires the JweHeader '" + name + "' value to be a positive integer. Actual value: " + iterations;
throw new MalformedJwtException(msg);
}
PBEKey pbeKey = toPBEKey(request.getKey(), iterations);
final SecretKey derivedKek = deriveKey(request, pbeKey, rfcSalt, iterations);
final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations);
KeyRequest<byte[], SecretKey> unwrapReq = new DefaultKeyRequest<>(request.getProvider(),
request.getSecureRandom(), request.getPayload(), derivedKek, header);
request.getSecureRandom(), request.getPayload(), derivedKek, header, request.getEncryptionAlgorithm());
return wrapAlg.getDecryptionKey(unwrapReq);
}

View File

@ -3,6 +3,7 @@ package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.impl.lang.Converter;
import io.jsonwebtoken.impl.lang.Converters;
import io.jsonwebtoken.impl.lang.ValueGetter;
import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Collections;
@ -127,7 +128,8 @@ class RsaPrivateJwkFactory extends AbstractFamilyJwkFactory<RSAPrivateKey, RsaPr
@Override
protected RsaPrivateJwk createJwkFromValues(JwkContext<RSAPrivateKey> ctx) {
final BigInteger privateExponent = getRequiredBigInt(ctx, DefaultRsaPrivateJwk.PRIVATE_EXPONENT, true);
final ValueGetter getter = new DefaultValueGetter(ctx);
final BigInteger privateExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.PRIVATE_EXPONENT, true);
//The [JWA Spec, Section 6.3.2](https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.2) requires
//RSA Private Keys to also encode the public key values, so we assert that we can acquire it successfully:
@ -155,11 +157,11 @@ class RsaPrivateJwkFactory extends AbstractFamilyJwkFactory<RSAPrivateKey, RsaPr
KeySpec spec;
if (containsOptional) { //if any one optional field exists, they are all required per JWA Section 6.3.2:
BigInteger firstPrime = getRequiredBigInt(ctx, DefaultRsaPrivateJwk.FIRST_PRIME, true);
BigInteger secondPrime = getRequiredBigInt(ctx, DefaultRsaPrivateJwk.SECOND_PRIME, true);
BigInteger firstCrtExponent = getRequiredBigInt(ctx, DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, true);
BigInteger secondCrtExponent = getRequiredBigInt(ctx, DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, true);
BigInteger firstCrtCoefficient = getRequiredBigInt(ctx, DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT, true);
BigInteger firstPrime = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_PRIME, true);
BigInteger secondPrime = getter.getRequiredBigInt(DefaultRsaPrivateJwk.SECOND_PRIME, true);
BigInteger firstCrtExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, true);
BigInteger secondCrtExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, true);
BigInteger firstCrtCoefficient = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT, true);
// Other Primes Info is actually optional even if the above ones are required:
if (ctx.containsKey(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO)) {
@ -226,9 +228,10 @@ class RsaPrivateJwkFactory extends AbstractFamilyJwkFactory<RSAPrivateKey, RsaPr
ctx.put(name, entry.getValue());
}
BigInteger prime = getRequiredBigInt(ctx, DefaultRsaPrivateJwk.PRIME_FACTOR, true);
BigInteger primeExponent = getRequiredBigInt(ctx, DefaultRsaPrivateJwk.FACTOR_CRT_EXPONENT, true);
BigInteger crtCoefficient = getRequiredBigInt(ctx, DefaultRsaPrivateJwk.FACTOR_CRT_COEFFICIENT, true);
final ValueGetter getter = new DefaultValueGetter(ctx);
BigInteger prime = getter.getRequiredBigInt(DefaultRsaPrivateJwk.PRIME_FACTOR, true);
BigInteger primeExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FACTOR_CRT_EXPONENT, true);
BigInteger crtCoefficient = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FACTOR_CRT_COEFFICIENT, true);
return new RSAOtherPrimeInfo(prime, primeExponent, crtCoefficient);
}

View File

@ -1,6 +1,7 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.impl.lang.ValueGetter;
import io.jsonwebtoken.security.RsaPublicJwk;
import java.math.BigInteger;
@ -26,9 +27,9 @@ class RsaPublicJwkFactory extends AbstractFamilyJwkFactory<RSAPublicKey, RsaPubl
@Override
protected RsaPublicJwk createJwkFromValues(JwkContext<RSAPublicKey> ctx) {
BigInteger modulus = getRequiredBigInt(ctx, DefaultRsaPublicJwk.MODULUS, false);
BigInteger publicExponent = getRequiredBigInt(ctx, DefaultRsaPublicJwk.PUBLIC_EXPONENT, false);
ValueGetter getter = new DefaultValueGetter(ctx);
BigInteger modulus = getter.getRequiredBigInt(DefaultRsaPublicJwk.MODULUS, false);
BigInteger publicExponent = getter.getRequiredBigInt(DefaultRsaPublicJwk.PUBLIC_EXPONENT, false);
final RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, publicExponent);
RSAPublicKey key = generateKey(ctx, new CheckedFunction<KeyFactory, RSAPublicKey>() {

View File

@ -1,10 +1,9 @@
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.impl.lang.ValueGetter;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.MalformedKeyException;
import io.jsonwebtoken.security.SecretJwk;
import io.jsonwebtoken.security.UnsupportedKeyException;
@ -58,18 +57,8 @@ class SecretJwkFactory extends AbstractFamilyJwkFactory<SecretKey, SecretJwk> {
@Override
protected SecretJwk createJwkFromValues(JwkContext<SecretKey> ctx) {
String encoded = getRequiredString(ctx, DefaultSecretJwk.K);
byte[] bytes;
try {
bytes = Decoders.BASE64URL.decode(encoded);
if (Arrays.length(bytes) == 0) {
throw new IllegalArgumentException("JWK 'k' member does not have any encoded bytes. JWK: {" + ctx + "}");
}
} catch (Exception e) {
String msg = "Unable to Base64Url-decode " + DefaultSecretJwk.TYPE_VALUE +
" JWK 'k' member value. JWK: {" + ctx + "}";
throw new MalformedKeyException(msg, e);
}
ValueGetter getter = new DefaultValueGetter(ctx);
byte[] bytes = getter.getRequiredBytes(DefaultSecretJwk.K);
SecretKey key = new SecretKeySpec(bytes, "NONE"); //TODO: do we need a JCA-specific ID here?
ctx.setKey(key);
return new DefaultSecretJwk(ctx);

View File

@ -2,14 +2,10 @@ package io.jsonwebtoken.impl
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.EncryptionAlgorithms
import io.jsonwebtoken.security.KeyAlgorithm
import io.jsonwebtoken.security.KeyAlgorithms
import io.jsonwebtoken.security.SymmetricAeadAlgorithm
import org.junit.Test
import java.security.Key
import static org.junit.Assert.*
import static org.junit.Assert.assertEquals
import static org.junit.Assert.fail
class DefaultJweBuilderTest {
@ -51,7 +47,7 @@ class DefaultJweBuilderTest {
try {
builder().setIssuer("me").withKey(key).compact()
} catch (IllegalStateException ise) {
assertEquals 'EncryptionAlgorithm is required.', ise.message
assertEquals 'Encryption algorithm is required.', ise.message
}
}

View File

@ -2,24 +2,24 @@ package io.jsonwebtoken.impl.security
import io.jsonwebtoken.MalformedJwtException
import io.jsonwebtoken.impl.DefaultJweHeader
import io.jsonwebtoken.impl.lang.Bytes
import io.jsonwebtoken.impl.lang.CheckedFunction
import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.lang.Arrays
import javax.crypto.SecretKey
import java.nio.charset.StandardCharsets
import static org.junit.Assert.*
import io.jsonwebtoken.security.EncryptionAlgorithms
import org.junit.Test
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import java.nio.charset.StandardCharsets
import static org.junit.Assert.*
class AesGcmKeyAlgorithmTest {
/**
* This tests asserts that our EncyrptionAlgorithm implementation and the JCA 'AES/GCM/NoPadding' wrap algorithm
* This tests asserts that our SymmetricAeadAlgorithm implementation and the JCA 'AES/GCM/NoPadding' wrap algorithm
* produce the exact same values. This should be the case when the transformation is identical, even though
* one uses Cipher.WRAP_MODE and the other uses a raw plaintext byte array.
*/
@ -35,9 +35,9 @@ class AesGcmKeyAlgorithmTest {
def cek = alg.generateKey();
JcaTemplate template = new JcaTemplate("AES/GCM/NoPadding", null)
byte[] jcaResult = template.execute(Cipher.class, new InstanceCallback<Cipher, byte[]>() {
byte[] jcaResult = template.execute(Cipher.class, new CheckedFunction<Cipher, byte[]>() {
@Override
byte[] doWithInstance(Cipher cipher) throws Exception {
byte[] apply(Cipher cipher) throws Exception {
cipher.init(Cipher.WRAP_MODE, kek, new GCMParameterSpec(128, iv))
return cipher.wrap(cek)
}
@ -55,7 +55,7 @@ class AesGcmKeyAlgorithmTest {
def encRequest = new DefaultSymmetricAeadRequest(null, null, cek.getEncoded(), kek, null, iv)
def encResult = EncryptionAlgorithms.A256GCM.encrypt(encRequest)
assertArrayEquals resultA.authenticationTag, encResult.authenticationTag
assertArrayEquals resultA.digest, encResult.digest
assertArrayEquals resultA.initializationVector, encResult.initializationVector
assertArrayEquals resultA.payload, encResult.payload
}
@ -70,16 +70,21 @@ class AesGcmKeyAlgorithmTest {
def header = new DefaultJweHeader()
def kek = template.generateSecretKey(keyLength)
def cek = template.generateSecretKey(keyLength)
def enc = new GcmAesAeadAlgorithm(keyLength) {
@Override
SecretKey generateKey() {
return cek;
}
}
def ereq = new DefaultKeyRequest(null, null, cek, kek, header)
def ereq = new DefaultKeyRequest(null, null, cek, kek, header, enc)
def result = alg.getEncryptionKey(ereq)
header.putAll(result.getHeaderParams())
byte[] encryptedKeyBytes = result.getPayload()
assertFalse "encryptedKey must be populated", Arrays.length(encryptedKeyBytes) == 0
def dcek = alg.getDecryptionKey(new DefaultKeyRequest<byte[], SecretKey>(null, null, encryptedKeyBytes, kek, header))
def dcek = alg.getDecryptionKey(new DefaultKeyRequest<byte[], SecretKey>(null, null, encryptedKeyBytes, kek, header, enc))
//Assert the decrypted key matches the original cek
assertEquals cek.algorithm, dcek.algorithm
@ -100,16 +105,21 @@ class AesGcmKeyAlgorithmTest {
def header = new DefaultJweHeader()
def kek = template.generateSecretKey(keyLength)
def cek = template.generateSecretKey(keyLength)
def ereq = new DefaultKeyRequest(null, null, cek, kek, header)
def enc = new GcmAesAeadAlgorithm(keyLength) {
@Override
SecretKey generateKey() {
return cek
}
}
def ereq = new DefaultKeyRequest(null, null, cek, kek, header, enc)
def result = alg.getEncryptionKey(ereq)
header.putAll(result.getHeaderParams())
header.put(headerName, value) //null value will remove it
byte[] encryptedKeyBytes = result.getPayload()
try {
alg.getDecryptionKey(new DefaultKeyRequest<byte[], SecretKey>(null, null, encryptedKeyBytes, kek, header))
alg.getDecryptionKey(new DefaultKeyRequest<byte[], SecretKey>(null, null, encryptedKeyBytes, kek, header, enc))
fail()
} catch (MalformedJwtException iae) {
assertEquals exmsg, iae.getMessage()
@ -117,17 +127,16 @@ class AesGcmKeyAlgorithmTest {
}
String missing(String name) {
return "The A128GCMKW Key Management Algorithm requires a JweHeader '${name}' value." as String
return "JWE header is missing required '${name}' value." as String
}
String type(String name) {
return "The A128GCMKW Key Management Algorithm requires the JweHeader '${name}' value to be a Base64URL-encoded String. Actual type: java.lang.Integer" as String
return "JWE header '${name}' value must be a String. Actual type: java.lang.Integer" as String
}
String base64Url(String name) {
return "JweHeader '${name}' value 'T#ZW@#' does not appear to be a valid Base64URL String: Illegal base64url character: '#'"
return "JWE header '${name}' value is not a valid Base64URL String: Illegal base64url character: '#'"
}
String length(String name, int requiredLen) {
return "The 'A128GCMKW' key management algorithm requires the JweHeader '${name}' value to be ${requiredLen * Byte.SIZE} bits (${requiredLen} bytes) in length. Actual length: 16 bits (2 bytes)."
String length(String name, int requiredBitLength) {
return "JWE header '${name}' decoded byte array must be ${Bytes.bitsMsg(requiredBitLength)} long. Actual length: ${Bytes.bitsMsg(16)}."
}
@Test
@ -151,7 +160,7 @@ class AesGcmKeyAlgorithmTest {
@Test
void testIncorrectLengths() {
def value = Encoders.BASE64URL.encode("hi".getBytes(StandardCharsets.US_ASCII))
testDecryptionHeader('iv', value, length('iv', 12))
testDecryptionHeader('tag', value, length('tag', 16))
testDecryptionHeader('iv', value, length('iv', 96))
testDecryptionHeader('tag', value, length('tag', 128))
}
}

View File

@ -1,14 +0,0 @@
package io.jsonwebtoken.impl.security
import org.junit.Test
/**
* @since JJWT_RELEASE_VERSION
*/
class DefaultJweFactoryTest {
@Test
void testDefaultCtor() {
new DefaultJweFactory()
}
}

View File

@ -21,7 +21,7 @@ class DirectKeyAlgorithmTest {
void testGetEncryptionKey() {
def alg = new DirectKeyAlgorithm()
def key = new SecretKeySpec(new byte[1], "AES")
def request = new DefaultKeyRequest(null, null, key, key, new DefaultJweHeader())
def request = new DefaultKeyRequest(null, null, key, key, new DefaultJweHeader(), null)
def result = alg.getEncryptionKey(request)
assertSame key, result.getKey()
assertEquals 0, Arrays.length(result.getPayload()) //must not have an encrypted key
@ -35,7 +35,7 @@ class DirectKeyAlgorithmTest {
@Test(expected = IllegalArgumentException)
void testGetEncryptionKeyWithNullRequestKey() {
def key = new SecretKeySpec(new byte[1], "AES")
def request = new DefaultKeyRequest(null, null, key, key, new DefaultJweHeader()) {
def request = new DefaultKeyRequest(null, null, key, key, new DefaultJweHeader(), null) {
@Override
Key getKey() {
return null
@ -48,7 +48,7 @@ class DirectKeyAlgorithmTest {
void testGetDecryptionKey() {
def alg = new DirectKeyAlgorithm()
def key = new SecretKeySpec(new byte[1], "AES")
def request = new DefaultKeyRequest(null, null, key, key, new DefaultJweHeader())
def request = new DefaultKeyRequest(null, null, key, key, new DefaultJweHeader(), null)
def result = alg.getDecryptionKey(request)
assertSame key, result
}
@ -61,7 +61,7 @@ class DirectKeyAlgorithmTest {
@Test(expected = IllegalArgumentException)
void testGetDecryptionKeyWithNullRequestKey() {
def key = new SecretKeySpec(new byte[1], "AES")
def request = new DefaultKeyRequest(null, null, key, key, new DefaultJweHeader()) {
def request = new DefaultKeyRequest(null, null, key, key, new DefaultJweHeader(), null) {
@Override
Key getKey() {
return null

View File

@ -51,7 +51,7 @@ class GcmAesAeadAlgorithmTest {
def result = alg.encrypt(req)
byte[] ciphertext = result.getPayload()
byte[] tag = result.getAuthenticationTag()
byte[] tag = result.getDigest()
byte[] iv = result.getInitializationVector()
assertArrayEquals E, ciphertext

View File

@ -33,7 +33,7 @@ class HmacAesAeadAlgorithmTest {
def req = new DefaultSymmetricAeadRequest(null, null, plaintext, key, null)
def result = alg.encrypt(req);
def realTag = result.getAuthenticationTag();
def realTag = result.getDigest();
//fake it:
def fakeTag = new byte[realTag.length]

View File

@ -1,5 +1,6 @@
package io.jsonwebtoken.impl.security
import io.jsonwebtoken.impl.lang.CheckedFunction
import io.jsonwebtoken.security.CryptoException
import io.jsonwebtoken.security.SignatureException
import org.junit.Test
@ -18,9 +19,9 @@ class JcaTemplateTest {
void testNewCipherWithExplicitProvider() {
Provider provider = Security.getProvider('SunJCE')
def template = new JcaTemplate('AES/CBC/PKCS5Padding', provider)
template.execute(Cipher.class, new InstanceCallback<Cipher, byte[]>() {
template.execute(Cipher.class, new CheckedFunction<Cipher, byte[]>() {
@Override
byte[] doWithInstance(Cipher cipher) throws Exception {
byte[] apply(Cipher cipher) throws Exception {
assertNotNull cipher
assertSame provider, cipher.provider
return new byte[0]
@ -97,9 +98,9 @@ class JcaTemplateTest {
def ex = new Exception("testing")
def template = new JcaTemplate('AES/CBC/PKCS5Padding', null)
try {
template.execute(Cipher.class, new InstanceCallback<Cipher,byte[]>() {
template.execute(Cipher.class, new CheckedFunction<Cipher, byte[]>() {
@Override
byte[] doWithInstance(Cipher cipher) throws Exception {
byte[] apply(Cipher cipher) throws Exception {
throw ex
}
})

View File

@ -0,0 +1,44 @@
package io.jsonwebtoken.impl.security
import io.jsonwebtoken.impl.DefaultJweHeader
import io.jsonwebtoken.security.EncryptionAlgorithms
import io.jsonwebtoken.security.KeyAlgorithms
import io.jsonwebtoken.security.Keys
import org.junit.Test
import java.nio.charset.StandardCharsets
class Pbes2HsAkwAlgorithmTest {
@Test
void test() {
def alg = KeyAlgorithms.PBES2_HS256_A128KW
int desiredMillis = 200
int iterations = KeyAlgorithms.estimateIterations(alg, desiredMillis)
println "Estimated iterations: $iterations"
int tries = 30
int skip = 6
//double scale = 0.5035246727
def payload = 'hello world'.getBytes(StandardCharsets.UTF_8)
def key = Keys.forPbe().setPassword('hellowor').setWorkFactor(iterations).build()
def req = new DefaultKeyRequest(null, null, null, key, new DefaultJweHeader(), EncryptionAlgorithms.A128GCM)
int sum = 0;
for(int i = 0; i < tries; i++) {
long start = System.currentTimeMillis()
alg.getEncryptionKey(req)
long end = System.currentTimeMillis()
long duration = end - start;
if (i >= skip) {
sum+= duration
}
println "Try $i: ${alg.id} took $duration millis"
}
long avg = Math.round(sum / (tries - skip))
println "Average duration: $avg"
println "scale factor: ${desiredMillis / avg}"
}
}

View File

@ -1,6 +1,5 @@
package io.jsonwebtoken.impl.security
import io.jsonwebtoken.Jwe
import io.jsonwebtoken.JweHeader
import io.jsonwebtoken.Jwts
@ -8,6 +7,8 @@ import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.io.SerializationException
import io.jsonwebtoken.io.Serializer
import io.jsonwebtoken.security.KeyRequest
import io.jsonwebtoken.security.Keys
import io.jsonwebtoken.security.PbeKey
import io.jsonwebtoken.security.SecurityRequest
import org.junit.Test
@ -303,11 +304,13 @@ class RFC7517AppendixCTest {
}
}
PbeKey pbeKey = Keys.forPbe().setPassword(RFC_SHARED_PASSPHRASE).setWorkFactor(RFC_P2C).build()
String compact = Jwts.jweBuilder()
.setPayload(RFC_JWK_JSON)
.setHeaderParam('cty', 'jwk+json')
.encryptWith(encAlg)
.withKeyFrom(RFC_SHARED_PASSPHRASE.toCharArray(), RFC_P2C, keyAlg)
.withKeyFrom(pbeKey, keyAlg)
.serializeToJsonWith(serializer) //ensure JJWT created the header as expected with an assertion serializer
.compact();

View File

@ -74,7 +74,7 @@ class RFC7518AppendixB1Test {
def result = alg.encrypt(request);
byte[] ciphertext = result.getPayload()
byte[] tag = result.getAuthenticationTag()
byte[] tag = result.getDigest()
byte[] iv = result.getInitializationVector()
assertArrayEquals E, ciphertext

View File

@ -74,7 +74,7 @@ class RFC7518AppendixB2Test {
AeadResult result = alg.encrypt(req)
byte[] resultCiphertext = result.getPayload()
byte[] resultTag = result.getAuthenticationTag()
byte[] resultTag = result.getDigest()
byte[] resultIv = result.getInitializationVector()
assertArrayEquals E, resultCiphertext

View File

@ -74,7 +74,7 @@ class RFC7518AppendixB3Test {
AeadResult result = alg.encrypt(req)
byte[] resultCiphertext = result.getPayload()
byte[] resultTag = result.getAuthenticationTag();
byte[] resultTag = result.getDigest();
byte[] resultIv = result.getInitializationVector();
assertArrayEquals E, resultCiphertext

View File

@ -49,7 +49,7 @@ class EncryptionAlgorithmsTest {
def result = alg.encrypt(request)
byte[] tag = result.getAuthenticationTag() //there is always a tag, even if there is no AAD
byte[] tag = result.getDigest() //there is always a tag, even if there is no AAD
assertNotNull tag
byte[] ciphertext = result.getPayload()
@ -87,7 +87,7 @@ class EncryptionAlgorithmsTest {
assertEquals(ciphertext.length, PLAINTEXT_BYTES.length)
}
def dreq = new DefaultAeadResult(null, null, result.getPayload(), key, AAD_BYTES, result.getAuthenticationTag(), result.getInitializationVector());
def dreq = new DefaultAeadResult(null, null, result.getPayload(), key, AAD_BYTES, result.getDigest(), result.getInitializationVector());
byte[] decryptedPlaintextBytes = alg.decrypt(dreq).getPayload()
assertArrayEquals(PLAINTEXT_BYTES, decryptedPlaintextBytes)
}