KEYCLOAK-5569 Added JWE

This commit is contained in:
mposolda 2017-09-22 11:30:17 +02:00
parent 4c71e2ec17
commit 63673c4328
13 changed files with 1222 additions and 3 deletions

View file

@ -0,0 +1,197 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.jose.jwe;
import java.io.IOException;
import java.security.GeneralSecurityException;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.BouncyIntegration;
import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class JWE {
static {
BouncyIntegration.init();
}
private JWEHeader header;
private String base64Header;
private JWEKeyStorage keyStorage = new JWEKeyStorage();
private String base64Cek;
private byte[] initializationVector;
private byte[] content;
private byte[] encryptedContent;
private byte[] authenticationTag;
public JWE header(JWEHeader header) {
this.header = header;
this.base64Header = null;
return this;
}
JWEHeader getHeader() {
if (header == null && base64Header != null) {
try {
byte[] decodedHeader = Base64Url.decode(base64Header);
header = JsonSerialization.readValue(decodedHeader, JWEHeader.class);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
return header;
}
public String getBase64Header() throws IOException {
if (base64Header == null && header != null) {
byte[] contentBytes = JsonSerialization.writeValueAsBytes(header);
base64Header = Base64Url.encode(contentBytes);
}
return base64Header;
}
public JWEKeyStorage getKeyStorage() {
return keyStorage;
}
public byte[] getInitializationVector() {
return initializationVector;
}
public JWE content(byte[] content) {
this.content = content;
return this;
}
public byte[] getContent() {
return content;
}
public byte[] getEncryptedContent() {
return encryptedContent;
}
public byte[] getAuthenticationTag() {
return authenticationTag;
}
public void setEncryptedContentInfo(byte[] initializationVector, byte[] encryptedContent, byte[] authenticationTag) {
this.initializationVector = initializationVector;
this.encryptedContent = encryptedContent;
this.authenticationTag = authenticationTag;
}
public String encodeJwe() {
try {
if (header == null) {
throw new IllegalStateException("Header must be set");
}
if (content == null) {
throw new IllegalStateException("Content must be set");
}
JWEAlgorithmProvider algorithmProvider = JWERegistry.getAlgProvider(header.getAlgorithm());
if (algorithmProvider == null) {
throw new IllegalArgumentException("No provider for alg '" + header.getAlgorithm() + "'");
}
JWEEncryptionProvider encryptionProvider = JWERegistry.getEncProvider(header.getEncryptionAlgorithm());
if (encryptionProvider == null) {
throw new IllegalArgumentException("No provider for enc '" + header.getAlgorithm() + "'");
}
keyStorage.setEncryptionProvider(encryptionProvider);
keyStorage.getCEKKey(JWEKeyStorage.KeyUse.ENCRYPTION, true); // Will generate CEK if it's not already present
byte[] encodedCEK = algorithmProvider.encodeCek(encryptionProvider, keyStorage, keyStorage.getEncryptionKey());
base64Cek = Base64Url.encode(encodedCEK);
encryptionProvider.encodeJwe(this);
return getEncodedJweString();
} catch (IOException | GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
private String getEncodedJweString() {
StringBuilder builder = new StringBuilder();
builder.append(base64Header).append(".")
.append(base64Cek).append(".")
.append(Base64Url.encode(initializationVector)).append(".")
.append(Base64Url.encode(encryptedContent)).append(".")
.append(Base64Url.encode(authenticationTag));
return builder.toString();
}
public JWE verifyAndDecodeJwe(String jweStr) {
try {
String[] parts = jweStr.split("\\.");
if (parts.length != 5) {
throw new IllegalStateException("Not a JWE String");
}
this.base64Header = parts[0];
this.base64Cek = parts[1];
this.initializationVector = Base64Url.decode(parts[2]);
this.encryptedContent = Base64Url.decode(parts[3]);
this.authenticationTag = Base64Url.decode(parts[4]);
this.header = getHeader();
JWEAlgorithmProvider algorithmProvider = JWERegistry.getAlgProvider(header.getAlgorithm());
if (algorithmProvider == null) {
throw new IllegalArgumentException("No provider for alg '" + header.getAlgorithm() + "'");
}
JWEEncryptionProvider encryptionProvider = JWERegistry.getEncProvider(header.getEncryptionAlgorithm());
if (encryptionProvider == null) {
throw new IllegalArgumentException("No provider for enc '" + header.getAlgorithm() + "'");
}
keyStorage.setEncryptionProvider(encryptionProvider);
byte[] decodedCek = algorithmProvider.decodeCek(Base64Url.decode(base64Cek), keyStorage.getEncryptionKey());
keyStorage.setCEKBytes(decodedCek);
encryptionProvider.verifyAndDecodeJwe(this);
return this;
} catch (IOException | GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.jose.jwe;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class JWEConstants {
public static final String DIR = "dir";
public static final String A128KW = "A128KW";
public static final String A128CBC_HS256 = "A128CBC-HS256";
public static final String A192CBC_HS384 = "A192CBC-HS384";
public static final String A256CBC_HS512 = "A256CBC-HS512";
}

View file

@ -0,0 +1,103 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.jose.jwe;
import java.io.IOException;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class JWEHeader implements Serializable {
@JsonProperty("alg")
private String algorithm;
@JsonProperty("enc")
private String encryptionAlgorithm;
@JsonProperty("zip")
private String compressionAlgorithm;
@JsonProperty("typ")
private String type;
@JsonProperty("cty")
private String contentType;
@JsonProperty("kid")
private String keyId;
public JWEHeader() {
}
public JWEHeader(String algorithm, String encryptionAlgorithm, String compressionAlgorithm) {
this.algorithm = algorithm;
this.encryptionAlgorithm = encryptionAlgorithm;
this.compressionAlgorithm = compressionAlgorithm;
}
public String getAlgorithm() {
return algorithm;
}
public String getEncryptionAlgorithm() {
return encryptionAlgorithm;
}
public String getCompressionAlgorithm() {
return compressionAlgorithm;
}
public String getType() {
return type;
}
public String getContentType() {
return contentType;
}
public String getKeyId() {
return keyId;
}
private static final ObjectMapper mapper = new ObjectMapper();
static {
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
public String toString() {
try {
return mapper.writeValueAsString(this);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,103 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.jose.jwe;
import java.security.Key;
import java.util.HashMap;
import java.util.Map;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class JWEKeyStorage {
private Key encryptionKey;
private byte[] cekBytes;
private Map<KeyUse, Key> decodedCEK = new HashMap<>();
private JWEEncryptionProvider encryptionProvider;
public Key getEncryptionKey() {
return encryptionKey;
}
public JWEKeyStorage setEncryptionKey(Key encryptionKey) {
this.encryptionKey = encryptionKey;
return this;
}
public void setCEKBytes(byte[] cekBytes) {
this.cekBytes = cekBytes;
}
public byte[] getCekBytes() {
if (cekBytes == null) {
cekBytes = encryptionProvider.serializeCEK(this);
}
return cekBytes;
}
public JWEKeyStorage setCEKKey(Key key, KeyUse keyUse) {
decodedCEK.put(keyUse, key);
return this;
}
public Key getCEKKey(KeyUse keyUse, boolean generateIfNotPresent) {
Key key = decodedCEK.get(keyUse);
if (key == null) {
if (encryptionProvider != null) {
if (cekBytes == null && generateIfNotPresent) {
generateCekBytes();
}
if (cekBytes != null) {
encryptionProvider.deserializeCEK(this);
}
} else {
throw new IllegalStateException("encryptionProvider needs to be set");
}
}
return decodedCEK.get(keyUse);
}
private void generateCekBytes() {
int cekLength = encryptionProvider.getExpectedCEKLength();
cekBytes = JWEUtils.generateSecret(cekLength);
}
public void setEncryptionProvider(JWEEncryptionProvider encryptionProvider) {
this.encryptionProvider = encryptionProvider;
}
public enum KeyUse {
ENCRYPTION,
SIGNATURE
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.jose.jwe;
import java.util.HashMap;
import java.util.Map;
import org.keycloak.jose.jwe.alg.AesKeyWrapAlgorithmProvider;
import org.keycloak.jose.jwe.alg.DirectAlgorithmProvider;
import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider;
import org.keycloak.jose.jwe.enc.AesCbcHmacShaEncryptionProvider;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
/**
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
class JWERegistry {
// https://tools.ietf.org/html/rfc7518#page-12
// Registry not pluggable for now. Just supported algorithms included
private static final Map<String, JWEEncryptionProvider> ENC_PROVIDERS = new HashMap<>();
// https://tools.ietf.org/html/rfc7518#page-22
// Registry not pluggable for now. Just supported algorithms included
private static final Map<String, JWEAlgorithmProvider> ALG_PROVIDERS = new HashMap<>();
static {
// Provider 'dir' just directly uses encryption keys for encrypt/decrypt content.
ALG_PROVIDERS.put(JWEConstants.DIR, new DirectAlgorithmProvider());
ALG_PROVIDERS.put(JWEConstants.A128KW, new AesKeyWrapAlgorithmProvider());
ENC_PROVIDERS.put(JWEConstants.A128CBC_HS256, new AesCbcHmacShaEncryptionProvider.Aes128CbcHmacSha256Provider());
ENC_PROVIDERS.put(JWEConstants.A192CBC_HS384, new AesCbcHmacShaEncryptionProvider.Aes192CbcHmacSha384Provider());
ENC_PROVIDERS.put(JWEConstants.A256CBC_HS512, new AesCbcHmacShaEncryptionProvider.Aes256CbcHmacSha512Provider());
}
static JWEAlgorithmProvider getAlgProvider(String alg) {
return ALG_PROVIDERS.get(alg);
}
static JWEEncryptionProvider getEncProvider(String enc) {
return ENC_PROVIDERS.get(enc);
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.jose.jwe;
import java.security.SecureRandom;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class JWEUtils {
private JWEUtils() {
}
public static byte[] generateSecret(int bytes) {
byte[] buf = new byte[bytes];
new SecureRandom().nextBytes(buf);
return buf;
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.jose.jwe.alg;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.Key;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.Wrapper;
import org.bouncycastle.crypto.engines.AESWrapEngine;
import org.bouncycastle.crypto.params.KeyParameter;
import org.keycloak.jose.jwe.JWEKeyStorage;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class AesKeyWrapAlgorithmProvider implements JWEAlgorithmProvider {
@Override
public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws IOException, GeneralSecurityException {
try {
Wrapper encrypter = new AESWrapEngine();
encrypter.init(false, new KeyParameter(encryptionKey.getEncoded()));
return encrypter.unwrap(encodedCek, 0, encodedCek.length);
} catch (InvalidCipherTextException icte) {
throw new IllegalStateException(icte);
}
}
@Override
public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws IOException, GeneralSecurityException {
Wrapper encrypter = new AESWrapEngine();
encrypter.init(true, new KeyParameter(encryptionKey.getEncoded()));
byte[] cekBytes = keyStorage.getCekBytes();
return encrypter.wrap(cekBytes, 0, cekBytes.length);
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.jose.jwe.alg;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.Key;
import org.keycloak.jose.jwe.JWEKeyStorage;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DirectAlgorithmProvider implements JWEAlgorithmProvider {
@Override
public byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws IOException, GeneralSecurityException {
return new byte[0];
}
@Override
public byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws IOException, GeneralSecurityException {
return new byte[0];
}
}

View file

@ -0,0 +1,36 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.jose.jwe.alg;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.Key;
import org.keycloak.jose.jwe.JWEKeyStorage;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface JWEAlgorithmProvider {
byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws IOException, GeneralSecurityException;
byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws IOException, GeneralSecurityException;
}

View file

@ -0,0 +1,272 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.jose.jwe.enc;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.keycloak.jose.jwe.JWE;
import org.keycloak.jose.jwe.JWEKeyStorage;
import org.keycloak.jose.jwe.JWEUtils;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class AesCbcHmacShaEncryptionProvider implements JWEEncryptionProvider {
@Override
public void encodeJwe(JWE jwe) throws IOException, GeneralSecurityException {
byte[] contentBytes = jwe.getContent();
byte[] initializationVector = JWEUtils.generateSecret(16);
Key aesKey = jwe.getKeyStorage().getCEKKey(JWEKeyStorage.KeyUse.ENCRYPTION, false);
if (aesKey == null) {
throw new IllegalArgumentException("AES CEK key not present");
}
Key hmacShaKey = jwe.getKeyStorage().getCEKKey(JWEKeyStorage.KeyUse.SIGNATURE, false);
if (hmacShaKey == null) {
throw new IllegalArgumentException("HMAC CEK key not present");
}
int expectedAesKeyLength = getExpectedAesKeyLength();
if (expectedAesKeyLength != aesKey.getEncoded().length) {
throw new IllegalStateException("Length of aes key should be " + expectedAesKeyLength +", but was " + aesKey.getEncoded().length);
}
byte[] cipherBytes = encryptBytes(contentBytes, initializationVector, aesKey);
byte[] aad = jwe.getBase64Header().getBytes("UTF-8");
byte[] authenticationTag = computeAuthenticationTag(aad, initializationVector, cipherBytes, hmacShaKey);
jwe.setEncryptedContentInfo(initializationVector, cipherBytes, authenticationTag);
}
@Override
public void verifyAndDecodeJwe(JWE jwe) throws IOException, GeneralSecurityException {
Key aesKey = jwe.getKeyStorage().getCEKKey(JWEKeyStorage.KeyUse.ENCRYPTION, false);
if (aesKey == null) {
throw new IllegalArgumentException("AES CEK key not present");
}
Key hmacShaKey = jwe.getKeyStorage().getCEKKey(JWEKeyStorage.KeyUse.SIGNATURE, false);
if (hmacShaKey == null) {
throw new IllegalArgumentException("HMAC CEK key not present");
}
int expectedAesKeyLength = getExpectedAesKeyLength();
if (expectedAesKeyLength != aesKey.getEncoded().length) {
throw new IllegalStateException("Length of aes key should be " + expectedAesKeyLength +", but was " + aesKey.getEncoded().length);
}
byte[] aad = jwe.getBase64Header().getBytes("UTF-8");
byte[] authenticationTag = computeAuthenticationTag(aad, jwe.getInitializationVector(), jwe.getEncryptedContent(), hmacShaKey);
byte[] expectedAuthTag = jwe.getAuthenticationTag();
boolean digitsEqual = MessageDigest.isEqual(expectedAuthTag, authenticationTag);
if (!digitsEqual) {
throw new IllegalArgumentException("Signature validations failed");
}
byte[] contentBytes = decryptBytes(jwe.getEncryptedContent(), jwe.getInitializationVector(), aesKey);
jwe.content(contentBytes);
}
protected abstract int getExpectedAesKeyLength();
protected abstract String getHmacShaAlgorithm();
protected abstract int getAuthenticationTagLength();
private byte[] encryptBytes(byte[] contentBytes, byte[] ivBytes, Key aesKey) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
AlgorithmParameterSpec ivParamSpec = new IvParameterSpec(ivBytes);
cipher.init(Cipher.ENCRYPT_MODE, aesKey, ivParamSpec);
return cipher.doFinal(contentBytes);
}
private byte[] decryptBytes(byte[] encryptedBytes, byte[] ivBytes, Key aesKey) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
AlgorithmParameterSpec ivParamSpec = new IvParameterSpec(ivBytes);
cipher.init(Cipher.DECRYPT_MODE, aesKey, ivParamSpec);
return cipher.doFinal(encryptedBytes);
}
private byte[] computeAuthenticationTag(byte[] aadBytes, byte[] ivBytes, byte[] cipherBytes, Key hmacKeySpec) throws NoSuchAlgorithmException, InvalidKeyException {
// Compute "al"
ByteBuffer b = ByteBuffer.allocate(4);
b.order(ByteOrder.BIG_ENDIAN); // optional, the initial order of a byte buffer is always BIG_ENDIAN.
int aadLengthInBits = aadBytes.length * 8;
b.putInt(aadLengthInBits);
byte[] result1 = b.array();
byte[] al = new byte[8];
System.arraycopy(result1, 0, al, 4, 4);
byte[] concatenatedHmacInput = new byte[aadBytes.length + ivBytes.length + cipherBytes.length + al.length];
System.arraycopy(aadBytes, 0, concatenatedHmacInput, 0, aadBytes.length);
System.arraycopy(ivBytes, 0, concatenatedHmacInput, aadBytes.length, ivBytes.length );
System.arraycopy(cipherBytes, 0, concatenatedHmacInput, aadBytes.length + ivBytes.length , cipherBytes.length);
System.arraycopy(al, 0, concatenatedHmacInput, aadBytes.length + ivBytes.length + cipherBytes.length, al.length);
String hmacShaAlg = getHmacShaAlgorithm();
Mac macImpl = Mac.getInstance(hmacShaAlg);
macImpl.init(hmacKeySpec);
macImpl.update(concatenatedHmacInput);
byte[] macEncoded = macImpl.doFinal();
int authTagLength = getAuthenticationTagLength();
return Arrays.copyOf(macEncoded, authTagLength);
}
@Override
public void deserializeCEK(JWEKeyStorage keyStorage) {
byte[] cekBytes = keyStorage.getCekBytes();
int cekLength = getExpectedCEKLength();
byte[] cekMacKey = Arrays.copyOf(cekBytes, cekLength / 2);
byte[] cekAesKey = Arrays.copyOfRange(cekBytes, cekLength / 2, cekLength);
SecretKeySpec aesKey = new SecretKeySpec(cekAesKey, "AES");
SecretKeySpec hmacKey = new SecretKeySpec(cekMacKey, "HMACSHA2");
keyStorage.setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION);
keyStorage.setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE);
}
@Override
public byte[] serializeCEK(JWEKeyStorage keyStorage) {
Key aesKey = keyStorage.getCEKKey(JWEKeyStorage.KeyUse.ENCRYPTION, false);
if (aesKey == null) {
throw new IllegalArgumentException("AES CEK key not present");
}
Key hmacShaKey = keyStorage.getCEKKey(JWEKeyStorage.KeyUse.SIGNATURE, false);
if (hmacShaKey == null) {
throw new IllegalArgumentException("HMAC CEK key not present");
}
byte[] hmacBytes = hmacShaKey.getEncoded();
byte[] aesBytes = aesKey.getEncoded();
byte[] result = new byte[hmacBytes.length + aesBytes.length];
System.arraycopy(hmacBytes, 0, result, 0, hmacBytes.length);
System.arraycopy(aesBytes, 0, result, hmacBytes.length, aesBytes.length);
return result;
}
public static class Aes128CbcHmacSha256Provider extends AesCbcHmacShaEncryptionProvider {
@Override
protected int getExpectedAesKeyLength() {
return 16;
}
@Override
protected String getHmacShaAlgorithm() {
return "HMACSHA256";
}
@Override
protected int getAuthenticationTagLength() {
return 16;
}
@Override
public int getExpectedCEKLength() {
return 32;
}
}
public static class Aes192CbcHmacSha384Provider extends AesCbcHmacShaEncryptionProvider {
@Override
protected int getExpectedAesKeyLength() {
return 24;
}
@Override
protected String getHmacShaAlgorithm() {
return "HMACSHA384";
}
@Override
protected int getAuthenticationTagLength() {
return 24;
}
@Override
public int getExpectedCEKLength() {
return 48;
}
}
public static class Aes256CbcHmacSha512Provider extends AesCbcHmacShaEncryptionProvider {
@Override
protected int getExpectedAesKeyLength() {
return 32;
}
@Override
protected String getHmacShaAlgorithm() {
return "HMACSHA512";
}
@Override
protected int getAuthenticationTagLength() {
return 32;
}
@Override
public int getExpectedCEKLength() {
return 64;
}
}
}

View file

@ -0,0 +1,75 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.jose.jwe.enc;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.Key;
import org.keycloak.jose.jwe.JWE;
import org.keycloak.jose.jwe.JWEKeyStorage;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface JWEEncryptionProvider {
/**
* This method usually has 3 outputs:
* - generated initialization vector
* - encrypted content
* - authenticationTag for MAC validation
*
* It is supposed to call {@link JWE#setEncryptedContentInfo(byte[], byte[], byte[])} after it's finished
*
* @param jwe
* @throws IOException
* @throws GeneralSecurityException
*/
void encodeJwe(JWE jwe) throws IOException, GeneralSecurityException;
/**
* This method is supposed to verify checksums and decrypt content. Then it needs to call {@link JWE#content(byte[])} after it's finished
*
* @param jwe
* @throws IOException
* @throws GeneralSecurityException
*/
void verifyAndDecodeJwe(JWE jwe) throws IOException, GeneralSecurityException;
/**
* This method requires that decoded CEK keys are present in the keyStorage.decodedCEK map before it's called
*
* @param keyStorage
* @return
*/
byte[] serializeCEK(JWEKeyStorage keyStorage);
/**
* This method is supposed to deserialize keys. It requires that {@link JWEKeyStorage#getCekBytes()} is set. After keys are deserialized,
* this method needs to call {@link JWEKeyStorage#setCEKKey(Key, JWEKeyStorage.KeyUse)} according to all uses, which this encryption algorithm requires.
*
* @param keyStorage
*/
void deserializeCEK(JWEKeyStorage keyStorage);
int getExpectedCEKLength();
}

View file

@ -0,0 +1,203 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.jose;
import java.io.UnsupportedEncodingException;
import java.security.Key;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.common.util.Base64Url;
import org.keycloak.jose.jwe.JWE;
import org.keycloak.jose.jwe.JWEConstants;
import org.keycloak.jose.jwe.JWEHeader;
import org.keycloak.jose.jwe.JWEKeyStorage;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class JWETest {
private static final String PAYLOAD = "Hello world! How are you? This is some quite a long text, which is much longer than just simple 'Hello World'";
private static final byte[] HMAC_SHA256_KEY = new byte[] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 13, 14, 15, 16 };
private static final byte[] AES_128_KEY = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
private static final byte[] HMAC_SHA512_KEY = new byte[] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 13, 14, 15, 16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
private static final byte[] AES_256_KEY = new byte[] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 13, 14, 15, 16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
@Test
public void testDirect_Aes128CbcHmacSha256() throws Exception {
SecretKey aesKey = new SecretKeySpec(AES_128_KEY, "AES");
SecretKey hmacKey = new SecretKeySpec(HMAC_SHA256_KEY, "HMACSHA2");
JWEHeader jweHeader = new JWEHeader(JWEConstants.DIR, JWEConstants.A128CBC_HS256, null);
JWE jwe = new JWE()
.header(jweHeader)
.content(PAYLOAD.getBytes("UTF-8"));
jwe.getKeyStorage()
.setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION)
.setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE);
String encodedContent = jwe.encodeJwe();
System.out.println("Encoded content: " + encodedContent);
System.out.println("Encoded content length: " + encodedContent.length());
jwe = new JWE();
jwe.getKeyStorage()
.setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION)
.setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE);
jwe.verifyAndDecodeJwe(encodedContent);
String decodedContent = new String(jwe.getContent(), "UTF-8");
Assert.assertEquals(PAYLOAD, decodedContent);
}
@Test
public void testDirect_Aes256CbcHmacSha512() throws Exception {
final SecretKey aesKey = new SecretKeySpec(AES_256_KEY, "AES");
final SecretKey hmacKey = new SecretKeySpec(HMAC_SHA512_KEY, "HMACSHA2");
JWEHeader jweHeader = new JWEHeader(JWEConstants.DIR, JWEConstants.A256CBC_HS512, null);
JWE jwe = new JWE()
.header(jweHeader)
.content(PAYLOAD.getBytes("UTF-8"));
jwe.getKeyStorage()
.setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION)
.setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE);
String encodedContent = jwe.encodeJwe();
System.out.println("Encoded content: " + encodedContent);
System.out.println("Encoded content length: " + encodedContent.length());
jwe = new JWE();
jwe.getKeyStorage()
.setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION)
.setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE);
jwe.verifyAndDecodeJwe(encodedContent);
String decodedContent = new String(jwe.getContent(), "UTF-8");
Assert.assertEquals(PAYLOAD, decodedContent);
}
@Test
public void testAesKW_Aes128CbcHmacSha256() throws Exception {
SecretKey aesKey = new SecretKeySpec(AES_128_KEY, "AES");
JWEHeader jweHeader = new JWEHeader(JWEConstants.A128KW, JWEConstants.A128CBC_HS256, null);
JWE jwe = new JWE()
.header(jweHeader)
.content(PAYLOAD.getBytes("UTF-8"));
jwe.getKeyStorage()
.setEncryptionKey(aesKey);
String encodedContent = jwe.encodeJwe();
System.out.println("Encoded content: " + encodedContent);
System.out.println("Encoded content length: " + encodedContent.length());
jwe = new JWE();
jwe.getKeyStorage()
.setEncryptionKey(aesKey);
jwe.verifyAndDecodeJwe(encodedContent);
String decodedContent = new String(jwe.getContent(), "UTF-8");
Assert.assertEquals(PAYLOAD, decodedContent);
}
@Test
public void externalJweAes128CbcHmacSha256Test() throws UnsupportedEncodingException {
String externalJwe = "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..qysUrI1iVtiG4Z4jyr7XXg.apdNSQhR7WDMg6IHf5aLVI0gGp6JuOHYmIUtflns4WHmyxOOnh_GShLI6DWaK_SiywTV5gZvZYtl8H8Iv5fTfLkc4tiDDjbdtmsOP7tqyRxVh069gU5UvEAgmCXbIKALutgYXcYe2WM4E6BIHPTSt8jXdkktFcm7XHiD7mpakZyjXsG8p3XVkQJ72WbJI_t6.Ks6gHeko7BRTZ4CFs5ijRA";
System.out.println("External encoded content length: " + externalJwe.length());
final SecretKey aesKey = new SecretKeySpec(AES_128_KEY, "AES");
final SecretKey hmacKey = new SecretKeySpec(HMAC_SHA256_KEY, "HMACSHA2");
JWE jwe = new JWE();
jwe.getKeyStorage()
.setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION)
.setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE);
jwe.verifyAndDecodeJwe(externalJwe);
String decodedContent = new String(jwe.getContent(), "UTF-8");
Assert.assertEquals(PAYLOAD, decodedContent);
}
@Test
public void externalJweAes256CbcHmacSha512Test() throws UnsupportedEncodingException {
String externalJwe = "eyJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYWxnIjoiZGlyIn0..xUPndQ5U69CYaWMKr4nyeg.AzSzba6OdNsvTIoNpub8d2TmYnkY7W8Sd-1S33DjJwJsSaNcfvfXBq5bqXAGVAnLHrLZJKWoEYsmOrYHz3Nao-kpLtUpc4XZI8yiYUqkHTjmxZnfD02R6hz31a5KBCnDTtUEv23VSxm8yUyQKoUTpVHbJ3b2VQvycg2XFUXPsA6oaSSEpz-uwe1Vmun2hUBB.Qal4rMYn1RrXQ9AQ9ONUjUXvlS2ow8np-T8QWMBR0ns";
System.out.println("External encoded content length: " + externalJwe.length());
final SecretKey aesKey = new SecretKeySpec(AES_256_KEY, "AES");
final SecretKey hmacKey = new SecretKeySpec(HMAC_SHA512_KEY, "HMACSHA2");
JWE jwe = new JWE();
jwe.getKeyStorage()
.setCEKKey(aesKey, JWEKeyStorage.KeyUse.ENCRYPTION)
.setCEKKey(hmacKey, JWEKeyStorage.KeyUse.SIGNATURE);
jwe.verifyAndDecodeJwe(externalJwe);
String decodedContent = new String(jwe.getContent(), "UTF-8");
Assert.assertEquals(PAYLOAD, decodedContent);
}
@Test
public void externalJweAesKeyWrapTest() throws Exception {
// See example "A.3" from JWE specification - https://tools.ietf.org/html/rfc7516#page-41
String externalJwe = "eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ.AxY8DCtDaGlsbGljb3RoZQ.KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.U0m_YmjN04DJvceFICbCVQ";
byte[] aesKey = Base64Url.decode("GawgguFyGrWKav7AX4VKUg");
SecretKeySpec aesKeySpec = new SecretKeySpec(aesKey, "AES");
JWE jwe = new JWE();
jwe.getKeyStorage()
.setEncryptionKey(aesKeySpec);
jwe.verifyAndDecodeJwe(externalJwe);
String decodedContent = new String(jwe.getContent(), "UTF-8");
Assert.assertEquals("Live long and prosper.", decodedContent);
}
}

View file

@ -48,16 +48,16 @@ public class KeycloakRemoteStoreConfiguration extends RemoteStoreConfiguration {
public String useConfigTemplateFromCache() {
return useConfigTemplateFromCache==null ? null : useConfigTemplateFromCache.get();
return useConfigTemplateFromCache.get();
}
public String remoteServers() {
return remoteServers==null ? null : remoteServers.get();
return remoteServers.get();
}
public Boolean sessionCache() {
return sessionCache==null ? false : sessionCache.get();
return sessionCache.get()==null ? false : sessionCache.get();
}
}