001/* 002 * Copyright (C) 2022-present 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; 026 027/** Class to implement a username / salted message digest password BasicAuthenticator */ 028public class MessageDigestAuthenticator extends BasicAuthenticator { 029 030 private static final int MAXIMUM_VALID_CACHE_SIZE_BYTES = 1000000; // 1 MB 031 private static final int MAXIMUM_INVALID_CACHE_SIZE_BYTES = 10000000; // 10 MB 032 033 private final String username; 034 private final String passwordHash; 035 private final String algorithm; 036 private final String salt; 037 private final CredentialsCache validCredentialsCache; 038 private final CredentialsCache invalidCredentialsCache; 039 040 /** 041 * Constructor 042 * 043 * @param realm realm 044 * @param username username 045 * @param passwordHash passwordHash 046 * @param algorithm algorithm 047 * @param salt salt 048 * @throws NoSuchAlgorithmException NoSuchAlgorithmException 049 */ 050 public MessageDigestAuthenticator( 051 String realm, String username, String passwordHash, String algorithm, String salt) 052 throws GeneralSecurityException { 053 super(realm); 054 055 Precondition.notNullOrEmpty(username); 056 Precondition.notNullOrEmpty(passwordHash); 057 Precondition.notNullOrEmpty(algorithm); 058 Precondition.notNullOrEmpty(salt); 059 060 MessageDigest.getInstance(algorithm); 061 062 this.username = username; 063 this.passwordHash = passwordHash.toLowerCase().replace(":", ""); 064 this.algorithm = algorithm; 065 this.salt = salt; 066 this.validCredentialsCache = new CredentialsCache(MAXIMUM_VALID_CACHE_SIZE_BYTES); 067 this.invalidCredentialsCache = new CredentialsCache(MAXIMUM_INVALID_CACHE_SIZE_BYTES); 068 } 069 070 /** 071 * called for each incoming request to verify the given name and password in the context of this 072 * Authenticator's realm. Any caching of credentials must be done by the implementation of this 073 * method 074 * 075 * @param username the username from the request 076 * @param password the password from the request 077 * @return <code>true</code> if the credentials are valid, <code>false</code> otherwise. 078 */ 079 @Override 080 public boolean checkCredentials(String username, String password) { 081 if (username == null || password == null) { 082 return false; 083 } 084 085 Credentials credentials = new Credentials(username, password); 086 if (validCredentialsCache.contains(credentials)) { 087 return true; 088 } else if (invalidCredentialsCache.contains(credentials)) { 089 return false; 090 } 091 092 boolean isValid = 093 this.username.equals(username) 094 && this.passwordHash.equals( 095 generatePasswordHash(algorithm, salt, password)); 096 097 if (isValid) { 098 validCredentialsCache.add(credentials); 099 } else { 100 invalidCredentialsCache.add(credentials); 101 } 102 103 return isValid; 104 } 105 106 /** 107 * Method to generate a hash based on the configured message digest algorithm 108 * 109 * @param algorithm algorithm 110 * @param salt salt 111 * @param password password 112 * @return the hash 113 */ 114 private static String generatePasswordHash(String algorithm, String salt, String password) { 115 try { 116 MessageDigest digest = MessageDigest.getInstance(algorithm); 117 byte[] hash = digest.digest((salt + ":" + password).getBytes(StandardCharsets.UTF_8)); 118 BigInteger number = new BigInteger(1, hash); 119 return number.toString(16).toLowerCase(); 120 } catch (GeneralSecurityException e) { 121 throw new RuntimeException(e); 122 } 123 } 124}