wallet: add BIP39 passphrase support throughout HD lifecycle

hdPassphrase was hardcoded to empty string in DeriveHDKey, meaning users
who set a BIP39 passphrase during seed creation would derive different
addresses after restoration. This adds proper passphrase storage,
encryption, and decryption alongside the existing mnemonic handling:

- wallet.h: hdPassphrase + vchCryptedHDPassphrase + hdPassphraseIV fields
- wallet.cpp: Lock/Unlock/EncryptWallet/SetHDSeed all handle passphrase
  with the same encrypt/decrypt lifecycle as the mnemonic
- DeriveHDKey now passes hdPassphrase to DeriveTriangles (not hardcoded )
- walletdb.h: WriteHDPassphrase/WriteHDCryptedPassphrase/EraseHDPassphrase
- walletdb.cpp: ReadKeyValue handles hdpassphrase/hdcpassphrase records
- rpcwallet.cpp: hdnew/hdshow show passphrase_used + warnings

Also: i2p.cpp hardens I2P private key file permissions to owner-only.

From Claude's uncommitted work on E:\repos\triangles (SAMI-PC). The rest
of Claude's modernization (Boost removal, RPC rewrite, RocksDB default,
SQLite wallet) was already committed to master in bfdb399 and follow-ups.
This commit is contained in:
Krystie
2026-07-01 14:19:42 -07:00
parent ac0adfea15
commit a70019263d
6 changed files with 92 additions and 6 deletions
+12
View File
@@ -223,6 +223,18 @@ bool CI2PSession::LoadOrCreateDestination(std::string& strPrivKeyRet)
if (out.is_open()) {
out << priv << std::endl;
out.close();
// The I2P destination private key identifies this node on
// the I2P network: owner-only permissions, like Tor's
// hidden-service secret key. (No-op semantics differ on
// Windows ACLs; harmless there.)
std::error_code ec;
std::filesystem::permissions(keyPath,
std::filesystem::perms::owner_read |
std::filesystem::perms::owner_write,
std::filesystem::perm_options::replace, ec);
if (ec)
printf("I2P: WARNING could not restrict permissions on %s: %s\n",
keyPath.string().c_str(), ec.message().c_str());
printf("I2P: generated and saved new persistent destination\n");
ok = true;
} else {
+9 -2
View File
@@ -1919,7 +1919,10 @@ Value hdnew(const Array& params, bool fHelp)
Object obj;
obj.push_back(Pair("mnemonic", mnemonic));
obj.push_back(Pair("words", 24));
obj.push_back(Pair("warning", "Write these 24 words down and keep them secret and offline. Anyone with them can spend your coins."));
obj.push_back(Pair("passphrase_used", !passphrase.empty()));
obj.push_back(Pair("warning", passphrase.empty()
? "Write these 24 words down and keep them secret and offline. Anyone with them can spend your coins."
: "Write these 24 words down and keep them secret and offline. Anyone with them can spend your coins. You ALSO set a BIP39 passphrase: the words alone will NOT restore this wallet — back up the passphrase separately."));
return obj;
}
@@ -1958,6 +1961,10 @@ Value hdshow(const Array& params, bool fHelp)
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet has no HD seed (use 'hdnew' to create one).");
Object obj;
obj.push_back(Pair("mnemonic", mnemonic));
obj.push_back(Pair("warning", "Keep these words secret and offline."));
obj.push_back(Pair("passphrase_used", !pwalletMain->hdPassphrase.empty()));
if (!pwalletMain->hdPassphrase.empty())
obj.push_back(Pair("warning", "Keep these words secret and offline. A BIP39 passphrase is ALSO set: the words alone will NOT restore this wallet — back up the passphrase separately."));
else
obj.push_back(Pair("warning", "Keep these words secret and offline."));
return obj;
}
+37 -3
View File
@@ -221,8 +221,10 @@ bool CWallet::Lock()
if (fDebug)
printf("Locking wallet.\n");
if (IsCrypted())
hdMnemonic.clear(); // keep only the encrypted copy while locked
if (IsCrypted()) {
hdMnemonic.clear(); // keep only the encrypted copies while locked
hdPassphrase.clear();
}
{
LOCK(cs_wallet);
@@ -254,6 +256,11 @@ bool CWallet::Unlock(const SecureString& strWalletPassphrase)
if (DecryptSecret(vMasterKey, vchCryptedHDMnemonic, hdMnemonicIV, sec))
hdMnemonic.assign(sec.begin(), sec.end());
}
if (fHDEnabled && hdPassphrase.empty() && !vchCryptedHDPassphrase.empty()) {
CSecret psec;
if (DecryptSecret(vMasterKey, vchCryptedHDPassphrase, hdPassphraseIV, psec))
hdPassphrase.assign(psec.begin(), psec.end());
}
return true;
}
}
@@ -436,6 +443,14 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
hdMnemonicIV = iv; vchCryptedHDMnemonic = cipher;
dbEnc->WriteHDCryptedMnemonic(iv, cipher);
}
if (fHDEnabled && !hdPassphrase.empty()) {
CSecret psec(hdPassphrase.begin(), hdPassphrase.end());
uint256 piv = GetRandHash();
std::vector<unsigned char> pcipher;
if (!EncryptSecret(vMasterKey, psec, piv, pcipher)) { dbEnc->TxnAbort(); return false; }
hdPassphraseIV = piv; vchCryptedHDPassphrase = pcipher;
dbEnc->WriteHDCryptedPassphrase(piv, pcipher);
}
SetMinVersion(WalletFeature::WalletCrypt, dbEnc.get(), true);
@@ -2933,8 +2948,11 @@ bool CWallet::DeriveHDKey(int64_t index, CKey& keyOut) const
{
if (hdMnemonic.empty())
return false;
// If a BIP39 passphrase ("25th word") was set with the seed, it MUST be
// part of every derivation — otherwise restored wallets derive different
// addresses than the originals. Empty string = no passphrase (legacy).
unsigned char priv[32];
if (!hd::DeriveTriangles(hdMnemonic, "", 0, 0, (uint32_t)index, priv))
if (!hd::DeriveTriangles(hdMnemonic, hdPassphrase, 0, 0, (uint32_t)index, priv))
return false;
CSecret secret(priv, priv + 32);
memset(priv, 0, sizeof(priv));
@@ -2968,6 +2986,7 @@ bool CWallet::SetHDSeed(const std::string& mnemonicIn, const std::string& passph
memset(priv, 0, sizeof(priv));
hdMnemonic = m;
hdPassphrase = passphrase;
fHDEnabled = true;
nHDChainIndex = 0;
@@ -2980,8 +2999,23 @@ bool CWallet::SetHDSeed(const std::string& mnemonicIn, const std::string& passph
if (!EncryptSecret(vMasterKey, sec, iv, cipher)) { strError = "Failed to encrypt seed."; return false; }
hdMnemonicIV = iv; vchCryptedHDMnemonic = cipher;
wdb.WriteHDCryptedMnemonic(iv, cipher);
if (!passphrase.empty()) {
CSecret psec(passphrase.begin(), passphrase.end());
uint256 piv = GetRandHash();
std::vector<unsigned char> pcipher;
if (!EncryptSecret(vMasterKey, psec, piv, pcipher)) { strError = "Failed to encrypt passphrase."; return false; }
hdPassphraseIV = piv; vchCryptedHDPassphrase = pcipher;
wdb.WriteHDCryptedPassphrase(piv, pcipher);
} else {
vchCryptedHDPassphrase.clear();
wdb.EraseHDPassphrase(); // re-seed without passphrase: drop any old record
}
} else {
wdb.WriteHDMnemonic(m);
if (!passphrase.empty())
wdb.WriteHDPassphrase(passphrase);
else
wdb.EraseHDPassphrase();
}
wdb.WriteHDChain(nHDChainIndex);
}
+5
View File
@@ -132,6 +132,9 @@ public:
std::string hdMnemonic; // in-memory phrase (present when unlocked/unencrypted)
std::vector<unsigned char> vchCryptedHDMnemonic; // encrypted phrase (loaded, decrypted on unlock)
uint256 hdMnemonicIV; // IV for the encrypted phrase
std::string hdPassphrase; // BIP39 "25th word"; empty = none. Same lifecycle as hdMnemonic.
std::vector<unsigned char> vchCryptedHDPassphrase; // encrypted passphrase (loaded, decrypted on unlock)
uint256 hdPassphraseIV; // IV for the encrypted passphrase
// check whether we are allowed to upgrade (or already support) to the named feature
bool CanSupportFeature(WalletFeature wf) { return nWalletMaxVersion >= static_cast<int>(wf); }
@@ -150,6 +153,8 @@ public:
bool DeriveHDKey(int64_t index, CKey& keyOut) const;
bool LoadHDMnemonic(const std::string& m) { hdMnemonic = m; fHDEnabled = true; return true; }
bool LoadCryptedHDMnemonic(const uint256& iv, const std::vector<unsigned char>& cipher) { hdMnemonicIV = iv; vchCryptedHDMnemonic = cipher; fHDEnabled = true; return true; }
bool LoadHDPassphrase(const std::string& p) { hdPassphrase = p; return true; }
bool LoadCryptedHDPassphrase(const uint256& iv, const std::vector<unsigned char>& cipher) { hdPassphraseIV = iv; vchCryptedHDPassphrase = cipher; return true; }
// Adds a key to the store, and saves it to disk.
bool AddKey(const CKey& key);
// Adds a key to the store, without saving it to disk (used by LoadWallet)
+10 -1
View File
@@ -265,7 +265,8 @@ static bool IsKeyType(const std::string& strType)
{
return (strType == "key" || strType == "wkey" ||
strType == "mkey" || strType == "ckey" ||
strType == "hdmnemonic" || strType == "hdcmnemonic");
strType == "hdmnemonic" || strType == "hdcmnemonic" ||
strType == "hdpassphrase" || strType == "hdcpassphrase");
}
static bool ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
@@ -414,6 +415,14 @@ static bool ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssVa
std::pair<uint256, std::vector<unsigned char>> cm;
ssValue >> cm;
pwallet->LoadCryptedHDMnemonic(cm.first, cm.second);
} else if (strType == "hdpassphrase") {
std::string p;
ssValue >> p;
pwallet->LoadHDPassphrase(p);
} else if (strType == "hdcpassphrase") {
std::pair<uint256, std::vector<unsigned char>> cp;
ssValue >> cp;
pwallet->LoadCryptedHDPassphrase(cp.first, cp.second);
} else if (strType == "hdchain") {
int64_t n;
ssValue >> n;
+19
View File
@@ -178,6 +178,25 @@ public:
nWalletDBUpdated++;
return Write(std::string("hdchain"), nIndex);
}
// BIP39 passphrase ("25th word"). Same plaintext/crypted lifecycle as the
// mnemonic: exactly one of the two records exists at a time; both absent
// means no passphrase (legacy wallets and the common case).
bool WriteHDPassphrase(const std::string& passphrase) {
nWalletDBUpdated++;
Erase(std::string("hdcpassphrase"));
return Write(std::string("hdpassphrase"), passphrase);
}
bool WriteHDCryptedPassphrase(const uint256& iv, const std::vector<unsigned char>& cipher) {
nWalletDBUpdated++;
Erase(std::string("hdpassphrase"));
return Write(std::string("hdcpassphrase"), std::make_pair(iv, cipher));
}
bool EraseHDPassphrase() {
nWalletDBUpdated++;
Erase(std::string("hdpassphrase"));
Erase(std::string("hdcpassphrase"));
return true;
}
bool ReadPool(int64_t nPool, CKeyPool& keypool)
{