Skip to content

Commit 4272966

Browse files
committed
Merge bitcoin/bitcoin#32423: rpc: Undeprecate rpcuser/rpcpassword, store all credentials hashed in memory
e49a727 rpc: Avoid join-split roundtrip for user:pass for auth credentials (Vasil Dimov) 98ff38a rpc: Perform HTTP user:pass split once in `RPCAuthorized` (laanwj) 879a17b rpc: Store all credentials hashed in memory (laanwj) 4ab9bed rpc: Undeprecate rpcuser/rpcpassword, change message to security warning (laanwj) Pull request description: This PR does two things: ### Undeprecate rpcuser/rpcpassword, change message to security warning Back in 2015, in bitcoin/bitcoin#7044, we added configuration option `rpcauth` for multiple RPC users. At the same time the old settings for single-user configuration `rpcuser` and `rpcpassword` were "soon" to be deprecated. The main reason for this deprecation is that while `rpcpassword` stores the password in plain text, `rpcauth` stores a hash, so it doesn't appear in the configuration in plain text. As the options are still in active use, actually removing them is expected to be a hassle to many, and it's not clear that is worth it. As for the security risk, in many kinds of setups (no wallet, containerized, single-user-single-application, local-only, etc) it is an unlikely point of escalation. In the end, it is good to encourage secure practices, but it is the responsibility of the user. Log a clear warning but remove the deprecation notice (this is also the only place where the options appear as deprecated, they were never marked as such in the -help output). <hr> ### 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. <hr> Closes #29240. ACKs for top commit: 1440000bytes: utACK bitcoin/bitcoin@e49a727 janb84: reACK bitcoin/bitcoin@e49a727 vasild: ACK e49a727 Tree-SHA512: 7162848ada4545bc07b5843d1ab6fb7e31fb26de8d6385464b7c166491cd122eac2ec5e70887c414fc136600482df8277dc0cc0541d7b7cf62c4f72e25bb6145
2 parents ff1ee10 + e49a727 commit 4272966

File tree

3 files changed

+75
-48
lines changed

3 files changed

+75
-48
lines changed

src/httprpc.cpp

Lines changed: 44 additions & 36 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,21 @@ 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, std::string_view pass)
105103
{
106-
if (strUserPass.find(':') == std::string::npos) {
107-
return false;
108-
}
109-
std::string strUser = strUserPass.substr(0, strUserPass.find(':'));
110-
std::string strPass = strUserPass.substr(strUserPass.find(':') + 1);
111-
112-
for (const auto& vFields : g_rpcauth) {
113-
std::string strName = vFields[0];
114-
if (!TimingResistantEqual(strName, strUser)) {
104+
for (const auto& fields : g_rpcauth) {
105+
if (!TimingResistantEqual(std::string_view(fields[0]), user)) {
115106
continue;
116107
}
117108

118-
std::string strSalt = vFields[1];
119-
std::string strHash = vFields[2];
109+
const std::string& salt = fields[1];
110+
const std::string& hash = fields[2];
120111

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

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)) {
116+
if (TimingResistantEqual(hash_from_pass, hash)) {
129117
return true;
130118
}
131119
}
@@ -142,15 +130,14 @@ static bool RPCAuthorized(const std::string& strAuth, std::string& strAuthUserna
142130
if (!userpass_data) return false;
143131
strUserPass.assign(userpass_data->begin(), userpass_data->end());
144132

145-
if (strUserPass.find(':') != std::string::npos)
146-
strAuthUsernameOut = strUserPass.substr(0, strUserPass.find(':'));
147-
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;
133+
size_t colon_pos = strUserPass.find(':');
134+
if (colon_pos == std::string::npos) {
135+
return false; // Invalid basic auth.
152136
}
153-
return multiUserAuthorized(strUserPass);
137+
std::string user = strUserPass.substr(0, colon_pos);
138+
std::string pass = strUserPass.substr(colon_pos + 1);
139+
strAuthUsernameOut = user;
140+
return CheckUserAuthorized(user, pass);
154141
}
155142

156143
static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
@@ -291,6 +278,9 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
291278

292279
static bool InitRPCAuthentication()
293280
{
281+
std::string user;
282+
std::string pass;
283+
294284
if (gArgs.GetArg("-rpcpassword", "") == "")
295285
{
296286
std::optional<fs::perms> cookie_perms{std::nullopt};
@@ -304,18 +294,36 @@ static bool InitRPCAuthentication()
304294
cookie_perms = *perm_opt;
305295
}
306296

307-
assert(strRPCUserColonPass.empty()); // Only support initializing once
308-
if (!GenerateAuthCookie(&strRPCUserColonPass, cookie_perms)) {
297+
switch (GenerateAuthCookie(cookie_perms, user, pass)) {
298+
case GenerateAuthCookieResult::ERR:
309299
return false;
310-
}
311-
if (strRPCUserColonPass.empty()) {
300+
case GenerateAuthCookieResult::DISABLED:
312301
LogInfo("RPC authentication cookie file generation is disabled.");
313-
} else {
302+
break;
303+
case GenerateAuthCookieResult::OK:
314304
LogInfo("Using random cookie authentication.");
305+
break;
315306
}
316307
} else {
317-
LogPrintf("Config options rpcuser and rpcpassword will soon be deprecated. Locally-run instances may remove rpcuser to use cookie-based auth, or may be replaced with rpcauth. Please see share/rpcauth for rpcauth auth generation.\n");
318-
strRPCUserColonPass = gArgs.GetArg("-rpcuser", "") + ":" + gArgs.GetArg("-rpcpassword", "");
308+
LogInfo("Using rpcuser/rpcpassword authentication.");
309+
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.");
310+
user = gArgs.GetArg("-rpcuser", "");
311+
pass = gArgs.GetArg("-rpcpassword", "");
312+
}
313+
314+
// If there is a plaintext credential, hash it with a random salt before storage.
315+
if (!user.empty() || !pass.empty()) {
316+
// Generate a random 16 byte hex salt.
317+
std::array<unsigned char, 16> raw_salt;
318+
GetStrongRandBytes(raw_salt);
319+
std::string salt = HexStr(raw_salt);
320+
321+
// Compute HMAC.
322+
std::array<unsigned char, CHMAC_SHA256::OUTPUT_SIZE> out;
323+
CHMAC_SHA256(UCharCast(salt.data()), salt.size()).Write(UCharCast(pass.data()), pass.size()).Finalize(out.data());
324+
std::string hash = HexStr(out);
325+
326+
g_rpcauth.push_back({user, salt, hash});
319327
}
320328

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

src/rpc/request.cpp

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,50 +97,52 @@ static fs::path GetAuthCookieFile(bool temp=false)
9797

9898
static bool g_generated_cookie = false;
9999

100-
bool GenerateAuthCookie(std::string* cookie_out, std::optional<fs::perms> cookie_perms)
100+
GenerateAuthCookieResult GenerateAuthCookie(const std::optional<fs::perms>& cookie_perms,
101+
std::string& user,
102+
std::string& pass)
101103
{
102104
const size_t COOKIE_SIZE = 32;
103105
unsigned char rand_pwd[COOKIE_SIZE];
104106
GetRandBytes(rand_pwd);
105-
std::string cookie = COOKIEAUTH_USER + ":" + HexStr(rand_pwd);
107+
const std::string rand_pwd_hex{HexStr(rand_pwd)};
106108

107109
/** the umask determines what permissions are used to create this file -
108110
* these are set to 0077 in common/system.cpp.
109111
*/
110112
std::ofstream file;
111113
fs::path filepath_tmp = GetAuthCookieFile(true);
112114
if (filepath_tmp.empty()) {
113-
return true; // -norpccookiefile
115+
return GenerateAuthCookieResult::DISABLED; // -norpccookiefile
114116
}
115117
file.open(filepath_tmp);
116118
if (!file.is_open()) {
117119
LogWarning("Unable to open cookie authentication file %s for writing", fs::PathToString(filepath_tmp));
118-
return false;
120+
return GenerateAuthCookieResult::ERR;
119121
}
120-
file << cookie;
122+
file << COOKIEAUTH_USER << ":" << rand_pwd_hex;
121123
file.close();
122124

123125
fs::path filepath = GetAuthCookieFile(false);
124126
if (!RenameOver(filepath_tmp, filepath)) {
125127
LogWarning("Unable to rename cookie authentication file %s to %s", fs::PathToString(filepath_tmp), fs::PathToString(filepath));
126-
return false;
128+
return GenerateAuthCookieResult::ERR;
127129
}
128130
if (cookie_perms) {
129131
std::error_code code;
130132
fs::permissions(filepath, cookie_perms.value(), fs::perm_options::replace, code);
131133
if (code) {
132134
LogWarning("Unable to set permissions on cookie authentication file %s", fs::PathToString(filepath));
133-
return false;
135+
return GenerateAuthCookieResult::ERR;
134136
}
135137
}
136138

137139
g_generated_cookie = true;
138140
LogInfo("Generated RPC authentication cookie %s\n", fs::PathToString(filepath));
139141
LogInfo("Permissions used for cookie: %s\n", PermsToSymbolicString(fs::status(filepath).permissions()));
140142

141-
if (cookie_out)
142-
*cookie_out = cookie;
143-
return true;
143+
user = COOKIEAUTH_USER;
144+
pass = rand_pwd_hex;
145+
return GenerateAuthCookieResult::OK;
144146
}
145147

146148
bool GetAuthCookie(std::string *cookie_out)

src/rpc/request.h

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,25 @@ UniValue JSONRPCRequestObj(const std::string& strMethod, const UniValue& params,
2323
UniValue JSONRPCReplyObj(UniValue result, UniValue error, std::optional<UniValue> id, JSONRPCVersion jsonrpc_version);
2424
UniValue JSONRPCError(int code, const std::string& message);
2525

26-
/** Generate a new RPC authentication cookie and write it to disk */
27-
bool GenerateAuthCookie(std::string* cookie_out, std::optional<fs::perms> cookie_perms=std::nullopt);
26+
enum class GenerateAuthCookieResult : uint8_t {
27+
DISABLED, // -norpccookiefile
28+
ERR,
29+
OK,
30+
};
31+
32+
/**
33+
* Generate a new RPC authentication cookie and write it to disk
34+
* @param[in] cookie_perms Filesystem permissions to use for the cookie file.
35+
* @param[out] user Generated username, only set if `OK` is returned.
36+
* @param[out] pass Generated password, only set if `OK` is returned.
37+
* @retval GenerateAuthCookieResult::DISABLED Authentication via cookie is disabled.
38+
* @retval GenerateAuthCookieResult::ERROR Error occurred, auth data could not be saved to disk.
39+
* @retval GenerateAuthCookieResult::OK Auth data was generated, saved to disk and in `user` and `pass`.
40+
*/
41+
GenerateAuthCookieResult GenerateAuthCookie(const std::optional<fs::perms>& cookie_perms,
42+
std::string& user,
43+
std::string& pass);
44+
2845
/** Read the RPC authentication cookie from disk */
2946
bool GetAuthCookie(std::string *cookie_out);
3047
/** Delete RPC authentication cookie from disk */

0 commit comments

Comments
 (0)