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}