Skip to content

Commit 5f79425

Browse files
Merge dashpay#6792: fix: follow-up bitcoin#27068 - apply changes for mnemonic too
28012eb test: test \0 for walletpassphrase too for rpc upgradetohd (Konstantin Akimov) bfacf09 test: enforce stricter validation of mnemonic passphrase in wallet_upgradetohd (Konstantin Akimov) 95e9e66 test: fix incorrect key name (Konstantin Akimov) 6f841f5 test: add more comprehensive testing for mnemonic_passphrase including null character (Konstantin Akimov) 7f53b9f fix: helper ToSeed for mnemonics doesn't loose end of string after 0-characters (Konstantin Akimov) 3ab8b5e doc: update release notes to add PR num (Konstantin Akimov) c85aa93 fix: follow-up bitcoin#27068 - apply changes for mnemonic too (Konstantin Akimov) Pull request description: ## Issue being fixed or feature implemented bitcoin#27068 introduced support of null character in SecureString initialization, but this feature has not been supported for mnemonic passphrases. ## What was done? This PR apply changed introduced in bitcoin#27078 for dash specific code. ## How Has This Been Tested? Run unit & functional tests. ## Breaking Changes RPC `upgradetohd` supports null characters in mnemonic passphrase and in wallet's passphrase. ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [x] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone ACKs for top commit: UdjinM6: utACK 28012eb PastaPastaPasta: utACK 28012eb Tree-SHA512: ced5766c8940fb22468e6d2b48ffa4bc845c26c858b8ea9a08450d1aff935f47af43eaf0f194ee3a3af919a05b47eae31d60daec9258933b0c66b60335f04756
2 parents ed5c410 + 28012eb commit 5f79425

File tree

9 files changed

+77
-51
lines changed

9 files changed

+77
-51
lines changed

doc/release-notes-27068.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ Wallet
22
------
33

44
- Wallet passphrases may now contain null characters.
5+
Mnemonic passphrases may now contain null characters.
56
Prior to this change, only characters up to the first
6-
null character were recognized and accepted. (#27068)
7+
null character were recognized and accepted. (#6780 #6792)

src/wallet/bip39.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ void CMnemonic::ToSeed(const SecureString& mnemonic, const SecureString& passphr
143143
{
144144

145145
SecureString ssSalt = SecureString("mnemonic") + passphrase;
146-
SecureVector vchSalt(ssSalt.begin(), ssSalt.begin() + strnlen(ssSalt.data(), 256));
146+
SecureVector vchSalt(ssSalt.begin(), ssSalt.begin() + std::min<size_t>(256, ssSalt.size()));
147147
seedRet.resize(64);
148+
// NOTE: c_str() here is fine because mnemonic has only [a-z ] characters
148149
PKCS5_PBKDF2_HMAC_SHA512(mnemonic.c_str(), mnemonic.size(), vchSalt.data(), vchSalt.size(), 2048, 64, seedRet.data());
149150
}

src/wallet/hdchain.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ bool CHDChain::SetMnemonic(const SecureString& ssMnemonic, const SecureString& s
6767

6868
// printf("mnemonic: %s\n", ssMnemonicTmp.c_str());
6969
if (!CMnemonic::Check(ssMnemonicTmp)) {
70-
throw std::runtime_error(std::string(__func__) + ": invalid mnemonic: `" + std::string(ssMnemonicTmp.c_str()) + "`");
70+
throw std::runtime_error(std::string(__func__) + ": invalid mnemonic: `" + std::string(ssMnemonicTmp) + "`");
7171
}
7272

7373
CMnemonic::ToSeed(ssMnemonicTmp, ssMnemonicPassphrase, vchSeed);

src/wallet/rpc/backup.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -908,8 +908,8 @@ RPCHelpMan dumphdinfo()
908908

909909
UniValue obj(UniValue::VOBJ);
910910
obj.pushKV("hdseed", HexStr(hdChainCurrent.GetSeed()));
911-
obj.pushKV("mnemonic", ssMnemonic.c_str());
912-
obj.pushKV("mnemonicpassphrase", ssMnemonicPassphrase.c_str());
911+
obj.pushKV("mnemonic", ssMnemonic);
912+
obj.pushKV("mnemonicpassphrase", ssMnemonicPassphrase);
913913

914914
return obj;
915915
},
@@ -2024,8 +2024,8 @@ RPCHelpMan listdescriptors()
20242024
SecureString mnemonic;
20252025
SecureString mnemonic_passphrase;
20262026
if (desc_spk_man->GetMnemonicString(mnemonic, mnemonic_passphrase) && !mnemonic.empty()) {
2027-
spk.pushKV("mnemonic", mnemonic.c_str());
2028-
spk.pushKV("mnemonicpassphrase", mnemonic_passphrase.c_str());
2027+
spk.pushKV("mnemonic", mnemonic);
2028+
spk.pushKV("mnemonicpassphrase", mnemonic_passphrase);
20292029
}
20302030
}
20312031
spk.pushKV("desc", descriptor);

src/wallet/rpc/wallet.cpp

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -375,29 +375,27 @@ static RPCHelpMan upgradetohd()
375375
{
376376
LOCK(pwallet->cs_wallet);
377377

378-
SecureString secureWalletPassphrase;
379-
secureWalletPassphrase.reserve(100);
378+
SecureString wallet_passphrase;
379+
wallet_passphrase.reserve(100);
380380

381381
if (request.params[2].isNull()) {
382382
if (pwallet->IsCrypted()) {
383383
throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Error: Wallet encrypted but passphrase not supplied to RPC.");
384384
}
385385
} else {
386-
// TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string)
387-
// Alternately, find a way to make request.params[0] mlock()'d to begin with.
388-
secureWalletPassphrase = request.params[2].get_str().c_str();
386+
wallet_passphrase = std::string_view{request.params[2].get_str()};
389387
}
390388

391-
SecureString secureMnemonic;
392-
secureMnemonic.reserve(256);
389+
SecureString mnemonic;
390+
mnemonic.reserve(256);
393391
if (!generate_mnemonic) {
394-
secureMnemonic = request.params[0].get_str().c_str();
392+
mnemonic = std::string_view{request.params[0].get_str()};
395393
}
396394

397-
SecureString secureMnemonicPassphrase;
398-
secureMnemonicPassphrase.reserve(256);
395+
SecureString mnemonic_passphrase;
396+
mnemonic_passphrase.reserve(256);
399397
if (!request.params[1].isNull()) {
400-
secureMnemonicPassphrase = request.params[1].get_str().c_str();
398+
mnemonic_passphrase = std::string_view{request.params[1].get_str()};
401399
}
402400

403401
// TODO: breaking changes kept for v21!
@@ -421,7 +419,7 @@ static RPCHelpMan upgradetohd()
421419
pwallet->SetMinVersion(FEATURE_HD);
422420

423421
if (pwallet->IsCrypted()) {
424-
if (secureWalletPassphrase.empty()) {
422+
if (wallet_passphrase.empty()) {
425423
throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: Wallet encrypted but supplied empty wallet passphrase");
426424
}
427425

@@ -430,13 +428,13 @@ static RPCHelpMan upgradetohd()
430428
pwallet->Lock();
431429

432430
// Unlock the wallet
433-
if (!pwallet->Unlock(secureWalletPassphrase)) {
431+
if (!pwallet->Unlock(wallet_passphrase)) {
434432
throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered was incorrect");
435433
}
436434
}
437435

438436
if (pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
439-
pwallet->SetupDescriptorScriptPubKeyMans(secureMnemonic, secureMnemonicPassphrase);
437+
pwallet->SetupDescriptorScriptPubKeyMans(mnemonic, mnemonic_passphrase);
440438
} else {
441439
auto spk_man = pwallet->GetLegacyScriptPubKeyMan();
442440
if (!spk_man) {
@@ -445,20 +443,20 @@ static RPCHelpMan upgradetohd()
445443

446444
if (pwallet->IsCrypted()) {
447445
pwallet->WithEncryptionKey([&](const CKeyingMaterial& encryption_key) {
448-
spk_man->GenerateNewHDChain(secureMnemonic, secureMnemonicPassphrase, encryption_key);
446+
spk_man->GenerateNewHDChain(mnemonic, mnemonic_passphrase, encryption_key);
449447
return true;
450448
});
451449
} else {
452-
spk_man->GenerateNewHDChain(secureMnemonic, secureMnemonicPassphrase);
450+
spk_man->GenerateNewHDChain(mnemonic, mnemonic_passphrase);
453451
}
454452
}
455453

456454
if (pwallet->IsCrypted()) {
457455
// Relock encrypted wallet
458456
pwallet->Lock();
459-
} else if (!secureWalletPassphrase.empty()) {
457+
} else if (!wallet_passphrase.empty()) {
460458
// Encrypt non-encrypted wallet
461-
if (!pwallet->EncryptWallet(secureWalletPassphrase)) {
459+
if (!pwallet->EncryptWallet(wallet_passphrase)) {
462460
throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Failed to encrypt HD wallet");
463461
}
464462
}

src/wallet/wallet.cpp

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2872,11 +2872,16 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
28722872
error = strprintf(_("%s -- Incorrect seed, it should be a hex string"), __func__);
28732873
return nullptr;
28742874
}
2875-
SecureString secureMnemonic = args.GetArg("-mnemonic", "").c_str();
2876-
SecureString secureMnemonicPassphrase = args.GetArg("-mnemonicpassphrase", "").c_str();
2875+
2876+
SecureString mnemonic, mnemonic_passphrase;
2877+
mnemonic.reserve(256);
2878+
mnemonic_passphrase.reserve(256);
2879+
2880+
mnemonic = args.GetArg("-mnemonic", "");
2881+
mnemonic_passphrase = args.GetArg("-mnemonicpassphrase", "");
28772882
LOCK(walletInstance->cs_wallet);
28782883
if (auto spk_man = walletInstance->GetLegacyScriptPubKeyMan()) {
2879-
spk_man->GenerateNewHDChain(secureMnemonic, secureMnemonicPassphrase);
2884+
spk_man->GenerateNewHDChain(mnemonic, mnemonic_passphrase);
28802885
}
28812886
}
28822887

@@ -2888,8 +2893,11 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
28882893

28892894
LOCK(walletInstance->cs_wallet);
28902895
if (walletInstance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
2891-
SecureString mnemonic = args.GetArg("-mnemonic", "").c_str();
2892-
SecureString mnemonic_passphrase = args.GetArg("-mnemonicpassphrase", "").c_str();
2896+
SecureString mnemonic, mnemonic_passphrase;
2897+
mnemonic.reserve(256);
2898+
mnemonic_passphrase.reserve(256);
2899+
mnemonic = args.GetArg("-mnemonic", "");
2900+
mnemonic_passphrase = args.GetArg("-mnemonicpassphrase", "");
28932901
args.ForceRemoveArg("mnemonic");
28942902
args.ForceRemoveArg("mnemonicpassphrase");
28952903
walletInstance->SetupDescriptorScriptPubKeyMans(mnemonic, mnemonic_passphrase);
@@ -3764,7 +3772,7 @@ void CWallet::SetupDescriptorScriptPubKeyMans(const SecureString& mnemonic_arg,
37643772
// TODO: remove duplicated code with CHDChain::SetMnemonic
37653773
const SecureString mnemonic = mnemonic_arg.empty() ? CMnemonic::Generate(m_args.GetIntArg("-mnemonicbits", CHDChain::DEFAULT_MNEMONIC_BITS)) : mnemonic_arg;
37663774
if (!CMnemonic::Check(mnemonic)) {
3767-
throw std::runtime_error(std::string(__func__) + ": invalid mnemonic: `" + std::string(mnemonic.c_str()) + "`");
3775+
throw std::runtime_error(std::string(__func__) + ": invalid mnemonic: `" + std::string(mnemonic) + "`");
37683776
}
37693777
SecureVector seed_key;
37703778
CMnemonic::ToSeed(mnemonic, mnemonic_passphrase, seed_key);

test/functional/test_framework/util.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -543,21 +543,26 @@ def get_mnemonic(node):
543543
Raises exception if there is none.
544544
"""
545545
if not node.getwalletinfo()['descriptors']:
546-
return node.dumphdinfo()["mnemonic"]
546+
hd = node.dumphdinfo()
547+
return (hd["mnemonic"], hd["mnemonicpassphrase"])
547548

548549
mnemonic = None
550+
mnemonic_passphrase = None
549551
descriptors = node.listdescriptors(True)['descriptors']
550552
for desc in descriptors:
551553
if desc['desc'][:4] == 'pkh(':
552554
if mnemonic is None:
553555
mnemonic = desc['mnemonic']
556+
mnemonic_passphrase = desc['mnemonicpassphrase']
554557
else:
555558
assert_equal(mnemonic, desc['mnemonic'])
559+
assert_equal(mnemonic_passphrase, desc['mnemonicpassphrase'])
556560
elif desc['desc'][:6] == 'combo(':
557561
assert 'mnemonic' not in desc
562+
assert 'mnemonicpassphrase' not in desc
558563
else:
559564
raise AssertionError(f"Unknown descriptor type: {desc['desc']}")
560-
return mnemonic
565+
return (mnemonic, mnemonic_passphrase)
561566

562567
# Transaction/Block functions
563568
#############################

test/functional/wallet_mnemonicbits.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def run_test(self):
2626
self.nodes[0].assert_start_raises_init_error(['-mnemonicbits=123'], "Error: Invalid '-mnemonicbits'. Allowed values: 128, 160, 192, 224, 256.")
2727
self.start_node(0)
2828

29-
mnemonic_pre = get_mnemonic(self.nodes[0])
29+
mnemonic_pre = get_mnemonic(self.nodes[0])[0]
3030

3131

3232
self.nodes[0].encryptwallet('pass')
@@ -74,11 +74,11 @@ def run_test(self):
7474
self.nodes[0].createwallet("wallet_256", blank=True, descriptors=self.options.descriptors) # blank wallet
7575
self.nodes[0].get_wallet_rpc("wallet_256").upgradetohd()
7676

77-
assert_equal(len(get_mnemonic(self.nodes[0].get_wallet_rpc(self.default_wallet_name)).split()), 12) # 12 words by default
78-
assert_equal(len(get_mnemonic(self.nodes[0].get_wallet_rpc("wallet_160")).split()), 15) # 15 words
79-
assert_equal(len(get_mnemonic(self.nodes[0].get_wallet_rpc("wallet_192")).split()), 18) # 18 words
80-
assert_equal(len(get_mnemonic(self.nodes[0].get_wallet_rpc("wallet_224")).split()), 21) # 21 words
81-
assert_equal(len(get_mnemonic(self.nodes[0].get_wallet_rpc("wallet_256")).split()), 24) # 24 words
77+
assert_equal(len(get_mnemonic(self.nodes[0].get_wallet_rpc(self.default_wallet_name))[0].split()), 12) # 12 words by default
78+
assert_equal(len(get_mnemonic(self.nodes[0].get_wallet_rpc("wallet_160"))[0].split()), 15) # 15 words
79+
assert_equal(len(get_mnemonic(self.nodes[0].get_wallet_rpc("wallet_192"))[0].split()), 18) # 18 words
80+
assert_equal(len(get_mnemonic(self.nodes[0].get_wallet_rpc("wallet_224"))[0].split()), 21) # 21 words
81+
assert_equal(len(get_mnemonic(self.nodes[0].get_wallet_rpc("wallet_256"))[0].split()), 24) # 24 words
8282

8383

8484
if __name__ == '__main__':

test/functional/wallet_upgradetohd.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,9 @@ def run_test(self):
110110

111111
self.log.info("Same mnemonic, another mnemonic passphrase, no wallet passphrase, should result in a different set of keys")
112112
new_mnemonic_passphrase = "somewords"
113-
assert node.upgradetohd(mnemonic, new_mnemonic_passphrase)
114-
assert_equal(mnemonic, get_mnemonic(node))
113+
assert node.upgradetohd(mnemonic[0], new_mnemonic_passphrase)
114+
assert_equal(mnemonic[0], get_mnemonic(node)[0])
115+
assert_equal(new_mnemonic_passphrase, get_mnemonic(node)[1])
115116
if not self.options.descriptors:
116117
new_chainid = node.getwalletinfo()['hdchainid']
117118
assert chainid != new_chainid
@@ -125,8 +126,9 @@ def run_test(self):
125126
self.recover_non_hd()
126127

127128
self.log.info("Same mnemonic, another mnemonic passphrase, no wallet passphrase, should result in a different set of keys (again)")
128-
assert node.upgradetohd(mnemonic, new_mnemonic_passphrase)
129-
assert_equal(mnemonic, get_mnemonic(node))
129+
assert node.upgradetohd(mnemonic[0], new_mnemonic_passphrase)
130+
assert_equal(mnemonic[0], get_mnemonic(node)[0])
131+
assert_equal(new_mnemonic_passphrase, get_mnemonic(node)[1])
130132
if not self.options.descriptors:
131133
assert_equal(new_chainid, node.getwalletinfo()['hdchainid'])
132134
assert_equal(balance_non_HD, node.getbalance())
@@ -139,7 +141,7 @@ def run_test(self):
139141
self.recover_non_hd()
140142

141143
self.log.info("Same mnemonic, no mnemonic passphrase, no wallet passphrase, should recover all coins after rescan")
142-
assert node.upgradetohd(mnemonic)
144+
assert node.upgradetohd(mnemonic[0], mnemonic[1])
143145
assert_equal(mnemonic, get_mnemonic(node))
144146
if not self.options.descriptors:
145147
assert_equal(chainid, node.getwalletinfo()['hdchainid'])
@@ -152,7 +154,7 @@ def run_test(self):
152154

153155
self.log.info("Same mnemonic, no mnemonic passphrase, no wallet passphrase, large enough keepool, should recover all coins with no extra rescan")
154156
self.restart_node(0, extra_args=['-keypool=10'])
155-
assert node.upgradetohd(mnemonic)
157+
assert node.upgradetohd(mnemonic[0], mnemonic[1])
156158
assert_equal(mnemonic, get_mnemonic(node))
157159
if not self.options.descriptors:
158160
assert_equal(chainid, node.getwalletinfo()['hdchainid'])
@@ -163,7 +165,7 @@ def run_test(self):
163165

164166
self.log.info("Same mnemonic, no mnemonic passphrase, no wallet passphrase, large enough keepool, rescan is skipped initially, should recover all coins after rescanblockchain")
165167
self.restart_node(0, extra_args=['-keypool=10'])
166-
assert node.upgradetohd(mnemonic, "", "", False)
168+
assert node.upgradetohd(mnemonic[0], mnemonic[1], "", False)
167169
assert_equal(mnemonic, get_mnemonic(node))
168170
if not self.options.descriptors:
169171
assert_equal(chainid, node.getwalletinfo()['hdchainid'])
@@ -176,7 +178,7 @@ def run_test(self):
176178

177179
self.log.info("Same mnemonic, same mnemonic passphrase, encrypt wallet on upgrade, should recover all coins after rescan")
178180
walletpass = "111pass222"
179-
assert node.upgradetohd(mnemonic, "", walletpass)
181+
assert node.upgradetohd(mnemonic[0], "", walletpass)
180182
node.stop()
181183
node.wait_until_stopped()
182184
self.start_node(0, extra_args=['-rescan'])
@@ -197,14 +199,15 @@ def run_test(self):
197199
self.recover_non_hd()
198200

199201
self.log.info("Same mnemonic, same mnemonic passphrase, encrypt wallet first, should recover all coins on upgrade after rescan")
200-
walletpass = "111pass222"
202+
# Null characters are allowed in wallet passphrases since v23
203+
walletpass = "111\0pass222"
201204
node.encryptwallet(walletpass)
202205
node.stop()
203206
node.wait_until_stopped()
204207
self.start_node(0, extra_args=['-rescan'])
205-
assert_raises_rpc_error(-13, "Error: Wallet encrypted but passphrase not supplied to RPC.", node.upgradetohd, mnemonic)
206-
assert_raises_rpc_error(-14, "Error: The wallet passphrase entered was incorrect", node.upgradetohd, mnemonic, "", "wrongpass")
207-
assert node.upgradetohd(mnemonic, "", walletpass)
208+
assert_raises_rpc_error(-13, "Error: Wallet encrypted but passphrase not supplied to RPC.", node.upgradetohd, mnemonic[0])
209+
assert_raises_rpc_error(-14, "Error: The wallet passphrase entered was incorrect", node.upgradetohd, mnemonic[0], "", "111")
210+
assert node.upgradetohd(mnemonic[0], "", walletpass)
208211
if not self.options.descriptors:
209212
assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", node.dumphdinfo)
210213
else:
@@ -241,9 +244,19 @@ def run_test(self):
241244
node.createwallet("wallet-12", blank=True)
242245
w12 = node.get_wallet_rpc("wallet-12")
243246
w12.upgradetohd(custom_mnemonic, "custom-passphrase")
247+
assert_equal(get_mnemonic(w12)[0], custom_mnemonic)
248+
assert_equal(get_mnemonic(w12)[1], "custom-passphrase")
244249
assert_equal(12, w12.getbalance())
245250
w12.unloadwallet()
246251

252+
self.log.info("Check if null character at the end of mnemonic-passphrase matters")
253+
node.createwallet("wallet-null", blank=True)
254+
w_null = node.get_wallet_rpc("wallet-null")
255+
w_null.upgradetohd(custom_mnemonic, "custom-passphrase\0")
256+
assert_equal(0, w_null.getbalance())
257+
assert_equal(get_mnemonic(w_null)[1], "custom-passphrase\0")
258+
w_null.unloadwallet()
259+
247260

248261
if __name__ == '__main__':
249262
WalletUpgradeToHDTest().main ()

0 commit comments

Comments
 (0)