Derive random HD seeds from ZIP-339 seed phrases.

Add support for dump of legacy HD seeds & persistence of mnemonic-based seeds.
This commit is contained in:
Kris Nuttycombe 2021-08-19 15:32:24 -06:00
parent d6fc064018
commit 5677649af1
16 changed files with 561 additions and 204 deletions

View File

@ -16,29 +16,31 @@
TEST(KeystoreTests, StoreAndRetrieveHDSeed) {
CBasicKeyStore keyStore;
HDSeed seedOut;
// When we haven't set a seed, we shouldn't get one
EXPECT_FALSE(keyStore.HaveHDSeed());
EXPECT_FALSE(keyStore.GetHDSeed(seedOut));
EXPECT_FALSE(keyStore.HaveMnemonicSeed());
auto seedOut = keyStore.GetMnemonicSeed();
EXPECT_FALSE(seedOut.has_value());
// Generate a random seed
auto seed = HDSeed::Random();
auto seed = MnemonicSeed::Random();
// We should be able to set and retrieve the seed
ASSERT_TRUE(keyStore.SetHDSeed(seed));
EXPECT_TRUE(keyStore.HaveHDSeed());
ASSERT_TRUE(keyStore.GetHDSeed(seedOut));
EXPECT_EQ(seed, seedOut);
ASSERT_TRUE(keyStore.SetMnemonicSeed(seed));
EXPECT_TRUE(keyStore.HaveMnemonicSeed());
seedOut = keyStore.GetMnemonicSeed();
ASSERT_TRUE(seedOut.has_value());
EXPECT_EQ(seed, seedOut.value());
// Generate another random seed
auto seed2 = HDSeed::Random();
auto seed2 = MnemonicSeed::Random();
EXPECT_NE(seed, seed2);
// We should not be able to set and retrieve a different seed
EXPECT_FALSE(keyStore.SetHDSeed(seed2));
ASSERT_TRUE(keyStore.GetHDSeed(seedOut));
EXPECT_EQ(seed, seedOut);
EXPECT_FALSE(keyStore.SetMnemonicSeed(seed2));
seedOut = keyStore.GetMnemonicSeed();
ASSERT_TRUE(seedOut.has_value());
EXPECT_EQ(seed, seedOut.value());
}
TEST(KeystoreTests, SaplingKeys) {
@ -285,20 +287,22 @@ TEST(KeystoreTests, StoreAndRetrieveHDSeedInEncryptedStore) {
TestCCryptoKeyStore keyStore;
CKeyingMaterial vMasterKey(32, 0);
GetRandBytes(vMasterKey.data(), 32);
HDSeed seedOut;
// 1) Test adding a seed to an unencrypted key store, then encrypting it
auto seed = HDSeed::Random();
EXPECT_FALSE(keyStore.HaveHDSeed());
EXPECT_FALSE(keyStore.GetHDSeed(seedOut));
auto seed = MnemonicSeed::Random();
EXPECT_FALSE(keyStore.HaveMnemonicSeed());
auto seedOut = keyStore.GetMnemonicSeed();
EXPECT_FALSE(seedOut.has_value());
ASSERT_TRUE(keyStore.SetHDSeed(seed));
EXPECT_TRUE(keyStore.HaveHDSeed());
ASSERT_TRUE(keyStore.GetHDSeed(seedOut));
EXPECT_EQ(seed, seedOut);
ASSERT_TRUE(keyStore.SetMnemonicSeed(seed));
EXPECT_TRUE(keyStore.HaveMnemonicSeed());
seedOut = keyStore.GetMnemonicSeed();
ASSERT_TRUE(seedOut.has_value());
EXPECT_EQ(seed, seedOut.value());
ASSERT_TRUE(keyStore.EncryptKeys(vMasterKey));
EXPECT_FALSE(keyStore.GetHDSeed(seedOut));
seedOut = keyStore.GetMnemonicSeed();
EXPECT_FALSE(seedOut.has_value());
// Unlocking with a random key should fail
CKeyingMaterial vRandomKey(32, 0);
@ -312,15 +316,17 @@ TEST(KeystoreTests, StoreAndRetrieveHDSeedInEncryptedStore) {
// Unlocking with vMasterKey should succeed
ASSERT_TRUE(keyStore.Unlock(vMasterKey));
ASSERT_TRUE(keyStore.GetHDSeed(seedOut));
EXPECT_EQ(seed, seedOut);
seedOut = keyStore.GetMnemonicSeed();
ASSERT_TRUE(seedOut.has_value());
EXPECT_EQ(seed, seedOut.value());
// 2) Test replacing the seed in an already-encrypted key store fails
auto seed2 = HDSeed::Random();
EXPECT_FALSE(keyStore.SetHDSeed(seed2));
EXPECT_TRUE(keyStore.HaveHDSeed());
ASSERT_TRUE(keyStore.GetHDSeed(seedOut));
EXPECT_EQ(seed, seedOut);
auto seed2 = MnemonicSeed::Random();
EXPECT_FALSE(keyStore.SetMnemonicSeed(seed2));
EXPECT_TRUE(keyStore.HaveMnemonicSeed());
seedOut = keyStore.GetMnemonicSeed();
ASSERT_TRUE(seedOut.has_value());
EXPECT_EQ(seed, seedOut.value());
// 3) Test adding a new seed to an already-encrypted key store
TestCCryptoKeyStore keyStore2;
@ -331,14 +337,16 @@ TEST(KeystoreTests, StoreAndRetrieveHDSeedInEncryptedStore) {
ASSERT_TRUE(keyStore2.EncryptKeys(vMasterKey));
ASSERT_TRUE(keyStore2.Unlock(vMasterKey));
EXPECT_FALSE(keyStore2.HaveHDSeed());
EXPECT_FALSE(keyStore2.GetHDSeed(seedOut));
EXPECT_FALSE(keyStore2.HaveMnemonicSeed());
seedOut = keyStore2.GetMnemonicSeed();
EXPECT_FALSE(seedOut.has_value());
auto seed3 = HDSeed::Random();
ASSERT_TRUE(keyStore2.SetHDSeed(seed3));
EXPECT_TRUE(keyStore2.HaveHDSeed());
ASSERT_TRUE(keyStore2.GetHDSeed(seedOut));
EXPECT_EQ(seed3, seedOut);
auto seed3 = MnemonicSeed::Random();
ASSERT_TRUE(keyStore2.SetMnemonicSeed(seed3));
EXPECT_TRUE(keyStore2.HaveMnemonicSeed());
seedOut = keyStore2.GetMnemonicSeed();
ASSERT_TRUE(seedOut.has_value());
EXPECT_EQ(seed3, seedOut.value());
}
TEST(KeystoreTests, StoreAndRetrieveSpendingKeyInEncryptedStore) {

View File

@ -11,6 +11,9 @@ TEST(ZIP32, TestVectors) {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
HDSeed seed(rawSeed);
// TODO: Regenerate test vectors using a BIP-44-derived seed.
// std::string mnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art");
// HDSeed seed(English, mnemonic);
auto m = libzcash::SaplingExtendedSpendingKey::Master(seed);
EXPECT_EQ(m.depth, 0);

View File

@ -30,33 +30,50 @@ bool CBasicKeyStore::GetPubKey(const CKeyID &address, CPubKey &vchPubKeyOut) con
return true;
}
bool CBasicKeyStore::SetHDSeed(const HDSeed& seed)
bool CBasicKeyStore::SetMnemonicSeed(const MnemonicSeed& seed)
{
LOCK(cs_KeyStore);
if (!hdSeed.IsNull()) {
if (mnemonicSeed.has_value()) {
// Don't allow an existing seed to be changed. We can maybe relax this
// restriction later once we have worked out the UX implications.
return false;
}
hdSeed = seed;
mnemonicSeed = seed;
return true;
}
bool CBasicKeyStore::HaveHDSeed() const
bool CBasicKeyStore::HaveMnemonicSeed() const
{
LOCK(cs_KeyStore);
return !hdSeed.IsNull();
return mnemonicSeed.has_value();
}
bool CBasicKeyStore::GetHDSeed(HDSeed& seedOut) const
std::optional<MnemonicSeed> CBasicKeyStore::GetMnemonicSeed() const
{
LOCK(cs_KeyStore);
if (hdSeed.IsNull()) {
return mnemonicSeed;
}
bool CBasicKeyStore::SetLegacyHDSeed(const HDSeed& seed)
{
if (legacySeed.has_value()) {
// Don't allow an existing seed to be changed.
return false;
} else {
seedOut = hdSeed;
return true;
}
legacySeed = seed;
return true;
}
bool CBasicKeyStore::HaveLegacyHDSeed() const
{
LOCK(cs_KeyStore);
return legacySeed.has_value();
}
std::optional<HDSeed> CBasicKeyStore::GetLegacyHDSeed() const
{
LOCK(cs_KeyStore);
return legacySeed;
}
bool CBasicKeyStore::AddKeyPubKey(const CKey& key, const CPubKey &pubkey)

View File

@ -25,11 +25,16 @@ protected:
public:
virtual ~CKeyStore() {}
//! Set the HD seed for this keystore
virtual bool SetHDSeed(const HDSeed& seed) =0;
virtual bool HaveHDSeed() const =0;
//! Get the HD seed for this keystore
virtual bool GetHDSeed(HDSeed& seedOut) const =0;
//! Set the mnemonic HD seed for this keystore
virtual bool SetMnemonicSeed(const MnemonicSeed& seed) =0;
virtual bool HaveMnemonicSeed() const =0;
//! Get the mnemonic HD seed for this keystore
virtual std::optional<MnemonicSeed> GetMnemonicSeed() const =0;
//! Set the legacy HD seed for this keystore
virtual bool SetLegacyHDSeed(const HDSeed& seed) =0;
//! Get the legacy HD seed for this keystore
virtual std::optional<HDSeed> GetLegacyHDSeed() const =0;
//! Add a key to the store.
virtual bool AddKeyPubKey(const CKey &key, const CPubKey &pubkey) =0;
@ -119,7 +124,8 @@ typedef std::map<libzcash::SaplingPaymentAddress, libzcash::SaplingIncomingViewi
class CBasicKeyStore : public CKeyStore
{
protected:
HDSeed hdSeed;
std::optional<MnemonicSeed> mnemonicSeed;
std::optional<HDSeed> legacySeed;
KeyMap mapKeys;
WatchKeyMap mapWatchKeys;
ScriptMap mapScripts;
@ -133,9 +139,13 @@ protected:
SaplingIncomingViewingKeyMap mapSaplingIncomingViewingKeys;
public:
bool SetHDSeed(const HDSeed& seed);
bool HaveHDSeed() const;
bool GetHDSeed(HDSeed& seedOut) const;
bool SetMnemonicSeed(const MnemonicSeed& seed);
bool HaveMnemonicSeed() const;
std::optional<MnemonicSeed> GetMnemonicSeed() const;
bool SetLegacyHDSeed(const HDSeed& seed);
bool HaveLegacyHDSeed() const;
std::optional<HDSeed> GetLegacyHDSeed() const;
bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey);
bool GetPubKey(const CKeyID &address, CPubKey& vchPubKeyOut) const;

View File

@ -316,8 +316,8 @@ void RegtestDeactivateNU5() {
}
libzcash::SaplingExtendedSpendingKey GetTestMasterSaplingSpendingKey() {
std::vector<unsigned char, secure_allocator<unsigned char>> rawSeed(32);
HDSeed seed(rawSeed);
std::string mnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art");
MnemonicSeed seed(English, mnemonic);
return libzcash::SaplingExtendedSpendingKey::Master(seed);
}

View File

@ -130,21 +130,56 @@ static bool DecryptSecret(const CKeyingMaterial& vMasterKey, const std::vector<u
return cKeyCrypter.Decrypt(vchCiphertext, *((CKeyingMaterial*)&vchPlaintext));
}
static bool DecryptHDSeed(
static CKeyingMaterial EncryptMnemonicSeed(const MnemonicSeed& seed)
{
CSecureDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << seed;
CKeyingMaterial vchSecret(ss.begin(), ss.end());
return vchSecret;
}
static std::optional<MnemonicSeed> DecryptMnemonicSeed(
const CKeyingMaterial& vMasterKey,
const std::vector<unsigned char>& vchCryptedSecret,
const uint256& seedFp,
HDSeed& seed)
const uint256& seedFp)
{
CKeyingMaterial vchSecret;
// Use seed's fingerprint as IV
// TODO: Handle IV properly when we make encryption a supported feature
if(!DecryptSecret(vMasterKey, vchCryptedSecret, seedFp, vchSecret))
return false;
if(DecryptSecret(vMasterKey, vchCryptedSecret, seedFp, vchSecret)) {
CSecureDataStream ss(vchSecret, SER_NETWORK, PROTOCOL_VERSION);
auto seed = MnemonicSeed::Read(ss);
if (seed.Fingerprint() == seedFp) {
return seed;
} else {
return std::nullopt;
}
} else {
return std::nullopt;
}
}
seed = HDSeed(vchSecret);
return seed.Fingerprint() == seedFp;
static std::optional<HDSeed> DecryptLegacyHDSeed(
const CKeyingMaterial& vMasterKey,
const std::vector<unsigned char>& vchCryptedSecret,
const uint256& seedFp)
{
CKeyingMaterial vchSecret;
// Use seed's fingerprint as IV
// TODO: Handle IV properly when we make encryption a supported feature
if(DecryptSecret(vMasterKey, vchCryptedSecret, seedFp, vchSecret)) {
auto seed = HDSeed(vchSecret);
if (seed.Fingerprint() == seedFp) {
return seed;
} else {
return std::nullopt;
}
} else {
return std::nullopt;
}
}
static bool DecryptKey(const CKeyingMaterial& vMasterKey, const std::vector<unsigned char>& vchCryptedSecret, const CPubKey& vchPubKey, CKey& key)
@ -227,9 +262,22 @@ bool CCryptoKeyStore::Unlock(const CKeyingMaterial& vMasterKeyIn)
bool keyPass = false;
bool keyFail = false;
if (!cryptedHDSeed.first.IsNull()) {
HDSeed seed;
if (!DecryptHDSeed(vMasterKeyIn, cryptedHDSeed.second, cryptedHDSeed.first, seed))
if (!cryptedMnemonicSeed.first.IsNull()) {
// Check that we can successfully decrypt the mnemonic seed, if present
auto seed = DecryptMnemonicSeed(vMasterKeyIn, cryptedMnemonicSeed.second, cryptedMnemonicSeed.first);
if (!seed.has_value())
{
keyFail = true;
} else {
keyPass = true;
}
}
if (cryptedLegacySeed.has_value()) {
// Check that we can successfully decrypt the legacy seed, if present
auto seed = DecryptLegacyHDSeed(vMasterKeyIn,
cryptedLegacySeed.value().second,
cryptedLegacySeed.value().first);
if (!seed.has_value())
{
keyFail = true;
} else {
@ -295,71 +343,146 @@ bool CCryptoKeyStore::Unlock(const CKeyingMaterial& vMasterKeyIn)
return true;
}
bool CCryptoKeyStore::SetHDSeed(const HDSeed& seed)
//
// Mnemonic HD seeds
//
bool CCryptoKeyStore::SetMnemonicSeed(const MnemonicSeed& seed)
{
{
LOCK(cs_KeyStore);
if (!fUseCrypto) {
return CBasicKeyStore::SetHDSeed(seed);
return CBasicKeyStore::SetMnemonicSeed(seed);
}
if (IsLocked())
return false;
std::vector<unsigned char> vchCryptedSecret;
// Use seed's fingerprint as IV
// TODO: Handle this properly when we make encryption a supported feature
auto seedFp = seed.Fingerprint();
if (!EncryptSecret(vMasterKey, seed.RawSeed(), seedFp, vchCryptedSecret))
CKeyingMaterial vchSecret = EncryptMnemonicSeed(seed);
std::vector<unsigned char> vchCryptedSecret;
if (!EncryptSecret(vMasterKey, vchSecret, seedFp, vchCryptedSecret))
return false;
// This will call into CWallet to store the crypted seed to disk
if (!SetCryptedHDSeed(seedFp, vchCryptedSecret))
if (!SetCryptedMnemonicSeed(seedFp, vchCryptedSecret))
return false;
}
return true;
}
bool CCryptoKeyStore::SetCryptedHDSeed(
bool CCryptoKeyStore::SetCryptedMnemonicSeed(
const uint256& seedFp,
const std::vector<unsigned char>& vchCryptedSecret)
{
LOCK(cs_KeyStore);
if (!fUseCrypto) {
if (!fUseCrypto || !cryptedMnemonicSeed.first.IsNull()) {
// Require encryption & don't allow an existing seed to be changed.
return false;
} else {
cryptedMnemonicSeed = std::make_pair(seedFp, vchCryptedSecret);
return true;
}
}
if (!cryptedHDSeed.first.IsNull()) {
// Don't allow an existing seed to be changed. We can maybe relax this
// restriction later once we have worked out the UX implications.
return false;
bool CCryptoKeyStore::HaveMnemonicSeed() const
{
LOCK(cs_KeyStore);
if (fUseCrypto) {
return !cryptedMnemonicSeed.second.empty();
} else {
return CBasicKeyStore::HaveMnemonicSeed();
}
}
cryptedHDSeed = std::make_pair(seedFp, vchCryptedSecret);
std::optional<MnemonicSeed> CCryptoKeyStore::GetMnemonicSeed() const
{
LOCK(cs_KeyStore);
if (fUseCrypto) {
if (cryptedMnemonicSeed.second.empty()) {
return std::nullopt;
} else {
return DecryptMnemonicSeed(vMasterKey, cryptedMnemonicSeed.second, cryptedMnemonicSeed.first);
}
} else {
return CBasicKeyStore::GetMnemonicSeed();
}
}
//
// Legacy HD seeds
//
bool CCryptoKeyStore::SetLegacyHDSeed(const HDSeed& seed)
{
{
LOCK(cs_KeyStore);
if (!fUseCrypto) {
return CBasicKeyStore::SetLegacyHDSeed(seed);
}
if (IsLocked())
return false;
// Use seed's fingerprint as IV
// TODO: Handle this properly when we make encryption a supported feature
auto seedFp = seed.Fingerprint();
std::vector<unsigned char> vchCryptedSecret;
if (!EncryptSecret(vMasterKey, seed.RawSeed(), seedFp, vchCryptedSecret))
return false;
// This will call into CWallet to store the crypted seed to disk
if (!SetCryptedLegacyHDSeed(seedFp, vchCryptedSecret))
return false;
}
return true;
}
bool CCryptoKeyStore::HaveHDSeed() const
bool CCryptoKeyStore::SetCryptedLegacyHDSeed(
const uint256& seedFp,
const std::vector<unsigned char>& vchCryptedSecret)
{
LOCK(cs_KeyStore);
if (!fUseCrypto)
return CBasicKeyStore::HaveHDSeed();
return !cryptedHDSeed.second.empty();
}
bool CCryptoKeyStore::GetHDSeed(HDSeed& seedOut) const
{
LOCK(cs_KeyStore);
if (!fUseCrypto)
return CBasicKeyStore::GetHDSeed(seedOut);
if (cryptedHDSeed.second.empty())
if (!fUseCrypto || cryptedLegacySeed.has_value()) {
// Require encryption & don't allow an existing seed to be changed.
return false;
return DecryptHDSeed(vMasterKey, cryptedHDSeed.second, cryptedHDSeed.first, seedOut);
} else {
cryptedLegacySeed = std::make_pair(seedFp, vchCryptedSecret);
return true;
}
}
bool CCryptoKeyStore::HaveLegacyHDSeed() const
{
LOCK(cs_KeyStore);
if (fUseCrypto) {
return cryptedLegacySeed.has_value();
} else {
return CBasicKeyStore::HaveLegacyHDSeed();
}
}
std::optional<HDSeed> CCryptoKeyStore::GetLegacyHDSeed() const {
LOCK(cs_KeyStore);
if (fUseCrypto) {
if (cryptedLegacySeed.has_value()) {
const auto cryptedPair = cryptedLegacySeed.value();
return DecryptLegacyHDSeed(vMasterKey, cryptedPair.second, cryptedPair.first);
} else {
return std::nullopt;
}
} else {
return CBasicKeyStore::GetLegacyHDSeed();
}
}
//
// Transparent public keys
//
bool CCryptoKeyStore::AddKeyPubKey(const CKey& key, const CPubKey &pubkey)
{
LOCK(cs_KeyStore);
@ -420,6 +543,10 @@ bool CCryptoKeyStore::GetPubKey(const CKeyID &address, CPubKey& vchPubKeyOut) co
return CBasicKeyStore::GetPubKey(address, vchPubKeyOut);
}
//
// Sprout & Sapling keys
//
bool CCryptoKeyStore::AddSproutSpendingKey(const libzcash::SproutSpendingKey &sk)
{
LOCK(cs_KeyStore);
@ -536,21 +663,38 @@ bool CCryptoKeyStore::EncryptKeys(CKeyingMaterial& vMasterKeyIn)
return false;
fUseCrypto = true;
if (!hdSeed.IsNull()) {
if (mnemonicSeed.has_value()) {
{
std::vector<unsigned char> vchCryptedSecret;
// Use seed's fingerprint as IV
// TODO: Handle this properly when we make encryption a supported feature
auto seedFp = hdSeed.Fingerprint();
if (!EncryptSecret(vMasterKeyIn, hdSeed.RawSeed(), seedFp, vchCryptedSecret)) {
auto seedFp = mnemonicSeed.value().Fingerprint();
auto seedData = EncryptMnemonicSeed(mnemonicSeed.value());
if (!EncryptSecret(vMasterKeyIn, seedData, seedFp, vchCryptedSecret)) {
return false;
}
// This will call into CWallet to store the crypted seed to disk
if (!SetCryptedHDSeed(seedFp, vchCryptedSecret)) {
if (!SetCryptedMnemonicSeed(seedFp, vchCryptedSecret)) {
return false;
}
}
hdSeed = HDSeed();
mnemonicSeed = std::nullopt;
}
if (legacySeed.has_value()) {
{
std::vector<unsigned char> vchCryptedSecret;
// Use seed's fingerprint as IV
// TODO: Handle this properly when we make encryption a supported feature
auto seedFp = legacySeed.value().Fingerprint();
if (!EncryptSecret(vMasterKeyIn, legacySeed.value().RawSeed(), seedFp, vchCryptedSecret)) {
return false;
}
// This will call into CWallet to store the crypted seed to disk
if (!SetCryptedLegacyHDSeed(seedFp, vchCryptedSecret)) {
return false;
}
}
legacySeed = std::nullopt;
}
for (KeyMap::value_type& mKey : mapKeys)
{

View File

@ -131,7 +131,8 @@ public:
class CCryptoKeyStore : public CBasicKeyStore
{
private:
std::pair<uint256, std::vector<unsigned char>> cryptedHDSeed;
std::pair<uint256, std::vector<unsigned char>> cryptedMnemonicSeed;
std::optional<std::pair<uint256, std::vector<unsigned char>>> cryptedLegacySeed;
CryptedKeyMap mapCryptedKeys;
CryptedSproutSpendingKeyMap mapCryptedSproutSpendingKeys;
CryptedSaplingSpendingKeyMap mapCryptedSaplingSpendingKeys;
@ -172,10 +173,15 @@ public:
bool Lock();
virtual bool SetCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char> &vchCryptedSecret);
bool SetHDSeed(const HDSeed& seed);
bool HaveHDSeed() const;
bool GetHDSeed(HDSeed& seedOut) const;
bool SetMnemonicSeed(const MnemonicSeed& seed);
virtual bool SetCryptedMnemonicSeed(const uint256& seedFp, const std::vector<unsigned char> &vchCryptedSecret);
bool HaveMnemonicSeed() const;
std::optional<MnemonicSeed> GetMnemonicSeed() const;
bool SetLegacyHDSeed(const HDSeed& seed);
virtual bool SetCryptedLegacyHDSeed(const uint256& seedFp, const std::vector<unsigned char> &vchCryptedSecret);
bool HaveLegacyHDSeed() const;
std::optional<HDSeed> GetLegacyHDSeed() const;
virtual bool AddCryptedKey(const CPubKey &vchPubKey, const std::vector<unsigned char> &vchCryptedSecret);
bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey);

View File

@ -32,9 +32,9 @@ TEST(WalletZkeysTest, StoreAndLoadSaplingZkeys) {
EXPECT_ANY_THROW(wallet.GenerateNewSaplingZKey());
// Load the all-zeroes seed
CKeyingMaterial rawSeed(32, 0);
HDSeed seed(rawSeed);
wallet.LoadHDSeed(seed);
std::string mnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art");
MnemonicSeed seed(English, mnemonic);
wallet.LoadMnemonicSeed(seed);
// Now this call succeeds
auto address = wallet.GenerateNewSaplingZKey();
@ -428,7 +428,7 @@ TEST(wallet_zkeys_tests, WriteCryptedSaplingZkeyDirectToDb) {
// No default CPubKey set
ASSERT_TRUE(fFirstRun);
ASSERT_FALSE(wallet.HaveHDSeed());
ASSERT_FALSE(wallet.HaveMnemonicSeed());
wallet.GenerateNewSeed();
// wallet should be empty
@ -477,7 +477,7 @@ TEST(wallet_zkeys_tests, WriteCryptedSaplingZkeyDirectToDb) {
// Confirm it's not the same as the other wallet
ASSERT_TRUE(&wallet != &wallet2);
ASSERT_TRUE(wallet2.HaveHDSeed());
ASSERT_TRUE(wallet2.HaveMnemonicSeed());
// wallet should have three addresses
wallet2.GetSaplingPaymentAddresses(addrs);

View File

@ -609,13 +609,27 @@ UniValue dumpwallet_impl(const UniValue& params, bool fDumpZKeys)
file << strprintf("# * Created on %s\n", EncodeDumpTime(GetTime()));
file << strprintf("# * Best block at time of backup was %i (%s),\n", chainActive.Height(), chainActive.Tip()->GetBlockHash().ToString());
file << strprintf("# mined on %s\n", EncodeDumpTime(chainActive.Tip()->GetBlockTime()));
{
HDSeed hdSeed;
pwalletMain->GetHDSeed(hdSeed);
auto rawSeed = hdSeed.RawSeed();
file << strprintf("# HDSeed=%s fingerprint=%s", HexStr(rawSeed.begin(), rawSeed.end()), hdSeed.Fingerprint().GetHex());
file << "\n";
std::optional<MnemonicSeed> hdSeed = pwalletMain->GetMnemonicSeed();
if (hdSeed.has_value()) {
auto mSeed = hdSeed.value();
file << strprintf(
"# Recovery Phrase=\"%s\" \n# language=%s \n# fingerprint=%s\n",
mSeed.GetMnemonic(),
MnemonicSeed::LanguageName(mSeed.GetLanguage()),
mSeed.Fingerprint().GetHex()
);
}
std::optional<HDSeed> legacySeed = pwalletMain->GetLegacyHDSeed();
if (legacySeed.has_value()) {
auto rawSeed = legacySeed.value().RawSeed();
file << strprintf("# Legacy HDSeed=%s fingerprint=%s\n",
HexStr(rawSeed.begin(), rawSeed.end()),
legacySeed.value().Fingerprint().GetHex()
);
}
file << "\n";
for (std::vector<std::pair<int64_t, CKeyID> >::const_iterator it = vKeyBirth.begin(); it != vKeyBirth.end(); it++) {
const CKeyID &keyid = it->second;

View File

@ -790,7 +790,7 @@ BOOST_AUTO_TEST_CASE(rpc_wallet_z_getnewaddress) {
using namespace libzcash;
UniValue addr;
if (!pwalletMain->HaveHDSeed()) {
if (!pwalletMain->HaveMnemonicSeed()) {
pwalletMain->GenerateNewSeed();
}
@ -1449,7 +1449,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_taddr_to_sapling)
LOCK2(cs_main, pwalletMain->cs_wallet);
if (!pwalletMain->HaveHDSeed()) {
if (!pwalletMain->HaveMnemonicSeed()) {
pwalletMain->GenerateNewSeed();
}
@ -1523,11 +1523,11 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_taddr_to_sapling)
// We should be able to decrypt the outCiphertext with the ovk
// generated for transparent addresses
HDSeed seed;
BOOST_ASSERT(pwalletMain->GetHDSeed(seed));
std::optional<MnemonicSeed> seed = pwalletMain->GetMnemonicSeed();
BOOST_ASSERT(seed.has_value());
BOOST_CHECK(AttemptSaplingOutDecryption(
tx.vShieldedOutput[0].outCiphertext,
ovkForShieldingFromTaddr(seed),
ovkForShieldingFromTaddr(seed.value()),
tx.vShieldedOutput[0].cv,
tx.vShieldedOutput[0].cmu,
tx.vShieldedOutput[0].ephemeralKey));
@ -1608,7 +1608,7 @@ BOOST_AUTO_TEST_CASE(rpc_wallet_encrypted_wallet_sapzkeys)
UniValue retValue;
int n = 100;
if(!pwalletMain->HaveHDSeed())
if(!pwalletMain->HaveMnemonicSeed())
{
pwalletMain->GenerateNewSeed();
}
@ -1657,7 +1657,7 @@ BOOST_AUTO_TEST_CASE(rpc_wallet_encrypted_wallet_sapzkeys)
// Verify the key has been added
BOOST_CHECK_NO_THROW(retValue = CallRPC("z_listaddresses"));
arr = retValue.get_array();
BOOST_CHECK(arr.size() == n+1);
BOOST_CHECK_EQUAL(arr.size(), n+1);
// We can't simulate over RPC the wallet closing and being reloaded
// but there are tests for this in gtest.

View File

@ -120,11 +120,11 @@ SaplingPaymentAddress CWallet::GenerateNewSaplingZKey()
CKeyMetadata metadata(nCreationTime);
// Try to get the seed
HDSeed seed;
if (!GetHDSeed(seed))
auto seed = GetMnemonicSeed();
if (!seed.has_value())
throw std::runtime_error("CWallet::GenerateNewSaplingZKey(): HD seed not found");
auto m = libzcash::SaplingExtendedSpendingKey::Master(seed);
auto m = libzcash::SaplingExtendedSpendingKey::Master(seed.value());
// We use a fixed keypath scheme of m/32'/coin_type'/account'
// Derive m/32'
@ -526,7 +526,7 @@ bool CWallet::Unlock(const SecureString& strWalletPassphrase)
if (CCryptoKeyStore::Unlock(vMasterKey)) {
// Now that the wallet is decrypted, ensure we have an HD seed.
// https://github.com/zcash/zcash/issues/3607
if (!this->HaveHDSeed()) {
if (!this->HaveMnemonicSeed()) {
this->GenerateNewSeed();
}
return true;
@ -2216,16 +2216,16 @@ bool CWallet::IsHDFullyEnabled() const
return false;
}
void CWallet::GenerateNewSeed()
void CWallet::GenerateNewSeed(Language language)
{
LOCK(cs_wallet);
auto seed = HDSeed::Random(HD_WALLET_SEED_LENGTH);
auto seed = MnemonicSeed::Random(language, HD_WALLET_SEED_LENGTH);
int64_t nCreationTime = GetTime();
// If the wallet is encrypted and locked, this will fail.
if (!SetHDSeed(seed))
if (!SetMnemonicSeed(seed))
throw std::runtime_error(std::string(__func__) + ": SetHDSeed failed");
// store the key creation time together with
@ -2238,9 +2238,9 @@ void CWallet::GenerateNewSeed()
SetHDChain(newHdChain, false);
}
bool CWallet::SetHDSeed(const HDSeed& seed)
bool CWallet::SetMnemonicSeed(const MnemonicSeed& seed)
{
if (!CCryptoKeyStore::SetHDSeed(seed)) {
if (!CCryptoKeyStore::SetMnemonicSeed(seed)) {
return false;
}
@ -2252,15 +2252,15 @@ bool CWallet::SetHDSeed(const HDSeed& seed)
LOCK(cs_wallet);
CWalletDB(strWalletFile).WriteNetworkInfo(networkIdString);
if (!IsCrypted()) {
return CWalletDB(strWalletFile).WriteHDSeed(seed);
return CWalletDB(strWalletFile).WriteMnemonicSeed(seed);
}
}
return true;
}
bool CWallet::SetCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char> &vchCryptedSecret)
bool CWallet::SetCryptedMnemonicSeed(const uint256& seedFp, const std::vector<unsigned char> &vchCryptedSecret)
{
if (!CCryptoKeyStore::SetCryptedHDSeed(seedFp, vchCryptedSecret)) {
if (!CCryptoKeyStore::SetCryptedMnemonicSeed(seedFp, vchCryptedSecret)) {
return false;
}
@ -2271,19 +2271,19 @@ bool CWallet::SetCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned
{
LOCK(cs_wallet);
if (pwalletdbEncryption)
return pwalletdbEncryption->WriteCryptedHDSeed(seedFp, vchCryptedSecret);
return pwalletdbEncryption->WriteCryptedMnemonicSeed(seedFp, vchCryptedSecret);
else
return CWalletDB(strWalletFile).WriteCryptedHDSeed(seedFp, vchCryptedSecret);
return CWalletDB(strWalletFile).WriteCryptedMnemonicSeed(seedFp, vchCryptedSecret);
}
return false;
}
HDSeed CWallet::GetHDSeedForRPC() const {
HDSeed seed;
if (!pwalletMain->GetHDSeed(seed)) {
auto seed = pwalletMain->GetMnemonicSeed();
if (!seed.has_value()) {
throw JSONRPCError(RPC_WALLET_ERROR, "HD seed not found");
}
return seed;
return seed.value();
}
void CWallet::SetHDChain(const CHDChain& chain, bool memonly)
@ -2307,14 +2307,24 @@ uint32_t CWallet::BIP44CoinType() {
}
bool CWallet::LoadHDSeed(const HDSeed& seed)
bool CWallet::LoadMnemonicSeed(const MnemonicSeed& seed)
{
return CBasicKeyStore::SetHDSeed(seed);
return CBasicKeyStore::SetMnemonicSeed(seed);
}
bool CWallet::LoadCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char>& seed)
bool CWallet::LoadLegacyHDSeed(const HDSeed& seed)
{
return CCryptoKeyStore::SetCryptedHDSeed(seedFp, seed);
return CBasicKeyStore::SetLegacyHDSeed(seed);
}
bool CWallet::LoadCryptedMnemonicSeed(const uint256& seedFp, const std::vector<unsigned char>& seed)
{
return CCryptoKeyStore::SetCryptedMnemonicSeed(seedFp, seed);
}
bool CWallet::LoadCryptedLegacyHDSeed(const uint256& seedFp, const std::vector<unsigned char>& seed)
{
return CCryptoKeyStore::SetCryptedLegacyHDSeed(seedFp, seed);
}
void CWalletTx::SetSproutNoteData(mapSproutNoteData_t &noteData)
@ -4781,7 +4791,7 @@ bool CWallet::InitLoadWallet(const CChainParams& params, bool clearWitnessCaches
walletInstance->SetMaxVersion(nMaxVersion);
}
if (!walletInstance->HaveHDSeed())
if (!walletInstance->HaveMnemonicSeed())
{
// We can't set the new HD seed until the wallet is decrypted.
// https://github.com/zcash/zcash/issues/3607

View File

@ -1284,10 +1284,20 @@ public:
Sets the seed's version based on the current wallet version (so the
caller must ensure the current wallet version is correct before calling
this function). */
void GenerateNewSeed();
void GenerateNewSeed(Language language = English);
bool SetHDSeed(const HDSeed& seed);
bool SetCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char> &vchCryptedSecret);
bool SetMnemonicSeed(const MnemonicSeed& seed);
bool SetCryptedMnemonicSeed(const uint256& seedFp, const std::vector<unsigned char> &vchCryptedSecret);
/* Set the current HD seed, without saving it to disk (used by LoadWallet) */
bool LoadMnemonicSeed(const MnemonicSeed& seed);
/* Set the legacy HD seed, without saving it to disk (used by LoadWallet) */
bool LoadLegacyHDSeed(const HDSeed& seed);
/* Set the current encrypted HD seed, without saving it to disk (used by LoadWallet) */
bool LoadCryptedMnemonicSeed(const uint256& seedFp, const std::vector<unsigned char>& seed);
/* Set the legacy encrypted HD seed, without saving it to disk (used by LoadWallet) */
bool LoadCryptedLegacyHDSeed(const uint256& seedFp, const std::vector<unsigned char>& seed);
/* Returns the wallet's HD seed or throw JSONRPCError(...) */
HDSeed GetHDSeedForRPC() const;
@ -1299,11 +1309,6 @@ public:
bool CheckNetworkInfo(std::pair<std::string, std::string> networkInfo);
uint32_t BIP44CoinType();
/* Set the current HD seed, without saving it to disk (used by LoadWallet) */
bool LoadHDSeed(const HDSeed& key);
/* Set the current encrypted HD seed, without saving it to disk (used by LoadWallet) */
bool LoadCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char>& seed);
/* Find notes filtered by payment address, min depth, ability to spend */
void GetFilteredNotes(std::vector<SproutNoteEntry>& sproutEntries,
std::vector<SaplingNoteEntry>& saplingEntries,

View File

@ -685,6 +685,37 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
{
ssValue >> pwallet->nWitnessCacheSize;
}
else if (strType == "mnemonicseed")
{
uint256 seedFp;
ssKey >> seedFp;
auto seed = MnemonicSeed::Read(ssValue);
if (seed.Fingerprint() != seedFp)
{
strErr = "Error reading wallet database: HDSeed corrupt";
return false;
}
if (!pwallet->LoadMnemonicSeed(seed))
{
strErr = "Error reading wallet database: LoadHDSeed failed";
return false;
}
}
else if (strType == "chdmnemonicseed")
{
uint256 seedFp;
vector<unsigned char> vchCryptedSecret;
ssKey >> seedFp;
ssValue >> vchCryptedSecret;
if (!pwallet->LoadCryptedMnemonicSeed(seedFp, vchCryptedSecret))
{
strErr = "Error reading wallet database: LoadCryptedMnemonicSeed failed";
return false;
}
wss.fIsEncrypted = true;
}
else if (strType == "hdseed")
{
uint256 seedFp;
@ -699,7 +730,7 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
return false;
}
if (!pwallet->LoadHDSeed(seed))
if (!pwallet->LoadLegacyHDSeed(seed))
{
strErr = "Error reading wallet database: LoadHDSeed failed";
return false;
@ -711,7 +742,7 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
vector<unsigned char> vchCryptedSecret;
ssKey >> seedFp;
ssValue >> vchCryptedSecret;
if (!pwallet->LoadCryptedHDSeed(seedFp, vchCryptedSecret))
if (!pwallet->LoadCryptedLegacyHDSeed(seedFp, vchCryptedSecret))
{
strErr = "Error reading wallet database: LoadCryptedHDSeed failed";
return false;
@ -744,6 +775,7 @@ static bool IsKeyType(string strType)
{
return (strType== "key" || strType == "wkey" ||
strType == "hdseed" || strType == "chdseed" ||
strType == "mnemonicseed" || strType == "chdmnemonicseed" ||
strType == "zkey" || strType == "czkey" ||
strType == "sapzkey" || strType == "csapzkey" ||
strType == "vkey" || strType == "sapextfvk" ||
@ -1160,18 +1192,19 @@ bool CWalletDB::WriteNetworkInfo(const std::string& networkId)
return Write(std::string("networkinfo"), networkInfo);
}
bool CWalletDB::WriteHDSeed(const HDSeed& seed)
bool CWalletDB::WriteMnemonicSeed(const MnemonicSeed& seed)
{
nWalletDBUpdateCounter++;
return Write(std::make_pair(std::string("hdseed"), seed.Fingerprint()), seed.RawSeed());
return Write(std::make_pair(std::string("mnemonicseed"), seed.Fingerprint()), seed);
}
bool CWalletDB::WriteCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char>& vchCryptedSecret)
bool CWalletDB::WriteCryptedMnemonicSeed(const uint256& seedFp, const std::vector<unsigned char>& vchCryptedSecret)
{
nWalletDBUpdateCounter++;
return Write(std::make_pair(std::string("chdseed"), seedFp), vchCryptedSecret);
return Write(std::make_pair(std::string("chdmnemonicseed"), seedFp), vchCryptedSecret);
}
bool CWalletDB::WriteHDChain(const CHDChain& chain)
{
nWalletDBUpdateCounter++;

View File

@ -48,7 +48,7 @@ public:
static const int VERSION_HD_BASE = 1;
static const int CURRENT_VERSION = VERSION_HD_BASE;
int nVersion;
uint256 seedFp;
uint256 seedFp; //NU5: Deprecate???
int64_t nCreateTime; // 0 means unknown
uint32_t saplingAccountCounter;
@ -170,10 +170,13 @@ public:
static bool Recover(CDBEnv& dbenv, const std::string& filename);
bool WriteNetworkInfo(const std::string& networkId);
bool WriteHDSeed(const HDSeed& seed);
bool WriteCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char>& vchCryptedSecret);
bool WriteMnemonicSeed(const MnemonicSeed& seed);
bool WriteCryptedMnemonicSeed(const uint256& seedFp, const std::vector<unsigned char>& vchCryptedSecret);
//! write the hdchain model (external chain child index counter)
bool WriteHDChain(const CHDChain& chain);
std::optional<HDSeed> ReadLegacyHDSeed();
std::optional<std::pair<uint256, std::vector<unsigned char>>> ReadLegacyCryptedHDSeed();
/// Write spending key to wallet database, where key is payment address and value is spending key.
bool WriteZKey(const libzcash::SproutPaymentAddress& addr, const libzcash::SproutSpendingKey& key, const CKeyMetadata &keyMeta);

View File

@ -19,12 +19,21 @@ const unsigned char ZCASH_HD_SEED_FP_PERSONAL[BLAKE2bPersonalBytes] =
const unsigned char ZCASH_TADDR_OVK_PERSONAL[BLAKE2bPersonalBytes] =
{'Z', 'c', 'T', 'a', 'd', 'd', 'r', 'T', 'o', 'S', 'a', 'p', 'l', 'i', 'n', 'g'};
HDSeed HDSeed::Random(size_t len)
MnemonicSeed MnemonicSeed::Random(Language language, size_t len)
{
assert(len >= 32);
RawHDSeed rawSeed(len, 0);
GetRandBytes(rawSeed.data(), len);
return HDSeed(rawSeed);
while (true) {
std::vector<unsigned char> entropy(len, 0);
GetRandBytes(entropy.data(), len);
const char* phrase = zip339_entropy_to_phrase(language, entropy.data(), len);
std::string mnemonic(phrase);
zip339_free_phrase(phrase);
MnemonicSeed seed(language, mnemonic);
// TODO: check for the validity of the Sapling spending key at account 0 for this seed.
return seed;
}
}
uint256 HDSeed::Fingerprint() const

View File

@ -9,6 +9,7 @@
#include "support/allocators/secure.h"
#include "uint256.h"
#include "zcash/address/sapling.hpp"
#include "rust/zip339.h"
#include <optional>
#include <string>
@ -18,27 +19,121 @@ const uint32_t ZIP32_HARDENED_KEY_LIMIT = 0x80000000;
const size_t ZIP32_XFVK_SIZE = 169;
const size_t ZIP32_XSK_SIZE = 169;
class CWalletDB;
typedef std::vector<unsigned char, secure_allocator<unsigned char>> RawHDSeed;
class HDSeed {
private:
protected:
RawHDSeed seed;
public:
HDSeed() {}
public:
//
HDSeed(RawHDSeed& seedIn) : seed(seedIn) {}
static HDSeed Random(size_t len = 32);
bool IsNull() const { return seed.empty(); };
template <typename Stream>
static HDSeed ReadLegacy(Stream& stream) {
RawHDSeed rawSeed;
stream >> rawSeed;
HDSeed seed(rawSeed);
return seed;
}
uint256 Fingerprint() const;
RawHDSeed RawSeed() const { return seed; }
};
friend bool operator==(const HDSeed& a, const HDSeed& b)
class MnemonicSeed: public HDSeed {
private:
Language language;
std::string mnemonic;
MnemonicSeed() {}
void Init() {
unsigned char buf[64];
zip339_phrase_to_seed(language, mnemonic.c_str(), &buf);
seed.assign(buf, std::end(buf));
}
public:
MnemonicSeed(Language languageIn, std::string& mnemonicIn): language(languageIn), mnemonic(mnemonicIn) {
unsigned char buf[64];
zip339_phrase_to_seed(languageIn, mnemonicIn.c_str(), &buf);
seed.assign(buf, std::end(buf));
}
static MnemonicSeed Random(Language language = English, size_t len = 32);
static std::string LanguageName(Language language) {
switch (language) {
case English:
return "English";
case SimplifiedChinese:
return "Simplified Chinese";
case TraditionalChinese:
return "Traditional Chinese";
case Czech:
return "Czech";
case French:
return "French";
case Italian:
return "Italian";
case Japanese:
return "Japanese";
case Korean:
return "Korean";
case Portuguese:
return "Portuguese";
case Spanish:
return "Spanish";
default:
return "INVALID";
}
}
ADD_SERIALIZE_METHODS;
template <typename Stream, typename Operation>
inline void SerializationOp(Stream& s, Operation ser_action) {
if (ser_action.ForRead()) {
uint32_t language0;
READWRITE(language0);
READWRITE(mnemonic);
language = (Language) language0;
Init();
} else {
uint32_t language0 = (uint32_t) language;
READWRITE(language0);
READWRITE(mnemonic);
}
}
template <typename Stream>
static MnemonicSeed Read(Stream& stream) {
MnemonicSeed seed;
stream >> seed;
return seed;
}
const Language GetLanguage() const {
return language;
}
const std::string GetMnemonic() const {
return mnemonic;
}
friend bool operator==(const MnemonicSeed& a, const MnemonicSeed& b)
{
return a.seed == b.seed;
}
friend bool operator!=(const HDSeed& a, const HDSeed& b)
friend bool operator!=(const MnemonicSeed& a, const MnemonicSeed& b)
{
return !(a == b);
}