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