From e111b06394aae128c81552e54020f14814d2d1dc Mon Sep 17 00:00:00 2001 From: imjalpreet Date: Tue, 8 Dec 2020 00:34:29 +0530 Subject: [PATCH] Add file based password authenticator plugin Cherry-pick of https://github.com/prestosql/presto/pull/1912 (https://github.com/prestosql/presto/pull/1912) Co-authored-by: David Phillips Co-authored-by: Rupam Kundu --- presto-password-authenticators/pom.xml | 12 ++ .../facebook/presto/password/Credential.java | 60 ++++++++ .../password/PasswordAuthenticatorPlugin.java | 3 + .../presto/password/file/EncryptionUtil.java | 144 ++++++++++++++++++ .../password/file/FileAuthenticator.java | 62 ++++++++ .../file/FileAuthenticatorFactory.java | 58 +++++++ .../presto/password/file/FileConfig.java | 73 +++++++++ .../file/HashedPasswordException.java | 32 ++++ .../password/file/HashingAlgorithm.java | 21 +++ .../presto/password/file/PasswordStore.java | 129 ++++++++++++++++ .../{ => ldap}/LdapAuthenticator.java | 2 +- .../{ => ldap}/LdapAuthenticatorFactory.java | 2 +- .../password/{ => ldap}/LdapConfig.java | 2 +- .../password/file/TestEncryptionUtil.java | 60 ++++++++ .../presto/password/file/TestFileConfig.java | 56 +++++++ .../password/file/TestPasswordStore.java | 74 +++++++++ .../password/{ => ldap}/TestLdapConfig.java | 2 +- 17 files changed, 788 insertions(+), 4 deletions(-) create mode 100644 presto-password-authenticators/src/main/java/com/facebook/presto/password/Credential.java create mode 100644 presto-password-authenticators/src/main/java/com/facebook/presto/password/file/EncryptionUtil.java create mode 100644 presto-password-authenticators/src/main/java/com/facebook/presto/password/file/FileAuthenticator.java create mode 100644 presto-password-authenticators/src/main/java/com/facebook/presto/password/file/FileAuthenticatorFactory.java create mode 100644 presto-password-authenticators/src/main/java/com/facebook/presto/password/file/FileConfig.java create mode 100644 presto-password-authenticators/src/main/java/com/facebook/presto/password/file/HashedPasswordException.java create mode 100644 presto-password-authenticators/src/main/java/com/facebook/presto/password/file/HashingAlgorithm.java create mode 100644 presto-password-authenticators/src/main/java/com/facebook/presto/password/file/PasswordStore.java rename presto-password-authenticators/src/main/java/com/facebook/presto/password/{ => ldap}/LdapAuthenticator.java (99%) rename presto-password-authenticators/src/main/java/com/facebook/presto/password/{ => ldap}/LdapAuthenticatorFactory.java (97%) rename presto-password-authenticators/src/main/java/com/facebook/presto/password/{ => ldap}/LdapConfig.java (98%) create mode 100644 presto-password-authenticators/src/test/java/com/facebook/presto/password/file/TestEncryptionUtil.java create mode 100644 presto-password-authenticators/src/test/java/com/facebook/presto/password/file/TestFileConfig.java create mode 100644 presto-password-authenticators/src/test/java/com/facebook/presto/password/file/TestPasswordStore.java rename presto-password-authenticators/src/test/java/com/facebook/presto/password/{ => ldap}/TestLdapConfig.java (98%) diff --git a/presto-password-authenticators/pom.xml b/presto-password-authenticators/pom.xml index 90fc0abddfed..4fcec2fa3d8d 100644 --- a/presto-password-authenticators/pom.xml +++ b/presto-password-authenticators/pom.xml @@ -57,6 +57,12 @@ validation-api + + at.favre.lib + bcrypt + 0.9.0 + + com.facebook.presto @@ -112,6 +118,12 @@ testing test + + + org.assertj + assertj-core + test + diff --git a/presto-password-authenticators/src/main/java/com/facebook/presto/password/Credential.java b/presto-password-authenticators/src/main/java/com/facebook/presto/password/Credential.java new file mode 100644 index 000000000000..77380f32d569 --- /dev/null +++ b/presto-password-authenticators/src/main/java/com/facebook/presto/password/Credential.java @@ -0,0 +1,60 @@ +/* + * 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 com.facebook.presto.password; + +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +public final class Credential +{ + private final String user; + private final String password; + + public Credential(String username, String password) + { + this.user = requireNonNull(username, "username is null"); + this.password = requireNonNull(password, "password is null"); + } + + public String getUser() + { + return user; + } + + public String getPassword() + { + return password; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + Credential o = (Credential) obj; + return Objects.equals(user, o.getUser()) && + Objects.equals(password, o.getPassword()); + } + + @Override + public int hashCode() + { + return Objects.hash(user, password); + } +} diff --git a/presto-password-authenticators/src/main/java/com/facebook/presto/password/PasswordAuthenticatorPlugin.java b/presto-password-authenticators/src/main/java/com/facebook/presto/password/PasswordAuthenticatorPlugin.java index 87e02bb4446e..84a3a8df6a14 100644 --- a/presto-password-authenticators/src/main/java/com/facebook/presto/password/PasswordAuthenticatorPlugin.java +++ b/presto-password-authenticators/src/main/java/com/facebook/presto/password/PasswordAuthenticatorPlugin.java @@ -13,6 +13,8 @@ */ package com.facebook.presto.password; +import com.facebook.presto.password.file.FileAuthenticatorFactory; +import com.facebook.presto.password.ldap.LdapAuthenticatorFactory; import com.facebook.presto.spi.Plugin; import com.facebook.presto.spi.security.PasswordAuthenticatorFactory; import com.google.common.collect.ImmutableList; @@ -25,6 +27,7 @@ public Iterable getPasswordAuthenticatorFactories( { return ImmutableList.builder() .add(new LdapAuthenticatorFactory()) + .add(new FileAuthenticatorFactory()) .build(); } } diff --git a/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/EncryptionUtil.java b/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/EncryptionUtil.java new file mode 100644 index 000000000000..2aa634992371 --- /dev/null +++ b/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/EncryptionUtil.java @@ -0,0 +1,144 @@ +/* + * 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 com.facebook.presto.password.file; + +import at.favre.lib.crypto.bcrypt.BCrypt; +import at.favre.lib.crypto.bcrypt.IllegalBCryptFormatException; +import com.google.common.base.Splitter; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.io.BaseEncoding.base16; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +public final class EncryptionUtil +{ + private static final int BCRYPT_MIN_COST = 8; + private static final int PBKDF2_MIN_ITERATIONS = 1000; + + private EncryptionUtil() {} + + public static int getBCryptCost(String password) + { + try { + return BCrypt.Version.VERSION_2A.parser.parse(password.getBytes(UTF_8)).cost; + } + catch (IllegalBCryptFormatException e) { + throw new HashedPasswordException("Invalid BCrypt password", e); + } + } + + public static int getPBKDF2Iterations(String password) + { + return PBKDF2Password.fromString(password).iterations(); + } + + public static boolean doesBCryptPasswordMatch(String inputPassword, String hashedPassword) + { + return BCrypt.verifyer().verify(inputPassword.toCharArray(), hashedPassword).verified; + } + + public static boolean doesPBKDF2PasswordMatch(String inputPassword, String hashedPassword) + { + PBKDF2Password password = PBKDF2Password.fromString(hashedPassword); + + try { + KeySpec spec = new PBEKeySpec(inputPassword.toCharArray(), password.salt(), password.iterations(), password.hash().length * 8); + SecretKeyFactory key = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + byte[] inputHash = key.generateSecret(spec).getEncoded(); + + if (password.hash().length != inputHash.length) { + throw new HashedPasswordException("PBKDF2 password input is malformed"); + } + return MessageDigest.isEqual(password.hash(), inputHash); + } + catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new HashedPasswordException("Invalid PBKDF2 password", e); + } + } + + public static HashingAlgorithm getHashingAlgorithm(String password) + { + if (password.startsWith("$2y")) { + if (getBCryptCost(password) < BCRYPT_MIN_COST) { + throw new HashedPasswordException("Minimum cost of BCrypt password must be " + BCRYPT_MIN_COST); + } + return HashingAlgorithm.BCRYPT; + } + + if (password.contains(":")) { + if (getPBKDF2Iterations(password) < PBKDF2_MIN_ITERATIONS) { + throw new HashedPasswordException("Minimum iterations of PBKDF2 password must be " + PBKDF2_MIN_ITERATIONS); + } + return HashingAlgorithm.PBKDF2; + } + + throw new HashedPasswordException("Password hashing algorithm cannot be determined"); + } + + private static class PBKDF2Password + { + private final int iterations; + private final byte[] salt; + private final byte[] hash; + + private PBKDF2Password(int iterations, byte[] salt, byte[] hash) + { + this.iterations = iterations; + this.salt = requireNonNull(salt, "salt is null"); + this.hash = requireNonNull(hash, "hash is null"); + } + + public int iterations() + { + return iterations; + } + + public byte[] salt() + { + return salt; + } + + public byte[] hash() + { + return hash; + } + + public static PBKDF2Password fromString(String password) + { + try { + List parts = Splitter.on(":").splitToList(password); + checkArgument(parts.size() == 3, "wrong part count"); + + int iterations = Integer.parseInt(parts.get(0)); + byte[] salt = base16().lowerCase().decode(parts.get(1)); + byte[] hash = base16().lowerCase().decode(parts.get(2)); + + return new PBKDF2Password(iterations, salt, hash); + } + catch (IllegalArgumentException e) { + throw new HashedPasswordException("Invalid PBKDF2 password"); + } + } + } +} diff --git a/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/FileAuthenticator.java b/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/FileAuthenticator.java new file mode 100644 index 000000000000..0ecb20759c01 --- /dev/null +++ b/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/FileAuthenticator.java @@ -0,0 +1,62 @@ +/* + * 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 com.facebook.presto.password.file; + +import com.facebook.airlift.http.server.BasicPrincipal; +import com.facebook.airlift.log.Logger; +import com.facebook.presto.spi.security.AccessDeniedException; +import com.facebook.presto.spi.security.PasswordAuthenticator; + +import javax.inject.Inject; + +import java.io.File; +import java.io.FileNotFoundException; +import java.security.Principal; +import java.util.function.Supplier; + +import static com.google.common.base.Suppliers.memoizeWithExpiration; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class FileAuthenticator + implements PasswordAuthenticator +{ + private static final Logger log = Logger.get(FileAuthenticator.class); + private final Supplier passwordStoreSupplier; + + @Inject + public FileAuthenticator(FileConfig config) throws FileNotFoundException + { + File file = config.getPasswordFile(); + if (!file.exists()) { + log.error("File %s does not exist", file.getAbsolutePath()); + throw new FileNotFoundException("File " + file.getAbsolutePath() + " does not exist"); + } + int cacheMaxSize = config.getAuthTokenCacheMaxSize(); + + passwordStoreSupplier = memoizeWithExpiration( + () -> new PasswordStore(file, cacheMaxSize), + config.getRefreshPeriod().toMillis(), + MILLISECONDS); + } + + @Override + public Principal createAuthenticatedPrincipal(String user, String password) + { + if (!passwordStoreSupplier.get().authenticate(user, password)) { + throw new AccessDeniedException("Invalid credentials"); + } + + return new BasicPrincipal(user); + } +} diff --git a/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/FileAuthenticatorFactory.java b/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/FileAuthenticatorFactory.java new file mode 100644 index 000000000000..004c68483bb0 --- /dev/null +++ b/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/FileAuthenticatorFactory.java @@ -0,0 +1,58 @@ +/* + * 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 com.facebook.presto.password.file; + +import com.facebook.airlift.bootstrap.Bootstrap; +import com.facebook.presto.spi.security.PasswordAuthenticator; +import com.facebook.presto.spi.security.PasswordAuthenticatorFactory; +import com.google.inject.Injector; +import com.google.inject.Scopes; + +import java.util.Map; + +import static com.facebook.airlift.configuration.ConfigBinder.configBinder; +import static com.google.common.base.Throwables.throwIfUnchecked; + +public class FileAuthenticatorFactory + implements PasswordAuthenticatorFactory +{ + @Override + public String getName() + { + return "file"; + } + + @Override + public PasswordAuthenticator create(Map config) + { + try { + Bootstrap app = new Bootstrap( + binder -> { + configBinder(binder).bindConfig(FileConfig.class); + binder.bind(FileAuthenticator.class).in(Scopes.SINGLETON); + }); + + Injector injector = app + .doNotInitializeLogging() + .setRequiredConfigurationProperties(config) + .initialize(); + + return injector.getInstance(FileAuthenticator.class); + } + catch (Exception e) { + throwIfUnchecked(e); + throw new RuntimeException(e); + } + } +} diff --git a/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/FileConfig.java b/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/FileConfig.java new file mode 100644 index 000000000000..95a9b4b5ec08 --- /dev/null +++ b/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/FileConfig.java @@ -0,0 +1,73 @@ +/* + * 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 com.facebook.presto.password.file; + +import com.facebook.airlift.configuration.Config; +import com.facebook.airlift.configuration.ConfigDescription; +import io.airlift.units.Duration; +import io.airlift.units.MinDuration; + +import javax.validation.constraints.NotNull; + +import java.io.File; + +import static java.util.concurrent.TimeUnit.SECONDS; + +public class FileConfig +{ + private File passwordFile; + private Duration refreshPeriod = new Duration(5, SECONDS); + private int authTokenCacheMaxSize = 1000; + + @NotNull + public File getPasswordFile() + { + return passwordFile; + } + + @Config("file.password-file") + @ConfigDescription("Location of the file that provides user names and passwords") + public FileConfig setPasswordFile(File passwordFile) + { + this.passwordFile = passwordFile; + return this; + } + + @MinDuration("1ms") + public Duration getRefreshPeriod() + { + return refreshPeriod; + } + + @Config("file.refresh-period") + @ConfigDescription("How often to reload the password file") + public FileConfig setRefreshPeriod(Duration refreshPeriod) + { + this.refreshPeriod = refreshPeriod; + return this; + } + + public int getAuthTokenCacheMaxSize() + { + return authTokenCacheMaxSize; + } + + @Config("file.auth-token-cache.max-size") + @ConfigDescription("Max number of cached authenticated passwords") + public FileConfig setAuthTokenCacheMaxSize(int maxSize) + { + this.authTokenCacheMaxSize = maxSize; + return this; + } +} diff --git a/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/HashedPasswordException.java b/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/HashedPasswordException.java new file mode 100644 index 000000000000..e492ef5b02b0 --- /dev/null +++ b/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/HashedPasswordException.java @@ -0,0 +1,32 @@ +/* + * 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 com.facebook.presto.password.file; + +import com.facebook.presto.spi.PrestoException; + +import static com.facebook.presto.spi.StandardErrorCode.CONFIGURATION_INVALID; + +public class HashedPasswordException + extends PrestoException +{ + public HashedPasswordException(String message) + { + this(message, null); + } + + public HashedPasswordException(String message, Throwable cause) + { + super(CONFIGURATION_INVALID, message, cause); + } +} diff --git a/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/HashingAlgorithm.java b/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/HashingAlgorithm.java new file mode 100644 index 000000000000..528f5ece5686 --- /dev/null +++ b/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/HashingAlgorithm.java @@ -0,0 +1,21 @@ +/* + * 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 com.facebook.presto.password.file; + +public enum HashingAlgorithm +{ + BCRYPT, + PBKDF2, + /**/; +} diff --git a/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/PasswordStore.java b/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/PasswordStore.java new file mode 100644 index 000000000000..ba5ed8345f78 --- /dev/null +++ b/presto-password-authenticators/src/main/java/com/facebook/presto/password/file/PasswordStore.java @@ -0,0 +1,129 @@ +/* + * 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 com.facebook.presto.password.file; + +import com.facebook.presto.password.Credential; +import com.facebook.presto.spi.PrestoException; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Splitter; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableMap; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.password.file.EncryptionUtil.doesBCryptPasswordMatch; +import static com.facebook.presto.password.file.EncryptionUtil.doesPBKDF2PasswordMatch; +import static com.facebook.presto.password.file.EncryptionUtil.getHashingAlgorithm; +import static com.facebook.presto.spi.StandardErrorCode.CONFIGURATION_INVALID; +import static com.facebook.presto.spi.StandardErrorCode.CONFIGURATION_UNAVAILABLE; +import static java.lang.String.format; + +public class PasswordStore +{ + private static final Splitter LINE_SPLITTER = Splitter.on(":").limit(2); + + private final Map credentials; + private final LoadingCache cache; + + public PasswordStore(File file, int cacheMaxSize) + { + this(readPasswordFile(file), cacheMaxSize); + } + + @VisibleForTesting + public PasswordStore(List lines, int cacheMaxSize) + { + credentials = loadPasswordFile(lines); + cache = CacheBuilder.newBuilder() + .maximumSize(cacheMaxSize) + .build(CacheLoader.from(this::matches)); + } + + public boolean authenticate(String user, String password) + { + return cache.getUnchecked(new Credential(user, password)); + } + + private boolean matches(Credential credential) + { + HashedPassword hashed = credentials.get(credential.getUser()); + return (hashed != null) && hashed.matches(credential.getPassword()); + } + + private static Map loadPasswordFile(List lines) + { + Map users = new HashMap<>(); + for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) { + String line = lines.get(lineNumber - 1).trim(); + if (line.isEmpty()) { + continue; + } + + List parts = LINE_SPLITTER.splitToList(line); + if (parts.size() != 2) { + throw invalidFile(lineNumber, "Expected two parts for user and password", null); + } + String user = parts.get(0); + String password = parts.get(1); + + try { + if (users.put(user, getHashedPassword(password)) != null) { + throw invalidFile(lineNumber, "Duplicate user: " + user, null); + } + } + catch (HashedPasswordException e) { + throw invalidFile(lineNumber, e.getMessage(), e); + } + } + return ImmutableMap.copyOf(users); + } + + private static RuntimeException invalidFile(int lineNumber, String message, Throwable cause) + { + return new PrestoException(CONFIGURATION_INVALID, format("Error in password file line %s: %s", lineNumber, message), cause); + } + + private static List readPasswordFile(File file) + { + try { + return Files.readAllLines(file.toPath()); + } + catch (IOException e) { + throw new PrestoException(CONFIGURATION_UNAVAILABLE, "Failed to read password file: " + file, e); + } + } + + private static HashedPassword getHashedPassword(String hashedPassword) + { + switch (getHashingAlgorithm(hashedPassword)) { + case BCRYPT: + return password -> doesBCryptPasswordMatch(password, hashedPassword); + case PBKDF2: + return password -> doesPBKDF2PasswordMatch(password, hashedPassword); + } + throw new HashedPasswordException("Hashing algorithm of password cannot be determined"); + } + + public interface HashedPassword + { + boolean matches(String password); + } +} diff --git a/presto-password-authenticators/src/main/java/com/facebook/presto/password/LdapAuthenticator.java b/presto-password-authenticators/src/main/java/com/facebook/presto/password/ldap/LdapAuthenticator.java similarity index 99% rename from presto-password-authenticators/src/main/java/com/facebook/presto/password/LdapAuthenticator.java rename to presto-password-authenticators/src/main/java/com/facebook/presto/password/ldap/LdapAuthenticator.java index 6df9460ed208..8cad20a4aa75 100644 --- a/presto-password-authenticators/src/main/java/com/facebook/presto/password/LdapAuthenticator.java +++ b/presto-password-authenticators/src/main/java/com/facebook/presto/password/ldap/LdapAuthenticator.java @@ -11,7 +11,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.facebook.presto.password; +package com.facebook.presto.password.ldap; import com.facebook.airlift.http.server.BasicPrincipal; import com.facebook.airlift.log.Logger; diff --git a/presto-password-authenticators/src/main/java/com/facebook/presto/password/LdapAuthenticatorFactory.java b/presto-password-authenticators/src/main/java/com/facebook/presto/password/ldap/LdapAuthenticatorFactory.java similarity index 97% rename from presto-password-authenticators/src/main/java/com/facebook/presto/password/LdapAuthenticatorFactory.java rename to presto-password-authenticators/src/main/java/com/facebook/presto/password/ldap/LdapAuthenticatorFactory.java index 05e5a9d939f6..c9cee1b86520 100644 --- a/presto-password-authenticators/src/main/java/com/facebook/presto/password/LdapAuthenticatorFactory.java +++ b/presto-password-authenticators/src/main/java/com/facebook/presto/password/ldap/LdapAuthenticatorFactory.java @@ -11,7 +11,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.facebook.presto.password; +package com.facebook.presto.password.ldap; import com.facebook.airlift.bootstrap.Bootstrap; import com.facebook.presto.spi.security.PasswordAuthenticator; diff --git a/presto-password-authenticators/src/main/java/com/facebook/presto/password/LdapConfig.java b/presto-password-authenticators/src/main/java/com/facebook/presto/password/ldap/LdapConfig.java similarity index 98% rename from presto-password-authenticators/src/main/java/com/facebook/presto/password/LdapConfig.java rename to presto-password-authenticators/src/main/java/com/facebook/presto/password/ldap/LdapConfig.java index 379ad8944257..e0e8ddf434a4 100644 --- a/presto-password-authenticators/src/main/java/com/facebook/presto/password/LdapConfig.java +++ b/presto-password-authenticators/src/main/java/com/facebook/presto/password/ldap/LdapConfig.java @@ -11,7 +11,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.facebook.presto.password; +package com.facebook.presto.password.ldap; import com.facebook.airlift.configuration.Config; import com.facebook.airlift.configuration.ConfigDescription; diff --git a/presto-password-authenticators/src/test/java/com/facebook/presto/password/file/TestEncryptionUtil.java b/presto-password-authenticators/src/test/java/com/facebook/presto/password/file/TestEncryptionUtil.java new file mode 100644 index 000000000000..4f754945bcd4 --- /dev/null +++ b/presto-password-authenticators/src/test/java/com/facebook/presto/password/file/TestEncryptionUtil.java @@ -0,0 +1,60 @@ +/* + * 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 com.facebook.presto.password.file; + +import org.testng.annotations.Test; + +import static com.facebook.presto.password.file.EncryptionUtil.getHashingAlgorithm; +import static com.facebook.presto.password.file.HashingAlgorithm.BCRYPT; +import static com.facebook.presto.password.file.HashingAlgorithm.PBKDF2; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.testng.Assert.assertEquals; + +public class TestEncryptionUtil +{ + // check whether the correct hashing algorithm can be identified + @Test + public void testHashingAlgorithmBCrypt() + { + String password = "$2y$10$BqTb8hScP5DfcpmHo5PeyugxHz5Ky/qf3wrpD7SNm8sWuA3VlGqsa"; + assertEquals(getHashingAlgorithm(password), BCRYPT); + } + + @Test + public void testHashingAlgorithmPBKDF2() + { + String password = "1000:5b4240333032306164:f38d165fce8ce42f59d366139ef5d9e1ca1247f0e06e503ee1a611dd9ec40876bb5edb8409f5abe5504aab6628e70cfb3d3a18e99d70357d295002c3d0a308a0"; + assertEquals(getHashingAlgorithm(password), PBKDF2); + } + + @Test + public void testMinBCryptCost() + { + // BCrypt password created with cost of 7 --> "htpasswd -n -B -C 7 test" + String password = "$2y$07$XxMSjoWesbX9s9LCD5Kp1OaFD/bcLUq0zoRCTsTNwjF6N/nwHVCVm"; + assertThatThrownBy(() -> getHashingAlgorithm(password)) + .isInstanceOf(HashedPasswordException.class) + .hasMessage("Minimum cost of BCrypt password must be 8"); + } + + @Test + public void testInvalidPasswordFormatPBKDF2() + { + // PBKDF2 password with iteration count of 100 + String password = "100:5b4240333032306164:f38d165fce8ce42f59d366139ef5d9e1ca1247f0e06e503ee1a611dd9ec40876bb5edb8409f5abe5504aab6628e70cfb3d3a18e99d70357d295002c3d0a308a0"; + assertThatThrownBy(() -> getHashingAlgorithm(password)) + .isInstanceOf(HashedPasswordException.class) + .hasMessage("Minimum iterations of PBKDF2 password must be 1000"); + } +} diff --git a/presto-password-authenticators/src/test/java/com/facebook/presto/password/file/TestFileConfig.java b/presto-password-authenticators/src/test/java/com/facebook/presto/password/file/TestFileConfig.java new file mode 100644 index 000000000000..20c63bd07c88 --- /dev/null +++ b/presto-password-authenticators/src/test/java/com/facebook/presto/password/file/TestFileConfig.java @@ -0,0 +1,56 @@ + +/* + * 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 com.facebook.presto.password.file; + +import com.google.common.collect.ImmutableMap; +import io.airlift.units.Duration; +import org.testng.annotations.Test; + +import java.io.File; +import java.util.Map; + +import static com.facebook.airlift.configuration.testing.ConfigAssertions.assertFullMapping; +import static com.facebook.airlift.configuration.testing.ConfigAssertions.assertRecordedDefaults; +import static com.facebook.airlift.configuration.testing.ConfigAssertions.recordDefaults; +import static java.util.concurrent.TimeUnit.SECONDS; + +public class TestFileConfig +{ + @Test + public void testDefault() + { + assertRecordedDefaults(recordDefaults(FileConfig.class) + .setPasswordFile(null) + .setRefreshPeriod(new Duration(5, SECONDS)) + .setAuthTokenCacheMaxSize(1000)); + } + + @Test + public void testExplicitConfig() + { + Map properties = new ImmutableMap.Builder() + .put("file.password-file", "/test/password") + .put("file.refresh-period", "42s") + .put("file.auth-token-cache.max-size", "1234") + .build(); + + FileConfig expected = new FileConfig() + .setPasswordFile(new File("/test/password")) + .setRefreshPeriod(new Duration(42, SECONDS)) + .setAuthTokenCacheMaxSize(1234); + + assertFullMapping(properties, expected); + } +} diff --git a/presto-password-authenticators/src/test/java/com/facebook/presto/password/file/TestPasswordStore.java b/presto-password-authenticators/src/test/java/com/facebook/presto/password/file/TestPasswordStore.java new file mode 100644 index 000000000000..f9c4e51046ed --- /dev/null +++ b/presto-password-authenticators/src/test/java/com/facebook/presto/password/file/TestPasswordStore.java @@ -0,0 +1,74 @@ +/* + * 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 com.facebook.presto.password.file; + +import com.google.common.collect.ImmutableList; +import org.testng.annotations.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class TestPasswordStore +{ + private static final String BCRYPT_PASSWORD = "$2y$10$BqTb8hScP5DfcpmHo5PeyugxHz5Ky/qf3wrpD7SNm8sWuA3VlGqsa"; + private static final String PBKDF2_PASSWORD = "1000:5b4240333032306164:f38d165fce8ce42f59d366139ef5d9e1ca1247f0e06e503ee1a611dd9ec40876bb5edb8409f5abe5504aab6628e70cfb3d3a18e99d70357d295002c3d0a308a0"; + + @Test + public void testAuthenticate() + { + PasswordStore store = createStore("userbcrypt:" + BCRYPT_PASSWORD, "userpbkdf2:" + PBKDF2_PASSWORD); + + assertTrue(store.authenticate("userbcrypt", "user123")); + assertFalse(store.authenticate("userbcrypt", "user999")); + assertFalse(store.authenticate("userbcrypt", "password")); + + assertTrue(store.authenticate("userpbkdf2", "password")); + assertFalse(store.authenticate("userpbkdf2", "password999")); + assertFalse(store.authenticate("userpbkdf2", "user123")); + + assertFalse(store.authenticate("baduser", "user123")); + assertFalse(store.authenticate("baduser", "password")); + } + + @Test + public void testEmptyFile() + { + createStore(); + } + + @Test + public void testInvalidFile() + { + assertThatThrownBy(() -> createStore("", "junk")) + .hasMessage("Error in password file line 2: Expected two parts for user and password"); + + assertThatThrownBy(() -> createStore("abc:" + BCRYPT_PASSWORD, "xyz:" + BCRYPT_PASSWORD, "abc:" + PBKDF2_PASSWORD)) + .hasMessage("Error in password file line 3: Duplicate user: abc"); + + assertThatThrownBy(() -> createStore("x:x")) + .hasMessage("Error in password file line 1: Password hashing algorithm cannot be determined"); + + assertThatThrownBy(() -> createStore("x:$2y$xxx")) + .hasMessage("Error in password file line 1: Invalid BCrypt password"); + + assertThatThrownBy(() -> createStore("x:x:x")) + .hasMessage("Error in password file line 1: Invalid PBKDF2 password"); + } + + private static PasswordStore createStore(String... lines) + { + return new PasswordStore(ImmutableList.copyOf(lines), 1000); + } +} diff --git a/presto-password-authenticators/src/test/java/com/facebook/presto/password/TestLdapConfig.java b/presto-password-authenticators/src/test/java/com/facebook/presto/password/ldap/TestLdapConfig.java similarity index 98% rename from presto-password-authenticators/src/test/java/com/facebook/presto/password/TestLdapConfig.java rename to presto-password-authenticators/src/test/java/com/facebook/presto/password/ldap/TestLdapConfig.java index 2651f1d28328..b73d9cbc279b 100644 --- a/presto-password-authenticators/src/test/java/com/facebook/presto/password/TestLdapConfig.java +++ b/presto-password-authenticators/src/test/java/com/facebook/presto/password/ldap/TestLdapConfig.java @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.facebook.presto.password; +package com.facebook.presto.password.ldap; import com.google.common.collect.ImmutableMap; import io.airlift.units.Duration;