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 #15504

Merged
merged 2 commits into from
Apr 9, 2021
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-docs/src/main/sphinx/security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Security
security/server
security/cli
security/ldap
security/password-file
security/tls
security/built-in-system-access-control
security/internal-communication
Expand Down
81 changes: 81 additions & 0 deletions presto-docs/src/main/sphinx/security/password-file.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
============================
Password File Authentication
============================

Presto can be configured to enable frontend password authentication over
HTTPS for clients, such as the CLI, or the JDBC and ODBC drivers. The
username and password are validated against usernames and passwords stored
in a file.

Password file authentication is very similar to :doc:`ldap`. Please see
the LDAP documentation for generic instructions on configuring the server
and clients to use TLS and authenticate with a username and password.

Password Authenticator Configuration
------------------------------------

Enable password file authentication by creating an
``etc/password-authenticator.properties`` file on the coordinator:

.. code-block:: none

password-authenticator.name=file
file.password-file=/path/to/password.db

The following configuration properties are available:

==================================== ==============================================
Property Description
==================================== ==============================================
``file.password-file`` Path of the password file.

``file.refresh-period`` How often to reload the password file.
Defaults to ``5s``.

``file.auth-token-cache.max-size`` Max number of cached authenticated passwords.
Defaults to ``1000``.
==================================== ==============================================

Password Files
--------------

File Format
^^^^^^^^^^^

The password file contains a list of usernames and passwords, one per line,
separated by a colon. Passwords must be securely hashed using bcrypt or PBKDF2.

bcrypt passwords start with ``$2y$`` and must use a minimum cost of ``8``:

.. code-block:: none

test:$2y$10$BqTb8hScP5DfcpmHo5PeyugxHz5Ky/qf3wrpD7SNm8sWuA3VlGqsa

PBKDF2 passwords are composed of the iteration count, followed by the
hex encoded salt and hash:

.. code-block:: none

test:1000:5b4240333032306164:f38d165fce8ce42f59d366139ef5d9e1ca1247f0e06e503ee1a611dd9ec40876bb5edb8409f5abe5504aab6628e70cfb3d3a18e99d70357d295002c3d0a308a0

Creating a Password File
^^^^^^^^^^^^^^^^^^^^^^^^

Password files utilizing the bcrypt format can be created using the
`htpasswd <https://httpd.apache.org/docs/current/programs/htpasswd.html>`_
utility from the `Apache HTTP Server <https://httpd.apache.org/>`_.
The cost must be specified, as Presto enforces a higher minimum cost
than the default.

Create an empty password file to get started:

.. code-block:: none

touch password.db

Add or update the password for the user ``test``:

.. code-block:: none

htpasswd -B -C 10 password.db test

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");
}
}
}
}
Loading