Skip to content

Commit

Permalink
Add file based password authenticator plugin
Browse files Browse the repository at this point in the history
Cherry-pick of trinodb/trino#1912 (trinodb/trino#1912)

Co-authored-by: David Phillips <[email protected]>
Co-authored-by: Rupam Kundu <[email protected]>
  • Loading branch information
3 people authored and aweisberg committed Apr 9, 2021
1 parent b5b6ccd commit e111b06
Show file tree
Hide file tree
Showing 17 changed files with 788 additions and 4 deletions.
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>com.facebook.presto</groupId>
Expand Down Expand Up @@ -112,6 +118,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
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,6 +27,7 @@ public Iterable<PasswordAuthenticatorFactory> getPasswordAuthenticatorFactories(
{
return ImmutableList.<PasswordAuthenticatorFactory>builder()
.add(new LdapAuthenticatorFactory())
.add(new FileAuthenticatorFactory())
.build();
}
}
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 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<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,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<PasswordStore> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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);
}
}
}
Loading

0 comments on commit e111b06

Please sign in to comment.