Skip to content

Commit 5e55534

Browse files
committed
Merge bitcoin/bitcoin#27068: wallet: SecureString to allow null characters
4bbf5dd Detailed error message for passphrases with null chars (John Moffett) b4bdabc doc: Release notes for 27068 (John Moffett) 4b1205b Test case for passphrases with null characters (John Moffett) 00a0861 Pass all characters to SecureString including nulls (John Moffett) Pull request description: `SecureString` is a `std::string` specialization with a secure allocator. However, in practice it's treated like a C- string (no explicit length and null-terminated). This can cause unexpected and potentially insecure behavior. For instance, if a user enters a passphrase with embedded null characters (which is possible through Qt and the JSON-RPC), it will ignore any characters after the first null, potentially giving the user a false sense of security. Instead of assigning to `SecureString` via `std::string::c_str()`, assign it via a `std::string_view` of the original. This explicitly captures the size and still doesn't make any extraneous copies in memory. Note to reviewers, the following all compile identically in recent `GCC` (x86-64 and ARM64) with `-O2` (and `-std=c++17`): ```C++ std::string orig_string; std::cin >> orig_string; SecureString s; s.reserve(100); // The following all compile identically s = orig_string; s = std::string_view{orig_string}; s.assign(std::string_view{orig_string}); s.assign(orig_string.data(), orig_string.size()); ``` So it's largely a matter of preference. However, one thing to keep in mind is that we want to avoid making unnecessary copies of any sensitive data in memory. Something like `SecureString s{orig_string};` is still invalid and probably unwanted in our case, since it'd get treated as a short string and optimized away from the secure allocator. I presume that's the reason for the `reserve()` calls. Fixes #27067. ACKs for top commit: achow101: re-ACK 4bbf5dd stickies-v: re-ACK [4bbf5dd](bitcoin/bitcoin@4bbf5dd) furszy: utACK 4bbf5dd Tree-SHA512: 47a96905a82ca674b18076a20a388123beedf70e9de73e42574ea68afbb434734e56021835dd9b148cdbf61709926b487cc95e9021d9bc534a7c93b3e143d2f7
2 parents 174f022 + 4bbf5dd commit 5e55534

File tree

6 files changed

+73
-22
lines changed

6 files changed

+73
-22
lines changed

doc/release-notes-27068.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Wallet
2+
------
3+
4+
- Wallet passphrases may now contain null characters.
5+
Prior to this change, only characters up to the first
6+
null character were recognized and accepted. (#27068)

src/qt/askpassphrasedialog.cpp

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,10 @@ void AskPassphraseDialog::accept()
8989
oldpass.reserve(MAX_PASSPHRASE_SIZE);
9090
newpass1.reserve(MAX_PASSPHRASE_SIZE);
9191
newpass2.reserve(MAX_PASSPHRASE_SIZE);
92-
// TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string)
93-
// Alternately, find a way to make this input mlock()'d to begin with.
94-
oldpass.assign(ui->passEdit1->text().toStdString().c_str());
95-
newpass1.assign(ui->passEdit2->text().toStdString().c_str());
96-
newpass2.assign(ui->passEdit3->text().toStdString().c_str());
92+
93+
oldpass.assign(std::string_view{ui->passEdit1->text().toStdString()});
94+
newpass1.assign(std::string_view{ui->passEdit2->text().toStdString()});
95+
newpass2.assign(std::string_view{ui->passEdit3->text().toStdString()});
9796

9897
secureClearPassFields();
9998

@@ -154,8 +153,19 @@ void AskPassphraseDialog::accept()
154153
case Unlock:
155154
try {
156155
if (!model->setWalletLocked(false, oldpass)) {
157-
QMessageBox::critical(this, tr("Wallet unlock failed"),
158-
tr("The passphrase entered for the wallet decryption was incorrect."));
156+
// Check if the passphrase has a null character (see #27067 for details)
157+
if (oldpass.find('\0') == std::string::npos) {
158+
QMessageBox::critical(this, tr("Wallet unlock failed"),
159+
tr("The passphrase entered for the wallet decryption was incorrect."));
160+
} else {
161+
QMessageBox::critical(this, tr("Wallet unlock failed"),
162+
tr("The passphrase entered for the wallet decryption is incorrect. "
163+
"It contains a null character (ie - a zero byte). "
164+
"If the passphrase was set with a version of this software prior to 25.0, "
165+
"please try again with only the characters up to — but not including — "
166+
"the first null character. If this is successful, please set a new "
167+
"passphrase to avoid this issue in the future."));
168+
}
159169
} else {
160170
QDialog::accept(); // Success
161171
}
@@ -174,8 +184,18 @@ void AskPassphraseDialog::accept()
174184
}
175185
else
176186
{
177-
QMessageBox::critical(this, tr("Wallet encryption failed"),
178-
tr("The passphrase entered for the wallet decryption was incorrect."));
187+
// Check if the old passphrase had a null character (see #27067 for details)
188+
if (oldpass.find('\0') == std::string::npos) {
189+
QMessageBox::critical(this, tr("Passphrase change failed"),
190+
tr("The passphrase entered for the wallet decryption was incorrect."));
191+
} else {
192+
QMessageBox::critical(this, tr("Passphrase change failed"),
193+
tr("The old passphrase entered for the wallet decryption is incorrect. "
194+
"It contains a null character (ie - a zero byte). "
195+
"If the passphrase was set with a version of this software prior to 25.0, "
196+
"please try again with only the characters up to — but not including — "
197+
"the first null character."));
198+
}
179199
}
180200
}
181201
else

src/support/allocators/secure.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ struct secure_allocator : public std::allocator<T> {
5656
};
5757

5858
// This is exactly like std::string, but with a custom allocator.
59+
// TODO: Consider finding a way to make incoming RPC request.params[i] mlock()ed as well
5960
typedef std::basic_string<char, std::char_traits<char>, secure_allocator<char> > SecureString;
6061

6162
#endif // BITCOIN_SUPPORT_ALLOCATORS_SECURE_H

src/wallet/rpc/encrypt.cpp

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,7 @@ RPCHelpMan walletpassphrase()
4949
// Note that the walletpassphrase is stored in request.params[0] which is not mlock()ed
5050
SecureString strWalletPass;
5151
strWalletPass.reserve(100);
52-
// TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string)
53-
// Alternately, find a way to make request.params[0] mlock()'d to begin with.
54-
strWalletPass = request.params[0].get_str().c_str();
52+
strWalletPass = std::string_view{request.params[0].get_str()};
5553

5654
// Get the timeout
5755
nSleepTime = request.params[1].getInt<int64_t>();
@@ -70,7 +68,17 @@ RPCHelpMan walletpassphrase()
7068
}
7169

7270
if (!pwallet->Unlock(strWalletPass)) {
73-
throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered was incorrect.");
71+
// Check if the passphrase has a null character (see #27067 for details)
72+
if (strWalletPass.find('\0') == std::string::npos) {
73+
throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered was incorrect.");
74+
} else {
75+
throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered is incorrect. "
76+
"It contains a null character (ie - a zero byte). "
77+
"If the passphrase was set with a version of this software prior to 25.0, "
78+
"please try again with only the characters up to — but not including — "
79+
"the first null character. If this is successful, please set a new "
80+
"passphrase to avoid this issue in the future.");
81+
}
7482
}
7583

7684
pwallet->TopUpKeyPool();
@@ -132,22 +140,29 @@ RPCHelpMan walletpassphrasechange()
132140

133141
LOCK2(pwallet->m_relock_mutex, pwallet->cs_wallet);
134142

135-
// TODO: get rid of these .c_str() calls by implementing SecureString::operator=(std::string)
136-
// Alternately, find a way to make request.params[0] mlock()'d to begin with.
137143
SecureString strOldWalletPass;
138144
strOldWalletPass.reserve(100);
139-
strOldWalletPass = request.params[0].get_str().c_str();
145+
strOldWalletPass = std::string_view{request.params[0].get_str()};
140146

141147
SecureString strNewWalletPass;
142148
strNewWalletPass.reserve(100);
143-
strNewWalletPass = request.params[1].get_str().c_str();
149+
strNewWalletPass = std::string_view{request.params[1].get_str()};
144150

145151
if (strOldWalletPass.empty() || strNewWalletPass.empty()) {
146152
throw JSONRPCError(RPC_INVALID_PARAMETER, "passphrase cannot be empty");
147153
}
148154

149155
if (!pwallet->ChangeWalletPassphrase(strOldWalletPass, strNewWalletPass)) {
150-
throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered was incorrect.");
156+
// Check if the old passphrase had a null character (see #27067 for details)
157+
if (strOldWalletPass.find('\0') == std::string::npos) {
158+
throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered was incorrect.");
159+
} else {
160+
throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The old wallet passphrase entered is incorrect. "
161+
"It contains a null character (ie - a zero byte). "
162+
"If the old passphrase was set with a version of this software prior to 25.0, "
163+
"please try again with only the characters up to — but not including — "
164+
"the first null character.");
165+
}
151166
}
152167

153168
return UniValue::VNULL;
@@ -241,11 +256,9 @@ RPCHelpMan encryptwallet()
241256

242257
LOCK2(pwallet->m_relock_mutex, pwallet->cs_wallet);
243258

244-
// TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string)
245-
// Alternately, find a way to make request.params[0] mlock()'d to begin with.
246259
SecureString strWalletPass;
247260
strWalletPass.reserve(100);
248-
strWalletPass = request.params[0].get_str().c_str();
261+
strWalletPass = std::string_view{request.params[0].get_str()};
249262

250263
if (strWalletPass.empty()) {
251264
throw JSONRPCError(RPC_INVALID_PARAMETER, "passphrase cannot be empty");

src/wallet/rpc/wallet.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ static RPCHelpMan createwallet()
359359
passphrase.reserve(100);
360360
std::vector<bilingual_str> warnings;
361361
if (!request.params[3].isNull()) {
362-
passphrase = request.params[3].get_str().c_str();
362+
passphrase = std::string_view{request.params[3].get_str()};
363363
if (passphrase.empty()) {
364364
// Empty string means unencrypted
365365
warnings.emplace_back(Untranslated("Empty string given as passphrase, wallet will not be encrypted."));

test/functional/wallet_encryption.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,17 @@ def run_test(self):
9090
self.nodes[0].walletpassphrase(passphrase2, MAX_VALUE + 1000)
9191
actual_time = self.nodes[0].getwalletinfo()['unlocked_until']
9292
assert_equal(actual_time, expected_time)
93+
self.nodes[0].walletlock()
94+
95+
# Test passphrase with null characters
96+
passphrase_with_nulls = "Phrase\0With\0Nulls"
97+
self.nodes[0].walletpassphrasechange(passphrase2, passphrase_with_nulls)
98+
# walletpassphrasechange should not stop at null characters
99+
assert_raises_rpc_error(-14, "wallet passphrase entered was incorrect", self.nodes[0].walletpassphrase, passphrase_with_nulls.partition("\0")[0], 10)
100+
self.nodes[0].walletpassphrase(passphrase_with_nulls, 10)
101+
sig = self.nodes[0].signmessage(address, msg)
102+
assert self.nodes[0].verifymessage(address, sig, msg)
103+
self.nodes[0].walletlock()
93104

94105

95106
if __name__ == '__main__':

0 commit comments

Comments
 (0)