Skip to content

Commit 879a17b

Browse files
committed
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.
1 parent 4ab9bed commit 879a17b

File tree

1 file changed

+41
-28
lines changed

1 file changed

+41
-28
lines changed

src/httprpc.cpp

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,6 @@ class HTTPRPCTimerInterface : public RPCTimerInterface
6969
};
7070

7171

72-
/* Pre-base64-encoded authentication token */
73-
static std::string strRPCUserColonPass;
7472
/* Stored RPC timer interface (for unregistration) */
7573
static std::unique_ptr<HTTPRPCTimerInterface> httpRPCTimerInterface;
7674
/* List of -rpcauth values */
@@ -101,31 +99,27 @@ static void JSONErrorReply(HTTPRequest* req, UniValue objError, const JSONRPCReq
10199

102100
//This function checks username and password against -rpcauth
103101
//entries from config file.
104-
static bool multiUserAuthorized(std::string strUserPass)
102+
static bool CheckUserAuthorized(std::string_view user_pass)
105103
{
106-
if (strUserPass.find(':') == std::string::npos) {
104+
if (user_pass.find(':') == std::string::npos) {
107105
return false;
108106
}
109-
std::string strUser = strUserPass.substr(0, strUserPass.find(':'));
110-
std::string strPass = strUserPass.substr(strUserPass.find(':') + 1);
107+
std::string_view user = user_pass.substr(0, user_pass.find(':'));
108+
std::string_view pass = user_pass.substr(user_pass.find(':') + 1);
111109

112-
for (const auto& vFields : g_rpcauth) {
113-
std::string strName = vFields[0];
114-
if (!TimingResistantEqual(strName, strUser)) {
110+
for (const auto& fields : g_rpcauth) {
111+
if (!TimingResistantEqual(std::string_view(fields[0]), user)) {
115112
continue;
116113
}
117114

118-
std::string strSalt = vFields[1];
119-
std::string strHash = vFields[2];
115+
const std::string& salt = fields[1];
116+
const std::string& hash = fields[2];
120117

121-
static const unsigned int KEY_SIZE = 32;
122-
unsigned char out[KEY_SIZE];
118+
std::array<unsigned char, CHMAC_SHA256::OUTPUT_SIZE> out;
119+
CHMAC_SHA256(UCharCast(salt.data()), salt.size()).Write(UCharCast(pass.data()), pass.size()).Finalize(out.data());
120+
std::string hash_from_pass = HexStr(out);
123121

124-
CHMAC_SHA256(reinterpret_cast<const unsigned char*>(strSalt.data()), strSalt.size()).Write(reinterpret_cast<const unsigned char*>(strPass.data()), strPass.size()).Finalize(out);
125-
std::vector<unsigned char> hexvec(out, out+KEY_SIZE);
126-
std::string strHashFromPass = HexStr(hexvec);
127-
128-
if (TimingResistantEqual(strHashFromPass, strHash)) {
122+
if (TimingResistantEqual(hash_from_pass, hash)) {
129123
return true;
130124
}
131125
}
@@ -145,12 +139,7 @@ static bool RPCAuthorized(const std::string& strAuth, std::string& strAuthUserna
145139
if (strUserPass.find(':') != std::string::npos)
146140
strAuthUsernameOut = strUserPass.substr(0, strUserPass.find(':'));
147141

148-
// Check if authorized under single-user field.
149-
// (strRPCUserColonPass is empty when -norpccookiefile is specified).
150-
if (!strRPCUserColonPass.empty() && TimingResistantEqual(strUserPass, strRPCUserColonPass)) {
151-
return true;
152-
}
153-
return multiUserAuthorized(strUserPass);
142+
return CheckUserAuthorized(strUserPass);
154143
}
155144

156145
static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
@@ -291,6 +280,8 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
291280

292281
static bool InitRPCAuthentication()
293282
{
283+
std::string user_colon_pass;
284+
294285
if (gArgs.GetArg("-rpcpassword", "") == "")
295286
{
296287
std::optional<fs::perms> cookie_perms{std::nullopt};
@@ -304,19 +295,41 @@ static bool InitRPCAuthentication()
304295
cookie_perms = *perm_opt;
305296
}
306297

307-
assert(strRPCUserColonPass.empty()); // Only support initializing once
308-
if (!GenerateAuthCookie(&strRPCUserColonPass, cookie_perms)) {
298+
if (!GenerateAuthCookie(&user_colon_pass, cookie_perms)) {
309299
return false;
310300
}
311-
if (strRPCUserColonPass.empty()) {
301+
if (user_colon_pass.empty()) {
312302
LogInfo("RPC authentication cookie file generation is disabled.");
313303
} else {
314304
LogInfo("Using random cookie authentication.");
315305
}
316306
} else {
317307
LogInfo("Using rpcuser/rpcpassword authentication.");
318308
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.");
319-
strRPCUserColonPass = gArgs.GetArg("-rpcuser", "") + ":" + gArgs.GetArg("-rpcpassword", "");
309+
user_colon_pass = gArgs.GetArg("-rpcuser", "") + ":" + gArgs.GetArg("-rpcpassword", "");
310+
}
311+
312+
// If there is a plaintext credential, hash it with a random salt before storage.
313+
if (!user_colon_pass.empty()) {
314+
std::vector<std::string> fields{SplitString(user_colon_pass, ':')};
315+
if (fields.size() != 2) {
316+
LogError("Unable to parse RPC credentials. The configured rpcuser or rpcpassword cannot contain a \":\".");
317+
return false;
318+
}
319+
const std::string& user = fields[0];
320+
const std::string& pass = fields[1];
321+
322+
// Generate a random 16 byte hex salt.
323+
std::array<unsigned char, 16> raw_salt;
324+
GetStrongRandBytes(raw_salt);
325+
std::string salt = HexStr(raw_salt);
326+
327+
// Compute HMAC.
328+
std::array<unsigned char, CHMAC_SHA256::OUTPUT_SIZE> out;
329+
CHMAC_SHA256(UCharCast(salt.data()), salt.size()).Write(UCharCast(pass.data()), pass.size()).Finalize(out.data());
330+
std::string hash = HexStr(out);
331+
332+
g_rpcauth.push_back({user, salt, hash});
320333
}
321334

322335
if (!gArgs.GetArgs("-rpcauth").empty()) {

0 commit comments

Comments
 (0)