Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add file based password authenticator plugin #1912

Merged
merged 2 commits into from
Dec 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions presto-main/etc/config.properties
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ query.min-expire-age=30m

plugin.bundles=\
../presto-resource-group-managers/pom.xml,\
../presto-password-authenticators/pom.xml, \
../presto-iceberg/pom.xml,\
../presto-blackhole/pom.xml,\
../presto-memory/pom.xml,\
Expand Down
12 changes: 12 additions & 0 deletions presto-password-authenticators/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@
<artifactId>validation-api</artifactId>
</dependency>

<dependency>
<groupId>at.favre.lib</groupId>
<artifactId>bcrypt</artifactId>
<version>0.9.0</version>
</dependency>

<!-- Presto SPI -->
<dependency>
<groupId>io.prestosql</groupId>
Expand Down Expand Up @@ -94,6 +100,12 @@
<artifactId>testing</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
package io.prestosql.plugin.password;

import com.google.common.collect.ImmutableList;
import io.prestosql.plugin.password.file.FileAuthenticatorFactory;
import io.prestosql.plugin.password.ldap.LdapAuthenticatorFactory;
import io.prestosql.spi.Plugin;
import io.prestosql.spi.security.PasswordAuthenticatorFactory;

Expand All @@ -24,6 +26,7 @@ public class PasswordAuthenticatorPlugin
public Iterable<PasswordAuthenticatorFactory> getPasswordAuthenticatorFactories()
{
return ImmutableList.<PasswordAuthenticatorFactory>builder()
.add(new FileAuthenticatorFactory())
.add(new LdapAuthenticatorFactory())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 io.prestosql.plugin.password.file;

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);
}
}
Original file line number Diff line number Diff line change
@@ -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 io.prestosql.plugin.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<String> 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");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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 io.prestosql.plugin.password.file;

import io.prestosql.spi.security.AccessDeniedException;
import io.prestosql.spi.security.BasicPrincipal;
import io.prestosql.spi.security.PasswordAuthenticator;

import javax.inject.Inject;

import java.io.File;
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 final Supplier<PasswordStore> passwordStoreSupplier;

@Inject
public FileAuthenticator(FileConfig config)
{
File file = config.getPasswordFile();
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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 io.prestosql.plugin.password.file;

import com.google.inject.Injector;
import com.google.inject.Scopes;
import io.airlift.bootstrap.Bootstrap;
import io.prestosql.spi.security.PasswordAuthenticator;
import io.prestosql.spi.security.PasswordAuthenticatorFactory;

import java.util.Map;

import static io.airlift.configuration.ConfigBinder.configBinder;

public class FileAuthenticatorFactory
implements PasswordAuthenticatorFactory
{
@Override
public String getName()
{
return "file";
}

@Override
public PasswordAuthenticator create(Map<String, String> config)
{
Bootstrap app = new Bootstrap(
binder -> {
configBinder(binder).bindConfig(FileConfig.class);
binder.bind(FileAuthenticator.class).in(Scopes.SINGLETON);
});

Injector injector = app
.strictConfig()
.doNotInitializeLogging()
.setRequiredConfigurationProperties(config)
.initialize();

return injector.getInstance(FileAuthenticator.class);
}
}
Loading