From 7c70b5e44dce18fc46c435be369d7e1dff904808 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 14 Jan 2026 16:04:48 +0100 Subject: [PATCH 01/12] crypto: add NonceFromBytes/NonceToBytes helpers Add helper methods to AEADChaCha20Poly1305 for converting between the internal Nonce96 type ({uint32_t, uint64_t}) and a 12-byte array representation (big-endian). RFC8439 defines the nonce as 96 opaque bits, but our implementation splits it. These helpers make it convenient to work with byte-based nonce representations. --- src/crypto/chacha20poly1305.h | 37 +++++++++++++++++++++++++++++++++++ src/test/crypto_tests.cpp | 29 +++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/crypto/chacha20poly1305.h b/src/crypto/chacha20poly1305.h index 9a863dda97b4..7b21aca54ed9 100644 --- a/src/crypto/chacha20poly1305.h +++ b/src/crypto/chacha20poly1305.h @@ -34,6 +34,43 @@ class AEADChaCha20Poly1305 /** 96-bit nonce type. */ using Nonce96 = ChaCha20::Nonce96; + /** Size of the nonce in bytes. */ + static constexpr unsigned NONCE_SIZE = 12; + + /** Convert a 12-byte array to a Nonce96. + * + * RFC8439 defines the nonce as 96 opaque bits. This helper converts + * a byte array (big-endian) to the internal {uint32_t, uint64_t} representation. + */ + static Nonce96 NonceFromBytes(std::span nonce_bytes) noexcept + { + return { + (uint32_t(uint8_t(nonce_bytes[0])) << 24) | (uint32_t(uint8_t(nonce_bytes[1])) << 16) | + (uint32_t(uint8_t(nonce_bytes[2])) << 8) | uint32_t(uint8_t(nonce_bytes[3])), + (uint64_t(uint8_t(nonce_bytes[4])) << 56) | (uint64_t(uint8_t(nonce_bytes[5])) << 48) | + (uint64_t(uint8_t(nonce_bytes[6])) << 40) | (uint64_t(uint8_t(nonce_bytes[7])) << 32) | + (uint64_t(uint8_t(nonce_bytes[8])) << 24) | (uint64_t(uint8_t(nonce_bytes[9])) << 16) | + (uint64_t(uint8_t(nonce_bytes[10])) << 8) | uint64_t(uint8_t(nonce_bytes[11])) + }; + } + + /** Convert a Nonce96 back to a 12-byte array (big-endian). */ + static void NonceToBytes(Nonce96 nonce, std::span nonce_bytes) noexcept + { + nonce_bytes[0] = std::byte((nonce.first >> 24) & 0xFF); + nonce_bytes[1] = std::byte((nonce.first >> 16) & 0xFF); + nonce_bytes[2] = std::byte((nonce.first >> 8) & 0xFF); + nonce_bytes[3] = std::byte(nonce.first & 0xFF); + nonce_bytes[4] = std::byte((nonce.second >> 56) & 0xFF); + nonce_bytes[5] = std::byte((nonce.second >> 48) & 0xFF); + nonce_bytes[6] = std::byte((nonce.second >> 40) & 0xFF); + nonce_bytes[7] = std::byte((nonce.second >> 32) & 0xFF); + nonce_bytes[8] = std::byte((nonce.second >> 24) & 0xFF); + nonce_bytes[9] = std::byte((nonce.second >> 16) & 0xFF); + nonce_bytes[10] = std::byte((nonce.second >> 8) & 0xFF); + nonce_bytes[11] = std::byte(nonce.second & 0xFF); + } + /** Encrypt a message with a specified 96-bit nonce and aad. * * Requires cipher.size() = plain.size() + EXPANSION. diff --git a/src/test/crypto_tests.cpp b/src/test/crypto_tests.cpp index 5588d4cdbc66..bb984acb703a 100644 --- a/src/test/crypto_tests.cpp +++ b/src/test/crypto_tests.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -1050,6 +1051,34 @@ BOOST_AUTO_TEST_CASE(chacha20poly1305_testvectors) "14b94829deb27f0b1923a2af704ae5d6"); } +BOOST_AUTO_TEST_CASE(chacha20poly1305_nonce_conversion) +{ + // Test NonceFromBytes/NonceToBytes roundtrip + auto key = ParseHex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f"); + auto nonce_bytes = ParseHex("000000000001020304050607"); + + // Convert bytes to Nonce96 + auto nonce = AEADChaCha20Poly1305::NonceFromBytes(std::span{nonce_bytes.data(), 12}); + // Expected: first 4 bytes = 0x00000000, next 8 bytes = 0x0001020304050607 + BOOST_CHECK_EQUAL(nonce.first, 0x00000000U); + BOOST_CHECK_EQUAL(nonce.second, 0x0001020304050607ULL); + + // Convert back to bytes and check roundtrip + std::array roundtrip_bytes; + AEADChaCha20Poly1305::NonceToBytes(nonce, roundtrip_bytes); + BOOST_CHECK(std::ranges::equal(nonce_bytes, roundtrip_bytes)); + + // Test with different values to ensure byte ordering is correct + auto nonce_bytes2 = ParseHex("aabbccdd11223344556677ff"); + auto nonce2 = AEADChaCha20Poly1305::NonceFromBytes(std::span{nonce_bytes2.data(), 12}); + BOOST_CHECK_EQUAL(nonce2.first, 0xaabbccddU); + BOOST_CHECK_EQUAL(nonce2.second, 0x11223344556677ffULL); + + std::array roundtrip_bytes2; + AEADChaCha20Poly1305::NonceToBytes(nonce2, roundtrip_bytes2); + BOOST_CHECK(std::ranges::equal(nonce_bytes2, roundtrip_bytes2)); +} + BOOST_AUTO_TEST_CASE(hkdf_hmac_sha256_l32_tests) { // Use rfc5869 test vectors but truncated to 32 bytes (our implementation only support length 32) From 755289261773b93fc40ff4b8a05f4de89a75cfc6 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Thu, 15 Jan 2026 09:13:33 +0100 Subject: [PATCH 02/12] util: ParseHDKeypath allow h as hardened indicator BIP-380 specifies that descriptors can use either ' or h as the hardened indicator. ParseHDKeypath only supported the former. This prepares for using ParseHDKeypath with paths extracted from descriptors which typically use 'h' for shell-escaping convenience. --- src/util/bip32.cpp | 3 +++ src/wallet/test/psbt_wallet_tests.cpp | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/util/bip32.cpp b/src/util/bip32.cpp index db40bfb5b9e7..94b1fcc027cd 100644 --- a/src/util/bip32.cpp +++ b/src/util/bip32.cpp @@ -26,6 +26,9 @@ bool ParseHDKeypath(const std::string& keypath_str, std::vector& keypa // Finds whether it is hardened uint32_t path = 0; size_t pos = item.find('\''); + if (pos == std::string::npos) { + pos = item.find('h'); + } if (pos != std::string::npos) { // The hardened tick can only be in the last index of the string if (pos != item.size() - 1) { diff --git a/src/wallet/test/psbt_wallet_tests.cpp b/src/wallet/test/psbt_wallet_tests.cpp index 91b69b9d624b..5d8955e8c523 100644 --- a/src/wallet/test/psbt_wallet_tests.cpp +++ b/src/wallet/test/psbt_wallet_tests.cpp @@ -132,6 +132,9 @@ BOOST_AUTO_TEST_CASE(parse_hd_keypath) BOOST_CHECK(ParseHDKeypath("m/0'/0'", keypath)); BOOST_CHECK(!ParseHDKeypath("m/'0/0'", keypath)); + BOOST_CHECK(ParseHDKeypath("m/0h/0h", keypath)); + BOOST_CHECK(!ParseHDKeypath("m/h0/0h", keypath)); + BOOST_CHECK(ParseHDKeypath("m/0/0", keypath)); BOOST_CHECK(!ParseHDKeypath("n/0/0", keypath)); From 31ec8f203c77ff46c215edea0c74eda345935c89 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 14 Jan 2026 15:09:32 +0100 Subject: [PATCH 03/12] wallet: add WalletDescriptorInfo helper for descriptor serialization Introduces WalletDescriptorInfo struct and DescriptorInfoToUniValue() helper to avoid code duplication when serializing descriptor metadata to UniValue. Refactors listdescriptors RPC to use the new helper. --- src/wallet/rpc/backup.cpp | 30 +++--------------------------- src/wallet/rpc/util.cpp | 21 +++++++++++++++++++++ src/wallet/rpc/util.h | 21 +++++++++++++++++++++ 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index 6217358c870f..37a899ca8cbd 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -505,16 +505,7 @@ RPCHelpMan listdescriptors() const auto active_spk_mans = wallet->GetActiveScriptPubKeyMans(); - struct WalletDescInfo { - std::string descriptor; - uint64_t creation_time; - bool active; - std::optional internal; - std::optional> range; - int64_t next_index; - }; - - std::vector wallet_descriptors; + std::vector wallet_descriptors; for (const auto& spk_man : wallet->GetAllScriptPubKeyMans()) { const auto desc_spk_man = dynamic_cast(spk_man); if (!desc_spk_man) { @@ -542,23 +533,8 @@ RPCHelpMan listdescriptors() }); UniValue descriptors(UniValue::VARR); - for (const WalletDescInfo& info : wallet_descriptors) { - UniValue spk(UniValue::VOBJ); - spk.pushKV("desc", info.descriptor); - spk.pushKV("timestamp", info.creation_time); - spk.pushKV("active", info.active); - if (info.internal.has_value()) { - spk.pushKV("internal", info.internal.value()); - } - if (info.range.has_value()) { - UniValue range(UniValue::VARR); - range.push_back(info.range->first); - range.push_back(info.range->second - 1); - spk.pushKV("range", std::move(range)); - spk.pushKV("next", info.next_index); - spk.pushKV("next_index", info.next_index); - } - descriptors.push_back(std::move(spk)); + for (const WalletDescriptorInfo& info : wallet_descriptors) { + descriptors.push_back(DescriptorInfoToUniValue(info)); } UniValue response(UniValue::VOBJ); diff --git a/src/wallet/rpc/util.cpp b/src/wallet/rpc/util.cpp index d68e6c652694..6f1fc603ec59 100644 --- a/src/wallet/rpc/util.cpp +++ b/src/wallet/rpc/util.cpp @@ -161,4 +161,25 @@ void AppendLastProcessedBlock(UniValue& entry, const CWallet& wallet) entry.pushKV("lastprocessedblock", std::move(lastprocessedblock)); } +UniValue DescriptorInfoToUniValue(const WalletDescriptorInfo& info) +{ + UniValue obj(UniValue::VOBJ); + obj.pushKV("desc", info.descriptor); + obj.pushKV("timestamp", info.creation_time); + obj.pushKV("active", info.active); + if (info.internal.has_value()) { + obj.pushKV("internal", info.internal.value()); + } + if (info.range.has_value()) { + UniValue range(UniValue::VARR); + range.push_back(info.range->first); + // range_end is exclusive internally, display as inclusive (hence -1) + range.push_back(info.range->second - 1); + obj.pushKV("range", std::move(range)); + obj.pushKV("next", info.next_index); + obj.pushKV("next_index", info.next_index); + } + return obj; +} + } // namespace wallet diff --git a/src/wallet/rpc/util.h b/src/wallet/rpc/util.h index d649721431a2..89fcb64a639f 100644 --- a/src/wallet/rpc/util.h +++ b/src/wallet/rpc/util.h @@ -56,6 +56,27 @@ void PushParentDescriptors(const CWallet& wallet, const CScript& script_pubkey, void HandleWalletError(const std::shared_ptr wallet, DatabaseStatus& status, bilingual_str& error); void AppendLastProcessedBlock(UniValue& entry, const CWallet& wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); + +/** + * Information about a wallet descriptor, used for serialization to JSON. + * This struct captures all the metadata needed for listdescriptors output + * and importdescriptors input. + */ +struct WalletDescriptorInfo { + std::string descriptor; + uint64_t creation_time; + bool active; + std::optional internal; + std::optional> range; + int64_t next_index; +}; + +/** + * Convert a WalletDescriptorInfo to a UniValue object. + * The output format is compatible with both listdescriptors output and + * importdescriptors input. + */ +UniValue DescriptorInfoToUniValue(const WalletDescriptorInfo& info); } // namespace wallet #endif // BITCOIN_WALLET_RPC_UTIL_H From c679a83cae6f0d6630bbb630bc7a5d1dc8464aa2 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 14 Jan 2026 16:24:06 +0100 Subject: [PATCH 04/12] wallet: BIP-xxxx key normalization primitives Add functions for normalizing public keys to x-only format as specified in BIP-xxxx (Bitcoin Encrypted Backup). These primitives form the foundation for the encryption scheme. Functions added: - NormalizeToXOnly(): Convert CPubKey or CExtPubKey to 32-byte x-only format - IsNUMSPoint(): Check if a key is the BIP341 unspendable NUMS point - ExtractKeysFromDescriptor(): Extract and normalize all keys from a descriptor Includes test vectors from the BIP specification. --- src/test/CMakeLists.txt | 1 + .../data/bip_encrypted_backup_keys_types.json | 27 +++++ src/wallet/CMakeLists.txt | 1 + src/wallet/encryptedbackup.cpp | 78 ++++++++++++ src/wallet/encryptedbackup.h | 72 ++++++++++++ src/wallet/test/CMakeLists.txt | 1 + src/wallet/test/encrypted_backup_tests.cpp | 111 ++++++++++++++++++ 7 files changed, 291 insertions(+) create mode 100644 src/test/data/bip_encrypted_backup_keys_types.json create mode 100644 src/wallet/encryptedbackup.cpp create mode 100644 src/wallet/encryptedbackup.h create mode 100644 src/wallet/test/encrypted_backup_tests.cpp diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index e70eaf5cf89a..9bdf043b73fc 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -138,6 +138,7 @@ include(TargetDataSources) target_json_data_sources(test_bitcoin data/base58_encode_decode.json data/bip341_wallet_vectors.json + data/bip_encrypted_backup_keys_types.json data/blockfilters.json data/key_io_invalid.json data/key_io_valid.json diff --git a/src/test/data/bip_encrypted_backup_keys_types.json b/src/test/data/bip_encrypted_backup_keys_types.json new file mode 100644 index 000000000000..2f2d1f155590 --- /dev/null +++ b/src/test/data/bip_encrypted_backup_keys_types.json @@ -0,0 +1,27 @@ +[ + { + "description": "Xpub with origin and multipath", + "key": "[58b7f8dc/48'/1'/0'/2']tpubDEPBvXvhta3pjVaKokqC3eeMQnszj9ehFaA2zD5nSdkaccwGAizu8jVB2NeSpvmP2P52MBoZvNCixqXRJnTyXx51FQzARR63tjxQSyP3Btw/<0;1>/*", + "expected": "ebd252ca0877aae09b9d058219682775aa3cbcd049c12f07832f2cf6a3b51708" + }, + { + "description": "Xpub with origin and w/o multipath", + "key": "[d4ab66f1/48'/1'/1'/2']tpubDFTxBKyUCgkwp5enwZh3t2FJ5AMJqmCWoh1NRT13qNYQb1iKTUrAG6u5gpsDYhG8cZGXouYWuQtzcuSVjPStTc4dwU6JqPMFtgaLGvSQXhi", + "expected": "8e886919a6b72579a28bd292505d2afd41c1b5012414c5e24d7b59f4abdfc0ce" + }, + { + "description": "Compressed public key", + "key": "02ebd252ca0877aae09b9d058219682775aa3cbcd049c12f07832f2cf6a3b51708", + "expected": "ebd252ca0877aae09b9d058219682775aa3cbcd049c12f07832f2cf6a3b51708" + }, + { + "description": "X only public key", + "key": "ebd252ca0877aae09b9d058219682775aa3cbcd049c12f07832f2cf6a3b51708", + "expected": "ebd252ca0877aae09b9d058219682775aa3cbcd049c12f07832f2cf6a3b51708" + }, + { + "description": "Uncompressed public key", + "key": "04ebd252ca0877aae09b9d058219682775aa3cbcd049c12f07832f2cf6a3b517089e956909c4c07e8529f45f3ff8904d28df5a181619e21bdf748a896322530039", + "expected": "ebd252ca0877aae09b9d058219682775aa3cbcd049c12f07832f2cf6a3b51708" + } +] diff --git a/src/wallet/CMakeLists.txt b/src/wallet/CMakeLists.txt index 8ec381df5a8c..d16413d83e3d 100644 --- a/src/wallet/CMakeLists.txt +++ b/src/wallet/CMakeLists.txt @@ -10,6 +10,7 @@ add_library(bitcoin_wallet STATIC EXCLUDE_FROM_ALL crypter.cpp db.cpp dump.cpp + encryptedbackup.cpp external_signer_scriptpubkeyman.cpp feebumper.cpp fees.cpp diff --git a/src/wallet/encryptedbackup.cpp b/src/wallet/encryptedbackup.cpp new file mode 100644 index 000000000000..3533e6644ddf --- /dev/null +++ b/src/wallet/encryptedbackup.cpp @@ -0,0 +1,78 @@ +// Copyright (c) 2025-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include + +#include +#include