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:
+12
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user