diff --git a/src/i2p.cpp b/src/i2p.cpp index fb365e6..8773876 100644 --- a/src/i2p.cpp +++ b/src/i2p.cpp @@ -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 { diff --git a/src/rpcwallet.cpp b/src/rpcwallet.cpp index b5aa930..34e9ab0 100644 --- a/src/rpcwallet.cpp +++ b/src/rpcwallet.cpp @@ -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; } diff --git a/src/wallet.cpp b/src/wallet.cpp index 5598416..e9c468f 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -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 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 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); } diff --git a/src/wallet.h b/src/wallet.h index b3567a7..f5329dd 100644 --- a/src/wallet.h +++ b/src/wallet.h @@ -132,6 +132,9 @@ public: std::string hdMnemonic; // in-memory phrase (present when unlocked/unencrypted) std::vector 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 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(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& 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& 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) diff --git a/src/walletdb.cpp b/src/walletdb.cpp index 88ee553..61f8848 100644 --- a/src/walletdb.cpp +++ b/src/walletdb.cpp @@ -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> 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> cp; + ssValue >> cp; + pwallet->LoadCryptedHDPassphrase(cp.first, cp.second); } else if (strType == "hdchain") { int64_t n; ssValue >> n; diff --git a/src/walletdb.h b/src/walletdb.h index 9c3eef3..07ccce4 100644 --- a/src/walletdb.h +++ b/src/walletdb.h @@ -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& 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) {