Skip to content

Commit

Permalink
Add file based password authenticator plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
rupamk authored and electrum committed Dec 6, 2019
1 parent 85891cb commit 93cdfa0
Show file tree
Hide file tree
Showing 14 changed files with 767 additions and 0 deletions.
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,7 @@
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 @@ -25,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

0 comments on commit 93cdfa0

Please sign in to comment.