Added new io.jsonwebtoken.crypto.Keys utility class for generating SecretKeys and KeyPairs. Resolves #350

This commit is contained in:
Les Hazlewood 2018-07-24 21:28:40 -04:00
parent c9d05361fa
commit 9d244b9fca
8 changed files with 418 additions and 57 deletions

View File

@ -5,14 +5,15 @@ language: java
jdk:
- openjdk7
- oraclejdk8
- oraclejdk9
- oraclejdk10
- openjdk10
# - oraclejdk9
# - oraclejdk10
# - openjdk10
# - openjdk11
# - oraclejdk-ea
before_install:
- export BUILD_COVERAGE="$([ $TRAVIS_JDK_VERSION == 'oraclejdk8' ] && echo 'true')"
# - if [[ "$TRAVIS_JDK_VERSION" != 'openjdk7' && "$TRAVIS_JDK_VERSION" != 'oraclejdk8' ]]; then export MAVEN_OPTS='--add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED'; fi;
install: true

View File

@ -0,0 +1,161 @@
package io.jsonwebtoken.crypto;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import javax.crypto.SecretKey;
import java.security.KeyPair;
/**
* Utility class for securely generating {@link SecretKey}s and {@link KeyPair}s.
*
* @since 0.10.0
*/
public final class Keys {
private static final String MAC = "io.jsonwebtoken.impl.crypto.MacProvider";
private static final String RSA = "io.jsonwebtoken.impl.crypto.RsaProvider";
private static final String EC = "io.jsonwebtoken.impl.crypto.EllipticCurveProvider";
private static final Class[] SIG_ARG_TYPES = new Class[]{SignatureAlgorithm.class};
//prevent instantiation
private Keys() {
}
/**
* Returns a new {@link SecretKey} with a key length suitable for use with the specified {@link SignatureAlgorithm}.
*
* <p><a href="https://tools.ietf.org/html/rfc7518#section-3.2">JWA Specification (RFC 7518), Section 3.2</a>
* requires minimum key lengths to be used for each respective Signature Algorithm. This method returns a
* secure-random generated SecretKey that adheres to the required minimum key length. The lengths are:</p>
*
* <table>
* <tr>
* <th>Algorithm</th>
* <th>Key Length</th>
* </tr>
* <tr>
* <td>HS256</td>
* <td>256 bits (32 bytes)</td>
* </tr>
* <tr>
* <td>HS384</td>
* <td>384 bits (48 bytes)</td>
* </tr>
* <tr>
* <td>HS512</td>
* <td>512 bits (64 bytes)</td>
* </tr>
* </table>
*
* @param alg the {@code SignatureAlgorithm} to inspect to determine which key length to use.
* @return a new {@link SecretKey} instance suitable for use with the specified {@link SignatureAlgorithm}.
* @throws IllegalArgumentException for any input value other than {@link SignatureAlgorithm#HS256},
* {@link SignatureAlgorithm#HS384}, or {@link SignatureAlgorithm#HS512}
*/
public static SecretKey secretKeyFor(SignatureAlgorithm alg) throws IllegalArgumentException {
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
switch (alg) {
case HS256:
case HS384:
case HS512:
return Classes.invokeStatic(MAC, "generateKey", SIG_ARG_TYPES, alg);
default:
String msg = "The " + alg.name() + " algorithm does not support shared secret keys.";
throw new IllegalArgumentException(msg);
}
}
/**
* Returns a new {@link KeyPair} suitable for use with the specified asymmetric algorithm.
*
* <p>If the {@code alg} argument is an RSA algorithm, a KeyPair is generated based on the following:</p>
*
* <table>
* <tr>
* <th>JWA Algorithm</th>
* <th>Key Size</th>
* </tr>
* <tr>
* <td>RS256</td>
* <td>2048 bits</td>
* </tr>
* <tr>
* <td>PS256</td>
* <td>2048 bits</td>
* </tr>
* <tr>
* <td>RS384</td>
* <td>3072 bits</td>
* </tr>
* <tr>
* <td>PS256</td>
* <td>3072 bits</td>
* </tr>
* <tr>
* <td>RS512</td>
* <td>4096 bits</td>
* </tr>
* <tr>
* <td>PS512</td>
* <td>4096 bits</td>
* </tr>
* </table>
*
* <p>If the {@code alg} argument is an Elliptic Curve algorithm, a KeyPair is generated based on the following:</p>
*
* <table>
* <tr>
* <th>JWA Algorithm</th>
* <th>Key Size</th>
* <th><a href="https://tools.ietf.org/html/rfc7518#section-7.6.2">JWA Curve Name</a></th>
* <th><a href="https://tools.ietf.org/html/rfc5480#section-2.1.1.1">ASN1 OID Curve Name</a></th>
* </tr>
* <tr>
* <td>EC256</td>
* <td>256 bits</td>
* <td>{@code P-256}</td>
* <td>{@code secp256r1}</td>
* </tr>
* <tr>
* <td>EC384</td>
* <td>384 bits</td>
* <td>{@code P-384}</td>
* <td>{@code secp384r1}</td>
* </tr>
* <tr>
* <td>EC512</td>
* <td>512 bits</td>
* <td>{@code P-512}</td>
* <td>{@code secp521r1}</td>
* </tr>
* </table>
*
* @param alg the {@code SignatureAlgorithm} to inspect to determine which asymmetric algorithm to use.
* @return a new {@link KeyPair} suitable for use with the specified asymmetric algorithm.
* @throws IllegalArgumentException if {@code alg} equals {@link SignatureAlgorithm#HS256 HS256},
* {@link SignatureAlgorithm#HS384 HS384}, {@link SignatureAlgorithm#HS512 HS512}
* or {@link SignatureAlgorithm#NONE NONE}.
*/
public static KeyPair keyPairFor(SignatureAlgorithm alg) throws IllegalArgumentException {
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
switch (alg) {
case RS256:
case PS256:
case RS384:
case PS384:
case RS512:
case PS512:
return Classes.invokeStatic(RSA, "generateKeyPair", SIG_ARG_TYPES, alg);
case ES256:
case ES384:
case ES512:
return Classes.invokeStatic(EC, "generateKeyPair", SIG_ARG_TYPES, alg);
default:
String msg = "The " + alg.name() + " algorithm does not support Key Pairs.";
throw new IllegalArgumentException(msg);
}
}
}

View File

@ -17,6 +17,7 @@ package io.jsonwebtoken.lang;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
/**
* @since 0.1
@ -185,6 +186,23 @@ public final class Classes {
}
}
/**
* @since 0.10.0
*/
@SuppressWarnings("unchecked")
public static <T> T invokeStatic(String fqcn, String methodName, Class[] argTypes, Object... args) {
try {
Class clazz = Classes.forName(fqcn);
Method method = clazz.getDeclaredMethod(methodName, argTypes);
method.setAccessible(true);
return(T)method.invoke(null, args);
} catch (Exception e) {
String msg = "Unable to invoke class method " + fqcn + "#" + methodName + ". Ensure the necessary " +
"implementation is in the runtime classpath.";
throw new IllegalStateException(msg, e);
}
}
/**
* @since 1.0
*/

View File

@ -0,0 +1,106 @@
package io.jsonwebtoken.crypto
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.lang.Classes
import org.junit.Test
import org.junit.runner.RunWith
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import javax.crypto.SecretKey
import java.security.KeyPair
import static org.easymock.EasyMock.eq
import static org.easymock.EasyMock.expect
import static org.easymock.EasyMock.same
import static org.junit.Assert.assertEquals
import static org.junit.Assert.assertSame
import static org.junit.Assert.fail
import static org.powermock.api.easymock.PowerMock.mockStatic
import static org.powermock.api.easymock.PowerMock.createMock
import static org.powermock.api.easymock.PowerMock.replay
import static org.powermock.api.easymock.PowerMock.reset
import static org.powermock.api.easymock.PowerMock.verify
/**
* This test class is for cursory API-level testing only (what is available to the API module at build time).
*
* The actual implementation assertions are done in KeysImplTest in the impl module.
*/
@RunWith(PowerMockRunner)
@PrepareForTest([Classes, Keys])
class KeysTest {
@Test
void testPrivateCtor() { //for code coverage only
new Keys()
}
@Test
void testSecretKeyFor() {
for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
String name = alg.name()
if (name.startsWith('H')) {
mockStatic(Classes)
def key = createMock(SecretKey)
expect(Classes.invokeStatic(eq(Keys.MAC), eq("generateKey"), same(Keys.SIG_ARG_TYPES), same(alg))).andReturn(key)
replay Classes, key
assertSame key, Keys.secretKeyFor(alg)
verify Classes, key
reset Classes, key
} else {
try {
Keys.secretKeyFor(alg)
fail()
} catch (IllegalArgumentException expected) {
assertEquals "The $name algorithm does not support shared secret keys." as String, expected.message
}
}
}
}
@Test
void testKeyPairFor() {
for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
String name = alg.name()
if (name.equals('NONE') || name.startsWith('H')) {
try {
Keys.keyPairFor(alg)
fail()
} catch (IllegalArgumentException expected) {
assertEquals "The $name algorithm does not support Key Pairs." as String, expected.message
}
} else {
String fqcn = name.startsWith('E') ? Keys.EC : Keys.RSA
mockStatic Classes
def pair = createMock(KeyPair)
expect(Classes.invokeStatic(eq(fqcn), eq("generateKeyPair"), same(Keys.SIG_ARG_TYPES), same(alg))).andReturn(pair)
replay Classes, pair
assertSame pair, Keys.keyPairFor(alg)
verify Classes, pair
reset Classes, pair
}
}
}
}

View File

@ -1,48 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2018 JWTK
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-root</artifactId>
<version>0.10.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>jjwt-core</artifactId>
<name>JJWT :: Core</name>
<packaging>jar</packaging>
<properties>
<jjwt.root>${basedir}/..</jjwt.root>
</properties>
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>

View File

@ -114,6 +114,36 @@ public abstract class RsaProvider extends SignatureProvider {
return generateKeyPair(keySizeInBits, DEFAULT_SECURE_RANDOM);
}
/**
* Generates a new RSA secure-randomly key pair suitable for the specified SignatureAlgorithm using JJWT's
* default {@link SignatureProvider#DEFAULT_SECURE_RANDOM SecureRandom instance}. This is a convenience method
* that immediately delegates to {@link #generateKeyPair(int)} based on the relevant key size for the specified
* algorithm.
*
* @param alg the signature algorithm to inspect to determine a size in bits.
* @return a new RSA secure-random key pair of the specified size.
* @see #generateKeyPair()
* @see #generateKeyPair(int, SecureRandom)
* @see #generateKeyPair(String, int, SecureRandom)
* @since 0.10.0
*/
@SuppressWarnings("unused") //used by io.jsonwebtoken.crypto.Keys
public static KeyPair generateKeyPair(SignatureAlgorithm alg) {
Assert.isTrue("RSA".equalsIgnoreCase(alg.getFamilyName()), "Only RSA algorithms are supported by this method.");
int keySizeInBits = 4096;
switch (alg) {
case RS256:
case PS256:
keySizeInBits = 2048;
break;
case RS384:
case PS384:
keySizeInBits = 3072;
break;
}
return generateKeyPair(keySizeInBits, DEFAULT_SECURE_RANDOM);
}
/**
* Generates a new RSA secure-random key pair of the specified size using the given SecureRandom number generator.
* This is a convenience method that immediately delegates to {@link #generateKeyPair(String, int, SecureRandom)}

View File

@ -0,0 +1,99 @@
package io.jsonwebtoken.crypto
import io.jsonwebtoken.SignatureAlgorithm
import org.junit.Test
import javax.crypto.SecretKey
import java.security.KeyPair
import java.security.PrivateKey
import java.security.PublicKey
import java.security.interfaces.ECPrivateKey
import java.security.interfaces.ECPublicKey
import java.security.interfaces.RSAPrivateKey
import static org.junit.Assert.*
class KeysImplTest {
@Test
void testPrivateCtor() { //for code coverage purposes only
new Keys()
}
@Test
void testSecretKeyFor() {
for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
String name = alg.name()
int bitLength = name.equalsIgnoreCase("NONE") ? 0 : name.substring(2).toInteger()
if (name.startsWith('H')) {
SecretKey key = Keys.secretKeyFor(alg)
assertEquals bitLength, key.getEncoded().length * 8 //convert byte count to bit count
assertEquals alg.jcaName, key.algorithm
} else {
try {
Keys.secretKeyFor(alg)
fail()
} catch (IllegalArgumentException expected) {
assertEquals "The $name algorithm does not support shared secret keys." as String, expected.message
}
}
}
}
@Test
void testKeyPairFor() {
for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
String name = alg.name()
int bitLength = name.equalsIgnoreCase("NONE") ? 0 : name.substring(2).toInteger()
if (name.startsWith('R') || name.startsWith('P')) {
KeyPair pair = Keys.keyPairFor(alg)
assertNotNull pair
PublicKey pub = pair.getPublic()
PrivateKey priv = pair.getPrivate()
assert priv instanceof RSAPrivateKey
assertEquals alg.familyName, pub.algorithm
assertEquals alg.familyName, priv.algorithm
assertEquals bitLength * 8, priv.modulus.bitLength()
} else if (name.startsWith('E')) {
KeyPair pair = Keys.keyPairFor(alg);
assertNotNull pair
if (alg == SignatureAlgorithm.ES512) {
bitLength = 521
}
String asn1oid = "secp${bitLength}r1"
PublicKey pub = pair.getPublic()
assert pub instanceof ECPublicKey
assertEquals "ECDSA", pub.algorithm
assertEquals asn1oid, pub.params.name
assertEquals bitLength, pub.params.order.bitLength()
PrivateKey priv = pair.getPrivate()
assert priv instanceof ECPrivateKey
assertEquals "ECDSA", priv.algorithm
assertEquals asn1oid, priv.params.name
} else {
try {
Keys.keyPairFor(alg)
fail()
} catch (IllegalArgumentException expected) {
assertEquals "The $name algorithm does not support Key Pairs." as String, expected.message
}
}
}
}
}

View File

@ -111,7 +111,6 @@
<module>api</module>
<module>impl</module>
<module>extensions</module>
<module>core</module>
</modules>
<dependencyManagement>
@ -127,11 +126,6 @@
<version>${project.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>