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;
021
022import javax.crypto.SecretKeyFactory;
023import javax.crypto.spec.PBEKeySpec;
024import java.nio.charset.StandardCharsets;
025import java.security.GeneralSecurityException;
026import java.security.NoSuchAlgorithmException;
027import java.security.spec.InvalidKeySpecException;
028import java.util.Collections;
029import java.util.HashSet;
030import java.util.LinkedList;
031import java.util.Set;
032
033/**
034 * Class to implement a username / salted message digest password BasicAuthenticator
035 */
036public class PBKDF2Authenticator extends BasicAuthenticator {
037
038    private static final int MAXIMUM_INVALID_CACHE_KEY_ENTRIES = 16;
039
040    private final String username;
041    private final String passwordHash;
042    private final String algorithm;
043    private final String salt;
044    private final int iterations;
045    private final int keyLength;
046    private final Set<CacheKey> cacheKeys;
047    private final LinkedList<CacheKey> invalidCacheKeys;
048
049    /**
050     * Constructor
051     *
052     * @param realm realm
053     * @param username username
054     * @param passwordHash passwordHash
055     * @param algorithm algorithm
056     * @param salt salt
057     * @param iterations iterations
058     * @param keyLength keyLength
059     * @throws NoSuchAlgorithmException NoSuchAlgorithmException
060     */
061    public PBKDF2Authenticator(
062            String realm,
063            String username,
064            String passwordHash,
065            String algorithm,
066            String salt,
067            int iterations,
068            int keyLength)
069            throws GeneralSecurityException {
070        super(realm);
071
072        Precondition.notNullOrEmpty(username);
073        Precondition.notNullOrEmpty(passwordHash);
074        Precondition.notNullOrEmpty(algorithm);
075        Precondition.notNullOrEmpty(salt);
076        Precondition.IsGreaterThanOrEqualTo(1, iterations);
077        Precondition.IsGreaterThanOrEqualTo(1, keyLength);
078
079        SecretKeyFactory.getInstance(algorithm);
080
081        this.username = username;
082        this.passwordHash = passwordHash.toLowerCase().replace(":", "");
083        this.algorithm = algorithm;
084        this.salt = salt;
085        this.iterations = iterations;
086        this.keyLength = keyLength;
087        this.cacheKeys = Collections.synchronizedSet(new HashSet<>());
088        this.invalidCacheKeys = new LinkedList<>();
089    }
090
091    /**
092     * called for each incoming request to verify the
093     * given name and password in the context of this
094     * Authenticator's realm. Any caching of credentials
095     * must be done by the implementation of this method
096     *
097     * @param username the username from the request
098     * @param password the password from the request
099     * @return <code>true</code> if the credentials are valid,
100     * <code>false</code> otherwise.
101     */
102    @Override
103    public boolean checkCredentials(String username, String password) {
104        if (username == null || password == null) {
105            return false;
106        }
107
108        CacheKey cacheKey = new CacheKey(username, password);
109        if (cacheKeys.contains(cacheKey)) {
110            return true;
111        } else {
112            synchronized (invalidCacheKeys) {
113                if (invalidCacheKeys.contains(cacheKey)) {
114                    return false;
115                }
116            }
117        }
118
119        boolean isValid = this.username.equals(username)
120                && this.passwordHash.equals(generatePasswordHash(algorithm, salt, iterations, keyLength, password));
121        if (isValid) {
122            cacheKeys.add(cacheKey);
123        } else {
124            synchronized (invalidCacheKeys) {
125                invalidCacheKeys.add(cacheKey);
126                if (invalidCacheKeys.size() > MAXIMUM_INVALID_CACHE_KEY_ENTRIES) {
127                    invalidCacheKeys.removeFirst();
128                }
129            }
130        }
131
132        return isValid;
133    }
134
135    /**
136     * Method to generate a hash based on the configured secret key algorithm
137     *
138     * @param algorithm algorithm
139     * @param salt salt
140     * @param iterations iterations
141     * @param keyLength keyLength
142     * @param password password
143     * @return the hash
144     */
145    private static String generatePasswordHash(
146            String algorithm,
147            String salt,
148            int iterations,
149            int keyLength,
150            String password) {
151        try {
152            PBEKeySpec pbeKeySpec =
153                    new PBEKeySpec(
154                            password.toCharArray(),
155                            salt.getBytes(StandardCharsets.UTF_8),
156                            iterations,
157                            keyLength * 8);
158            SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm);
159            byte[] secretKeyBytes = secretKeyFactory.generateSecret(pbeKeySpec).getEncoded();
160            return HexString.toHex(secretKeyBytes);
161        } catch (GeneralSecurityException e) {
162            throw new RuntimeException(e);
163        }
164    }
165}