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; package io.jsonwebtoken;
import io.jsonwebtoken.security.EncryptedKeyAlgorithm;
import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.KeyAlgorithm;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm; import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
@ -17,6 +16,4 @@ public interface JweBuilder extends JwtBuilder<JweBuilder> {
JweBuilder withKey(SecretKey key); JweBuilder withKey(SecretKey key);
<K extends Key> JweBuilder withKeyFrom(K key, KeyAlgorithm<K, ?> alg); <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). * @param header the header to set (and potentially replace any existing header).
* @return the builder for method chaining. * @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 * 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. * @param params the header name/value pairs to append to the header.
* @return the builder for method chaining. * @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. //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. * @return the builder for method chaining.
* @since 0.8 * @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"> * 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); * .parseClaimsJws(compact);
* </pre> * </pre>
* <p> * <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. * @param keyLocator the locator used to retrieve decryption or signature verification keys.
* @return the parser builder for method chaining. * @return the parser builder for method chaining.

View File

@ -3,5 +3,5 @@ package io.jsonwebtoken.security;
/** /**
* @since JJWT_RELEASE_VERSION * @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 * @since JJWT_RELEASE_VERSION
*/ */
public interface AssociatedDataSource { public interface AssociatedDataSupplier {
byte[] getAssociatedData(); byte[] getAssociatedData();

View File

@ -18,8 +18,8 @@ package io.jsonwebtoken.security;
/** /**
* @since JJWT_RELEASE_VERSION * @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 * @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 * 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; 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 * A {@code KeyAlgorithm} produces the {@link SecretKey} used to encrypt or decrypt a JWE. The {@code KeyAlgorithm}
* produces a {@link SecretKey} used to encrypt or decrypt a JWE. The Key Management Algorithm used for a particular * used for a particular JWE is {@link #getId() identified} in the JWE's
* JWE is {@link #getId() identified} in the * <a href="https://tools.ietf.org/html/rfc7516#section-4.1.1">{@code alg} header</a>.
* <a href="https://tools.ietf.org/html/rfc7516#section-4.1.1">JWE's {@code alg} header</a>. * <p/>
* <h4>Key Management Mode</h4> * <p>The {@code KeyAlgorithm} interface is JJWT's idiomatic approach to the JWE specification's
* The JWE specification indicates that all {@code Key Management Algorithm}s utilize what is called a * <code><a href="https://tools.ietf.org/html/rfc7516#section-2">{@code Key Management Mode}</a></code> concept.</p>
* {@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>
* *
* @since JJWT_RELEASE_VERSION * @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 { 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 io.jsonwebtoken.lang.Classes;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Collection; 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 String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.KeyAlgorithmsBridge";
private static final Class<?>[] ID_ARG_TYPES = new Class[]{String.class}; 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); 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 KeyAlgorithm<SecretKey, SecretKey> DIRECT = forId0("dir");
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> A128KW = forId0("A128KW"); public static final KeyAlgorithm<SecretKey, SecretKey> A128KW = forId0("A128KW");
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> A192KW = forId0("A192KW"); public static final KeyAlgorithm<SecretKey, SecretKey> A192KW = forId0("A192KW");
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> A256KW = forId0("A256KW"); public static final KeyAlgorithm<SecretKey, SecretKey> A256KW = forId0("A256KW");
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> A128GCMKW = forId0("A128GCMKW"); public static final KeyAlgorithm<SecretKey, SecretKey> A128GCMKW = forId0("A128GCMKW");
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> A192GCMKW = forId0("A192GCMKW"); public static final KeyAlgorithm<SecretKey, SecretKey> A192GCMKW = forId0("A192GCMKW");
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> A256GCMKW = forId0("A256GCMKW"); public static final KeyAlgorithm<SecretKey, SecretKey> A256GCMKW = forId0("A256GCMKW");
public static final EncryptedKeyAlgorithm<RSAPublicKey, RSAPrivateKey> RSA1_5 = forId0("RSA1_5"); @SuppressWarnings("rawtypes")
public static final EncryptedKeyAlgorithm<RSAPublicKey, RSAPrivateKey> RSA_OAEP = forId0("RSA-OAEP"); public static final RsaKeyAlgorithm RSA1_5 = forId0("RSA1_5");
public static final EncryptedKeyAlgorithm<RSAPublicKey, RSAPrivateKey> RSA_OAEP_256 = forId0("RSA-OAEP-256"); @SuppressWarnings("rawtypes")
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> PBES2_HS256_A128KW = forId0("PBES2-HS256+A128KW"); public static final RsaKeyAlgorithm RSA_OAEP = forId0("RSA-OAEP");
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> PBES2_HS384_A192KW = forId0("PBES2-HS384+A192KW"); @SuppressWarnings("rawtypes")
public static final EncryptedKeyAlgorithm<SecretKey, SecretKey> PBES2_HS512_A256KW = forId0("PBES2-HS512+A256KW"); 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> { public interface KeyRequest<T, K extends Key> extends CryptoRequest<T, K> {
SymmetricAeadAlgorithm getEncryptionAlgorithm();
JweHeader getHeader(); 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; package io.jsonwebtoken.security;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import java.util.Map;
/** /**
* @since JJWT_RELEASE_VERSION * @since JJWT_RELEASE_VERSION
*/ */
public interface KeyResult extends PayloadSupplier<byte[]>, KeySupplier<SecretKey> { public interface KeyResult extends PayloadSupplier<byte[]>, KeySupplier<SecretKey> {
Map<String,?> getHeaderParams();
} }

View File

@ -16,8 +16,10 @@
package io.jsonwebtoken.security; package io.jsonwebtoken.security;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import javax.crypto.interfaces.PBEKey;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import java.security.KeyPair; import java.security.KeyPair;
@ -28,6 +30,8 @@ import java.security.KeyPair;
*/ */
public final class Keys { public final class Keys {
private static final String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.KeysBridge";
//prevent instantiation //prevent instantiation
private Keys() { private Keys() {
} }
@ -115,7 +119,7 @@ public final class Keys {
@Deprecated @Deprecated
public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) throws IllegalArgumentException { public static SecretKey secretKeyFor(io.jsonwebtoken.SignatureAlgorithm alg) throws IllegalArgumentException {
Assert.notNull(alg, "SignatureAlgorithm cannot be null."); Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
SignatureAlgorithm<?,?> salg = SignatureAlgorithms.forId(alg.name()); SignatureAlgorithm<?, ?> salg = SignatureAlgorithms.forId(alg.name());
if (!(salg instanceof SecretKeySignatureAlgorithm)) { if (!(salg instanceof SecretKeySignatureAlgorithm)) {
String msg = "The " + alg.name() + " algorithm does not support shared secret keys."; String msg = "The " + alg.name() + " algorithm does not support shared secret keys.";
throw new IllegalArgumentException(msg); throw new IllegalArgumentException(msg);
@ -210,12 +214,20 @@ public final class Keys {
@Deprecated @Deprecated
public static KeyPair keyPairFor(io.jsonwebtoken.SignatureAlgorithm alg) throws IllegalArgumentException { public static KeyPair keyPairFor(io.jsonwebtoken.SignatureAlgorithm alg) throws IllegalArgumentException {
Assert.notNull(alg, "SignatureAlgorithm cannot be null."); Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
SignatureAlgorithm<?,?> salg = SignatureAlgorithms.forId(alg.name()); SignatureAlgorithm<?, ?> salg = SignatureAlgorithms.forId(alg.name());
if (!(salg instanceof AsymmetricKeySignatureAlgorithm)) { if (!(salg instanceof AsymmetricKeySignatureAlgorithm)) {
String msg = "The " + alg.name() + " algorithm does not support Key Pairs."; String msg = "The " + alg.name() + " algorithm does not support Key Pairs.";
throw new IllegalArgumentException(msg); throw new IllegalArgumentException(msg);
} }
AsymmetricKeySignatureAlgorithm<?,?> asalg = ((AsymmetricKeySignatureAlgorithm<?,?>) salg); AsymmetricKeySignatureAlgorithm<?, ?> asalg = ((AsymmetricKeySignatureAlgorithm<?, ?>) salg);
return asalg.generateKeyPair(); 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); return locate((JwsHeader) header);
} else if (header instanceof JweHeader) { } else if (header instanceof JweHeader) {
return locate((JweHeader) header); 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) { 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) { protected R locate(JwsHeader header) {
return null; 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 { 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 * @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 * @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 * @since JJWT_RELEASE_VERSION
*/ */
public interface VerifySignatureRequest<K extends Key> extends SignatureRequest<K> { public interface VerifySignatureRequest<K extends Key> extends SignatureRequest<K>, DigestSupplier {
byte[] getSignature();
} }

View File

@ -7,7 +7,6 @@ import io.jsonwebtoken.impl.lang.Function;
import io.jsonwebtoken.impl.lang.PropagatingExceptionFunction; import io.jsonwebtoken.impl.lang.PropagatingExceptionFunction;
import io.jsonwebtoken.impl.lang.Services; import io.jsonwebtoken.impl.lang.Services;
import io.jsonwebtoken.impl.security.DefaultKeyRequest; import io.jsonwebtoken.impl.security.DefaultKeyRequest;
import io.jsonwebtoken.impl.security.DefaultPBEKey;
import io.jsonwebtoken.impl.security.DefaultSymmetricAeadRequest; import io.jsonwebtoken.impl.security.DefaultSymmetricAeadRequest;
import io.jsonwebtoken.io.SerializationException; import io.jsonwebtoken.io.SerializationException;
import io.jsonwebtoken.io.Serializer; import io.jsonwebtoken.io.Serializer;
@ -16,25 +15,24 @@ import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.lang.Strings;
import io.jsonwebtoken.security.AeadResult; import io.jsonwebtoken.security.AeadResult;
import io.jsonwebtoken.security.EncryptedKeyAlgorithm;
import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.KeyAlgorithm;
import io.jsonwebtoken.security.KeyAlgorithms; import io.jsonwebtoken.security.KeyAlgorithms;
import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyRequest;
import io.jsonwebtoken.security.KeyResult; import io.jsonwebtoken.security.KeyResult;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.PbeKey;
import io.jsonwebtoken.security.SecurityException; import io.jsonwebtoken.security.SecurityException;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm; import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
import io.jsonwebtoken.security.SymmetricAeadRequest; import io.jsonwebtoken.security.SymmetricAeadRequest;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.interfaces.PBEKey;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.Key; import java.security.Key;
import java.util.Map; import java.util.Map;
public class DefaultJweBuilder extends DefaultJwtBuilder<JweBuilder> implements JweBuilder { 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 SymmetricAeadAlgorithm enc; // MUST be Symmetric AEAD per https://tools.ietf.org/html/rfc7516#section-4.1.2
private Function<SymmetricAeadRequest, AeadResult> encFunction; private Function<SymmetricAeadRequest, AeadResult> encFunction;
@ -82,6 +80,12 @@ public class DefaultJweBuilder extends DefaultJwtBuilder<JweBuilder> implements
@Override @Override
public JweBuilder withKey(SecretKey key) { 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); return withKeyFrom(key, KeyAlgorithms.DIRECT);
} }
@ -104,11 +108,6 @@ public class DefaultJweBuilder extends DefaultJwtBuilder<JweBuilder> implements
return this; return this;
} }
@Override
public JweBuilder withKeyFrom(char[] password, int iterations, EncryptedKeyAlgorithm<SecretKey, SecretKey> alg) {
return withKeyFrom(new DefaultPBEKey(password, iterations, alg.getId()), alg);
}
@Override @Override
public String compact() { public String compact() {
@ -146,15 +145,13 @@ public class DefaultJweBuilder extends DefaultJwtBuilder<JweBuilder> implements
jweHeader.setCompressionAlgorithm(compressionCodec.getAlgorithmName()); 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, null, this.key, jweHeader, enc);
KeyRequest<SecretKey, Key> keyRequest = new DefaultKeyRequest<>(this.provider, this.secureRandom, cek, this.key, jweHeader);
KeyResult keyResult = algFunction.apply(keyRequest); KeyResult keyResult = algFunction.apply(keyRequest);
Assert.state(keyResult != null, "KeyAlgorithm must return a KeyResult."); 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."); 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.setAlgorithm(alg.getId());
jweHeader.setEncryptionAlgorithm(enc.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[] 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[] 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 base64UrlEncodedEncryptedCek = base64UrlEncoder.encode(encryptedCek);
String base64UrlEncodedIv = base64UrlEncoder.encode(iv); String base64UrlEncodedIv = base64UrlEncoder.encode(iv);

View File

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

View File

@ -455,7 +455,7 @@ public class DefaultJwtParser implements JwtParser {
throw new UnsupportedJwtException(msg); 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); final SecretKey cek = keyAlg.getDecryptionKey(request);
SymmetricAeadDecryptionRequest decryptRequest = SymmetricAeadDecryptionRequest decryptRequest =

View File

@ -216,7 +216,7 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
@Override @Override
public JwtParserBuilder addEncryptionAlgorithms(Collection<SymmetricAeadAlgorithm> encAlgs) { 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); this.extraEncryptionAlgorithms.addAll(encAlgs);
return this; return this;
} }

View File

@ -66,7 +66,7 @@ public final class Bytes {
return ints; return ints;
} }
public static byte[] plus(byte[]... arrays) { public static byte[] concat(byte[]... arrays) {
int len = 0; int len = 0;
int count = Arrays.length(arrays); int count = Arrays.length(arrays);
for(int i = 0; i < count; i++) { for(int i = 0; i < count; i++) {
@ -90,6 +90,14 @@ public final class Bytes {
return bytes == null ? 0 : bytes.length * (long)Byte.SIZE; 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) { public static void increment(byte[] a) {
for (int i = a.length - 1; i >= 0; --i) { for (int i = a.length - 1; i >= 0; --i) {
if (++a[i] != 0) { 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. * Returns {@code true} if a given elliptic {@code curve} contains the specified {@code point}, {@code false}
* Assumes elliptic curves over finite fields adhering to the reduced (a.k.a short or narrow) Weierstrass form: * otherwise. Assumes elliptic curves over finite fields adhering to the reduced (a.k.a short or narrow)
* Weierstrass form:
* <p> * <p>
* <code>y<sup>2</sup> = x<sup>3</sup> + ax + b</code> * <code>y<sup>2</sup> = x<sup>3</sup> + ax + b</code>
* </p> * </p>
@ -125,24 +126,6 @@ abstract class AbstractEcJwkFactory<K extends Key & ECKey, J extends Jwk<K>> ext
return lhs.equals(rhs); 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}. * 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) { AbstractEcJwkFactory(Class<K> keyType) {
super(DefaultEcPublicJwk.TYPE_VALUE, 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; package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders; import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Strings;
import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.InvalidKeyException;
import io.jsonwebtoken.security.Jwk; import io.jsonwebtoken.security.Jwk;
import io.jsonwebtoken.security.MalformedKeyException;
import java.math.BigInteger; import java.math.BigInteger;
import java.security.Key; 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> { 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: // 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 // 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) { 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) { 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 @Override
public T doWithInstance(KeyFactory instance) throws Exception { public T apply(KeyFactory instance) throws Exception {
try { try {
return fn.apply(instance); return fn.apply(instance);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) { } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {

View File

@ -62,10 +62,7 @@ abstract class AbstractJwk<K extends Key> implements Jwk<K> {
@Override @Override
public boolean containsKey(Object key) { public boolean containsKey(Object key) {
if (key instanceof String) { return this.context.containsKey(key);
return this.context.containsKey((String) key);
}
return false;
} }
@Override @Override
@ -75,10 +72,7 @@ abstract class AbstractJwk<K extends Key> implements Jwk<K> {
@Override @Override
public Object get(Object key) { public Object get(Object key) {
if (key instanceof String) { return this.context.get(key);
return this.context.get((String) key);
}
return null;
} }
@Override @Override
@ -132,9 +126,8 @@ abstract class AbstractJwk<K extends Key> implements Jwk<K> {
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj instanceof AbstractJwk) { if (obj instanceof Map) {
AbstractJwk<?> other = (AbstractJwk<?>) obj; return this.context.equals(obj);
return this.context.equals(other.context);
} }
return false; 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 { public boolean verify(VerifySignatureRequest<VK> request) throws SecurityException {
final VK key = Assert.notNull(request.getKey(), "Request key cannot be null."); 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.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 { try {
validateKey(key, false); validateKey(key, false);
return doVerify(request); 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 { 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."); Assert.notEmpty(providedSignature, "Request signature byte array cannot be null or empty.");
@SuppressWarnings("unchecked") byte[] computedSignature = sign((SignatureRequest<SK>)request); @SuppressWarnings("unchecked") byte[] computedSignature = sign((SignatureRequest<SK>)request);
return MessageDigest.isEqual(providedSignature, computedSignature); return MessageDigest.isEqual(providedSignature, computedSignature);

View File

@ -1,12 +1,13 @@
package io.jsonwebtoken.impl.security; package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.AssociatedDataSource; import io.jsonwebtoken.security.AssociatedDataSupplier;
import io.jsonwebtoken.security.CryptoRequest; import io.jsonwebtoken.security.InitializationVectorSupplier;
import io.jsonwebtoken.security.InitializationVectorSource; import io.jsonwebtoken.security.KeySupplier;
import io.jsonwebtoken.security.SecurityRequest;
import io.jsonwebtoken.security.SecretKeyGenerator; import io.jsonwebtoken.security.SecretKeyGenerator;
import io.jsonwebtoken.security.SecurityRequest;
import io.jsonwebtoken.security.WeakKeyException; import io.jsonwebtoken.security.WeakKeyException;
import javax.crypto.SecretKey; 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 " + "requests that do not include initialization vectors. AES ciphertext without an IV is weak and " +
"susceptible to attack."; "susceptible to attack.";
protected final int keyLength; protected final int keyBitLength;
protected final int ivLength; protected final int ivBitLength;
protected final int tagLength; protected final int tagBitLength;
protected final boolean gcm; protected final boolean gcm;
AesAlgorithm(String id, String jcaTransformation, int keyLength) { AesAlgorithm(String id, String jcaTransformation, int keyBitLength) {
super(id, jcaTransformation); super(id, jcaTransformation);
Assert.isTrue(keyLength == 128 || keyLength == 192 || keyLength == 256, "Invalid AES key length: it must equal 128, 192, or 256."); Assert.isTrue(keyBitLength == 128 || keyBitLength == 192 || keyBitLength == 256, "Invalid AES key length: it must equal 128, 192, or 256.");
this.keyLength = keyLength; this.keyBitLength = keyBitLength;
this.gcm = jcaTransformation.startsWith("AES/GCM"); this.gcm = jcaTransformation.startsWith("AES/GCM");
this.ivLength = jcaTransformation.equals("AESWrap") ? 0 : (this.gcm ? GCM_IV_SIZE : BLOCK_SIZE); this.ivBitLength = 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 : // https://tools.ietf.org/html/rfc7518#section-5.2.3 through https://tools.ietf.org/html/rfc7518#section-5.3 :
this.tagLength = this.gcm ? BLOCK_SIZE : this.keyLength; this.tagBitLength = this.gcm ? BLOCK_SIZE : this.keyBitLength;
} }
@Override @Override
public SecretKey generateKey() { 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? //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."); SecretKey key = Assert.notNull(request.getKey(), "Request key cannot be null.");
validateLengthIfPossible(key); validateLengthIfPossible(key);
return key; return key;
} }
private void validateLengthIfPossible(SecretKey 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) { protected static String lengthMsg(String id, String type, int requiredLengthInBits, int actualLengthInBits) {
return "The '" + id + "' algorithm requires " + type + " with a length of " + requiredLengthInBits + return "The '" + id + "' algorithm requires " + type + " with a length of " +
" bits (" + (requiredLengthInBits / Byte.SIZE) + " bytes). The provided key has a length of " + Bytes.bitsMsg(requiredLengthInBits) + ". The provided key has a length of " +
actualLengthInBits + " bits (" + actualLengthInBits / Byte.SIZE + " bytes)."; Bytes.bitsMsg(actualLengthInBits) + ".";
} }
protected byte[] validateLength(SecretKey key, int requiredBitLength, boolean propagate) { 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) { byte[] assertIvLength(final byte[] iv) {
int length = length(iv); int length = length(iv);
if ((this.ivLength / Byte.SIZE) != length) { if ((this.ivBitLength / Byte.SIZE) != length) {
String msg = lengthMsg(getId(), "initialization vectors", this.ivLength, length * Byte.SIZE); String msg = lengthMsg(getId(), "initialization vectors", this.ivBitLength, length * Byte.SIZE);
throw new IllegalArgumentException(msg); throw new IllegalArgumentException(msg);
} }
return iv; return iv;
@ -97,14 +98,14 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerato
byte[] assertTag(byte[] tag) { byte[] assertTag(byte[] tag) {
int len = Arrays.length(tag) * Byte.SIZE; int len = Arrays.length(tag) * Byte.SIZE;
if (this.tagLength != len) { if (this.tagBitLength != len) {
String msg = lengthMsg(getId(), "authentication tags", this.tagLength, len); String msg = lengthMsg(getId(), "authentication tags", this.tagBitLength, len);
throw new IllegalArgumentException(msg); throw new IllegalArgumentException(msg);
} }
return tag; return tag;
} }
byte[] assertDecryptionIv(InitializationVectorSource src) throws IllegalArgumentException { byte[] assertDecryptionIv(InitializationVectorSupplier src) throws IllegalArgumentException {
byte[] iv = src.getInitializationVector(); byte[] iv = src.getInitializationVector();
Assert.notEmpty(iv, DECRYPT_NO_IV); Assert.notEmpty(iv, DECRYPT_NO_IV);
return assertIvLength(iv); return assertIvLength(iv);
@ -112,10 +113,10 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerato
protected byte[] ensureInitializationVector(SecurityRequest request) { protected byte[] ensureInitializationVector(SecurityRequest request) {
byte[] iv = null; byte[] iv = null;
if (request instanceof InitializationVectorSource) { if (request instanceof InitializationVectorSupplier) {
iv = Arrays.clean(((InitializationVectorSource) request).getInitializationVector()); iv = Arrays.clean(((InitializationVectorSupplier) request).getInitializationVector());
} }
int ivByteLength = this.ivLength / Byte.SIZE; int ivByteLength = this.ivBitLength / Byte.SIZE;
if (iv == null || iv.length == 0) { if (iv == null || iv.length == 0) {
iv = new byte[ivByteLength]; iv = new byte[ivByteLength];
SecureRandom random = ensureSecureRandom(request); SecureRandom random = ensureSecureRandom(request);
@ -135,16 +136,9 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements SecretKeyGenerato
protected byte[] getAAD(SecurityRequest request) { protected byte[] getAAD(SecurityRequest request) {
byte[] aad = null; byte[] aad = null;
if (request instanceof AssociatedDataSource) { if (request instanceof AssociatedDataSupplier) {
aad = Arrays.clean(((AssociatedDataSource) request).getAssociatedData()); aad = Arrays.clean(((AssociatedDataSupplier) request).getAssociatedData());
} }
return aad; 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; package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.JweHeader; 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.io.Encoders;
import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Maps; import io.jsonwebtoken.security.KeyAlgorithm;
import io.jsonwebtoken.security.EncryptedKeyAlgorithm;
import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyRequest;
import io.jsonwebtoken.security.KeyResult; import io.jsonwebtoken.security.KeyResult;
import io.jsonwebtoken.security.SecurityException; import io.jsonwebtoken.security.SecurityException;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import java.security.Key; import java.security.Key;
import java.security.spec.AlgorithmParameterSpec; import java.security.spec.AlgorithmParameterSpec;
import java.util.Map;
/** /**
* @since JJWT_RELEASE_VERSION * @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"; 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."); Assert.notNull(request, "request cannot be null.");
final SecretKey kek = assertKey(request); 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 byte[] iv = ensureInitializationVector(request);
final AlgorithmParameterSpec ivSpec = getIvSpec(iv); 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 @Override
public byte[] doWithInstance(Cipher cipher) throws Exception { public byte[] apply(Cipher cipher) throws Exception {
cipher.init(Cipher.WRAP_MODE, kek, ivSpec); cipher.init(Cipher.WRAP_MODE, kek, ivSpec);
return cipher.wrap(cek); 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: // When using GCM mode, the JDK appends the authentication tag to the ciphertext, so let's extract it:
int ciphertextLength = taggedCiphertext.length - tagByteLength; int ciphertextLength = taggedCiphertext.length - tagByteLength;
byte[] ciphertext = new byte[ciphertextLength]; byte[] ciphertext = new byte[ciphertextLength];
@ -56,9 +56,10 @@ public class AesGcmKeyAlgorithm extends AesAlgorithm implements EncryptedKeyAlgo
String encodedIv = Encoders.BASE64URL.encode(iv); String encodedIv = Encoders.BASE64URL.encode(iv);
String encodedTag = Encoders.BASE64URL.encode(tag); 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 @Override
@ -67,53 +68,22 @@ public class AesGcmKeyAlgorithm extends AesAlgorithm implements EncryptedKeyAlgo
final SecretKey kek = assertKey(request); final SecretKey kek = assertKey(request);
final byte[] cekBytes = Assert.notEmpty(request.getPayload(), "Decryption request payload (ciphertext) cannot be null or empty."); 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 JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null.");
final byte[] tag = getHeaderByteArray(header, "tag", this.tagLength / Byte.SIZE); final ValueGetter getter = new DefaultValueGetter(header);
final byte[] iv = getHeaderByteArray(header, "iv", this.ivLength / Byte.SIZE); 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); 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: //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 @Override
public SecretKey doWithInstance(Cipher cipher) throws Exception { public SecretKey apply(Cipher cipher) throws Exception {
cipher.init(Cipher.UNWRAP_MODE, kek, ivSpec); cipher.init(Cipher.UNWRAP_MODE, kek, ivSpec);
Key key = cipher.unwrap(taggedCiphertext, KEY_ALG_NAME, Cipher.SECRET_KEY); Key key = cipher.unwrap(taggedCiphertext, KEY_ALG_NAME, Cipher.SECRET_KEY);
Assert.state(key instanceof SecretKey, "cipher.unwrap must produce a SecretKey instance."); 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; package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.EncryptedKeyAlgorithm; import io.jsonwebtoken.security.KeyAlgorithm;
import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyRequest;
import io.jsonwebtoken.security.KeyResult; import io.jsonwebtoken.security.KeyResult;
import io.jsonwebtoken.security.SecurityException; import io.jsonwebtoken.security.SecurityException;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
@ -13,7 +15,7 @@ import java.security.Key;
/** /**
* @since JJWT_RELEASE_VERSION * @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"; 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 { public KeyResult getEncryptionKey(KeyRequest<SecretKey, SecretKey> request) throws SecurityException {
Assert.notNull(request, "request cannot be null."); Assert.notNull(request, "request cannot be null.");
final SecretKey kek = assertKey(request); 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 @Override
public byte[] doWithInstance(Cipher cipher) throws Exception { public byte[] apply(Cipher cipher) throws Exception {
cipher.init(Cipher.WRAP_MODE, kek); cipher.init(Cipher.WRAP_MODE, kek);
return cipher.wrap(cek); return cipher.wrap(cek);
} }
}); });
return new DefaultKeyResult(ciphertext, cek); return new DefaultKeyResult(cek, ciphertext);
} }
@Override @Override
@ -44,9 +48,9 @@ public class AesWrapKeyAlgorithm extends AesAlgorithm implements EncryptedKeyAlg
final SecretKey kek = assertKey(request); final SecretKey kek = assertKey(request);
final byte[] cekBytes = Assert.notEmpty(request.getPayload(), "Request encrypted key (request.getPayload()) cannot be null or empty."); 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 @Override
public SecretKey doWithInstance(Cipher cipher) throws Exception { public SecretKey apply(Cipher cipher) throws Exception {
cipher.init(Cipher.UNWRAP_MODE, kek); cipher.init(Cipher.UNWRAP_MODE, kek);
Key key = cipher.unwrap(cekBytes, KEY_ALG_NAME, Cipher.SECRET_KEY); Key key = cipher.unwrap(cekBytes, KEY_ALG_NAME, Cipher.SECRET_KEY);
Assert.state(key instanceof SecretKey, "Cipher unwrap must return a SecretKey instance."); Assert.state(key instanceof SecretKey, "Cipher unwrap must return a SecretKey instance.");

View File

@ -1,5 +1,6 @@
package io.jsonwebtoken.impl.security; package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.CryptoException; import io.jsonwebtoken.security.CryptoException;
@ -29,9 +30,9 @@ final class ConcatKDF extends CryptoAlgorithm {
ConcatKDF(String jcaName) { ConcatKDF(String jcaName) {
super("ConcatKDF", 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 @Override
public Integer doWithInstance(MessageDigest instance) { public Integer apply(MessageDigest instance) {
return instance.getDigestLength(); 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: // 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) { if (derivedKeyBitLength > MAX_DERIVED_KEY_BIT_LENGTH) {
String msg = "derivedKeyBitLength for " + getJcaName() + " derived keys may not exceed " + String msg = "derivedKeyBitLength for " + getJcaName() + "-derived keys may not exceed " +
MAX_DERIVED_KEY_BIT_LENGTH + " bits (" + MAX_DERIVED_KEY_BIT_LENGTH / Byte.SIZE + " bytes). " + bitsMsg(MAX_DERIVED_KEY_BIT_LENGTH) + ". Specified size: " + bitsMsg(derivedKeyBitLength) + ".";
"Specified size: " + derivedKeyBitLength + " bits (" + derivedKeyBitLength / Byte.SIZE + " bytes).";
throw new IllegalArgumentException(msg); throw new IllegalArgumentException(msg);
} }
@ -94,11 +94,11 @@ final class ConcatKDF extends CryptoAlgorithm {
long inputBitLength = bitLength(counter) + bitLength(Z) + bitLength(OtherInfo); long inputBitLength = bitLength(counter) + bitLength(Z) + bitLength(OtherInfo);
assert inputBitLength <= MAX_HASH_INPUT_BIT_LENGTH : "Hash input is too large."; 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 @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; long kLastIndex = reps - 1;
// Section 5.8.1.1, Process step #5: // 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: // Section 5.8.1.1, Process step #6:
if (i == kLastIndex && repsd != (double) reps) { //repsd calculation above didn't result in a whole number: if (i == kLastIndex && repsd != (double) reps) { //repsd calculation above didn't result in a whole number:
long leftmostBitLength = derivedKeyBitLength % hashBitLength; long leftmostBitLength = derivedKeyBitLength % hashBitLength;
int leftmostByteLength = (int)(leftmostBitLength / Byte.SIZE); int leftmostByteLength = (int) (leftmostBitLength / Byte.SIZE);
byte[] kLast = new byte[leftmostByteLength]; byte[] kLast = new byte[leftmostByteLength];
System.arraycopy(Ki, 0, kLast, 0, kLast.length); System.arraycopy(Ki, 0, kLast, 0, kLast.length);
Ki = kLast; Ki = kLast;

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package io.jsonwebtoken.impl.security; package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.JwtException; import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.EllipticCurveSignatureAlgorithm; import io.jsonwebtoken.security.EllipticCurveSignatureAlgorithm;
import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.InvalidKeyException;
@ -55,9 +56,9 @@ public class DefaultEllipticCurveSignatureAlgorithm<SK extends ECKey & PrivateKe
public KeyPair generateKeyPair() { public KeyPair generateKeyPair() {
final ECGenParameterSpec spec = new ECGenParameterSpec(this.curveName); final ECGenParameterSpec spec = new ECGenParameterSpec(this.curveName);
JcaTemplate template = new JcaTemplate("EC", null); 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 @Override
public KeyPair doWithInstance(KeyPairGenerator generator) throws Exception { public KeyPair apply(KeyPairGenerator generator) throws Exception {
generator.initialize(spec, Randoms.secureRandom()); generator.initialize(spec, Randoms.secureRandom());
return generator.generateKeyPair(); return generator.generateKeyPair();
} }
@ -104,9 +105,9 @@ public class DefaultEllipticCurveSignatureAlgorithm<SK extends ECKey & PrivateKe
@Override @Override
protected byte[] doSign(final SignatureRequest<SK> request) { 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 @Override
public byte[] doWithInstance(Signature sig) throws Exception { public byte[] apply(Signature sig) throws Exception {
sig.initSign(request.getKey()); sig.initSign(request.getKey());
sig.update(request.getPayload()); sig.update(request.getPayload());
byte[] signature = sig.sign(); byte[] signature = sig.sign();
@ -117,10 +118,10 @@ public class DefaultEllipticCurveSignatureAlgorithm<SK extends ECKey & PrivateKe
@Override @Override
protected boolean doVerify(final VerifySignatureRequest<VK> request) { 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 @Override
public Boolean doWithInstance(Signature sig) throws Exception { public Boolean apply(Signature sig) throws Exception {
byte[] signature = request.getSignature(); byte[] signature = request.getDigest();
/* /*
* If the expected size is not valid for JOSE, fall back to ASN.1 DER signature. * 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) * 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); SETTERS = java.util.Collections.unmodifiableMap(s);
} }
private final Map<String, Object> values; private final Map<String, Object> values; // canonical values formatted per RFC requirements
private final Map<String, Object> canonicalValues; 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; 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; private final Set<String> privateMemberNames; // names of values that should be redacted for toString output
private K key; private K key;
private PublicKey publicKey; private PublicKey publicKey;
private Provider provider; private Provider provider;
@ -84,7 +84,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
public DefaultJwkContext(Set<String> privateMemberNames) { public DefaultJwkContext(Set<String> privateMemberNames) {
this.privateMemberNames = Assert.notEmpty(privateMemberNames, "privateMemberNames cannot be null or empty."); this.privateMemberNames = Assert.notEmpty(privateMemberNames, "privateMemberNames cannot be null or empty.");
this.values = new LinkedHashMap<>(); this.values = new LinkedHashMap<>();
this.canonicalValues = new LinkedHashMap<>(); this.idiomaticValues = new LinkedHashMap<>();
this.redactedValues = new LinkedHashMap<>(); this.redactedValues = new LinkedHashMap<>();
} }
@ -111,7 +111,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
DefaultJwkContext<?> src = (DefaultJwkContext<?>) other; DefaultJwkContext<?> src = (DefaultJwkContext<?>) other;
this.provider = other.getProvider(); this.provider = other.getProvider();
this.values = new LinkedHashMap<>(src.values); this.values = new LinkedHashMap<>(src.values);
this.canonicalValues = new LinkedHashMap<>(src.canonicalValues); this.idiomaticValues = new LinkedHashMap<>(src.idiomaticValues);
this.redactedValues = new LinkedHashMap<>(src.redactedValues); this.redactedValues = new LinkedHashMap<>(src.redactedValues);
if (removePrivate) { if (removePrivate) {
for (String name : this.privateMemberNames) { for (String name : this.privateMemberNames) {
@ -126,7 +126,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
} else { } else {
Object redactedValue = this.privateMemberNames.contains(name) ? AbstractJwk.REDACTED_VALUE : value; Object redactedValue = this.privateMemberNames.contains(name) ? AbstractJwk.REDACTED_VALUE : value;
this.redactedValues.put(name, redactedValue); this.redactedValues.put(name, redactedValue);
this.canonicalValues.put(name, value); this.idiomaticValues.put(name, value);
return this.values.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()) { } else if (Objects.isArray(value) && !value.getClass().getComponentType().isPrimitive()) {
value = Collections.arrayToList(value); 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. assert name != null; //asserted by caller.
Canonicalizer<?> fn = SETTERS.get(name); 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); return fn.apply(this, value);
} else { //non-standard/custom property: } else { //non-standard/custom property:
return nullSafePut(name, value); return nullSafePut(name, value);
@ -153,17 +155,17 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
} }
@Override @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."); Assert.notEmpty(m, "JWK values cannot be null or empty.");
for (Map.Entry<? extends String, ?> entry : m.entrySet()) { for (Map.Entry<? extends String, ?> entry : m.entrySet()) {
put(entry.getKey(), entry.getValue()); put(entry.getKey(), entry.getValue());
} }
return this;
} }
private Object remove(String key) { @Override
public Object remove(Object key) {
this.redactedValues.remove(key); this.redactedValues.remove(key);
this.canonicalValues.remove(key); this.idiomaticValues.remove(key);
return this.values.remove(key); return this.values.remove(key);
} }
@ -178,7 +180,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
} }
@Override @Override
public boolean containsKey(String key) { public boolean containsKey(Object key) {
return this.values.containsKey(key); return this.values.containsKey(key);
} }
@ -188,10 +190,15 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
} }
@Override @Override
public Object get(String key) { public Object get(Object key) {
return this.values.get(key); return this.values.get(key);
} }
@Override
public void clear() {
throw new UnsupportedOperationException("Cannot clear JwkContext objects.");
}
@Override @Override
public Set<String> keySet() { public Set<String> keySet() {
return this.values.keySet(); return this.values.keySet();
@ -207,14 +214,9 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
return this.values.entrySet(); return this.values.entrySet();
} }
@Override
public Map<String, Object> getValues() {
return this.values;
}
@Override @Override
public String getAlgorithm() { public String getAlgorithm() {
return (String) this.canonicalValues.get(AbstractJwk.ALGORITHM); return (String) this.values.get(AbstractJwk.ALGORITHM);
} }
@Override @Override
@ -225,7 +227,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override @Override
public String getId() { public String getId() {
return (String) this.canonicalValues.get(AbstractJwk.ID); return (String) this.values.get(AbstractJwk.ID);
} }
@Override @Override
@ -237,7 +239,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override @Override
public Set<String> getOperations() { public Set<String> getOperations() {
//noinspection unchecked //noinspection unchecked
return (Set<String>) this.canonicalValues.get(AbstractJwk.OPERATIONS); return (Set<String>) this.idiomaticValues.get(AbstractJwk.OPERATIONS);
} }
@Override @Override
@ -248,7 +250,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override @Override
public String getType() { public String getType() {
return (String) this.canonicalValues.get(AbstractJwk.TYPE); return (String) this.values.get(AbstractJwk.TYPE);
} }
@Override @Override
@ -259,7 +261,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override @Override
public String getPublicKeyUse() { public String getPublicKeyUse() {
return (String) this.canonicalValues.get(AbstractAsymmetricJwk.PUBLIC_KEY_USE); return (String) this.values.get(AbstractAsymmetricJwk.PUBLIC_KEY_USE);
} }
@Override @Override
@ -271,7 +273,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override @Override
public List<X509Certificate> getX509CertificateChain() { public List<X509Certificate> getX509CertificateChain() {
//noinspection unchecked //noinspection unchecked
return (List<X509Certificate>) this.canonicalValues.get(AbstractAsymmetricJwk.X509_CERT_CHAIN); return (List<X509Certificate>) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_CERT_CHAIN);
} }
@Override @Override
@ -282,7 +284,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override @Override
public byte[] getX509CertificateSha1Thumbprint() { public byte[] getX509CertificateSha1Thumbprint() {
return (byte[]) this.canonicalValues.get(AbstractAsymmetricJwk.X509_SHA1_THUMBPRINT); return (byte[]) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_SHA1_THUMBPRINT);
} }
@Override @Override
@ -293,7 +295,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override @Override
public byte[] getX509CertificateSha256Thumbprint() { public byte[] getX509CertificateSha256Thumbprint() {
return (byte[]) this.canonicalValues.get(AbstractAsymmetricJwk.X509_SHA256_THUMBPRINT); return (byte[]) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_SHA256_THUMBPRINT);
} }
@Override @Override
@ -304,7 +306,7 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override @Override
public URI getX509Url() { public URI getX509Url() {
return (URI) this.canonicalValues.get(AbstractAsymmetricJwk.X509_URL); return (URI) this.idiomaticValues.get(AbstractAsymmetricJwk.X509_URL);
} }
@Override @Override
@ -353,18 +355,13 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
@Override @Override
public int hashCode() { public int hashCode() {
int hash = 7; return this.values.hashCode();
hash = hash * 31 + Objects.nullSafeHashCode(this.key);
hash = hash * 31 + Objects.nullSafeHashCode(this.values);
return hash;
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj instanceof DefaultJwkContext) { if (obj instanceof Map) {
DefaultJwkContext<?> c = (DefaultJwkContext<?>) obj; return this.values.equals(obj);
return Objects.nullSafeEquals(this.key, c.key) &&
Objects.nullSafeEquals(this.values, c.values);
} }
return false; return false;
} }
@ -403,23 +400,23 @@ public class DefaultJwkContext<K extends Key> implements JwkContext<K> {
public T apply(DefaultJwkContext<?> ctx, Object rawValue) { public T apply(DefaultJwkContext<?> ctx, Object rawValue) {
if (JwtMap.isReduceableToNull(rawValue)) { if (JwtMap.isReduceableToNull(rawValue)) {
//noinspection unchecked ctx.remove(id);
return (T) ctx.remove(id); return null;
} }
T canonicalValue; T idiomaticValue; // preferred Java format
Object encodedValue; Object canonicalValue; //as required by the RFC
try { try {
canonicalValue = converter.applyFrom(rawValue); idiomaticValue = converter.applyFrom(rawValue);
encodedValue = converter.applyTo(canonicalValue); canonicalValue = converter.applyTo(idiomaticValue);
} catch (Exception e) { } 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); throw new MalformedKeyException(msg, e);
} }
ctx.nullSafePut(id, encodedValue); ctx.nullSafePut(id, canonicalValue);
ctx.canonicalValues.put(id, canonicalValue); ctx.idiomaticValues.put(id, idiomaticValue);
//noinspection unchecked //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.JweHeader;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyRequest;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
import java.security.Key; import java.security.Key;
import java.security.Provider; 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> { public class DefaultKeyRequest<T, K extends Key> extends DefaultCryptoRequest<T, K> implements KeyRequest<T, K> {
private final JweHeader header; private final JweHeader header;
private final SymmetricAeadAlgorithm encryptionAlgorithm;
public DefaultKeyRequest(Provider provider, SecureRandom secureRandom, T data, K key, JweHeader header) { public DefaultKeyRequest(Provider provider, SecureRandom secureRandom, T payload, K key, JweHeader header, SymmetricAeadAlgorithm encryptionAlgorithm) {
super(provider, secureRandom, data, key); super(provider, secureRandom, payload, key);
this.header = Assert.notNull(header, "JweHeader cannot be null."); 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 @Override
public JweHeader getHeader() { public JweHeader getHeader() {
return this.header; return this.header;
} }
@Override
public SymmetricAeadAlgorithm getEncryptionAlgorithm() {
return this.encryptionAlgorithm;
}
} }

View File

@ -1,34 +1,23 @@
package io.jsonwebtoken.impl.security; package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.KeyResult; import io.jsonwebtoken.security.KeyResult;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
public class DefaultKeyResult implements KeyResult { public class DefaultKeyResult implements KeyResult {
private static final byte[] EMPTY_BYTES = new byte[0];
private final byte[] payload; private final byte[] payload;
private final SecretKey key; private final SecretKey key;
private final Map<String, ?> headerParams;
public DefaultKeyResult(SecretKey key) { public DefaultKeyResult(SecretKey key) {
this(EMPTY_BYTES, key); this(key, Bytes.EMPTY);
} }
public DefaultKeyResult(byte[] encryptedKey, SecretKey key) { public DefaultKeyResult(SecretKey key, byte[] encryptedKey) {
this(encryptedKey, key, Collections.<String, Object>emptyMap());
}
public DefaultKeyResult(byte[] encryptedKey, SecretKey key, Map<String, ?> headerParams) {
this.payload = Assert.notNull(encryptedKey, "encryptedKey cannot be null (but can be empty)."); this.payload = Assert.notNull(encryptedKey, "encryptedKey cannot be null (but can be empty).");
this.key = Assert.notNull(key, "Key argument cannot be null."); 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 @Override
@ -40,9 +29,4 @@ public class DefaultKeyResult implements KeyResult {
public SecretKey getKey() { public SecretKey getKey() {
return this.key; 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; private final T payload;
DefaultPayloadSupplier(T payload) { DefaultPayloadSupplier(T payload) {
this.payload = Assert.notNull(payload, "payload cannot be null."); this.payload = assertValidPayload(payload);
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."); 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 @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; package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.EncryptedKeyAlgorithm;
import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyRequest;
import io.jsonwebtoken.security.KeyResult; import io.jsonwebtoken.security.KeyResult;
import io.jsonwebtoken.security.RsaKeyAlgorithm;
import io.jsonwebtoken.security.SecurityException; import io.jsonwebtoken.security.SecurityException;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
@ -14,8 +16,8 @@ import java.security.PublicKey;
import java.security.interfaces.RSAKey; import java.security.interfaces.RSAKey;
import java.security.spec.AlgorithmParameterSpec; import java.security.spec.AlgorithmParameterSpec;
public class DefaultRsaKeyAlgorithm<EK extends RSAKey & PublicKey, DK extends RSAKey & PrivateKey> extends CryptoAlgorithm public class DefaultRsaKeyAlgorithm<E extends RSAKey & PublicKey, D extends RSAKey & PrivateKey> extends CryptoAlgorithm
implements EncryptedKeyAlgorithm<EK, DK> { implements RsaKeyAlgorithm<E, D> {
private final AlgorithmParameterSpec SPEC; //can be null private final AlgorithmParameterSpec SPEC; //can be null
@ -29,14 +31,15 @@ public class DefaultRsaKeyAlgorithm<EK extends RSAKey & PublicKey, DK extends RS
} }
@Override @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."); Assert.notNull(request, "Request cannot be null.");
final EK kek = Assert.notNull(request.getKey(), "Request key encryption key cannot be null."); final E 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."); 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 @Override
public byte[] doWithInstance(Cipher cipher) throws Exception { public byte[] apply(Cipher cipher) throws Exception {
if (SPEC == null) { if (SPEC == null) {
cipher.init(Cipher.WRAP_MODE, kek, ensureSecureRandom(request)); cipher.init(Cipher.WRAP_MODE, kek, ensureSecureRandom(request));
} else { } 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 @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."); 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."); 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 @Override
public SecretKey doWithInstance(Cipher cipher) throws Exception { public SecretKey apply(Cipher cipher) throws Exception {
if (SPEC == null) { if (SPEC == null) {
cipher.init(Cipher.UNWRAP_MODE, kek); cipher.init(Cipher.UNWRAP_MODE, kek);
} else { } else {

View File

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

View File

@ -1,6 +1,7 @@
package io.jsonwebtoken.impl.security; package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.impl.lang.ValueGetter;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.EcPrivateJwk; import io.jsonwebtoken.security.EcPrivateJwk;
import io.jsonwebtoken.security.EcPublicJwk; import io.jsonwebtoken.security.EcPublicJwk;
@ -55,8 +56,9 @@ class EcPrivateJwkFactory extends AbstractEcJwkFactory<ECPrivateKey, EcPrivateJw
@Override @Override
protected EcPrivateJwk createJwkFromValues(final JwkContext<ECPrivateKey> ctx) { protected EcPrivateJwk createJwkFromValues(final JwkContext<ECPrivateKey> ctx) {
String curveId = getRequiredString(ctx, DefaultEcPublicJwk.CURVE_ID); ValueGetter getter = new DefaultValueGetter(ctx);
BigInteger d = getRequiredBigInt(ctx, DefaultEcPrivateJwk.D, true); 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 // 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) // [JWA spec](https://tools.ietf.org/html/rfc7518#section-6.2.2)

View File

@ -1,6 +1,7 @@
package io.jsonwebtoken.impl.security; package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction; import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.impl.lang.ValueGetter;
import io.jsonwebtoken.security.EcPublicJwk; import io.jsonwebtoken.security.EcPublicJwk;
import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.InvalidKeyException;
@ -45,9 +46,10 @@ class EcPublicJwkFactory extends AbstractEcJwkFactory<ECPublicKey, EcPublicJwk>
@Override @Override
protected EcPublicJwk createJwkFromValues(final JwkContext<ECPublicKey> ctx) { protected EcPublicJwk createJwkFromValues(final JwkContext<ECPublicKey> ctx) {
String curveId = getRequiredString(ctx, DefaultEcPublicJwk.CURVE_ID); ValueGetter getter = new DefaultValueGetter(ctx);
BigInteger x = getRequiredBigInt(ctx, DefaultEcPublicJwk.X, false); String curveId = getter.getRequiredString(DefaultEcPublicJwk.CURVE_ID);
BigInteger y = getRequiredBigInt(ctx, DefaultEcPublicJwk.Y, false); BigInteger x = getter.getRequiredBigInt(DefaultEcPublicJwk.X, false);
BigInteger y = getter.getRequiredBigInt(DefaultEcPublicJwk.Y, false);
ECParameterSpec spec = getCurveByJwaId(curveId); ECParameterSpec spec = getCurveByJwaId(curveId);
ECPoint point = new ECPoint(x, y); 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) { public static SymmetricAeadAlgorithm forId(String id) {
SymmetricAeadAlgorithm alg = findById(id); SymmetricAeadAlgorithm alg = findById(id);
if (alg == null) { if (alg == null) {
String msg = "Unrecognized JWA EncryptionAlgorithm identifier: " + id; String msg = "Unrecognized JWA SymmetricAeadAlgorithm identifier: " + id;
throw new UnsupportedJwtException(msg); throw new UnsupportedJwtException(msg);
} }
return alg; return alg;

View File

@ -1,14 +1,16 @@
package io.jsonwebtoken.impl.security; 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.Arrays;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.RuntimeEnvironment; import io.jsonwebtoken.lang.RuntimeEnvironment;
import io.jsonwebtoken.security.AeadResult;
import io.jsonwebtoken.security.CryptoException; import io.jsonwebtoken.security.CryptoException;
import io.jsonwebtoken.security.KeyException; import io.jsonwebtoken.security.KeyException;
import io.jsonwebtoken.security.PayloadSupplier; import io.jsonwebtoken.security.PayloadSupplier;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm; import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
import io.jsonwebtoken.security.SymmetricAeadDecryptionRequest; import io.jsonwebtoken.security.SymmetricAeadDecryptionRequest;
import io.jsonwebtoken.security.AeadResult;
import io.jsonwebtoken.security.SymmetricAeadRequest; import io.jsonwebtoken.security.SymmetricAeadRequest;
import javax.crypto.Cipher; import javax.crypto.Cipher;
@ -42,9 +44,9 @@ public class GcmAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadAl
final byte[] iv = ensureInitializationVector(req); final byte[] iv = ensureInitializationVector(req);
final AlgorithmParameterSpec ivSpec = getIvSpec(iv); 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 @Override
public byte[] doWithInstance(Cipher cipher) throws Exception { public byte[] apply(Cipher cipher) throws Exception {
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec); cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
if (Arrays.length(aad) > 0) { if (Arrays.length(aad) > 0) {
cipher.updateAAD(aad); cipher.updateAAD(aad);
@ -61,7 +63,7 @@ public class GcmAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadAl
byte[] tag = new byte[BLOCK_BYTE_SIZE]; byte[] tag = new byte[BLOCK_BYTE_SIZE];
System.arraycopy(taggedCiphertext, ciphertextLength, tag, 0, 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 @Override
@ -71,16 +73,16 @@ public class GcmAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadAl
final SecretKey key = assertKey(req); final SecretKey key = assertKey(req);
final byte[] ciphertext = Assert.notEmpty(req.getPayload(), "Decryption request payload (ciphertext) cannot be null or empty."); final byte[] ciphertext = Assert.notEmpty(req.getPayload(), "Decryption request payload (ciphertext) cannot be null or empty.");
final byte[] aad = getAAD(req); 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 byte[] iv = assertDecryptionIv(req);
final AlgorithmParameterSpec ivSpec = getIvSpec(iv); 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: //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 @Override
public byte[] doWithInstance(Cipher cipher) throws Exception { public byte[] apply(Cipher cipher) throws Exception {
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
if (Arrays.length(aad) > 0) { if (Arrays.length(aad) > 0) {
cipher.updateAAD(aad); cipher.updateAAD(aad);

View File

@ -1,5 +1,6 @@
package io.jsonwebtoken.impl.security; package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.AeadResult; import io.jsonwebtoken.security.AeadResult;
import io.jsonwebtoken.security.CryptoRequest; import io.jsonwebtoken.security.CryptoRequest;
@ -46,12 +47,12 @@ public class HmacAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadA
@Override @Override
public SecretKey generateKey() { 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) { byte[] assertKeyBytes(CryptoRequest<?, SecretKey> request) {
SecretKey key = Assert.notNull(request.getKey(), "Request key cannot be null."); 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 @Override
@ -70,9 +71,9 @@ public class HmacAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadA
final byte[] iv = ensureInitializationVector(req); final byte[] iv = ensureInitializationVector(req);
final AlgorithmParameterSpec ivSpec = getIvSpec(iv); 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 @Override
public byte[] doWithInstance(Cipher cipher) throws Exception { public byte[] apply(Cipher cipher) throws Exception {
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, ivSpec); cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, ivSpec);
return cipher.doFinal(plaintext); 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[] ciphertext = Assert.notEmpty(req.getPayload(), "Decryption request payload (ciphertext) cannot be null or empty.");
final byte[] aad = getAAD(req); final byte[] aad = getAAD(req);
final byte[] tag = assertTag(req.getAuthenticationTag()); final byte[] tag = assertTag(req.getDigest());
final byte[] iv = assertDecryptionIv(req); final byte[] iv = assertDecryptionIv(req);
final AlgorithmParameterSpec ivSpec = getIvSpec(iv); final AlgorithmParameterSpec ivSpec = getIvSpec(iv);
@ -147,9 +148,9 @@ public class HmacAesAeadAlgorithm extends AesAlgorithm implements SymmetricAeadA
throw new SignatureException(msg); 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 @Override
public byte[] doWithInstance(Cipher cipher) throws Exception { public byte[] apply(Cipher cipher) throws Exception {
cipher.init(Cipher.DECRYPT_MODE, decryptionKey, ivSpec); cipher.init(Cipher.DECRYPT_MODE, decryptionKey, ivSpec);
return cipher.doFinal(ciphertext); 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; package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes; import io.jsonwebtoken.lang.Classes;
import io.jsonwebtoken.security.CryptoException; import io.jsonwebtoken.security.CryptoException;
@ -32,34 +33,34 @@ public class JcaTemplate {
this.secureRandom = Assert.notNull(secureRandom, "SecureRandom cannot be null."); this.secureRandom = Assert.notNull(secureRandom, "SecureRandom cannot be null.");
} }
public <I, T> T execute(Class<I> clazz, InstanceCallback<I, T> callback) throws CryptoException { public <T, R> R execute(Class<T> clazz, CheckedFunction<T, R> fn) throws CryptoException {
return execute(new JcaInstanceSupplier<>(clazz, this.jcaName, this.provider), callback); return execute(new JcaInstanceSupplier<>(clazz, this.jcaName, this.provider), fn);
} }
public SecretKey generateSecretKey(final int keyLength) { public SecretKey generateSecretKey(final int keyBitLength) {
return execute(KeyGenerator.class, new InstanceCallback<KeyGenerator, SecretKey>() { return execute(KeyGenerator.class, new CheckedFunction<KeyGenerator, SecretKey>() {
@Override @Override
public SecretKey doWithInstance(KeyGenerator generator) { public SecretKey apply(KeyGenerator generator) {
generator.init(keyLength, secureRandom); generator.init(keyBitLength, secureRandom);
return generator.generateKey(); return generator.generateKey();
} }
}); });
} }
public KeyPair generateKeyPair(final int keyLength) { public KeyPair generateKeyPair(final int keyBitLength) {
return execute(KeyPairGenerator.class, new InstanceCallback<KeyPairGenerator, KeyPair>() { return execute(KeyPairGenerator.class, new CheckedFunction<KeyPairGenerator, KeyPair>() {
@Override @Override
public KeyPair doWithInstance(KeyPairGenerator generator) { public KeyPair apply(KeyPairGenerator generator) {
generator.initialize(keyLength, secureRandom); generator.initialize(keyBitLength, secureRandom);
return generator.generateKeyPair(); 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 { try {
I instance = supplier.getInstance(); T instance = supplier.getInstance();
return callback.doWithInstance(instance); return callback.apply(instance);
} catch (SecurityException se) { } catch (SecurityException se) {
throw se; //propagate throw se; //propagate
} catch (Exception e) { } catch (Exception e) {

View File

@ -7,34 +7,11 @@ import java.security.Key;
import java.security.Provider; import java.security.Provider;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
public interface JwkContext<K extends Key> extends Identifiable { public interface JwkContext<K extends Key> extends Identifiable, Map<String,Object> {
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);
JwkContext<K> setId(String id); JwkContext<K> setId(String id);

View File

@ -1,16 +1,29 @@
package io.jsonwebtoken.impl.security; package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.impl.DefaultJweHeader;
import io.jsonwebtoken.impl.IdRegistry; import io.jsonwebtoken.impl.IdRegistry;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.impl.lang.Registry; import io.jsonwebtoken.impl.lang.Registry;
import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.security.EncryptionAlgorithms;
import io.jsonwebtoken.security.KeyAlgorithm; 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.OAEPParameterSpec;
import javax.crypto.spec.PSource; import javax.crypto.spec.PSource;
import java.security.spec.AlgorithmParameterSpec; import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.MGF1ParameterSpec; import java.security.spec.MGF1ParameterSpec;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List;
@SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.KeyAlgorithms implementation @SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.KeyAlgorithms implementation
public final class KeyAlgorithmsBridge { public final class KeyAlgorithmsBridge {
@ -65,4 +78,153 @@ public final class KeyAlgorithmsBridge {
} }
return instance; 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; package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Collections;
@ -134,9 +135,9 @@ public class MacSignatureAlgorithm extends AbstractSignatureAlgorithm<SecretKey,
@Override @Override
public byte[] doSign(final SignatureRequest<SecretKey> request) throws Exception { 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 @Override
public byte[] doWithInstance(Mac mac) throws Exception { public byte[] apply(Mac mac) throws Exception {
mac.init(request.getKey()); mac.init(request.getKey());
return mac.doFinal(request.getPayload()); return mac.doFinal(request.getPayload());
} }

View File

@ -1,17 +1,17 @@
package io.jsonwebtoken.impl.security; package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.JweHeader; import io.jsonwebtoken.JweHeader;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.impl.lang.Bytes; 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.io.Encoders;
import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.EncryptedKeyAlgorithm;
import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.KeyAlgorithm;
import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyRequest;
import io.jsonwebtoken.security.KeyResult; import io.jsonwebtoken.security.KeyResult;
import io.jsonwebtoken.security.PbeKey;
import io.jsonwebtoken.security.SecurityException; import io.jsonwebtoken.security.SecurityException;
import io.jsonwebtoken.security.SymmetricAeadAlgorithm;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory; import javax.crypto.SecretKeyFactory;
@ -21,10 +21,11 @@ import java.nio.ByteBuffer;
import java.nio.CharBuffer; import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets; 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 SALT_HEADER_NAME = "p2s";
private static final String ITERATION_HEADER_NAME = "p2c"; // iteration count 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 HASH_BYTE_LENGTH;
private final int DERIVED_KEY_BIT_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(); 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) { public Pbes2HsAkwAlgorithm(int keyBitLength) {
this(hashBitLength(keyBitLength), new AesWrapKeyAlgorithm(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)); this.SALT_PREFIX = toRfcSaltPrefix(getId().getBytes(StandardCharsets.UTF_8));
} }
private SecretKey deriveKey(final KeyRequest<?, ?> request, final PBEKey pbeKey, final byte[] salt, final int iterations) { // protected visibility for testing
return execute(request, SecretKeyFactory.class, new InstanceCallback<SecretKeyFactory, SecretKey>() { protected SecretKey deriveKey(SecretKeyFactory factory, final char[] password, final byte[] rfcSalt, int iterations) throws Exception {
@Override PBEKeySpec spec = null;
public SecretKey doWithInstance(SecretKeyFactory factory) throws Exception { try {
PBEKeySpec spec = null; spec = new PBEKeySpec(password, rfcSalt, iterations, DERIVED_KEY_BIT_LENGTH);
try { return factory.generateSecret(spec);
spec = new PBEKeySpec(pbeKey.getPassword(), salt, iterations, DERIVED_KEY_BIT_LENGTH); } finally {
return factory.generateSecret(spec); if (spec != null) {
} finally { spec.clearPassword();
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]; byte[] inputSalt = new byte[this.HASH_BYTE_LENGTH];
ensureSecureRandom(request).nextBytes(inputSalt); ensureSecureRandom(request).nextBytes(inputSalt);
return inputSalt; return inputSalt;
} }
// protected visibility for testing
protected byte[] toRfcSalt(byte[] inputSalt) {
return Bytes.concat(this.SALT_PREFIX, inputSalt);
}
@Override @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."); Assert.notNull(request, "request cannot be null.");
final SecretKey cek = Assert.notNull(request.getPayload(), "request.getPayload() (content 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.");
SecretKey reqKey = request.getKey(); final PbeKey pbeKey = Assert.notNull(request.getKey(), "request.getKey() cannot be null.");
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);
}
final int iterations = assertIterations(pbeKey.getWorkFactor());
byte[] inputSalt = generateInputSalt(request); 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 String p2s = Encoders.BASE64URL.encode(inputSalt);
char[] password = pbeKey.getPassword(); // will be safely cleaned/zeroed in deriveKey next:
final SecretKey derivedKek = deriveKey(request, pbeKey, rfcSalt, iterations); final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations);
// now encrypt (wrap) the CEK with the PBE-derived key: // now encrypt (wrap) the CEK with the PBE-derived key:
DefaultKeyRequest<SecretKey, SecretKey> wrapReq = new DefaultKeyRequest<>(request.getProvider(), 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); KeyResult result = wrapAlg.getEncryptionKey(wrapReq);
request.getHeader().put(SALT_HEADER_NAME, p2s); request.getHeader().put(SALT_HEADER_NAME, p2s);
@ -138,30 +149,25 @@ public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements EncryptedKey
private static char[] toChars(byte[] bytes) { private static char[] toChars(byte[] bytes) {
ByteBuffer buf = ByteBuffer.wrap(bytes); ByteBuffer buf = ByteBuffer.wrap(bytes);
CharBuffer cbuf = StandardCharsets.UTF_8.decode(buf); CharBuffer cbuf = StandardCharsets.UTF_8.decode(buf);
char[] chars = new char[cbuf.limit()]; char[] chars = cbuf.compact().array();
cbuf.get(chars);
return chars; return chars;
} }
private PBEKey toPBEKey(SecretKey key, int iterations) { private char[] toPasswordChars(SecretKey key) {
if (key instanceof PBEKey) { 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; byte[] keyBytes = null;
char[] keyChars = null;
try { try {
keyBytes = key.getEncoded(); keyBytes = key.getEncoded();
keyChars = toChars(keyBytes); return toChars(keyBytes);
return new DefaultPBEKey(keyChars, iterations, getId());
} finally { } finally {
try { if (keyBytes != null) {
if (keyChars != null) { java.util.Arrays.fill(keyBytes, (byte) 0);
java.util.Arrays.fill(keyChars, '\u0000');
}
} finally {
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 { public SecretKey getDecryptionKey(KeyRequest<byte[], SecretKey> request) throws SecurityException {
JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null."); 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); ValueGetter getter = new DefaultValueGetter(header);
if (value == null) { final byte[] inputSalt = getter.getRequiredBytes(SALT_HEADER_NAME);
String msg = "The " + getId() + " Key Management Algorithm requires a JweHeader '" + name + "' value."; final byte[] rfcSalt = Bytes.concat(SALT_PREFIX, inputSalt);
throw new MalformedJwtException(msg); final int iterations = getter.getRequiredPositiveInteger(ITERATION_HEADER_NAME);
} final char[] password = toPasswordChars(key); // will be safely cleaned/zeroed in deriveKey next:
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;
final byte[] inputSalt = Decoders.BASE64URL.decode(encoded); final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations);
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);
KeyRequest<byte[], SecretKey> unwrapReq = new DefaultKeyRequest<>(request.getProvider(), 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); 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.CheckedFunction;
import io.jsonwebtoken.impl.lang.Converter; import io.jsonwebtoken.impl.lang.Converter;
import io.jsonwebtoken.impl.lang.Converters; import io.jsonwebtoken.impl.lang.Converters;
import io.jsonwebtoken.impl.lang.ValueGetter;
import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Collections;
@ -127,7 +128,8 @@ class RsaPrivateJwkFactory extends AbstractFamilyJwkFactory<RSAPrivateKey, RsaPr
@Override @Override
protected RsaPrivateJwk createJwkFromValues(JwkContext<RSAPrivateKey> ctx) { 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 //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: //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; KeySpec spec;
if (containsOptional) { //if any one optional field exists, they are all required per JWA Section 6.3.2: 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 firstPrime = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_PRIME, true);
BigInteger secondPrime = getRequiredBigInt(ctx, DefaultRsaPrivateJwk.SECOND_PRIME, true); BigInteger secondPrime = getter.getRequiredBigInt(DefaultRsaPrivateJwk.SECOND_PRIME, true);
BigInteger firstCrtExponent = getRequiredBigInt(ctx, DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, true); BigInteger firstCrtExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_CRT_EXPONENT, true);
BigInteger secondCrtExponent = getRequiredBigInt(ctx, DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, true); BigInteger secondCrtExponent = getter.getRequiredBigInt(DefaultRsaPrivateJwk.SECOND_CRT_EXPONENT, true);
BigInteger firstCrtCoefficient = getRequiredBigInt(ctx, DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT, true); BigInteger firstCrtCoefficient = getter.getRequiredBigInt(DefaultRsaPrivateJwk.FIRST_CRT_COEFFICIENT, true);
// Other Primes Info is actually optional even if the above ones are required: // Other Primes Info is actually optional even if the above ones are required:
if (ctx.containsKey(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO)) { if (ctx.containsKey(DefaultRsaPrivateJwk.OTHER_PRIMES_INFO)) {
@ -226,9 +228,10 @@ class RsaPrivateJwkFactory extends AbstractFamilyJwkFactory<RSAPrivateKey, RsaPr
ctx.put(name, entry.getValue()); ctx.put(name, entry.getValue());
} }
BigInteger prime = getRequiredBigInt(ctx, DefaultRsaPrivateJwk.PRIME_FACTOR, true); final ValueGetter getter = new DefaultValueGetter(ctx);
BigInteger primeExponent = getRequiredBigInt(ctx, DefaultRsaPrivateJwk.FACTOR_CRT_EXPONENT, true); BigInteger prime = getter.getRequiredBigInt(DefaultRsaPrivateJwk.PRIME_FACTOR, true);
BigInteger crtCoefficient = getRequiredBigInt(ctx, DefaultRsaPrivateJwk.FACTOR_CRT_COEFFICIENT, 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); return new RSAOtherPrimeInfo(prime, primeExponent, crtCoefficient);
} }

View File

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

View File

@ -1,10 +1,9 @@
package io.jsonwebtoken.impl.security; package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.impl.lang.ValueGetter;
import io.jsonwebtoken.io.Encoders; import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.MalformedKeyException;
import io.jsonwebtoken.security.SecretJwk; import io.jsonwebtoken.security.SecretJwk;
import io.jsonwebtoken.security.UnsupportedKeyException; import io.jsonwebtoken.security.UnsupportedKeyException;
@ -58,18 +57,8 @@ class SecretJwkFactory extends AbstractFamilyJwkFactory<SecretKey, SecretJwk> {
@Override @Override
protected SecretJwk createJwkFromValues(JwkContext<SecretKey> ctx) { protected SecretJwk createJwkFromValues(JwkContext<SecretKey> ctx) {
String encoded = getRequiredString(ctx, DefaultSecretJwk.K); ValueGetter getter = new DefaultValueGetter(ctx);
byte[] bytes; byte[] bytes = getter.getRequiredBytes(DefaultSecretJwk.K);
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);
}
SecretKey key = new SecretKeySpec(bytes, "NONE"); //TODO: do we need a JCA-specific ID here? SecretKey key = new SecretKeySpec(bytes, "NONE"); //TODO: do we need a JCA-specific ID here?
ctx.setKey(key); ctx.setKey(key);
return new DefaultSecretJwk(ctx); return new DefaultSecretJwk(ctx);

View File

@ -2,14 +2,10 @@ package io.jsonwebtoken.impl
import io.jsonwebtoken.Jwts import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.EncryptionAlgorithms 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 org.junit.Test
import java.security.Key import static org.junit.Assert.assertEquals
import static org.junit.Assert.fail
import static org.junit.Assert.*
class DefaultJweBuilderTest { class DefaultJweBuilderTest {
@ -51,7 +47,7 @@ class DefaultJweBuilderTest {
try { try {
builder().setIssuer("me").withKey(key).compact() builder().setIssuer("me").withKey(key).compact()
} catch (IllegalStateException ise) { } 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.MalformedJwtException
import io.jsonwebtoken.impl.DefaultJweHeader 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.io.Encoders
import io.jsonwebtoken.lang.Arrays 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 io.jsonwebtoken.security.EncryptionAlgorithms
import org.junit.Test import org.junit.Test
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.GCMParameterSpec
import java.nio.charset.StandardCharsets
import static org.junit.Assert.*
class AesGcmKeyAlgorithmTest { 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 * 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. * one uses Cipher.WRAP_MODE and the other uses a raw plaintext byte array.
*/ */
@ -35,9 +35,9 @@ class AesGcmKeyAlgorithmTest {
def cek = alg.generateKey(); def cek = alg.generateKey();
JcaTemplate template = new JcaTemplate("AES/GCM/NoPadding", null) 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 @Override
byte[] doWithInstance(Cipher cipher) throws Exception { byte[] apply(Cipher cipher) throws Exception {
cipher.init(Cipher.WRAP_MODE, kek, new GCMParameterSpec(128, iv)) cipher.init(Cipher.WRAP_MODE, kek, new GCMParameterSpec(128, iv))
return cipher.wrap(cek) return cipher.wrap(cek)
} }
@ -55,7 +55,7 @@ class AesGcmKeyAlgorithmTest {
def encRequest = new DefaultSymmetricAeadRequest(null, null, cek.getEncoded(), kek, null, iv) def encRequest = new DefaultSymmetricAeadRequest(null, null, cek.getEncoded(), kek, null, iv)
def encResult = EncryptionAlgorithms.A256GCM.encrypt(encRequest) def encResult = EncryptionAlgorithms.A256GCM.encrypt(encRequest)
assertArrayEquals resultA.authenticationTag, encResult.authenticationTag assertArrayEquals resultA.digest, encResult.digest
assertArrayEquals resultA.initializationVector, encResult.initializationVector assertArrayEquals resultA.initializationVector, encResult.initializationVector
assertArrayEquals resultA.payload, encResult.payload assertArrayEquals resultA.payload, encResult.payload
} }
@ -70,16 +70,21 @@ class AesGcmKeyAlgorithmTest {
def header = new DefaultJweHeader() def header = new DefaultJweHeader()
def kek = template.generateSecretKey(keyLength) def kek = template.generateSecretKey(keyLength)
def cek = 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) def result = alg.getEncryptionKey(ereq)
header.putAll(result.getHeaderParams())
byte[] encryptedKeyBytes = result.getPayload() byte[] encryptedKeyBytes = result.getPayload()
assertFalse "encryptedKey must be populated", Arrays.length(encryptedKeyBytes) == 0 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 //Assert the decrypted key matches the original cek
assertEquals cek.algorithm, dcek.algorithm assertEquals cek.algorithm, dcek.algorithm
@ -100,16 +105,21 @@ class AesGcmKeyAlgorithmTest {
def header = new DefaultJweHeader() def header = new DefaultJweHeader()
def kek = template.generateSecretKey(keyLength) def kek = template.generateSecretKey(keyLength)
def cek = 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) def result = alg.getEncryptionKey(ereq)
header.putAll(result.getHeaderParams())
header.put(headerName, value) //null value will remove it header.put(headerName, value) //null value will remove it
byte[] encryptedKeyBytes = result.getPayload() byte[] encryptedKeyBytes = result.getPayload()
try { 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() fail()
} catch (MalformedJwtException iae) { } catch (MalformedJwtException iae) {
assertEquals exmsg, iae.getMessage() assertEquals exmsg, iae.getMessage()
@ -117,17 +127,16 @@ class AesGcmKeyAlgorithmTest {
} }
String missing(String name) { 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) { 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) { 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) { String length(String name, int requiredBitLength) {
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)." return "JWE header '${name}' decoded byte array must be ${Bytes.bitsMsg(requiredBitLength)} long. Actual length: ${Bytes.bitsMsg(16)}."
} }
@Test @Test
@ -151,7 +160,7 @@ class AesGcmKeyAlgorithmTest {
@Test @Test
void testIncorrectLengths() { void testIncorrectLengths() {
def value = Encoders.BASE64URL.encode("hi".getBytes(StandardCharsets.US_ASCII)) def value = Encoders.BASE64URL.encode("hi".getBytes(StandardCharsets.US_ASCII))
testDecryptionHeader('iv', value, length('iv', 12)) testDecryptionHeader('iv', value, length('iv', 96))
testDecryptionHeader('tag', value, length('tag', 16)) 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() { void testGetEncryptionKey() {
def alg = new DirectKeyAlgorithm() def alg = new DirectKeyAlgorithm()
def key = new SecretKeySpec(new byte[1], "AES") 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) def result = alg.getEncryptionKey(request)
assertSame key, result.getKey() assertSame key, result.getKey()
assertEquals 0, Arrays.length(result.getPayload()) //must not have an encrypted key assertEquals 0, Arrays.length(result.getPayload()) //must not have an encrypted key
@ -35,7 +35,7 @@ class DirectKeyAlgorithmTest {
@Test(expected = IllegalArgumentException) @Test(expected = IllegalArgumentException)
void testGetEncryptionKeyWithNullRequestKey() { void testGetEncryptionKeyWithNullRequestKey() {
def key = new SecretKeySpec(new byte[1], "AES") 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 @Override
Key getKey() { Key getKey() {
return null return null
@ -48,7 +48,7 @@ class DirectKeyAlgorithmTest {
void testGetDecryptionKey() { void testGetDecryptionKey() {
def alg = new DirectKeyAlgorithm() def alg = new DirectKeyAlgorithm()
def key = new SecretKeySpec(new byte[1], "AES") 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) def result = alg.getDecryptionKey(request)
assertSame key, result assertSame key, result
} }
@ -61,7 +61,7 @@ class DirectKeyAlgorithmTest {
@Test(expected = IllegalArgumentException) @Test(expected = IllegalArgumentException)
void testGetDecryptionKeyWithNullRequestKey() { void testGetDecryptionKeyWithNullRequestKey() {
def key = new SecretKeySpec(new byte[1], "AES") 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 @Override
Key getKey() { Key getKey() {
return null return null

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package io.jsonwebtoken.impl.security package io.jsonwebtoken.impl.security
import io.jsonwebtoken.impl.lang.CheckedFunction
import io.jsonwebtoken.security.CryptoException import io.jsonwebtoken.security.CryptoException
import io.jsonwebtoken.security.SignatureException import io.jsonwebtoken.security.SignatureException
import org.junit.Test import org.junit.Test
@ -18,9 +19,9 @@ class JcaTemplateTest {
void testNewCipherWithExplicitProvider() { void testNewCipherWithExplicitProvider() {
Provider provider = Security.getProvider('SunJCE') Provider provider = Security.getProvider('SunJCE')
def template = new JcaTemplate('AES/CBC/PKCS5Padding', provider) 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 @Override
byte[] doWithInstance(Cipher cipher) throws Exception { byte[] apply(Cipher cipher) throws Exception {
assertNotNull cipher assertNotNull cipher
assertSame provider, cipher.provider assertSame provider, cipher.provider
return new byte[0] return new byte[0]
@ -97,9 +98,9 @@ class JcaTemplateTest {
def ex = new Exception("testing") def ex = new Exception("testing")
def template = new JcaTemplate('AES/CBC/PKCS5Padding', null) def template = new JcaTemplate('AES/CBC/PKCS5Padding', null)
try { try {
template.execute(Cipher.class, new InstanceCallback<Cipher,byte[]>() { template.execute(Cipher.class, new CheckedFunction<Cipher, byte[]>() {
@Override @Override
byte[] doWithInstance(Cipher cipher) throws Exception { byte[] apply(Cipher cipher) throws Exception {
throw ex 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 package io.jsonwebtoken.impl.security
import io.jsonwebtoken.Jwe import io.jsonwebtoken.Jwe
import io.jsonwebtoken.JweHeader import io.jsonwebtoken.JweHeader
import io.jsonwebtoken.Jwts import io.jsonwebtoken.Jwts
@ -8,6 +7,8 @@ import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.io.SerializationException import io.jsonwebtoken.io.SerializationException
import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.io.Serializer
import io.jsonwebtoken.security.KeyRequest import io.jsonwebtoken.security.KeyRequest
import io.jsonwebtoken.security.Keys
import io.jsonwebtoken.security.PbeKey
import io.jsonwebtoken.security.SecurityRequest import io.jsonwebtoken.security.SecurityRequest
import org.junit.Test 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() String compact = Jwts.jweBuilder()
.setPayload(RFC_JWK_JSON) .setPayload(RFC_JWK_JSON)
.setHeaderParam('cty', 'jwk+json') .setHeaderParam('cty', 'jwk+json')
.encryptWith(encAlg) .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 .serializeToJsonWith(serializer) //ensure JJWT created the header as expected with an assertion serializer
.compact(); .compact();

View File

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

View File

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

View File

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

View File

@ -49,7 +49,7 @@ class EncryptionAlgorithmsTest {
def result = alg.encrypt(request) 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 assertNotNull tag
byte[] ciphertext = result.getPayload() byte[] ciphertext = result.getPayload()
@ -87,7 +87,7 @@ class EncryptionAlgorithmsTest {
assertEquals(ciphertext.length, PLAINTEXT_BYTES.length) 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() byte[] decryptedPlaintextBytes = alg.decrypt(dreq).getPayload()
assertArrayEquals(PLAINTEXT_BYTES, decryptedPlaintextBytes) assertArrayEquals(PLAINTEXT_BYTES, decryptedPlaintextBytes)
} }