From 59782d44b6bbbfb3708894d9c34f64276213dc41 Mon Sep 17 00:00:00 2001 From: Ruben Vargas Palma Date: Wed, 7 Aug 2019 16:58:05 -0500 Subject: [PATCH] Add support for SHA526, SHA512 Crypt as well as PBKDF2 algorithms for openshift submodule (#1020) (#1024) * Add SHA256 to password storage types for htpasswd file. * Add PBKDF2 Algorithms for Passwords. --- containers/hawkular-htpasswd/pom.xml | 67 +++++++ .../openshift/htpasswd/CreatePassword.java | 106 +++++++++++ .../openshift/auth/BasicAuthenticator.java | 36 +--- .../openshift/auth/PasswordManager.java | 167 ++++++++++++++++++ containers/pom.xml | 1 + 5 files changed, 347 insertions(+), 30 deletions(-) create mode 100644 containers/hawkular-htpasswd/pom.xml create mode 100644 containers/hawkular-htpasswd/src/main/java/org/hawkular/openshift/htpasswd/CreatePassword.java create mode 100644 containers/hawkular-openshift-security-filter/src/main/java/org/hawkular/openshift/auth/PasswordManager.java diff --git a/containers/hawkular-htpasswd/pom.xml b/containers/hawkular-htpasswd/pom.xml new file mode 100644 index 000000000..9896fe62c --- /dev/null +++ b/containers/hawkular-htpasswd/pom.xml @@ -0,0 +1,67 @@ + + + + 4.0.0 + + + org.hawkular.metrics + hawkular-metrics-containers + 0.32.0-SNAPSHOT + + + hawkular-htpasswd + + Hawkular Password Creator + + + + ${project.groupId} + hawkular-openshift-security-filter + ${project.version} + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.1.0 + + + package + + shade + + + false + + + org.hawkular.openshift.htpasswd.CreatePassword + + + + + + + + + diff --git a/containers/hawkular-htpasswd/src/main/java/org/hawkular/openshift/htpasswd/CreatePassword.java b/containers/hawkular-htpasswd/src/main/java/org/hawkular/openshift/htpasswd/CreatePassword.java new file mode 100644 index 000000000..797284165 --- /dev/null +++ b/containers/hawkular-htpasswd/src/main/java/org/hawkular/openshift/htpasswd/CreatePassword.java @@ -0,0 +1,106 @@ +/* + * Copyright 2014-2019 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.hawkular.openshift.htpasswd; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.hawkular.openshift.auth.PasswordManager; + +public class CreatePassword { + + private static final String SHA256_ALGORITHM = "pbkdf2-sha256"; + private static final String SHA512_ALGORITHM = "pbkdf2-sha512"; + + private String algorithm; + private String fileName; + private String password; + private String username; + private boolean overwrite; + + private PasswordManager passwordManager = new PasswordManager(); + + public CreatePassword() { + password = System.getProperty("htpasswd.password", ""); + username = System.getProperty("htpasswd.username", ""); + algorithm = System.getProperty("htpasswd.algorithm", SHA256_ALGORITHM); + fileName = System.getProperty("htpasswd.file", ""); + overwrite = Boolean.getBoolean("htpasswd.overwrite"); + + } + + private void validateArguments() { + if (password.isEmpty()) { + throw new RuntimeException("htpasswd.password cannot be empty"); + } + + if (username.isEmpty()) { + throw new RuntimeException("htpasswd.username cannot be empty"); + } + + if (!algorithm.equals(SHA256_ALGORITHM) && !algorithm.equals(SHA512_ALGORITHM)) { + throw new RuntimeException("Invalid algorithm, permitted values: [" + SHA256_ALGORITHM + "," + SHA512_ALGORITHM + "]"); + } + } + + private void run() throws Exception { + + String output = null; + OutputStream outputStream; + + // First validate arguments. + this.validateArguments(); + + if (algorithm.equals(SHA256_ALGORITHM)) { + output = passwordManager.createPBDKF2SHA256Password(password); + } else if (algorithm.equals(SHA512_ALGORITHM)) { + output = passwordManager.createPBDKF2SHA512Password(password); + } + + if (!fileName.isEmpty()) { + outputStream = new FileOutputStream(this.fileName, !overwrite); + } else { + outputStream = System.out; + } + + if (output != null) { + try { + outputStream.write(username.getBytes()); + outputStream.write(":".getBytes()); + outputStream.write(output.getBytes()); + outputStream.write("\n".getBytes()); + } catch (IOException e) { + throw new RuntimeException(e.getMessage()); + } finally { + outputStream.close(); + } + } else { + throw new RuntimeException("Invalid output from algorithm, this should not happens, could be a bug"); + } + } + + public static void main(String[] args) { + CreatePassword passwordCreator = new CreatePassword(); + try { + passwordCreator.run(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/containers/hawkular-openshift-security-filter/src/main/java/org/hawkular/openshift/auth/BasicAuthenticator.java b/containers/hawkular-openshift-security-filter/src/main/java/org/hawkular/openshift/auth/BasicAuthenticator.java index 7cc0ef08d..b4fadbe70 100644 --- a/containers/hawkular-openshift-security-filter/src/main/java/org/hawkular/openshift/auth/BasicAuthenticator.java +++ b/containers/hawkular-openshift-security-filter/src/main/java/org/hawkular/openshift/auth/BasicAuthenticator.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 Red Hat, Inc. and/or its affiliates + * Copyright 2014-2019 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"); @@ -30,8 +30,6 @@ import java.util.HashMap; import java.util.Map; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.codec.digest.Md5Crypt; import org.jboss.logging.Logger; import io.undertow.server.HttpHandler; @@ -49,13 +47,15 @@ class BasicAuthenticator implements Authenticator { private static final String HTPASSWD_FILE_SYSPROP_SUFFIX = ".openshift.htpasswd-file"; private final File htpasswdFile; - private static final String MD5_PREFIX = "$apr1$"; - private static final String SHA_PREFIX = "{SHA}"; private final HttpHandler containerHandler; private final Map users; + private PasswordManager passwordManager; + BasicAuthenticator(HttpHandler containerHandler, String componentName) { + passwordManager = new PasswordManager(); + String htpasswdPath = System.getProperty(componentName + HTPASSWD_FILE_SYSPROP_SUFFIX); if (htpasswdPath == null) { htpasswdFile = new File(System.getProperty("user.home"), ".htpasswd"); @@ -102,37 +102,13 @@ public void handleRequest(HttpServerExchange serverExchange) throws Exception { String username = entries[0]; String password = entries[1]; - if (users.containsKey(username) && isAuthorized(username, password)) { + if (users.containsKey(username) && passwordManager.isAuthorized(users.get(username), password)) { containerHandler.handleRequest(serverExchange); } else { endExchange(serverExchange, FORBIDDEN); } } - private boolean isAuthorized(String username, String password) { - String storedPassword = users.get(username); - return (storedPassword.startsWith(MD5_PREFIX) && verifyMD5Password(storedPassword, password)) - || (storedPassword.startsWith(SHA_PREFIX) && verifySHA1Password(storedPassword, password)); - } - - - private boolean verifyMD5Password(String storedPassword, String passedPassword) { - // We send in the password presented by the user and use the stored password as the salt - // If they match, then the password matches the original non-encrypted stored password - return Md5Crypt.apr1Crypt(passedPassword, storedPassword).equals(storedPassword); - } - - private boolean verifySHA1Password(String storedPassword, String passedPassword) { - //Remove the SHA_PREFIX from the password string - storedPassword = storedPassword.substring(SHA_PREFIX.length()); - - //Get the SHA digest and encode it in Base64 - byte[] digestedPasswordBytes = DigestUtils.sha1(passedPassword); - String digestedPassword = Base64.getEncoder().encodeToString(digestedPasswordBytes); - - //Check if the stored password matches the passed one - return digestedPassword.equals(storedPassword); - } @Override public void stop() { diff --git a/containers/hawkular-openshift-security-filter/src/main/java/org/hawkular/openshift/auth/PasswordManager.java b/containers/hawkular-openshift-security-filter/src/main/java/org/hawkular/openshift/auth/PasswordManager.java new file mode 100644 index 000000000..db63bba95 --- /dev/null +++ b/containers/hawkular-openshift-security-filter/src/main/java/org/hawkular/openshift/auth/PasswordManager.java @@ -0,0 +1,167 @@ +/* + * Copyright 2014-2019 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.hawkular.openshift.auth; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Base64; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.codec.digest.Md5Crypt; +import org.apache.commons.codec.digest.Sha2Crypt; + +public class PasswordManager { + + private static final String MD5_PREFIX = "$apr1$"; + private static final String SHA_PREFIX = "{SHA}"; + private static final String SHA256_PREFIX = "$5$"; + private static final String SHA512_PREFIX = "$6$"; + private static final String PBKDF2_SHA256_PREFIX = "$pbkdf2-sha256$"; + private static final String PBKDF2_SHA512_PREFIX = "$pbkdf2-sha512$"; + private static final int DEFAULT_ITERATIONS_PBKDF2 = 25000; + + public boolean isAuthorized(String storedCredential, String passedPassword) { + return (storedCredential.startsWith(MD5_PREFIX) && verifyMD5Password(storedCredential, passedPassword)) + || (storedCredential.startsWith(SHA_PREFIX) && verifySHA1Password(storedCredential, passedPassword)) + || + (storedCredential.startsWith(SHA256_PREFIX) && verifySHA256Password(storedCredential, passedPassword)) + || + (storedCredential.startsWith(SHA512_PREFIX) && verifySHA512Password(storedCredential, passedPassword)) + || (storedCredential.startsWith(PBKDF2_SHA256_PREFIX) && + verifyPBDKF2Password(storedCredential, passedPassword)) + || (storedCredential.startsWith(PBKDF2_SHA512_PREFIX) && + verifyPBDKF2Password(storedCredential, passedPassword)); + } + + + private boolean verifyMD5Password(String storedCredential, String passedPassword) { + // We send in the password presented by the user and use the stored password as the salt + // If they match, then the password matches the original non-encrypted stored password + return Md5Crypt.apr1Crypt(passedPassword, storedCredential).equals(storedCredential); + } + + private boolean verifySHA1Password(String storedCredential, String passedPassword) { + //Remove the SHA_PREFIX from the password string + String storedPassword = storedCredential.substring(SHA_PREFIX.length()); + + //Get the SHA digest and encode it in Base64 + byte[] digestedPasswordBytes = DigestUtils.sha1(passedPassword); + String digestedPassword = Base64.getEncoder().encodeToString(digestedPasswordBytes); + + //Check if the stored password matches the passed one + return digestedPassword.equals(storedPassword); + } + + private boolean verifySHA256Password(String storedCredential, String passedPassword) { + //Obtain the salt from the beginning of the storedPassword + String salt = storedCredential.substring(0, storedCredential.lastIndexOf("$") + 1); + + String digestedPassword = Sha2Crypt.sha256Crypt(passedPassword.getBytes(), salt); + + //Check if the stored password matches the passed one + return digestedPassword.equals(storedCredential); + } + + private boolean verifySHA512Password(String storedCredential, String passedPassword) { + //Obtain the salt from the beginning of the storedPassword + String salt = storedCredential.substring(0, storedCredential.lastIndexOf("$") + 1); + + String digestedPassword = Sha2Crypt.sha512Crypt(passedPassword.getBytes(), salt); + + //Check if the stored password matches the passed one + return digestedPassword.equals(storedCredential); + } + + private boolean verifyPBDKF2Password(String storedCredential, String passedPassword) { + String[] creds = storedCredential.split("\\$"); + if (creds.length != 5) { + // TODO verify ok to throw exception here if using other password types + throw new RuntimeException("Stored password checksum not valid. Check password store."); + } + String pbkdf2Algorithm = "PBKDF2WithHmacSHA256"; + int keySize = Base64.getDecoder().decode(creds[4]).length * 8; + if (keySize == 256) { + // default + } else if (keySize == 512) { + pbkdf2Algorithm = "PBKDF2WithHmacSHA512"; + } else { + throw new RuntimeException("Stored password is not a valid size. Check password store."); + } + byte[] salt = Base64.getDecoder().decode(creds[3].getBytes()); + String encoded = encodePBDKF2(passedPassword, Integer.parseInt(creds[2]), salt, keySize, pbkdf2Algorithm); + + return encoded.equals(creds[4]); + } + + private String encodePBDKF2(String rawPassword, int iterations, byte[] salt, int keySize, String pbkdf2Algorithm) { + KeySpec spec = new PBEKeySpec(rawPassword.toCharArray(), salt, iterations, keySize); + + try { + byte[] key = SecretKeyFactory.getInstance(pbkdf2Algorithm).generateSecret(spec).getEncoded(); + return Base64.getEncoder().encodeToString(key); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("PBKDF2 algorithm not found", e); + } catch (InvalidKeySpecException e) { + throw new RuntimeException("Password could not be encoded", e); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String createPBDKF2SHA256Password(String password) { + return createPBDKF2Password(password, DEFAULT_ITERATIONS_PBKDF2, 256); + } + + public String createPBDKF2SHA512Password(String password) { + return createPBDKF2Password(password, DEFAULT_ITERATIONS_PBKDF2, 512); + } + + private String createPBDKF2Password(String password, int iterations, int keySize) { + String prefix = PBKDF2_SHA256_PREFIX; + String pbkdf2Algorithm = "PBKDF2WithHmacSHA256"; + if (keySize == 256) { + // default + } else if (keySize == 512) { + prefix = PBKDF2_SHA512_PREFIX; + pbkdf2Algorithm = "PBKDF2WithHmacSHA512"; + } else { + throw new RuntimeException("Keysize must be 256 or 512"); + } + if (iterations < 10000) { + throw new RuntimeException("Iterations must be above 10000 when specified."); + } + + byte[] salt = getSalt(); + String saltEncoded = Base64.getEncoder().encodeToString(salt); + String checksum = encodePBDKF2(password, iterations, salt, keySize, pbkdf2Algorithm); + + return prefix + iterations + "$" + saltEncoded + "$" + checksum; + } + + private byte[] getSalt() { + byte[] buffer = new byte[16]; + SecureRandom secureRandom = new SecureRandom(); + secureRandom.nextBytes(buffer); + return buffer; + } +} diff --git a/containers/pom.xml b/containers/pom.xml index 3fab76c69..9596fb367 100644 --- a/containers/pom.xml +++ b/containers/pom.xml @@ -34,5 +34,6 @@ hawkular-openshift-security-filter + hawkular-htpasswd