rpc: Store all credentials hashed in memory

This gets rid of the special-casing of `strRPCUserColonPass` by hashing
cookies as well as manually provided `-rpcuser`/`-rpcpassword` with a
random salt before storing them.

Also take the opportunity to modernize the surrounding code a bit. There
should be no end-user visible differences in behavior.
This commit is contained in:
laanwj
2025-05-06 11:08:30 +02:00
parent 4ab9bedee9
commit 879a17bcb1

View File

@ -69,8 +69,6 @@ private:
}; };
/* Pre-base64-encoded authentication token */
static std::string strRPCUserColonPass;
/* Stored RPC timer interface (for unregistration) */ /* Stored RPC timer interface (for unregistration) */
static std::unique_ptr<HTTPRPCTimerInterface> httpRPCTimerInterface; static std::unique_ptr<HTTPRPCTimerInterface> httpRPCTimerInterface;
/* List of -rpcauth values */ /* List of -rpcauth values */
@ -101,31 +99,27 @@ static void JSONErrorReply(HTTPRequest* req, UniValue objError, const JSONRPCReq
//This function checks username and password against -rpcauth //This function checks username and password against -rpcauth
//entries from config file. //entries from config file.
static bool multiUserAuthorized(std::string strUserPass) static bool CheckUserAuthorized(std::string_view user_pass)
{ {
if (strUserPass.find(':') == std::string::npos) { if (user_pass.find(':') == std::string::npos) {
return false; return false;
} }
std::string strUser = strUserPass.substr(0, strUserPass.find(':')); std::string_view user = user_pass.substr(0, user_pass.find(':'));
std::string strPass = strUserPass.substr(strUserPass.find(':') + 1); std::string_view pass = user_pass.substr(user_pass.find(':') + 1);
for (const auto& vFields : g_rpcauth) { for (const auto& fields : g_rpcauth) {
std::string strName = vFields[0]; if (!TimingResistantEqual(std::string_view(fields[0]), user)) {
if (!TimingResistantEqual(strName, strUser)) {
continue; continue;
} }
std::string strSalt = vFields[1]; const std::string& salt = fields[1];
std::string strHash = vFields[2]; const std::string& hash = fields[2];
static const unsigned int KEY_SIZE = 32; std::array<unsigned char, CHMAC_SHA256::OUTPUT_SIZE> out;
unsigned char out[KEY_SIZE]; CHMAC_SHA256(UCharCast(salt.data()), salt.size()).Write(UCharCast(pass.data()), pass.size()).Finalize(out.data());
std::string hash_from_pass = HexStr(out);
CHMAC_SHA256(reinterpret_cast<const unsigned char*>(strSalt.data()), strSalt.size()).Write(reinterpret_cast<const unsigned char*>(strPass.data()), strPass.size()).Finalize(out); if (TimingResistantEqual(hash_from_pass, hash)) {
std::vector<unsigned char> hexvec(out, out+KEY_SIZE);
std::string strHashFromPass = HexStr(hexvec);
if (TimingResistantEqual(strHashFromPass, strHash)) {
return true; return true;
} }
} }
@ -145,12 +139,7 @@ static bool RPCAuthorized(const std::string& strAuth, std::string& strAuthUserna
if (strUserPass.find(':') != std::string::npos) if (strUserPass.find(':') != std::string::npos)
strAuthUsernameOut = strUserPass.substr(0, strUserPass.find(':')); strAuthUsernameOut = strUserPass.substr(0, strUserPass.find(':'));
// Check if authorized under single-user field. return CheckUserAuthorized(strUserPass);
// (strRPCUserColonPass is empty when -norpccookiefile is specified).
if (!strRPCUserColonPass.empty() && TimingResistantEqual(strUserPass, strRPCUserColonPass)) {
return true;
}
return multiUserAuthorized(strUserPass);
} }
static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req) static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
@ -291,6 +280,8 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
static bool InitRPCAuthentication() static bool InitRPCAuthentication()
{ {
std::string user_colon_pass;
if (gArgs.GetArg("-rpcpassword", "") == "") if (gArgs.GetArg("-rpcpassword", "") == "")
{ {
std::optional<fs::perms> cookie_perms{std::nullopt}; std::optional<fs::perms> cookie_perms{std::nullopt};
@ -304,11 +295,10 @@ static bool InitRPCAuthentication()
cookie_perms = *perm_opt; cookie_perms = *perm_opt;
} }
assert(strRPCUserColonPass.empty()); // Only support initializing once if (!GenerateAuthCookie(&user_colon_pass, cookie_perms)) {
if (!GenerateAuthCookie(&strRPCUserColonPass, cookie_perms)) {
return false; return false;
} }
if (strRPCUserColonPass.empty()) { if (user_colon_pass.empty()) {
LogInfo("RPC authentication cookie file generation is disabled."); LogInfo("RPC authentication cookie file generation is disabled.");
} else { } else {
LogInfo("Using random cookie authentication."); LogInfo("Using random cookie authentication.");
@ -316,7 +306,30 @@ static bool InitRPCAuthentication()
} else { } else {
LogInfo("Using rpcuser/rpcpassword authentication."); LogInfo("Using rpcuser/rpcpassword authentication.");
LogWarning("The use of rpcuser/rpcpassword is less secure, because credentials are configured in plain text. It is recommended that locally-run instances switch to cookie-based auth, or otherwise to use hashed rpcauth credentials. See share/rpcauth in the source directory for more information."); LogWarning("The use of rpcuser/rpcpassword is less secure, because credentials are configured in plain text. It is recommended that locally-run instances switch to cookie-based auth, or otherwise to use hashed rpcauth credentials. See share/rpcauth in the source directory for more information.");
strRPCUserColonPass = gArgs.GetArg("-rpcuser", "") + ":" + gArgs.GetArg("-rpcpassword", ""); user_colon_pass = gArgs.GetArg("-rpcuser", "") + ":" + gArgs.GetArg("-rpcpassword", "");
}
// If there is a plaintext credential, hash it with a random salt before storage.
if (!user_colon_pass.empty()) {
std::vector<std::string> fields{SplitString(user_colon_pass, ':')};
if (fields.size() != 2) {
LogError("Unable to parse RPC credentials. The configured rpcuser or rpcpassword cannot contain a \":\".");
return false;
}
const std::string& user = fields[0];
const std::string& pass = fields[1];
// Generate a random 16 byte hex salt.
std::array<unsigned char, 16> raw_salt;
GetStrongRandBytes(raw_salt);
std::string salt = HexStr(raw_salt);
// Compute HMAC.
std::array<unsigned char, CHMAC_SHA256::OUTPUT_SIZE> out;
CHMAC_SHA256(UCharCast(salt.data()), salt.size()).Write(UCharCast(pass.data()), pass.size()).Finalize(out.data());
std::string hash = HexStr(out);
g_rpcauth.push_back({user, salt, hash});
} }
if (!gArgs.GetArgs("-rpcauth").empty()) { if (!gArgs.GetArgs("-rpcauth").empty()) {