mirror of https://github.com/jwtk/jjwt.git
EncryptionAlgorithm changes, class cleanup, test coverage, etc. AES encryption, both GCM and HmacSha2 variants are complete. Classes might be moved to another package. Have not yet started Builder and Parser work to support JWE compact strings.
This commit is contained in:
parent
d111dc8b22
commit
9b434cdf9c
24
pom.xml
24
pom.xml
|
@ -288,6 +288,30 @@
|
|||
<packageBranchRate>100</packageBranchRate> -->
|
||||
<haltOnFailure>true</haltOnFailure>
|
||||
<regexes>
|
||||
<regex>
|
||||
<!-- Work in progress: -->
|
||||
<pattern>io.jsonwebtoken.impl.serialization.*</pattern>
|
||||
<branchRate>0</branchRate>
|
||||
<lineRate>0</lineRate>
|
||||
</regex>
|
||||
<regex>
|
||||
<!-- Work in progress: -->
|
||||
<pattern>io.jsonwebtoken.impl.DefaultJweFactory</pattern>
|
||||
<branchRate>0</branchRate>
|
||||
<lineRate>0</lineRate>
|
||||
</regex>
|
||||
<regex>
|
||||
<!-- Work in progress: -->
|
||||
<pattern>io.jsonwebtoken.impl.DefaultJweHeader</pattern>
|
||||
<branchRate>0</branchRate>
|
||||
<lineRate>0</lineRate>
|
||||
</regex>
|
||||
<regex>
|
||||
<!-- Work in progress: -->
|
||||
<pattern>io.jsonwebtoken.impl.DispatchingParser</pattern>
|
||||
<branchRate>0</branchRate>
|
||||
<lineRate>0</lineRate>
|
||||
</regex>
|
||||
<regex>
|
||||
<!-- This was pulled in from another project (without pulling in the tests) -
|
||||
we don't care about coverage here really.: -->
|
||||
|
|
|
@ -64,7 +64,7 @@ public enum EncryptionAlgorithmName {
|
|||
}
|
||||
}
|
||||
|
||||
throw new CryptoException("Unsupported JWE Content Encryption Algorithm name: " + name);
|
||||
throw new IllegalArgumentException("Unsupported JWE Content Encryption Algorithm name: " + name);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package io.jsonwebtoken;
|
||||
|
||||
import io.jsonwebtoken.impl.crypto.AesEncryptionAlgorithm;
|
||||
import io.jsonwebtoken.impl.crypto.AbstractAesEncryptionAlgorithm;
|
||||
import io.jsonwebtoken.impl.crypto.GcmAesEncryptionAlgorithm;
|
||||
import io.jsonwebtoken.impl.crypto.HmacAesEncryptionAlgorithm;
|
||||
import io.jsonwebtoken.lang.Collections;
|
||||
|
@ -9,6 +9,12 @@ import java.util.List;
|
|||
|
||||
public final class EncryptionAlgorithms {
|
||||
|
||||
//for code coverage only
|
||||
private static final EncryptionAlgorithms INSTANCE = new EncryptionAlgorithms();
|
||||
|
||||
//prevent instantiation
|
||||
private EncryptionAlgorithms(){}
|
||||
|
||||
public static final HmacAesEncryptionAlgorithm A128CBC_HS256 =
|
||||
new HmacAesEncryptionAlgorithm(EncryptionAlgorithmName.A128CBC_HS256.getValue(), SignatureAlgorithm.HS256);
|
||||
|
||||
|
@ -27,6 +33,6 @@ public final class EncryptionAlgorithms {
|
|||
public static final GcmAesEncryptionAlgorithm A256GCM =
|
||||
new GcmAesEncryptionAlgorithm(EncryptionAlgorithmName.A256GCM.getValue(), 32);
|
||||
|
||||
public static List<? extends AesEncryptionAlgorithm> VALUES =
|
||||
public static List<? extends AbstractAesEncryptionAlgorithm> VALUES =
|
||||
Collections.of(A128CBC_HS256, A192CBC_HS384, A256CBC_HS512, A128GCM, A192GCM, A256GCM);
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ public interface JweHeader<T extends JweHeader<T>> extends Header<T> {
|
|||
* @return the JWE Key Management Algorithm header value or {@code null} if not present. This will always be
|
||||
* {@code non-null} on validly constructed JWE instances, but could be {@code null} during construction.
|
||||
*/
|
||||
KeyManagementAlgorithm getKeyManagementAlgorithm();
|
||||
KeyManagementAlgorithmName getKeyManagementAlgorithm();
|
||||
|
||||
/**
|
||||
* Sets the JWE Key Management
|
||||
|
@ -88,5 +88,5 @@ public interface JweHeader<T extends JweHeader<T>> extends Header<T> {
|
|||
* @return the JWE Key Management Algorithm header value or {@code null} if not present. This will always be
|
||||
* {@code non-null} on validly constructed JWE instances, but could be {@code null} during construction.
|
||||
*/
|
||||
T setKeyManagementAlgorithm(KeyManagementAlgorithm alg);
|
||||
T setKeyManagementAlgorithm(KeyManagementAlgorithmName alg);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import java.util.List;
|
|||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
public enum KeyManagementAlgorithm {
|
||||
public enum KeyManagementAlgorithmName {
|
||||
|
||||
RSA1_5("RSA1_5", "RSAES-PKCS1-v1_5", Collections.<String>emptyList(), "RSA/ECB/PKCS1Padding"),
|
||||
RSA_OAEP("RSA-OAEP", "RSAES OAEP using default parameters", Collections.<String>emptyList(), "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"),
|
||||
|
@ -36,7 +36,7 @@ public enum KeyManagementAlgorithm {
|
|||
private final List<String> moreHeaderParams;
|
||||
private final String jcaName;
|
||||
|
||||
KeyManagementAlgorithm(String value, String description, List<String> moreHeaderParams, String jcaName) {
|
||||
KeyManagementAlgorithmName(String value, String description, List<String> moreHeaderParams, String jcaName) {
|
||||
this.value = value;
|
||||
this.description = description;
|
||||
this.moreHeaderParams = moreHeaderParams;
|
||||
|
@ -82,22 +82,22 @@ public enum KeyManagementAlgorithm {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the corresponding {@code KeyManagementAlgorithm} enum instance based on a
|
||||
* Returns the corresponding {@code KeyManagementAlgorithmName} enum instance based on a
|
||||
* case-<em>insensitive</em> name comparison of the specified JWE <code>alg</code> value.
|
||||
*
|
||||
* @param name the case-insensitive JWE <code>alg</code> header value.
|
||||
* @return Returns the corresponding {@code KeyManagementAlgorithm} enum instance based on a
|
||||
* @return Returns the corresponding {@code KeyManagementAlgorithmName} enum instance based on a
|
||||
* case-<em>insensitive</em> name comparison of the specified JWE <code>alg</code> value.
|
||||
* @throws CryptoException if the specified value does not match any JWE {@code KeyManagementAlgorithm} value.
|
||||
* @throws CryptoException if the specified value does not match any JWE {@code KeyManagementAlgorithmName} value.
|
||||
*/
|
||||
public static KeyManagementAlgorithm forName(String name) throws CryptoException {
|
||||
for (KeyManagementAlgorithm alg : values()) {
|
||||
public static KeyManagementAlgorithmName forName(String name) throws CryptoException {
|
||||
for (KeyManagementAlgorithmName alg : values()) {
|
||||
if (alg.getValue().equalsIgnoreCase(name)) {
|
||||
return alg;
|
||||
}
|
||||
}
|
||||
|
||||
throw new CryptoException("Unsupported JWE Key Management Algorithm name: " + name);
|
||||
throw new IllegalArgumentException("Unsupported JWE Key Management Algorithm name: " + name);
|
||||
}
|
||||
|
||||
@Override
|
|
@ -29,7 +29,7 @@ public class DefaultJweFactory {
|
|||
EncryptionAlgorithm encryptionAlgorithm, DecryptionKeyResolver decryptionKeyResolver) {
|
||||
Assert.notNull(base64UrlCodec, "Base64Url TextCodec cannot be null.");
|
||||
Assert.notNull(serializationCodec, "SerializationCodec cannot be null.");
|
||||
Assert.notNull(encryptionAlgorithm, "EncryptionService cannot be null.");
|
||||
Assert.notNull(encryptionAlgorithm, "EncryptionAlgorithm cannot be null.");
|
||||
Assert.notNull(decryptionKeyResolver, "DecryptionKeyResolver cannot be null.");
|
||||
this.serializationCodec = serializationCodec;
|
||||
this.encryptionAlgorithm = encryptionAlgorithm;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package io.jsonwebtoken.impl;
|
||||
|
||||
import io.jsonwebtoken.JweHeader;
|
||||
import io.jsonwebtoken.KeyManagementAlgorithm;
|
||||
import io.jsonwebtoken.KeyManagementAlgorithmName;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -16,16 +16,16 @@ public class DefaultJweHeader extends DefaultHeader implements JweHeader {
|
|||
}
|
||||
|
||||
@Override
|
||||
public KeyManagementAlgorithm getKeyManagementAlgorithm() {
|
||||
public KeyManagementAlgorithmName getKeyManagementAlgorithm() {
|
||||
String value = getString(JweHeader.ALGORITHM);
|
||||
if (value != null) {
|
||||
return KeyManagementAlgorithm.forName(value);
|
||||
return KeyManagementAlgorithmName.forName(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JweHeader setKeyManagementAlgorithm(KeyManagementAlgorithm alg) {
|
||||
public JweHeader setKeyManagementAlgorithm(KeyManagementAlgorithmName alg) {
|
||||
setValue(ALGORITHM, alg.getValue());
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -12,8 +12,7 @@ import java.security.spec.AlgorithmParameterSpec;
|
|||
|
||||
import static io.jsonwebtoken.lang.Arrays.length;
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
public abstract class AesEncryptionAlgorithm implements EncryptionAlgorithm {
|
||||
public abstract class AbstractAesEncryptionAlgorithm implements EncryptionAlgorithm {
|
||||
|
||||
public static final SecureRandom DEFAULT_RANDOM = new SecureRandom();
|
||||
|
||||
|
@ -30,7 +29,7 @@ public abstract class AesEncryptionAlgorithm implements EncryptionAlgorithm {
|
|||
private final int generatedIvLength;
|
||||
private final int requiredKeyLength;
|
||||
|
||||
public AesEncryptionAlgorithm(String name, String transformationString, int generatedIvLength, int requiredKeyLength) {
|
||||
public AbstractAesEncryptionAlgorithm(String name, String transformationString, int generatedIvLength, int requiredKeyLength) {
|
||||
|
||||
Assert.hasText(name, "Name cannot be null or empty.");
|
||||
this.name = name;
|
||||
|
@ -52,7 +51,7 @@ public abstract class AesEncryptionAlgorithm implements EncryptionAlgorithm {
|
|||
try {
|
||||
return doGenerateKey();
|
||||
} catch (Exception e) {
|
||||
throw new CryptoException("Unable to obtain AES KeyGenerator: " + e.getMessage(), e);
|
||||
throw new CryptoException("Unable to generate a new " + getName() + " SecretKey: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,7 +99,7 @@ public abstract class AesEncryptionAlgorithm implements EncryptionAlgorithm {
|
|||
return iv;
|
||||
}
|
||||
|
||||
protected SecureRandom getSecureRandom(CryptoRequest request) {
|
||||
protected SecureRandom getSecureRandom(EncryptionRequest request) {
|
||||
SecureRandom random = request.getSecureRandom();
|
||||
return random != null ? random : DEFAULT_RANDOM;
|
||||
}
|
|
@ -15,25 +15,16 @@
|
|||
*/
|
||||
package io.jsonwebtoken.impl.crypto;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public abstract class AbstractCryptoRequest implements CryptoRequest {
|
||||
|
||||
private final SecureRandom random;
|
||||
private final byte[] key;
|
||||
private final byte[] iv;
|
||||
|
||||
public AbstractCryptoRequest(SecureRandom random, byte[] key, byte[] iv) {
|
||||
this.random = random;
|
||||
public AbstractCryptoRequest(byte[] key, byte[] iv) {
|
||||
this.key = key;
|
||||
this.iv = iv;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecureRandom getSecureRandom() {
|
||||
return this.random;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getKey() {
|
||||
return this.key;
|
||||
|
|
|
@ -15,26 +15,15 @@
|
|||
*/
|
||||
package io.jsonwebtoken.impl.crypto;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
/**
|
||||
* @since 0.7.0
|
||||
*/
|
||||
public interface CryptoRequest {
|
||||
|
||||
/**
|
||||
* Returns the {@code SecureRandom} to use when performing cryptographic operations when processing the request, or
|
||||
* {@code null} if a default {@link SecureRandom} should be used.
|
||||
* Returns the key to use for encryption or decryption depending on the type of request.
|
||||
*
|
||||
* @return the {@code SecureRandom} to use when performing cryptographic operations when processing the request, or
|
||||
* {@code null} if a default {@link SecureRandom} should be used.
|
||||
*/
|
||||
SecureRandom getSecureRandom();
|
||||
|
||||
/**
|
||||
* Returns the encryption key to use for encryption or decryption depending on the type of request.
|
||||
*
|
||||
* @return the encryption key to use for encryption or decryption depending on the type of request.
|
||||
* @return the key to use for encryption or decryption depending on the type of request.
|
||||
*/
|
||||
byte[] getKey();
|
||||
|
||||
|
|
|
@ -15,12 +15,8 @@
|
|||
*/
|
||||
package io.jsonwebtoken.impl.crypto;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public interface DecryptionRequestBuilder {
|
||||
|
||||
DecryptionRequestBuilder setSecureRandom(SecureRandom secureRandom);
|
||||
|
||||
DecryptionRequestBuilder setInitializationVector(byte[] iv);
|
||||
|
||||
DecryptionRequestBuilder setKey(byte[] key);
|
||||
|
|
|
@ -1,240 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2016 jsonwebtoken.io
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.jsonwebtoken.impl.crypto;
|
||||
|
||||
import io.jsonwebtoken.lang.Assert;
|
||||
|
||||
import javax.crypto.*;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import static io.jsonwebtoken.lang.Arrays.length;
|
||||
|
||||
/**
|
||||
* Default {@link EncryptionService} implementation that uses AES in GCM mode.
|
||||
*/
|
||||
@SuppressWarnings("Duplicates")
|
||||
public class DefaultAesEncryptionService implements EncryptionService {
|
||||
|
||||
private static final int GCM_TAG_SIZE = 16; //number of bytes, not bits. Highest for GCM is 128 bits and recommended
|
||||
private static final int GCM_NONCE_SIZE = 12; //number of bytes, not bits. 12 is recommended for GCM for efficiency
|
||||
|
||||
public static final SecureRandom DEFAULT_RANDOM = new SecureRandom();
|
||||
protected static final String DECRYPT_NO_IV = "This EncryptionService implementation rejects decryption " +
|
||||
"requests that do not include initialization vectors. AES " +
|
||||
"ciphertext without an IV is weak and should never be used.";
|
||||
|
||||
private static final byte[] RANDOM_KEY; //TODO: remove this concept
|
||||
|
||||
static {
|
||||
byte[] key = new byte[32];
|
||||
DEFAULT_RANDOM.nextBytes(key);
|
||||
RANDOM_KEY = key;
|
||||
}
|
||||
|
||||
private final SecretKey key;
|
||||
|
||||
private final SecureRandom random;
|
||||
|
||||
public DefaultAesEncryptionService() {
|
||||
this(RANDOM_KEY, DEFAULT_RANDOM);
|
||||
}
|
||||
|
||||
public DefaultAesEncryptionService(byte[] key) {
|
||||
this(key, DEFAULT_RANDOM);
|
||||
}
|
||||
|
||||
public DefaultAesEncryptionService(byte[] key, SecureRandom random) {
|
||||
Assert.notEmpty(key, "Encryption key cannot be null or empty.");
|
||||
Assert.notNull(random, "SecureRandom instance cannot be null or empty.");
|
||||
this.key = new SecretKeySpec(key, "AES");
|
||||
this.random = random;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encrypt(byte[] plaintext) {
|
||||
EncryptionRequest req = EncryptionRequests.builder().setPlaintext(plaintext).build();
|
||||
EncryptionResult res = encrypt(req);
|
||||
return res.compact();
|
||||
}
|
||||
|
||||
protected Cipher newCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
|
||||
return Cipher.getInstance("AES/GCM/NoPadding");
|
||||
}
|
||||
|
||||
protected Cipher newCipher(int mode, byte[] nonce, SecretKey key)
|
||||
throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException,
|
||||
InvalidKeyException {
|
||||
|
||||
Cipher aesGcm = newCipher();
|
||||
|
||||
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce);
|
||||
|
||||
aesGcm.init(mode, key, spec);
|
||||
|
||||
return aesGcm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decrypt(byte[] compact) {
|
||||
byte[] nonce = getCiphertextNonce(compact);
|
||||
byte[] ciphertext = getTaggedCiphertext(compact);
|
||||
DecryptionRequest req = DecryptionRequests.builder()
|
||||
.setInitializationVector(nonce).setCiphertext(ciphertext).build();
|
||||
return decrypt(req);
|
||||
}
|
||||
|
||||
protected byte[] getCiphertextNonce(byte[] ciphertext) {
|
||||
byte[] nonce = new byte[GCM_NONCE_SIZE];
|
||||
System.arraycopy(ciphertext, 0, nonce, 0, GCM_NONCE_SIZE);
|
||||
return nonce;
|
||||
}
|
||||
|
||||
protected byte[] getTaggedCiphertext(byte[] ciphertext) {
|
||||
int taggedCiphertextLength = ciphertext.length - GCM_NONCE_SIZE;
|
||||
byte[] taggedCipherText = new byte[taggedCiphertextLength];
|
||||
//remaining data is the tagged ciphertext. Isolate it:
|
||||
System.arraycopy(ciphertext, GCM_NONCE_SIZE, taggedCipherText, 0, taggedCiphertextLength);
|
||||
return taggedCipherText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EncryptionResult encrypt(EncryptionRequest req) throws CryptoException {
|
||||
try {
|
||||
Assert.notNull(req, "EncryptionRequest cannot be null.");
|
||||
return doEncrypt(req);
|
||||
} catch (Exception e) {
|
||||
String msg = "Unable to perform encryption: " + e.getMessage();
|
||||
throw new CryptoException(msg, e);
|
||||
}
|
||||
}
|
||||
|
||||
protected EncryptionResult doEncrypt(EncryptionRequest req)
|
||||
throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException,
|
||||
NoSuchPaddingException, BadPaddingException, IllegalBlockSizeException {
|
||||
|
||||
//Ensure IV:
|
||||
byte[] iv = req.getInitializationVector();
|
||||
int ivLength = length(iv);
|
||||
if (ivLength == 0) {
|
||||
iv = new byte[GCM_NONCE_SIZE]; //for AES GCM, the IV is often called the nonce
|
||||
random.nextBytes(iv);
|
||||
}
|
||||
|
||||
//Ensure Key:
|
||||
SecretKey key = this.key;
|
||||
byte[] keyBytes = req.getKey();
|
||||
int keyBytesLength = length(keyBytes);
|
||||
if (keyBytesLength > 0) {
|
||||
key = new SecretKeySpec(keyBytes, "AES");
|
||||
}
|
||||
|
||||
Cipher aesGcm = newCipher(Cipher.ENCRYPT_MODE, iv, key);
|
||||
|
||||
//Support Additional Associated Data if necessary:
|
||||
int aadLength = 0;
|
||||
if (req instanceof AssociatedDataSource) {
|
||||
byte[] aad = ((AssociatedDataSource) req).getAssociatedData();
|
||||
aadLength = length(aad);
|
||||
if (aadLength > 0) {
|
||||
aesGcm.updateAAD(aad);
|
||||
}
|
||||
}
|
||||
|
||||
//now for the actual encryption:
|
||||
byte[] plaintext = req.getPlaintext();
|
||||
byte[] ciphertext = aesGcm.doFinal(plaintext);
|
||||
|
||||
if (aadLength > 0) { //authenticated
|
||||
|
||||
byte[] taggedCiphertext = ciphertext; //ciphertext is actually tagged
|
||||
|
||||
//separate the tag from the ciphertext:
|
||||
int ciphertextLength = taggedCiphertext.length - GCM_TAG_SIZE;
|
||||
ciphertext = new byte[ciphertextLength];
|
||||
System.arraycopy(taggedCiphertext, 0, ciphertext, 0, ciphertextLength);
|
||||
|
||||
byte[] tag = new byte[GCM_TAG_SIZE];
|
||||
System.arraycopy(taggedCiphertext, ciphertextLength, tag, 0, GCM_TAG_SIZE);
|
||||
return new DefaultAuthenticatedEncryptionResult(iv, ciphertext, tag);
|
||||
}
|
||||
|
||||
return new DefaultEncryptionResult(iv, ciphertext);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decrypt(DecryptionRequest req) throws CryptoException {
|
||||
try {
|
||||
Assert.notNull(req, "DecryptionRequest cannot be null.");
|
||||
return doDecrypt(req);
|
||||
} catch (Exception e) {
|
||||
String msg = "Unable to perform decryption: " + e.getMessage();
|
||||
throw new CryptoException(msg, e);
|
||||
}
|
||||
}
|
||||
|
||||
protected byte[] doDecrypt(DecryptionRequest req)
|
||||
throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException,
|
||||
NoSuchPaddingException, BadPaddingException, IllegalBlockSizeException {
|
||||
|
||||
byte[] iv = req.getInitializationVector();
|
||||
Assert.notEmpty(iv, DECRYPT_NO_IV);
|
||||
|
||||
//Ensure Key:
|
||||
SecretKey key = this.key;
|
||||
byte[] keyBytes = req.getKey();
|
||||
int keyLen = length(keyBytes);
|
||||
if (keyLen > 0) {
|
||||
key = new SecretKeySpec(keyBytes, "AES");
|
||||
}
|
||||
|
||||
final byte[] ciphertext = req.getCiphertext();
|
||||
byte[] finalBytes = ciphertext; //by default, unless there is an authentication tag
|
||||
|
||||
Cipher aesGcm = newCipher(Cipher.DECRYPT_MODE, iv, key);
|
||||
|
||||
//Support Additional Associated Data:
|
||||
if (req instanceof AuthenticatedDecryptionRequest) {
|
||||
|
||||
AuthenticatedDecryptionRequest areq = (AuthenticatedDecryptionRequest) req;
|
||||
|
||||
byte[] aad = areq.getAssociatedData();
|
||||
Assert.notEmpty(aad, "AuthenticatedDecryptionRequests must include Additional Authenticated Data.");
|
||||
|
||||
aesGcm.updateAAD(aad);
|
||||
|
||||
//for tagged GCM, the JVM spec requires that the tag be appended to the end of the ciphertext
|
||||
//byte array. So we'll append it here:
|
||||
|
||||
byte[] tag = areq.getAuthenticationTag();
|
||||
Assert.notEmpty(tag, "AuthenticatedDecryptionRequests must include an authentication tag.");
|
||||
|
||||
byte[] taggedCiphertext = new byte[ciphertext.length + tag.length];
|
||||
System.arraycopy(ciphertext, 0, taggedCiphertext, 0, ciphertext.length);
|
||||
|
||||
System.arraycopy(tag, 0, taggedCiphertext, ciphertext.length, tag.length);
|
||||
|
||||
finalBytes = taggedCiphertext;
|
||||
}
|
||||
|
||||
return aesGcm.doFinal(finalBytes);
|
||||
}
|
||||
}
|
|
@ -17,8 +17,6 @@ package io.jsonwebtoken.impl.crypto;
|
|||
|
||||
import io.jsonwebtoken.lang.Assert;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class DefaultAuthenticatedDecryptionRequest extends DefaultDecryptionRequest
|
||||
implements AuthenticatedDecryptionRequest {
|
||||
|
||||
|
@ -26,8 +24,8 @@ public class DefaultAuthenticatedDecryptionRequest extends DefaultDecryptionRequ
|
|||
|
||||
private final byte[] tag;
|
||||
|
||||
public DefaultAuthenticatedDecryptionRequest(SecureRandom secureRandom, byte[] key, byte[] iv, byte[] ciphertext, byte[] aad, byte[] tag) {
|
||||
super(secureRandom, key, iv, ciphertext);
|
||||
public DefaultAuthenticatedDecryptionRequest(byte[] key, byte[] iv, byte[] ciphertext, byte[] aad, byte[] tag) {
|
||||
super(key, iv, ciphertext);
|
||||
Assert.notEmpty(tag, "Authentication tag cannot be null or empty.");
|
||||
this.aad = aad;
|
||||
this.tag = tag;
|
||||
|
|
|
@ -17,14 +17,12 @@ package io.jsonwebtoken.impl.crypto;
|
|||
|
||||
import io.jsonwebtoken.lang.Assert;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class DefaultDecryptionRequest extends AbstractCryptoRequest implements DecryptionRequest {
|
||||
|
||||
private final byte[] ciphertext;
|
||||
|
||||
public DefaultDecryptionRequest(SecureRandom secureRandom, byte[] key, byte[] iv, byte[] ciphertext) {
|
||||
super(secureRandom, key, iv);
|
||||
public DefaultDecryptionRequest(byte[] key, byte[] iv, byte[] ciphertext) {
|
||||
super(key, iv);
|
||||
Assert.notEmpty(ciphertext, "ciphertext cannot be null or empty.");
|
||||
this.ciphertext = ciphertext;
|
||||
}
|
||||
|
|
|
@ -17,29 +17,20 @@ package io.jsonwebtoken.impl.crypto;
|
|||
|
||||
import io.jsonwebtoken.lang.Assert;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import static io.jsonwebtoken.lang.Arrays.clean;
|
||||
|
||||
public class DefaultDecryptionRequestBuilder implements DecryptionRequestBuilder {
|
||||
|
||||
public static final String AAD_NEEDS_TAG_MSG = "If you specify additional authentication data during " +
|
||||
"decryption, you must also specify the authentication tag " +
|
||||
"computed during encryption.";
|
||||
"decryption, you must also specify the authentication tag " +
|
||||
"computed during encryption.";
|
||||
|
||||
private SecureRandom secureRandom;
|
||||
private byte[] iv;
|
||||
private byte[] key;
|
||||
private byte[] ciphertext;
|
||||
private byte[] aad;
|
||||
private byte[] tag;
|
||||
|
||||
@Override
|
||||
public DecryptionRequestBuilder setSecureRandom(SecureRandom secureRandom) {
|
||||
this.secureRandom = secureRandom;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecryptionRequestBuilder setInitializationVector(byte[] iv) {
|
||||
this.iv = clean(iv);
|
||||
|
@ -80,10 +71,10 @@ public class DefaultDecryptionRequestBuilder implements DecryptionRequestBuilder
|
|||
}
|
||||
|
||||
if (aad != null || tag != null) {
|
||||
return new DefaultAuthenticatedDecryptionRequest(secureRandom, key, iv, ciphertext, aad, tag);
|
||||
return new DefaultAuthenticatedDecryptionRequest(key, iv, ciphertext, aad, tag);
|
||||
}
|
||||
|
||||
return new DefaultDecryptionRequest(secureRandom, key, iv, ciphertext);
|
||||
return new DefaultDecryptionRequest(key, iv, ciphertext);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,12 +21,20 @@ import java.security.SecureRandom;
|
|||
|
||||
public class DefaultEncryptionRequest extends AbstractCryptoRequest implements EncryptionRequest {
|
||||
|
||||
private final SecureRandom random;
|
||||
|
||||
private final byte[] plaintext;
|
||||
|
||||
public DefaultEncryptionRequest(SecureRandom secureRandom, byte[] key, byte[] iv, byte[] plaintext) {
|
||||
super(secureRandom, key, iv);
|
||||
super(key, iv);
|
||||
Assert.notEmpty(plaintext, "plaintext cannot be null or empty.");
|
||||
this.plaintext = plaintext;
|
||||
this.random = secureRandom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecureRandom getSecureRandom() {
|
||||
return this.random;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -10,6 +10,11 @@ import java.security.Key;
|
|||
*/
|
||||
public class DisabledDecryptionKeyResolver implements DecryptionKeyResolver {
|
||||
|
||||
/**
|
||||
* Singleton instance that may be used if direct instantiation is not desired.
|
||||
*/
|
||||
public static final DisabledDecryptionKeyResolver INSTANCE = new DisabledDecryptionKeyResolver();
|
||||
|
||||
@Override
|
||||
public Key resolveDecryptionKey(JweHeader header) {
|
||||
return null;
|
||||
|
|
|
@ -15,8 +15,19 @@
|
|||
*/
|
||||
package io.jsonwebtoken.impl.crypto;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public interface EncryptionRequest extends CryptoRequest {
|
||||
|
||||
/**
|
||||
* Returns the {@code SecureRandom} to use when performing cryptographic operations when processing the request, or
|
||||
* {@code null} if a default {@link SecureRandom} should be used.
|
||||
*
|
||||
* @return the {@code SecureRandom} to use when performing cryptographic operations when processing the request, or
|
||||
* {@code null} if a default {@link SecureRandom} should be used.
|
||||
*/
|
||||
SecureRandom getSecureRandom();
|
||||
|
||||
byte[] getPlaintext();
|
||||
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2016 jsonwebtoken.io
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.jsonwebtoken.impl.crypto;
|
||||
|
||||
public interface EncryptionService {
|
||||
|
||||
byte[] encrypt(byte[] plaintext) throws CryptoException;
|
||||
|
||||
byte[] decrypt(byte[] ciphertext) throws CryptoException;
|
||||
|
||||
EncryptionResult encrypt(EncryptionRequest request) throws CryptoException;
|
||||
|
||||
byte[] decrypt(DecryptionRequest request) throws CryptoException;
|
||||
|
||||
}
|
|
@ -8,7 +8,7 @@ import javax.crypto.spec.GCMParameterSpec;
|
|||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
|
||||
public class GcmAesEncryptionAlgorithm extends AesEncryptionAlgorithm {
|
||||
public class GcmAesEncryptionAlgorithm extends AbstractAesEncryptionAlgorithm {
|
||||
|
||||
private static final int GCM_IV_SIZE = 12; //number of bytes, not bits. 12 is recommended for GCM for efficiency
|
||||
private static final String TRANSFORMATION_STRING = "AES/GCM/NoPadding";
|
||||
|
|
|
@ -11,7 +11,7 @@ import java.util.Arrays;
|
|||
|
||||
import static io.jsonwebtoken.lang.Arrays.length;
|
||||
|
||||
public class HmacAesEncryptionAlgorithm extends AesEncryptionAlgorithm {
|
||||
public class HmacAesEncryptionAlgorithm extends AbstractAesEncryptionAlgorithm {
|
||||
|
||||
protected static final String TRANSFORMATION_STRING = "AES/CBC/PKCS5Padding";
|
||||
|
||||
|
@ -46,8 +46,8 @@ public class HmacAesEncryptionAlgorithm extends AesEncryptionAlgorithm {
|
|||
|
||||
int subKeyLength = getRequiredKeyLength() / 2;
|
||||
|
||||
SecretKey macKey = MacProvider.generateKey(SIGALG);
|
||||
byte[] macKeyBytes = macKey.getEncoded();
|
||||
byte[] macKeyBytes = generateHmacKeyBytes();
|
||||
Assert.notEmpty(macKeyBytes, "Generated HMAC key byte array cannot be null or empty.");
|
||||
|
||||
if (macKeyBytes.length > subKeyLength) {
|
||||
byte[] subKeyBytes = new byte[subKeyLength];
|
||||
|
@ -55,6 +55,14 @@ public class HmacAesEncryptionAlgorithm extends AesEncryptionAlgorithm {
|
|||
macKeyBytes = subKeyBytes;
|
||||
}
|
||||
|
||||
if (macKeyBytes.length != subKeyLength) {
|
||||
String msg = "Generated HMAC key must be " + subKeyLength + " bytes (" +
|
||||
subKeyLength * Byte.SIZE + " bits) long. The " + getClass().getName() + " implementation " +
|
||||
"generated a key " + macKeyBytes.length + " bytes (" +
|
||||
macKeyBytes.length * Byte.SIZE + " bits) long";
|
||||
throw new IllegalStateException(msg);
|
||||
}
|
||||
|
||||
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
|
||||
keyGenerator.init(subKeyLength * Byte.SIZE);
|
||||
|
||||
|
@ -71,6 +79,11 @@ public class HmacAesEncryptionAlgorithm extends AesEncryptionAlgorithm {
|
|||
return new SecretKeySpec(combinedKeyBytes, getName());
|
||||
}
|
||||
|
||||
protected byte[] generateHmacKeyBytes() {
|
||||
SecretKey macKey = MacProvider.generateKey(SIGALG);
|
||||
return macKey.getEncoded();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected EncryptionResult doEncrypt(EncryptionRequest req) throws Exception {
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package io.jsonwebtoken
|
||||
|
||||
import org.junit.Test
|
||||
import static org.junit.Assert.*
|
||||
|
||||
class EncryptionAlgorithmNameTest {
|
||||
|
||||
@Test
|
||||
void testGetValue() {
|
||||
assertEquals 'A128CBC-HS256', EncryptionAlgorithmName.A128CBC_HS256.getValue()
|
||||
assertEquals 'A192CBC-HS384', EncryptionAlgorithmName.A192CBC_HS384.getValue()
|
||||
assertEquals 'A256CBC-HS512', EncryptionAlgorithmName.A256CBC_HS512.getValue()
|
||||
assertEquals 'A128GCM', EncryptionAlgorithmName.A128GCM.getValue()
|
||||
assertEquals 'A192GCM', EncryptionAlgorithmName.A192GCM.getValue()
|
||||
assertEquals 'A256GCM', EncryptionAlgorithmName.A256GCM.getValue()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDescription() {
|
||||
assertEquals 'AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.3', EncryptionAlgorithmName.A128CBC_HS256.getDescription()
|
||||
assertEquals 'AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.4', EncryptionAlgorithmName.A192CBC_HS384.getDescription()
|
||||
assertEquals 'AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm, as defined in https://tools.ietf.org/html/rfc7518#section-5.2.5', EncryptionAlgorithmName.A256CBC_HS512.getDescription()
|
||||
assertEquals 'AES GCM using 128-bit key', EncryptionAlgorithmName.A128GCM.getDescription()
|
||||
assertEquals 'AES GCM using 192-bit key', EncryptionAlgorithmName.A192GCM.getDescription()
|
||||
assertEquals 'AES GCM using 256-bit key', EncryptionAlgorithmName.A256GCM.getDescription()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetJcaName() {
|
||||
for( def name : EncryptionAlgorithmName.values() ) {
|
||||
if (name.getValue().contains("GCM")) {
|
||||
assertEquals 'AES/GCM/NoPadding', name.getJcaName()
|
||||
} else {
|
||||
assertEquals 'AES/CBC/PKCS5Padding', name.getJcaName()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToString() {
|
||||
for( def name : EncryptionAlgorithmName.values() ) {
|
||||
assertEquals name.toString(), name.getValue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testForName() {
|
||||
def name = EncryptionAlgorithmName.forName('A128GCM')
|
||||
assertSame name, EncryptionAlgorithmName.A128GCM
|
||||
}
|
||||
|
||||
@Test
|
||||
void testForNameFailure() {
|
||||
try {
|
||||
EncryptionAlgorithmName.forName('foo')
|
||||
fail()
|
||||
} catch (IllegalArgumentException expected) {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package io.jsonwebtoken
|
||||
|
||||
import org.junit.Test
|
||||
import static org.junit.Assert.*
|
||||
|
||||
class KeyManagementAlgorithmNameTest {
|
||||
|
||||
@Test
|
||||
void testToString() {
|
||||
for( def name : KeyManagementAlgorithmName.values()) {
|
||||
assertEquals name.value, name.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDescription() {
|
||||
for( def name : KeyManagementAlgorithmName.values()) {
|
||||
assertNotNull name.getDescription() //TODO improve this for actual value testing
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetMoreHeaderParams() {
|
||||
for( def name : KeyManagementAlgorithmName.values()) {
|
||||
assertNotNull name.getMoreHeaderParams() //TODO improve this for actual value testing
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetJcaName() {
|
||||
for( def name : KeyManagementAlgorithmName.values()) {
|
||||
assertNotNull name.getJcaName() //TODO improve this for actual value testing
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testForName() {
|
||||
def name = KeyManagementAlgorithmName.forName('A128KW')
|
||||
assertSame name, KeyManagementAlgorithmName.A128KW
|
||||
}
|
||||
|
||||
@Test
|
||||
void testForNameFailure() {
|
||||
try {
|
||||
KeyManagementAlgorithmName.forName('foo')
|
||||
fail()
|
||||
} catch (IllegalArgumentException expected) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -112,4 +112,11 @@ class SignatureAlgorithmTest {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToString() {
|
||||
for(SignatureAlgorithm alg : SignatureAlgorithm.values()) {
|
||||
assertEquals alg.value, alg.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
package io.jsonwebtoken.impl.crypto
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import java.security.SecureRandom
|
||||
|
||||
import static org.junit.Assert.*
|
||||
|
||||
class AbstractAesEncryptionAlgorithmTest {
|
||||
|
||||
@Test
|
||||
void testConstructorWithIvLargerThanAesBlockSize() {
|
||||
|
||||
try {
|
||||
new TestAesEncryptionAlgorithm('foo', 'foo', 17, 16);
|
||||
fail()
|
||||
} catch (IllegalArgumentException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorWithoutIvLength() {
|
||||
|
||||
try {
|
||||
new TestAesEncryptionAlgorithm('foo', 'foo', 0, 16);
|
||||
fail()
|
||||
} catch (IllegalArgumentException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorWithoutRequiredKeyLength() {
|
||||
|
||||
try {
|
||||
new TestAesEncryptionAlgorithm('foo', 'foo', 16, 0);
|
||||
fail()
|
||||
} catch (IllegalArgumentException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDoEncryptFailure() {
|
||||
|
||||
def alg = new TestAesEncryptionAlgorithm('foo', 'foo', 16, 16) {
|
||||
@Override
|
||||
protected EncryptionResult doEncrypt(EncryptionRequest req) throws Exception {
|
||||
throw new IllegalArgumentException('broken')
|
||||
}
|
||||
}
|
||||
|
||||
def req = EncryptionRequests.builder()
|
||||
.setAdditionalAuthenticatedData('foo'.getBytes())
|
||||
.setInitializationVector('iv'.getBytes())
|
||||
.setKey(alg.generateKey().getEncoded())
|
||||
.setPlaintext('bar'.getBytes())
|
||||
.build();
|
||||
|
||||
try {
|
||||
alg.encrypt(req)
|
||||
} catch (CryptoException expected) {
|
||||
assertTrue expected.getCause() instanceof IllegalArgumentException
|
||||
assertTrue expected.getCause().getMessage().equals('broken')
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAssertKeyLength() {
|
||||
|
||||
def requiredKeyLength = 16
|
||||
|
||||
def alg = new TestAesEncryptionAlgorithm('foo', 'foo', 16, requiredKeyLength)
|
||||
|
||||
byte[] bytes = new byte[requiredKeyLength + 1] //not same as requiredKeyLength, but it should be
|
||||
AbstractAesEncryptionAlgorithm.DEFAULT_RANDOM.nextBytes(bytes)
|
||||
|
||||
try {
|
||||
alg.assertKeyLength(bytes)
|
||||
fail()
|
||||
} catch (CryptoException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetSecureRandomWhenRequestHasSpecifiedASecureRandom() {
|
||||
|
||||
def alg = new TestAesEncryptionAlgorithm('foo', 'foo', 16, 16)
|
||||
|
||||
def secureRandom = new SecureRandom()
|
||||
|
||||
def req = EncryptionRequests.builder()
|
||||
.setAdditionalAuthenticatedData('foo'.getBytes())
|
||||
.setInitializationVector('iv'.getBytes())
|
||||
.setKey(alg.generateKey().getEncoded())
|
||||
.setPlaintext('bar'.getBytes())
|
||||
.setSecureRandom(secureRandom)
|
||||
.build();
|
||||
|
||||
def returnedSecureRandom = alg.getSecureRandom(req)
|
||||
|
||||
assertSame(secureRandom, returnedSecureRandom)
|
||||
}
|
||||
|
||||
static class TestAesEncryptionAlgorithm extends AbstractAesEncryptionAlgorithm {
|
||||
|
||||
TestAesEncryptionAlgorithm(String name, String transformationString, int generatedIvLength, int requiredKeyLength) {
|
||||
super(name, transformationString, generatedIvLength, requiredKeyLength)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected EncryptionResult doEncrypt(EncryptionRequest req) throws Exception {
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] doDecrypt(DecryptionRequest req) throws Exception {
|
||||
return new byte[0]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package io.jsonwebtoken.impl.crypto
|
||||
|
||||
import io.jsonwebtoken.EncryptionAlgorithmName
|
||||
import io.jsonwebtoken.EncryptionAlgorithms
|
||||
import org.junit.Test
|
||||
|
||||
|
@ -87,7 +86,7 @@ class Aes128CbcHmacSha256Test {
|
|||
|
||||
// now test decryption:
|
||||
|
||||
AuthenticatedDecryptionRequest decryptionRequest = DecryptionRequests.builder()
|
||||
def dreq = DecryptionRequests.builder()
|
||||
.setAdditionalAuthenticatedData(A)
|
||||
.setCiphertext(resultCiphertext)
|
||||
.setInitializationVector(resultIv)
|
||||
|
@ -95,7 +94,7 @@ class Aes128CbcHmacSha256Test {
|
|||
.setAuthenticationTag(resultTag)
|
||||
.build();
|
||||
|
||||
byte[] decryptionResult = alg.decrypt(decryptionRequest)
|
||||
byte[] decryptionResult = alg.decrypt(dreq)
|
||||
|
||||
assertArrayEquals(P, decryptionResult);
|
||||
}
|
||||
|
|
|
@ -1,286 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2016 jsonwebtoken.io
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.jsonwebtoken.impl.crypto
|
||||
|
||||
import groovy.json.internal.Charsets
|
||||
import org.junit.Test
|
||||
|
||||
import javax.crypto.*
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.SecureRandom
|
||||
|
||||
import static org.junit.Assert.*
|
||||
|
||||
class DefaultAesEncryptionServiceTest {
|
||||
|
||||
private static final String PLAINTEXT =
|
||||
'''Bacon ipsum dolor amet venison beef pork chop, doner jowl pastrami ground round alcatra.
|
||||
Beef leberkas filet mignon ball tip pork spare ribs kevin short loin ribeye ground round
|
||||
biltong jerky short ribs corned beef. Strip steak turducken meatball porchetta beef ribs
|
||||
shoulder pork belly doner salami corned beef kielbasa cow filet mignon drumstick. Bacon
|
||||
tenderloin pancetta flank frankfurter ham kevin leberkas meatball turducken beef ribs.
|
||||
Cupim short loin short ribs shankle tenderloin. Ham ribeye hamburger flank tenderloin
|
||||
cupim t-bone, shank tri-tip venison salami sausage pancetta. Pork belly chuck salami
|
||||
alcatra sirloin.
|
||||
|
||||
以ケ ホゥ婧詃 橎ちゅぬ蛣埣 禧ざしゃ蟨廩 椥䤥グ曣わ 基覧 滯っ䶧きょメ Ủ䧞以ケ妣 择禤槜谣お 姨のドゥ,
|
||||
らボみょば䪩 苯礊觊ツュ婃 䩦ディふげセ げセりょ 禤槜 Ủ䧞以ケ妣 せがみゅちょ䰯 择禤槜谣お 難ゞ滧 蝥ちゃ,
|
||||
滯っ䶧きょメ らボみょば䪩 礯みゃ楦と饥 椥䤥グ ウァ槚 訤をりゃしゑ びゃ驨も氩簥 栨キョ奎婨榞 ヌに楃 以ケ,
|
||||
姚奊べ 椥䤥グ曣わ 栨キョ奎婨榞 ちょ䰯 Ủ䧞以ケ妣 誧姨のドゥろ よ苯礊 く涥, りゅぽ槞 馣ぢゃ尦䦎ぎ
|
||||
大た䏩䰥ぐ 郎きや楺橯 䧎キェ, 難ゞ滧 栧择 谯䧟簨訧ぎょ 椥䤥グ曣わ'''
|
||||
|
||||
private static final byte[] PLAINTEXT_BYTES = PLAINTEXT.getBytes(Charsets.UTF_8)
|
||||
|
||||
private static final String AAD = 'You can get with this, or you can get with that'
|
||||
private static final byte[] AAD_BYTES = AAD.getBytes(Charsets.UTF_8)
|
||||
|
||||
private byte[] generateKey() {
|
||||
KeyGenerator kg = KeyGenerator.getInstance("AES");
|
||||
kg.init(256);
|
||||
return kg.generateKey().getEncoded();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSimple() {
|
||||
|
||||
byte[] key = generateKey();
|
||||
|
||||
def service = new DefaultAesEncryptionService(key);
|
||||
|
||||
def ciphertext = service.encrypt(PLAINTEXT_BYTES);
|
||||
|
||||
def decryptedPlaintextBytes = service.decrypt(ciphertext);
|
||||
|
||||
def decryptedPlaintext = new String(decryptedPlaintextBytes, Charsets.UTF_8);
|
||||
|
||||
assertEquals(PLAINTEXT, decryptedPlaintext);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDoEncryptFailure() {
|
||||
String msg = 'foo'
|
||||
def key = generateKey()
|
||||
def service = new DefaultAesEncryptionService(key) {
|
||||
@Override
|
||||
protected EncryptionResult doEncrypt(EncryptionRequest req) throws InvalidAlgorithmParameterException, InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException, ShortBufferException, BadPaddingException, IllegalBlockSizeException {
|
||||
throw new IllegalArgumentException(msg)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
service.encrypt(key /*any byte array will do */)
|
||||
fail("Encryption should have failed")
|
||||
} catch (CryptoException expected) {
|
||||
assertEquals('Unable to perform encryption: ' + msg, expected.message)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDoDecryptFailure() {
|
||||
String msg = 'foo'
|
||||
def key = generateKey()
|
||||
def service = new DefaultAesEncryptionService(key) {
|
||||
@Override
|
||||
protected byte[] doDecrypt(DecryptionRequest req) throws InvalidAlgorithmParameterException, InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException, ShortBufferException, BadPaddingException, IllegalBlockSizeException {
|
||||
throw new IllegalArgumentException(msg)
|
||||
}
|
||||
}
|
||||
try {
|
||||
service.decrypt(key /*any byte array will do */)
|
||||
fail("Decryption should have failed")
|
||||
} catch (CryptoException expected) {
|
||||
assertEquals('Unable to perform decryption: ' + msg, expected.message)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEncryptWithSpecifiedKey() {
|
||||
|
||||
def service = new DefaultAesEncryptionService(generateKey());
|
||||
|
||||
//use a custom key for this request:
|
||||
def key = generateKey()
|
||||
|
||||
EncryptionRequest ereq = EncryptionRequests.builder().setKey(key).setPlaintext(PLAINTEXT_BYTES).build()
|
||||
|
||||
EncryptionResult eres = service.encrypt(ereq);
|
||||
|
||||
DecryptionRequest dreq = DecryptionRequests.builder().setKey(key)
|
||||
.setInitializationVector(eres.getInitializationVector())
|
||||
.setCiphertext(eres.getCiphertext())
|
||||
.build()
|
||||
|
||||
def decryptedPlaintextBytes = service.decrypt(dreq)
|
||||
|
||||
def decryptedPlaintext = new String(decryptedPlaintextBytes, Charsets.UTF_8);
|
||||
|
||||
assertEquals(PLAINTEXT, decryptedPlaintext);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEncryptWithSpecifiedIv() {
|
||||
|
||||
def service = new DefaultAesEncryptionService(generateKey());
|
||||
|
||||
byte[] iv = new byte[12]; //AES GCM tends to use nonces of 12 bytes for efficiency
|
||||
new SecureRandom().nextBytes(iv);
|
||||
|
||||
EncryptionRequest ereq = EncryptionRequests.builder()
|
||||
.setInitializationVector(iv)
|
||||
.setPlaintext(PLAINTEXT_BYTES)
|
||||
.build()
|
||||
|
||||
EncryptionResult eres = service.encrypt(ereq);
|
||||
|
||||
DecryptionRequest dreq = DecryptionRequests.builder()
|
||||
.setInitializationVector(iv)
|
||||
.setCiphertext(eres.getCiphertext())
|
||||
.build()
|
||||
|
||||
def decryptedPlaintextBytes = service.decrypt(dreq)
|
||||
|
||||
def decryptedPlaintext = new String(decryptedPlaintextBytes, Charsets.UTF_8);
|
||||
|
||||
assertEquals(PLAINTEXT, decryptedPlaintext);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecryptWithEmptyIv() {
|
||||
|
||||
def service = new DefaultAesEncryptionService(generateKey());
|
||||
|
||||
DecryptionRequest dreq = DecryptionRequests.builder().setCiphertext(generateKey()).build()
|
||||
|
||||
try {
|
||||
service.decrypt(dreq)
|
||||
fail()
|
||||
} catch (CryptoException expected) {
|
||||
assertTrue expected.getMessage().endsWith(DefaultAesEncryptionService.DECRYPT_NO_IV);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEncryptAdditionalAuthenticatedData() {
|
||||
|
||||
def service = new DefaultAesEncryptionService(generateKey());
|
||||
|
||||
EncryptionRequest ereq = EncryptionRequests.builder()
|
||||
.setPlaintext(PLAINTEXT_BYTES)
|
||||
.setAdditionalAuthenticatedData(AAD_BYTES)
|
||||
.build()
|
||||
|
||||
AuthenticatedEncryptionResult eres = (AuthenticatedEncryptionResult) service.encrypt(ereq);
|
||||
|
||||
DecryptionRequest dreq = DecryptionRequests.builder()
|
||||
.setInitializationVector(eres.getInitializationVector())
|
||||
.setCiphertext(eres.getCiphertext())
|
||||
.setAdditionalAuthenticatedData(AAD_BYTES)
|
||||
.setAuthenticationTag(eres.getAuthenticationTag())
|
||||
.build()
|
||||
|
||||
def decryptedPlaintextBytes = service.decrypt(dreq)
|
||||
|
||||
def decryptedPlaintext = new String(decryptedPlaintextBytes, Charsets.UTF_8);
|
||||
|
||||
assertEquals(PLAINTEXT, decryptedPlaintext);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEncryptWithEmptyAdditionalAuthenticatedData() {
|
||||
|
||||
def service = new DefaultAesEncryptionService(generateKey());
|
||||
|
||||
def ereq = new DummyEncryptionRequest(plaintext: PLAINTEXT_BYTES)
|
||||
|
||||
def eres = service.encrypt(ereq)
|
||||
|
||||
assertTrue eres instanceof DefaultEncryptionResult
|
||||
assertFalse eres instanceof AuthenticatedEncryptionResult
|
||||
|
||||
DecryptionRequest dreq = DecryptionRequests.builder()
|
||||
.setInitializationVector(eres.getInitializationVector())
|
||||
.setCiphertext(eres.getCiphertext())
|
||||
.build()
|
||||
|
||||
def decryptedPlaintextBytes = service.decrypt(dreq)
|
||||
|
||||
def decryptedPlaintext = new String(decryptedPlaintextBytes, Charsets.UTF_8);
|
||||
|
||||
assertEquals(PLAINTEXT, decryptedPlaintext);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecryptWithSpecifiedKey() {
|
||||
|
||||
def key = generateKey()
|
||||
|
||||
def service = new DefaultAesEncryptionService(key);
|
||||
|
||||
EncryptionRequest ereq = EncryptionRequests.builder()
|
||||
.setPlaintext(PLAINTEXT_BYTES)
|
||||
.build()
|
||||
|
||||
def eres = service.encrypt(ereq)
|
||||
|
||||
assertTrue eres instanceof DefaultEncryptionResult
|
||||
assertFalse eres instanceof AuthenticatedEncryptionResult
|
||||
|
||||
DecryptionRequest dreq = DecryptionRequests.builder()
|
||||
.setKey(key)
|
||||
.setInitializationVector(eres.getInitializationVector())
|
||||
.setCiphertext(eres.getCiphertext())
|
||||
.build()
|
||||
|
||||
def decryptedPlaintextBytes = service.decrypt(dreq)
|
||||
|
||||
def decryptedPlaintext = new String(decryptedPlaintextBytes, Charsets.UTF_8);
|
||||
|
||||
assertEquals(PLAINTEXT, decryptedPlaintext);
|
||||
}
|
||||
|
||||
private static class DummyEncryptionRequest implements EncryptionRequest, AssociatedDataSource {
|
||||
|
||||
byte[] plaintext;
|
||||
|
||||
@Override
|
||||
SecureRandom getSecureRandom() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
byte[] getAssociatedData() {
|
||||
return new byte[0]
|
||||
}
|
||||
|
||||
@Override
|
||||
byte[] getPlaintext() {
|
||||
return this.plaintext
|
||||
}
|
||||
|
||||
@Override
|
||||
byte[] getKey() {
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
byte[] getInitializationVector() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package io.jsonwebtoken.impl.crypto
|
||||
|
||||
import org.junit.Test
|
||||
import static org.junit.Assert.*
|
||||
|
||||
class DisabledDecryptionKeyResolverTest {
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
assertNull DisabledDecryptionKeyResolver.INSTANCE.resolveDecryptionKey(null)
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ class EncryptionAlgorithmsTest {
|
|||
@Test
|
||||
void testWithoutAad() {
|
||||
|
||||
for (AesEncryptionAlgorithm alg : EncryptionAlgorithms.VALUES) {
|
||||
for (AbstractAesEncryptionAlgorithm alg : EncryptionAlgorithms.VALUES) {
|
||||
|
||||
def skey = alg.generateKey()
|
||||
def key = skey.getEncoded()
|
||||
|
@ -67,7 +67,7 @@ class EncryptionAlgorithmsTest {
|
|||
@Test
|
||||
void testWithAad() {
|
||||
|
||||
for (AesEncryptionAlgorithm alg : EncryptionAlgorithms.VALUES) {
|
||||
for (AbstractAesEncryptionAlgorithm alg : EncryptionAlgorithms.VALUES) {
|
||||
|
||||
def skey = alg.generateKey()
|
||||
def key = skey.getEncoded()
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
package io.jsonwebtoken.impl.crypto
|
||||
|
||||
import io.jsonwebtoken.EncryptionAlgorithmName
|
||||
import io.jsonwebtoken.EncryptionAlgorithms
|
||||
import org.junit.Test
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals
|
||||
import static org.junit.Assert.assertTrue
|
||||
import static org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Test defined in https://tools.ietf.org/html/rfc7516#appendix-A.1
|
||||
*/
|
||||
class GcmAesEncryptionServiceTest {
|
||||
|
||||
final byte[] K =
|
||||
|
@ -33,9 +30,11 @@ class GcmAesEncryptionServiceTest {
|
|||
final byte[] T =
|
||||
[0x5c, 0x50, 0x68, 0x31, 0x85, 0x19, 0xa1, 0xd7, 0xad, 0x65, 0xdb, 0xd3, 0x88, 0x5b, 0xd2, 0x91] as byte[]
|
||||
|
||||
|
||||
/**
|
||||
* Test that reflects https://tools.ietf.org/html/rfc7516#appendix-A.1
|
||||
*/
|
||||
@Test
|
||||
void test() {
|
||||
void testEncryptionAndDecryption() {
|
||||
|
||||
def alg = EncryptionAlgorithms.A256GCM
|
||||
|
||||
|
@ -78,4 +77,13 @@ class GcmAesEncryptionServiceTest {
|
|||
|
||||
println '[' + c.join(', ') + ']' */
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInstantiationWithInvalidKeyLength() {
|
||||
try {
|
||||
new GcmAesEncryptionAlgorithm(EncryptionAlgorithmName.A128GCM.getValue(), 5);
|
||||
fail()
|
||||
} catch (IllegalArgumentException expected) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
package io.jsonwebtoken.impl.crypto
|
||||
|
||||
import io.jsonwebtoken.EncryptionAlgorithmName
|
||||
import io.jsonwebtoken.EncryptionAlgorithms
|
||||
import io.jsonwebtoken.SignatureAlgorithm
|
||||
import org.junit.Test
|
||||
|
||||
import static org.junit.Assert.*
|
||||
|
||||
class HmacAesEncryptionAlgorithmTest {
|
||||
|
||||
@Test
|
||||
void testGetRequiredKeyLengthWithNullSignatureAlgorithm() {
|
||||
try {
|
||||
HmacAesEncryptionAlgorithm.getRequiredKeyLength(null)
|
||||
fail()
|
||||
} catch (IllegalArgumentException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRequiredKeyLengthWithInvalidSignatureAlgorithm() {
|
||||
try {
|
||||
HmacAesEncryptionAlgorithm.getRequiredKeyLength(SignatureAlgorithm.ES256)
|
||||
fail()
|
||||
} catch (IllegalArgumentException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGenerateHmacKeyBytesWithExactNumExpectedBytes() {
|
||||
|
||||
int hmacKeySize = EncryptionAlgorithms.A128CBC_HS256.getRequiredKeyLength() / 2;
|
||||
|
||||
def alg = new TestHmacAesEncryptionAlgorithm() {
|
||||
@Override
|
||||
protected byte[] generateHmacKeyBytes() {
|
||||
byte[] bytes = new byte[hmacKeySize]
|
||||
AbstractAesEncryptionAlgorithm.DEFAULT_RANDOM.nextBytes(bytes);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
def skey = alg.generateKey()
|
||||
def key = skey.getEncoded()
|
||||
|
||||
def plaintext = "Hello World! Nice to meet you!".getBytes("UTF-8")
|
||||
|
||||
def request = EncryptionRequests.builder().setKey(key).setPlaintext(plaintext).build()
|
||||
|
||||
def result = alg.encrypt(request);
|
||||
assert result instanceof AuthenticatedEncryptionResult
|
||||
|
||||
def dreq = DecryptionRequests.builder()
|
||||
.setKey(key)
|
||||
.setInitializationVector(result.getInitializationVector())
|
||||
.setAuthenticationTag(result.getAuthenticationTag())
|
||||
.setCiphertext(result.getCiphertext())
|
||||
.build()
|
||||
|
||||
byte[] decryptedPlaintextBytes = alg.decrypt(dreq)
|
||||
|
||||
assertArrayEquals(plaintext, decryptedPlaintextBytes);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGenerateHmacKeyBytesWithInsufficientNumExpectedBytes() {
|
||||
|
||||
int hmacKeySize = EncryptionAlgorithms.A128CBC_HS256.getRequiredKeyLength() / 2;
|
||||
|
||||
def alg = new TestHmacAesEncryptionAlgorithm() {
|
||||
@Override
|
||||
protected byte[] generateHmacKeyBytes() {
|
||||
byte[] bytes = new byte[hmacKeySize - 1]
|
||||
AbstractAesEncryptionAlgorithm.DEFAULT_RANDOM.nextBytes(bytes);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
alg.generateKey()
|
||||
fail()
|
||||
} catch (CryptoException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecryptWithInvalidTag() {
|
||||
|
||||
def alg = EncryptionAlgorithms.A128CBC_HS256;
|
||||
|
||||
def skey = alg.generateKey()
|
||||
def key = skey.getEncoded()
|
||||
|
||||
def plaintext = "Hello World! Nice to meet you!".getBytes("UTF-8")
|
||||
|
||||
def request = EncryptionRequests.builder().setKey(key).setPlaintext(plaintext).build()
|
||||
|
||||
def result = alg.encrypt(request);
|
||||
assert result instanceof AuthenticatedEncryptionResult
|
||||
|
||||
def realTag = result.getAuthenticationTag();
|
||||
|
||||
//fake it:
|
||||
|
||||
def fakeTag = new byte[realTag.length]
|
||||
AbstractAesEncryptionAlgorithm.DEFAULT_RANDOM.nextBytes(fakeTag)
|
||||
|
||||
def dreq = DecryptionRequests.builder()
|
||||
.setKey(key)
|
||||
.setInitializationVector(result.getInitializationVector())
|
||||
.setAuthenticationTag(fakeTag)
|
||||
.setCiphertext(result.getCiphertext())
|
||||
.build()
|
||||
|
||||
try {
|
||||
alg.decrypt(dreq)
|
||||
fail()
|
||||
} catch (CryptoException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
static class TestHmacAesEncryptionAlgorithm extends HmacAesEncryptionAlgorithm {
|
||||
TestHmacAesEncryptionAlgorithm() {
|
||||
super(EncryptionAlgorithmName.A128CBC_HS256.getValue(), SignatureAlgorithm.HS256);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue