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}