mirror of https://github.com/jwtk/jjwt.git
Added new io.jsonwebtoken.crypto.Keys utility class for generating SecretKeys and KeyPairs. Resolves #350
This commit is contained in:
parent
c9d05361fa
commit
9d244b9fca
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
48
core/pom.xml
48
core/pom.xml
|
@ -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>
|
|
@ -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)}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
6
pom.xml
6
pom.xml
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue