Repository: ambari Updated Branches: refs/heads/trunk bde8a5bb1 -> 93fc4221d
AMBARI-8660. Provide encrypt and decrypt functionality in KerberosCredential Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/93fc4221 Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/93fc4221 Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/93fc4221 Branch: refs/heads/trunk Commit: 93fc4221dd3dda65115dc366798d840766377b9f Parents: bde8a5b Author: Robert Levas <rle...@hortonworks.com> Authored: Mon Dec 15 10:50:38 2014 -0500 Committer: John Speidel <jspei...@hortonworks.com> Committed: Mon Dec 15 10:52:13 2014 -0500 ---------------------------------------------------------------------- .../kerberos/KerberosCredential.java | 181 +++++++++++++++++++ .../kerberos/KerberosCredentialTest.java | 103 +++++++++++ 2 files changed, 284 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/93fc4221/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KerberosCredential.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KerberosCredential.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KerberosCredential.java index adb2edc..19997e7 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KerberosCredential.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KerberosCredential.java @@ -17,16 +17,183 @@ */ package org.apache.ambari.server.serveraction.kerberos; +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import org.apache.ambari.server.AmbariException; +import org.apache.commons.codec.binary.Base64; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Map; + /** * KerberosCredential encapsulates data needed to authenticate an identity to a KDC. + * <p/> + * This class has the ability to encrypt and decrypt itself using the AES encryption algorithm. */ public class KerberosCredential { + /** + * A property name used to hold the KDC administrator's principal value. + */ + public static final String KEY_NAME_PRINCIPAL = "principal"; + /** + * A property name used to hold the KDC administrator's password value. + */ + public static final String KEY_NAME_PASSWORD = "password"; + /** + * A property name used to hold the KDC administrator's (base64-encoded) keytab + * value. + */ + public static final String KEY_NAME_KEYTAB = "keytab"; + + /** + * This principal value + */ private String principal = null; + + /** + * The plaintext password value + */ private String password = null; + + /** + * A base64-encoded keytab + */ private String keytab = null; /** + * Given a Map of attributes, attempts to safely retrieve the data needed to create a + * KerberosCredential representing a KDC administrator. + * <p/> + * It is expected that the following properties exist in the Map: + * <ul> + * <li>principal</li> + * <li>password (optional)</li> + * <li>keytab (optional)</li> + * </ul> + * <p/> + * Each of these properties may be prefixed with some prefix value to generate a relevant key value. + * If prefix was "kerberos_admin/", then the key representing the principal would be computed + * to be "kerberos_admin/principal". + * + * @param map a Map of attributes containing the values needed to create a new KerberosCredential + * @param prefix a String containing the prefix to used along with the base key name (principal, etc...) + * to create the relevant key name ([prefix]base_key. etc...) + * @return a KerberosCredential or null if commandParameters is null + */ + public static KerberosCredential fromMap(Map<String, Object> map, String prefix) { + KerberosCredential credential = null; + + if (map != null) { + Object attribute; + String principal; + String password; + String keytab; + + if (prefix == null) { + prefix = ""; + } + + attribute = map.get(prefix + KEY_NAME_PRINCIPAL); + principal = (attribute == null) ? null : attribute.toString(); + + attribute = map.get(prefix + KEY_NAME_PASSWORD); + password = (attribute == null) ? null : attribute.toString(); + + attribute = map.get(prefix + KEY_NAME_KEYTAB); + keytab = (attribute == null) ? null : attribute.toString(); + + if (((principal != null) && !principal.isEmpty()) || + ((password != null) && !password.isEmpty()) || + ((keytab != null) && !keytab.isEmpty())) { + credential = new KerberosCredential(principal, password, keytab); + } + } + + return credential; + } + + /** + * Decrypts a String containing base64-encoded encrypted data into a new KerberosCredential. + * <p/> + * Given a key and a base64-encoded set of bytes containing encrypted data (ideally obtained from + * {@link #encrypt(KerberosCredential, byte[])} or {@link #encrypt(byte[])}, decodes and decrypts + * into a new KerberosCredential. + * + * @param cipherText a String containing base64-encoded encrypted data + * @param key an array of bytes used to decrypt the encrypted data + * @return a new KerberosCredential + * @throws AmbariException if an error occurs while decrypting the data + */ + public static KerberosCredential decrypt(String cipherText, byte[] key) throws AmbariException { + if (cipherText == null) { + return null; + } else { + try { + SecretKeySpec secretKey = new SecretKeySpec(Arrays.copyOf(key, 16), "AES"); + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + byte[] plaintext = cipher.doFinal(Base64.decodeBase64(cipherText)); + return new Gson().fromJson(new String(plaintext), KerberosCredential.class); + } catch (NoSuchAlgorithmException e) { + throw new AmbariException("Failed to decrypt cipher text due to invalid encryption algorithm", e); + } catch (NoSuchPaddingException e) { + throw new AmbariException("Failed to decrypt cipher text due to invalid padding scheme algorithm", e); + } catch (IllegalBlockSizeException e) { + throw new AmbariException("Failed to decrypt cipher text due to invalid block size", e); + } catch (BadPaddingException e) { + throw new AmbariException("Failed to decrypt cipher text due to invalid padding", e); + } catch (InvalidKeyException e) { + throw new AmbariException("Failed to decrypt cipher text due to invalid key", e); + } catch (JsonSyntaxException e) { + throw new AmbariException("Failed to decrypt cipher, cannot parse data into a KerberosCredential", e); + } + } + } + + /** + * Encrypts a KerberosCredential into a base64-encoded set of bytes. + * <p/> + * Given a KerberosCredential and a key, serializes the data into a JSON-formatted string and + * encrypts it. + * + * @param kerberosCredential the KerberosCredential to encrypt + * @param key an array of bytes used to decrypt the encrypted data + * @return a String containing base64-encoded encrypted data + * @throws AmbariException if an error occurs while encrypting the KerberosCredential + */ + public static String encrypt(KerberosCredential kerberosCredential, byte[] key) throws AmbariException { + if (kerberosCredential == null) { + return null; + } else { + try { + SecretKeySpec secretKey = new SecretKeySpec(Arrays.copyOf(key, 16), "AES"); + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + String plaintext = new Gson().toJson(kerberosCredential); + return Base64.encodeBase64String(cipher.doFinal(plaintext.getBytes())); + } catch (NoSuchAlgorithmException e) { + throw new AmbariException("Failed to encrypt plaintext due to invalid encryption algorithm", e); + } catch (NoSuchPaddingException e) { + throw new AmbariException("Failed to encrypt plaintext due to invalid padding scheme algorithm", e); + } catch (IllegalBlockSizeException e) { + throw new AmbariException("Failed to encrypt plaintext due to invalid key", e); + } catch (BadPaddingException e) { + throw new AmbariException("Failed to encrypt plaintext due to unexpected reasons", e); + } catch (InvalidKeyException e) { + throw new AmbariException("Failed to encrypt plaintext due to invalid key", e); + } + } + } + + /** * Creates an empty KerberosCredential */ public KerberosCredential() { @@ -89,4 +256,18 @@ public class KerberosCredential { public void setKeytab(String keytab) { this.keytab = keytab; } + + /** + * Encrypts this KerberosCredential into a base64-encoded set of bytes. + * <p/> + * Serializes this KerberosCredential into a JSON-formatted string and + * encrypts it using the supplied key. + * + * @param key an array of bytes used to decrypt the encrypted data + * @return a String containing base64-encoded encrypted data + * @throws AmbariException if an error occurs while encrypting the KerberosCredential + */ + public String encrypt(byte[] key) throws AmbariException { + return encrypt(this, key); + } } http://git-wip-us.apache.org/repos/asf/ambari/blob/93fc4221/ambari-server/src/test/java/org/apache/ambari/server/serveraction/kerberos/KerberosCredentialTest.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/test/java/org/apache/ambari/server/serveraction/kerberos/KerberosCredentialTest.java b/ambari-server/src/test/java/org/apache/ambari/server/serveraction/kerberos/KerberosCredentialTest.java new file mode 100644 index 0000000..305b122 --- /dev/null +++ b/ambari-server/src/test/java/org/apache/ambari/server/serveraction/kerberos/KerberosCredentialTest.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.ambari.server.serveraction.kerberos; + +import junit.framework.Assert; +import org.apache.ambari.server.AmbariException; +import org.eclipse.persistence.internal.helper.Helper; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +public class KerberosCredentialTest { + + @Test + public void testFromMap() throws Exception { + KerberosCredential kerberosCredential; + Map<String, Object> attributes = new HashMap<String, Object>(); + attributes.put(KerberosCredential.KEY_NAME_PRINCIPAL, "admin/ad...@example.com"); + attributes.put(KerberosCredential.KEY_NAME_KEYTAB, "bogus_base64-encoded_data"); + + attributes.put("kerberos_admin/" + KerberosCredential.KEY_NAME_PRINCIPAL, "admin/ad...@foobar.com"); + attributes.put("kerberos_admin/" + KerberosCredential.KEY_NAME_PASSWORD, "t0p_s3cr3t"); + + // Test with an empty prefix + kerberosCredential = KerberosCredential.fromMap(attributes, ""); + Assert.assertNotNull(kerberosCredential); + Assert.assertEquals("admin/ad...@example.com", kerberosCredential.getPrincipal()); + Assert.assertNull(kerberosCredential.getPassword()); + Assert.assertEquals("bogus_base64-encoded_data", kerberosCredential.getKeytab()); + + // Test with a NULL prefix + kerberosCredential = KerberosCredential.fromMap(attributes, null); + Assert.assertNotNull(kerberosCredential); + Assert.assertEquals("admin/ad...@example.com", kerberosCredential.getPrincipal()); + Assert.assertNull(kerberosCredential.getPassword()); + Assert.assertEquals("bogus_base64-encoded_data", kerberosCredential.getKeytab()); + + // Test with a prefix + kerberosCredential = KerberosCredential.fromMap(attributes, "kerberos_admin/"); + Assert.assertNotNull(kerberosCredential); + Assert.assertEquals("admin/ad...@foobar.com", kerberosCredential.getPrincipal()); + Assert.assertEquals("t0p_s3cr3t", kerberosCredential.getPassword()); + Assert.assertNull(kerberosCredential.getKeytab()); + + // Test with a prefix that does not resolve to any existing keys + kerberosCredential = KerberosCredential.fromMap(attributes, "invalid/"); + Assert.assertNull(kerberosCredential); + } + + @Test + public void testEncryptAndDecrypt() throws Exception { + byte[] key = "This is my key".getBytes(); + KerberosCredential credential; + String cipherText; + KerberosCredential decryptedCredential; + + credential = new KerberosCredential("admin/ad...@foobar.com", "t0p_s3cr3t", null); + cipherText = credential.encrypt(key); + Assert.assertNotNull(cipherText); + + // Test a successful case + decryptedCredential = KerberosCredential.decrypt(cipherText, key); + Assert.assertNotNull(decryptedCredential); + Assert.assertEquals(credential.getPrincipal(), decryptedCredential.getPrincipal()); + Assert.assertEquals(credential.getPassword(), decryptedCredential.getPassword()); + Assert.assertEquals(credential.getKeytab(), decryptedCredential.getKeytab()); + + // Test an invalid key + try { + decryptedCredential = KerberosCredential.decrypt(cipherText, "not the key".getBytes()); + Assert.fail("Should have thrown AmbariException"); + } catch (AmbariException e) { + // this is expected + } + + // Test an invalid cipher text + try { + decryptedCredential = KerberosCredential.decrypt("I am not encrypted data", key); + Assert.fail("Should have thrown AmbariException"); + } catch (AmbariException e) { + // this is expected + } + } +} \ No newline at end of file