113: initial JWE (shared key AES) encryption support

This commit is contained in:
Les Hazlewood 2016-04-12 18:50:24 -07:00
parent 3dfae9a31d
commit 716c6fd500
30 changed files with 1388 additions and 0 deletions

View File

@ -0,0 +1,37 @@
/*
* 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 abstract class AbstractCryptoRequest implements CryptoRequest {
private final byte[] key;
private final byte[] iv;
public AbstractCryptoRequest(byte[] key, byte[] iv) {
this.key = key;
this.iv = iv;
}
@Override
public byte[] getKey() {
return this.key;
}
@Override
public byte[] getInitializationVector() {
return this.iv;
}
}

View File

@ -0,0 +1,22 @@
/*
* 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 AssociatedDataSource {
byte[] getAssociatedData();
}

View File

@ -0,0 +1,20 @@
/*
* 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 AuthenticatedDecryptionRequest extends DecryptionRequest, AssociatedDataSource, AuthenticationTagSource {
}

View File

@ -0,0 +1,20 @@
/*
* 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 AuthenticatedEncryptionRequest extends EncryptionRequest, AssociatedDataSource {
}

View File

@ -0,0 +1,20 @@
/*
* 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 AuthenticatedEncryptionResult extends EncryptionResult, AuthenticationTagSource {
}

View File

@ -0,0 +1,22 @@
/*
* 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 AuthenticationTagSource {
byte[] getAuthenticationTag();
}

View File

@ -0,0 +1,23 @@
/*
* 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 class CryptoException extends RuntimeException {
public CryptoException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,24 @@
/*
* 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 CryptoRequest {
byte[] getKey();
byte[] getInitializationVector();
}

View File

@ -0,0 +1,22 @@
/*
* 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 DecryptionRequest extends CryptoRequest {
byte[] getCiphertext();
}

View File

@ -0,0 +1,31 @@
/*
* 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 DecryptionRequestBuilder {
DecryptionRequestBuilder setInitializationVector(byte[] iv);
DecryptionRequestBuilder setKey(byte[] key);
DecryptionRequestBuilder setCiphertext(byte[] plaintext);
DecryptionRequestBuilder setAdditionalAuthenticatedData(byte[] aad);
DecryptionRequestBuilder setAuthenticationTag(byte[] tag);
DecryptionRequest build();
}

View File

@ -0,0 +1,28 @@
/*
* 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 final class DecryptionRequests {
//only for 100% test coverage.
private static final DecryptionRequests INSTANCE = new DecryptionRequests();
private DecryptionRequests(){}
public static DecryptionRequestBuilder builder() {
return new DefaultDecryptionRequestBuilder();
}
}

View File

@ -0,0 +1,225 @@
/*
* 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.Arrays;
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;
/**
* Default {@link EncryptionService} implementation that uses AES in GCM mode.
*/
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 final SecretKey key;
private final SecureRandom 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 = Arrays.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 = Arrays.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 = Arrays.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();
if (keyBytes != null && keyBytes.length > 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, "AuthenticatedDecryptionRRequests 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, "AuthenticatedDecryptionReqeusts 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);
}
}

View File

@ -0,0 +1,44 @@
/*
* 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;
public class DefaultAuthenticatedDecryptionRequest extends DefaultDecryptionRequest
implements AuthenticatedDecryptionRequest {
private final byte[] aad;
private final byte[] tag;
public DefaultAuthenticatedDecryptionRequest(byte[] key, byte[] iv, byte[] ciphertext, byte[] aad, byte[] tag) {
super(key, iv, ciphertext);
Assert.notEmpty(aad, "Additional Authenticated Data cannot be null or empty.");
Assert.notEmpty(tag, "Authentication tag cannot be null or empty.");
this.aad = aad;
this.tag = tag;
}
@Override
public byte[] getAssociatedData() {
return this.aad;
}
@Override
public byte[] getAuthenticationTag() {
return this.tag;
}
}

View File

@ -0,0 +1,35 @@
/*
* 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;
public class DefaultAuthenticatedEncryptionRequest extends DefaultEncryptionRequest
implements AuthenticatedEncryptionRequest {
private final byte[] aad;
public DefaultAuthenticatedEncryptionRequest(byte[] key, byte[] iv, byte[] plaintext, byte[] aad) {
super(key, iv, plaintext);
Assert.notEmpty(aad, "Additional Authenticated Data cannot be null or empty.");
this.aad = aad;
}
@Override
public byte[] getAssociatedData() {
return this.aad;
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.Arrays;
import io.jsonwebtoken.lang.Assert;
public class DefaultAuthenticatedEncryptionResult extends DefaultEncryptionResult
implements AuthenticatedEncryptionResult {
private final byte[] tag;
public DefaultAuthenticatedEncryptionResult(byte[] iv, byte[] ciphertext, byte[] tag) {
super(iv, ciphertext);
Assert.notEmpty(tag, "authentication tag cannot be null or empty.");
this.tag = tag;
}
@Override
public byte[] getAuthenticationTag() {
return this.tag;
}
@Override
public byte[] compact() {
int ivLength = Arrays.length(iv);
int ciphertextLength = Arrays.length(ciphertext);
int tagLength = Arrays.length(tag);
int outputLength = ivLength + ciphertextLength + tagLength;
byte[] output = new byte[outputLength];
//iv
if (ivLength > 0) {
output = new byte[outputLength];
System.arraycopy(iv, 0, output, 0, ivLength);
}
//ciphertext
System.arraycopy(ciphertext, 0, output, ivLength, ciphertextLength);
//tag can never be empty based on the assertion in the constructor
assert tagLength > 0;
System.arraycopy(tag, 0, output, ivLength + ciphertextLength, tagLength);
return output;
}
}

View File

@ -0,0 +1,34 @@
/*
* 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;
public class DefaultDecryptionRequest extends AbstractCryptoRequest implements DecryptionRequest {
private final byte[] ciphertext;
public DefaultDecryptionRequest(byte[] key, byte[] iv, byte[] ciphertext) {
super(key, iv);
Assert.notEmpty(ciphertext, "ciphertext cannot be null or empty.");
this.ciphertext = ciphertext;
}
@Override
public byte[] getCiphertext() {
return this.ciphertext;
}
}

View File

@ -0,0 +1,90 @@
/*
* 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 static io.jsonwebtoken.lang.Arrays.length;
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.";
public static final String TAG_NEEDS_AAD_MSG = "If you specify an authentication tag during decryption, you must " +
"also specify the additional authenticated data used " +
"during encryption.";
private byte[] iv;
private byte[] key;
private byte[] ciphertext;
private byte[] aad;
private byte[] tag;
@Override
public DecryptionRequestBuilder setInitializationVector(byte[] iv) {
this.iv = length(iv) > 0 ? iv : null;
return this;
}
@Override
public DecryptionRequestBuilder setKey(byte[] key) {
this.key = length(key) > 0 ? key : null;
return this;
}
public DecryptionRequestBuilder setCiphertext(byte[] ciphertext) {
Assert.notEmpty(ciphertext, "Ciphertext cannot be null or empty.");
this.ciphertext = ciphertext;
return this;
}
@Override
public DecryptionRequestBuilder setAdditionalAuthenticatedData(byte[] aad) {
this.aad = length(aad) > 0 ? aad : null;
return this;
}
@Override
public DecryptionRequestBuilder setAuthenticationTag(byte[] tag) {
this.tag = length(tag) > 0 ? tag : null;
return this;
}
@Override
public DecryptionRequest build() {
Assert.notEmpty(ciphertext, "Ciphertext cannot be null or empty.");
int aadLength = length(aad);
int tagLength = length(tag);
if (aadLength > 0 && tagLength == 0) {
String msg = AAD_NEEDS_TAG_MSG;
throw new IllegalArgumentException(msg);
}
if (tagLength > 0 && aadLength == 0) {
String msg = TAG_NEEDS_AAD_MSG;
throw new IllegalArgumentException(msg);
}
if (aadLength > 0 || tagLength > 0) {
return new DefaultAuthenticatedDecryptionRequest(key, iv, ciphertext, aad, tag);
}
return new DefaultDecryptionRequest(key, iv, ciphertext);
}
}

View File

@ -0,0 +1,34 @@
/*
* 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;
public class DefaultEncryptionRequest extends AbstractCryptoRequest implements EncryptionRequest {
private final byte[] plaintext;
public DefaultEncryptionRequest(byte[] key, byte[] iv, byte[] plaintext) {
super(key, iv);
Assert.notEmpty(plaintext, "plaintext cannot be null or empty.");
this.plaintext = plaintext;
}
@Override
public byte[] getPlaintext() {
return this.plaintext;
}
}

View File

@ -0,0 +1,64 @@
/*
* 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 static io.jsonwebtoken.lang.Arrays.length;
public class DefaultEncryptionRequestBuilder implements EncryptionRequestBuilder {
private byte[] iv;
private byte[] key;
private byte[] plaintext;
private byte[] aad;
@Override
public EncryptionRequestBuilder setInitializationVector(byte[] iv) {
this.iv = length(iv) > 0 ? iv : null;
return this;
}
@Override
public EncryptionRequestBuilder setKey(byte[] key) {
this.key = length(key) > 0 ? key : null;
return this;
}
@Override
public EncryptionRequestBuilder setPlaintext(byte[] plaintext) {
Assert.notEmpty(plaintext, "Plaintext cannot be null or empty.");
this.plaintext = plaintext;
return this;
}
@Override
public EncryptionRequestBuilder setAdditionalAuthenticatedData(byte[] aad) {
this.aad = length(aad) > 0 ? aad : null;
return this;
}
@Override
public EncryptionRequest build() {
Assert.notEmpty(plaintext, "Plaintext cannot be null or empty.");
if (length(aad) > 0) {
return new DefaultAuthenticatedEncryptionRequest(key, iv, plaintext, aad);
}
return new DefaultEncryptionRequest(key, iv, plaintext);
}
}

View File

@ -0,0 +1,65 @@
/*
* 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 static io.jsonwebtoken.lang.Arrays.length;
public class DefaultEncryptionResult implements EncryptionResult {
protected final byte[] iv;
protected final byte[] ciphertext;
public DefaultEncryptionResult(byte[] iv, byte[] ciphertext) {
Assert.notEmpty(ciphertext, "ciphertext cannot be null or empty.");
this.ciphertext = ciphertext;
this.iv = iv;
}
@Override
public byte[] getInitializationVector() {
return this.iv;
}
@Override
public byte[] getCiphertext() {
return this.ciphertext;
}
@Override
public byte[] compact() {
int ivLength = length(iv);
int ciphertextLength = length(ciphertext);
int outputLength = ivLength + ciphertextLength;
byte[] output = ciphertext; //default
if (ivLength > 0) {
output = new byte[outputLength];
System.arraycopy(iv, 0, output, 0, ivLength);
System.arraycopy(ciphertext, 0, output, ivLength, ciphertextLength);
}
return output;
}
}

View File

@ -0,0 +1,22 @@
/*
* 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 EncryptionRequest extends CryptoRequest {
byte[] getPlaintext();
}

View File

@ -0,0 +1,30 @@
/*
* 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 EncryptionRequestBuilder {
EncryptionRequestBuilder setInitializationVector(byte[] iv);
EncryptionRequestBuilder setKey(byte[] key);
EncryptionRequestBuilder setPlaintext(byte[] plaintext);
EncryptionRequestBuilder setAdditionalAuthenticatedData(byte[] aad);
EncryptionRequest build();
}

View File

@ -0,0 +1,13 @@
package io.jsonwebtoken.impl.crypto;
public final class EncryptionRequests {
//100% code coverage
private static final EncryptionRequests INSTANCE = new EncryptionRequests();
private EncryptionRequests(){} //prevent instantiation
public static EncryptionRequestBuilder builder() {
return new DefaultEncryptionRequestBuilder();
}
}

View File

@ -0,0 +1,25 @@
/*
* 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 EncryptionResult {
byte[] getInitializationVector();
byte[] getCiphertext();
byte[] compact();
}

View File

@ -0,0 +1,28 @@
/*
* 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;
}

View File

@ -0,0 +1,21 @@
/*
* 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 InitializationVectorSource {
byte[] getInitializationVector();
}

View File

@ -0,0 +1,11 @@
package io.jsonwebtoken.lang;
/**
* @since 0.6
*/
public abstract class Arrays {
public static int length(byte[] bytes) {
return bytes != null ? bytes.length : 0;
}
}

View File

@ -0,0 +1,203 @@
/*
* 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);
}
}

View File

@ -0,0 +1,62 @@
/*
* 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 org.junit.Test
import static org.junit.Assert.*
class DefaultAuthenticatedEncryptionResultTest {
private byte[] generateData() {
byte[] data = new byte[32];
new Random().nextBytes(data) //does not need to be secure for this test
return data;
}
@Test
void testCompactWithoutIv() {
byte[] ciphertext = generateData()
byte[] tag = generateData()
byte[] combined = new byte[ciphertext.length + tag.length];
System.arraycopy(ciphertext, 0, combined, 0, ciphertext.length);
System.arraycopy(tag, 0, combined, ciphertext.length, tag.length);
def res = new DefaultAuthenticatedEncryptionResult(null, ciphertext, tag)
byte[] compact = res.compact()
assertTrue(Arrays.equals(combined, compact))
}
@Test
void testCompactWithIv() {
byte[] iv = generateData()
byte[] ciphertext = generateData()
byte[] tag = generateData()
byte[] combined = new byte[iv.length + ciphertext.length + tag.length];
System.arraycopy(iv, 0, combined, 0, iv.length)
System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length);
System.arraycopy(tag, 0, combined, iv.length + ciphertext.length, tag.length);
def res = new DefaultAuthenticatedEncryptionResult(iv, ciphertext, tag)
byte[] compact = res.compact()
assertTrue(Arrays.equals(combined, compact))
}
}

View File

@ -0,0 +1,50 @@
/*
* 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 org.junit.Test
import static org.junit.Assert.*
class DefaultDecryptionRequestBuilderTest {
private byte[] generateData() {
byte[] data = new byte[32];
new Random().nextBytes(data) //does not need to be secure for this test
return data;
}
@Test
void testAadWithoutTag() {
try {
new DefaultDecryptionRequestBuilder().setCiphertext(generateData())
.setAdditionalAuthenticatedData(generateData()).build()
fail()
} catch (IllegalArgumentException expected) {
assertEquals(DefaultDecryptionRequestBuilder.AAD_NEEDS_TAG_MSG, expected.getMessage())
}
}
@Test
void testTagWithoutAad() {
try {
new DefaultDecryptionRequestBuilder().setCiphertext(generateData())
.setAuthenticationTag(generateData()).build()
fail()
} catch (IllegalArgumentException expected) {
assertEquals(DefaultDecryptionRequestBuilder.TAG_NEEDS_AAD_MSG, expected.getMessage())
}
}
}