001/*
002 * Copyright (C) 2022-2023 The Prometheus jmx_exporter Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package io.prometheus.jmx.common.http.authenticator;
018
019import com.sun.net.httpserver.BasicAuthenticator;
020import io.prometheus.jmx.common.util.Precondition;
021import java.nio.charset.StandardCharsets;
022import java.security.GeneralSecurityException;
023import java.security.NoSuchAlgorithmException;
024import java.util.Collections;
025import java.util.HashSet;
026import java.util.LinkedList;
027import java.util.Set;
028import javax.crypto.SecretKeyFactory;
029import javax.crypto.spec.PBEKeySpec;
030
031/** Class to implement a username / salted message digest password BasicAuthenticator */
032public class PBKDF2Authenticator extends BasicAuthenticator {
033
034    private static final int MAXIMUM_INVALID_CACHE_KEY_ENTRIES = 16;
035
036    private final String username;
037    private final String passwordHash;
038    private final String algorithm;
039    private final String salt;
040    private final int iterations;
041    private final int keyLength;
042    private final Set<CacheKey> cacheKeys;
043    private final LinkedList<CacheKey> invalidCacheKeys;
044
045    /**
046     * Constructor
047     *
048     * @param realm realm
049     * @param username username
050     * @param passwordHash passwordHash
051     * @param algorithm algorithm
052     * @param salt salt
053     * @param iterations iterations
054     * @param keyLength keyLength
055     * @throws NoSuchAlgorithmException NoSuchAlgorithmException
056     */
057    public PBKDF2Authenticator(
058            String realm,
059            String username,
060            String passwordHash,
061            String algorithm,
062            String salt,
063            int iterations,
064            int keyLength)
065            throws GeneralSecurityException {
066        super(realm);
067
068        Precondition.notNullOrEmpty(username);
069        Precondition.notNullOrEmpty(passwordHash);
070        Precondition.notNullOrEmpty(algorithm);
071        Precondition.notNullOrEmpty(salt);
072        Precondition.isGreaterThanOrEqualTo(iterations, 1);
073        Precondition.isGreaterThanOrEqualTo(keyLength, 1);
074
075        SecretKeyFactory.getInstance(algorithm);
076
077        this.username = username;
078        this.passwordHash = passwordHash.toLowerCase().replace(":", "");
079        this.algorithm = algorithm;
080        this.salt = salt;
081        this.iterations = iterations;
082        this.keyLength = keyLength;
083        this.cacheKeys = Collections.synchronizedSet(new HashSet<>());
084        this.invalidCacheKeys = new LinkedList<>();
085    }
086
087    /**
088     * called for each incoming request to verify the given name and password in the context of this
089     * Authenticator's realm. Any caching of credentials must be done by the implementation of this
090     * method
091     *
092     * @param username the username from the request
093     * @param password the password from the request
094     * @return <code>true</code> if the credentials are valid, <code>false</code> otherwise.
095     */
096    @Override
097    public boolean checkCredentials(String username, String password) {
098        if (username == null || password == null) {
099            return false;
100        }
101
102        CacheKey cacheKey = new CacheKey(username, password);
103        if (cacheKeys.contains(cacheKey)) {
104            return true;
105        } else {
106            synchronized (invalidCacheKeys) {
107                if (invalidCacheKeys.contains(cacheKey)) {
108                    return false;
109                }
110            }
111        }
112
113        boolean isValid =
114                this.username.equals(username)
115                        && this.passwordHash.equals(
116                                generatePasswordHash(
117                                        algorithm, salt, iterations, keyLength, password));
118        if (isValid) {
119            cacheKeys.add(cacheKey);
120        } else {
121            synchronized (invalidCacheKeys) {
122                invalidCacheKeys.add(cacheKey);
123                if (invalidCacheKeys.size() > MAXIMUM_INVALID_CACHE_KEY_ENTRIES) {
124                    invalidCacheKeys.removeFirst();
125                }
126            }
127        }
128
129        return isValid;
130    }
131
132    /**
133     * Method to generate a hash based on the configured secret key algorithm
134     *
135     * @param algorithm algorithm
136     * @param salt salt
137     * @param iterations iterations
138     * @param keyLength keyLength
139     * @param password password
140     * @return the hash
141     */
142    private static String generatePasswordHash(
143            String algorithm, String salt, int iterations, int keyLength, String password) {
144        try {
145            PBEKeySpec pbeKeySpec =
146                    new PBEKeySpec(
147                            password.toCharArray(),
148                            salt.getBytes(StandardCharsets.UTF_8),
149                            iterations,
150                            keyLength * 8);
151            SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm);
152            byte[] secretKeyBytes = secretKeyFactory.generateSecret(pbeKeySpec).getEncoded();
153            return HexString.toHex(secretKeyBytes);
154        } catch (GeneralSecurityException e) {
155            throw new RuntimeException(e);
156        }
157    }
158}