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.math.BigInteger; 022import java.nio.charset.StandardCharsets; 023import java.security.GeneralSecurityException; 024import java.security.MessageDigest; 025import java.security.NoSuchAlgorithmException; 026import java.util.Collections; 027import java.util.HashSet; 028import java.util.LinkedList; 029import java.util.Set; 030 031/** Class to implement a username / salted message digest password BasicAuthenticator */ 032public class MessageDigestAuthenticator 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 Set<CacheKey> validCacheKeys; 041 private final LinkedList<CacheKey> invalidCacheKeys; 042 043 /** 044 * Constructor 045 * 046 * @param realm realm 047 * @param username username 048 * @param passwordHash passwordHash 049 * @param algorithm algorithm 050 * @param salt salt 051 * @throws NoSuchAlgorithmException NoSuchAlgorithmException 052 */ 053 public MessageDigestAuthenticator( 054 String realm, String username, String passwordHash, String algorithm, String salt) 055 throws GeneralSecurityException { 056 super(realm); 057 058 Precondition.notNullOrEmpty(username); 059 Precondition.notNullOrEmpty(passwordHash); 060 Precondition.notNullOrEmpty(algorithm); 061 Precondition.notNullOrEmpty(salt); 062 063 MessageDigest.getInstance(algorithm); 064 065 this.username = username; 066 this.passwordHash = passwordHash.toLowerCase().replace(":", ""); 067 this.algorithm = algorithm; 068 this.salt = salt; 069 this.validCacheKeys = Collections.synchronizedSet(new HashSet<>()); 070 this.invalidCacheKeys = new LinkedList<>(); 071 } 072 073 /** 074 * called for each incoming request to verify the given name and password in the context of this 075 * Authenticator's realm. Any caching of credentials must be done by the implementation of this 076 * method 077 * 078 * @param username the username from the request 079 * @param password the password from the request 080 * @return <code>true</code> if the credentials are valid, <code>false</code> otherwise. 081 */ 082 @Override 083 public boolean checkCredentials(String username, String password) { 084 if (username == null || password == null) { 085 return false; 086 } 087 088 CacheKey cacheKey = new CacheKey(username, password); 089 if (validCacheKeys.contains(cacheKey)) { 090 return true; 091 } else { 092 synchronized (invalidCacheKeys) { 093 if (invalidCacheKeys.contains(cacheKey)) { 094 return false; 095 } 096 } 097 } 098 099 boolean isValid = 100 this.username.equals(username) 101 && this.passwordHash.equals( 102 generatePasswordHash(algorithm, salt, password)); 103 104 if (isValid) { 105 validCacheKeys.add(cacheKey); 106 } else { 107 synchronized (invalidCacheKeys) { 108 invalidCacheKeys.add(cacheKey); 109 if (invalidCacheKeys.size() > MAXIMUM_INVALID_CACHE_KEY_ENTRIES) { 110 invalidCacheKeys.removeFirst(); 111 } 112 } 113 } 114 115 return isValid; 116 } 117 118 /** 119 * Method to generate a hash based on the configured message digest algorithm 120 * 121 * @param algorithm algorithm 122 * @param salt salt 123 * @param password password 124 * @return the hash 125 */ 126 private static String generatePasswordHash(String algorithm, String salt, String password) { 127 try { 128 MessageDigest digest = MessageDigest.getInstance(algorithm); 129 byte[] hash = digest.digest((salt + ":" + password).getBytes(StandardCharsets.UTF_8)); 130 BigInteger number = new BigInteger(1, hash); 131 return number.toString(16).toLowerCase(); 132 } catch (GeneralSecurityException e) { 133 throw new RuntimeException(e); 134 } 135 } 136}