diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWE.java b/core/src/main/java/org/keycloak/jose/jwe/JWE.java new file mode 100644 index 0000000000..03bb43f99d --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/JWE.java @@ -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 Marek Posolda + */ +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); + } + } + +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWEConstants.java b/core/src/main/java/org/keycloak/jose/jwe/JWEConstants.java new file mode 100644 index 0000000000..d81141dbd1 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/JWEConstants.java @@ -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 Marek Posolda + */ +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"; + +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java b/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java new file mode 100644 index 0000000000..30b5150a46 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java @@ -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 Marek Posolda + */ +@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); + } + + + } + +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWEKeyStorage.java b/core/src/main/java/org/keycloak/jose/jwe/JWEKeyStorage.java new file mode 100644 index 0000000000..9ce042ce03 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/JWEKeyStorage.java @@ -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 Marek Posolda + */ +public class JWEKeyStorage { + + private Key encryptionKey; + + private byte[] cekBytes; + + private Map 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 + } +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java b/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java new file mode 100644 index 0000000000..80aaea5a87 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/JWERegistry.java @@ -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 Marek Posolda + */ +class JWERegistry { + + // https://tools.ietf.org/html/rfc7518#page-12 + // Registry not pluggable for now. Just supported algorithms included + private static final Map 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 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); + } + + +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWEUtils.java b/core/src/main/java/org/keycloak/jose/jwe/JWEUtils.java new file mode 100644 index 0000000000..d88ec56ac9 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/JWEUtils.java @@ -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 Marek Posolda + */ +public class JWEUtils { + + private JWEUtils() { + } + + public static byte[] generateSecret(int bytes) { + byte[] buf = new byte[bytes]; + new SecureRandom().nextBytes(buf); + return buf; + } +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/alg/AesKeyWrapAlgorithmProvider.java b/core/src/main/java/org/keycloak/jose/jwe/alg/AesKeyWrapAlgorithmProvider.java new file mode 100644 index 0000000000..2a6eedebb8 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/alg/AesKeyWrapAlgorithmProvider.java @@ -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 Marek Posolda + */ +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); + } + + +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java b/core/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java new file mode 100644 index 0000000000..98ab7b39a9 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/alg/DirectAlgorithmProvider.java @@ -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 Marek Posolda + */ +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]; + } +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java b/core/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java new file mode 100644 index 0000000000..057f487c60 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/alg/JWEAlgorithmProvider.java @@ -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 Marek Posolda + */ +public interface JWEAlgorithmProvider { + + byte[] decodeCek(byte[] encodedCek, Key encryptionKey) throws IOException, GeneralSecurityException; + + byte[] encodeCek(JWEEncryptionProvider encryptionProvider, JWEKeyStorage keyStorage, Key encryptionKey) throws IOException, GeneralSecurityException; + +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/enc/AesCbcHmacShaEncryptionProvider.java b/core/src/main/java/org/keycloak/jose/jwe/enc/AesCbcHmacShaEncryptionProvider.java new file mode 100644 index 0000000000..dcec260137 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/enc/AesCbcHmacShaEncryptionProvider.java @@ -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 Marek Posolda + */ +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; + } + } + + +} diff --git a/core/src/main/java/org/keycloak/jose/jwe/enc/JWEEncryptionProvider.java b/core/src/main/java/org/keycloak/jose/jwe/enc/JWEEncryptionProvider.java new file mode 100644 index 0000000000..c49fe242e5 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwe/enc/JWEEncryptionProvider.java @@ -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 Marek Posolda + */ +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(); + +} diff --git a/core/src/test/java/org/keycloak/jose/JWETest.java b/core/src/test/java/org/keycloak/jose/JWETest.java new file mode 100644 index 0000000000..74b75f1b4d --- /dev/null +++ b/core/src/test/java/org/keycloak/jose/JWETest.java @@ -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 Marek Posolda + */ +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); + + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStoreConfiguration.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStoreConfiguration.java index fa9b7db3d0..3f7d258186 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStoreConfiguration.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/KeycloakRemoteStoreConfiguration.java @@ -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(); } }