Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ if(ENABLE_WALLET)
bitcoin_wallet
bitcoin_common
bitcoin_util
univalue
Boost::headers
)
install_binary_component(bitcoin-wallet HAS_MANPAGE)
Expand Down
5 changes: 5 additions & 0 deletions src/bitcoin-wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,16 @@ static void SetupWalletToolArgs(ArgsManager& argsman)
argsman.AddArg("-dumpfile=<file name>", "When used with 'dump', writes out the records to this file. When used with 'createfromdump', loads the records into a new wallet.", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS);
argsman.AddArg("-debug=<category>", "Output debugging information (default: 0).", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-printtoconsole", "Send trace/debug info to console (default: 1 when no -debug is true, 0 otherwise).", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-pubkey=<key>", "Extended public key (xpub/tpub) for decrypting a backup", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS);
argsman.AddArg("-xpub=<key>", "Extended public key (xpub/tpub) whose derivation path to include in backup header", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS);

argsman.AddCommand("info", "Get wallet info");
argsman.AddCommand("create", "Create a new descriptor wallet file");
argsman.AddCommand("dump", "Print out all of the wallet key-value records");
argsman.AddCommand("createfromdump", "Create new wallet file from dumped records");
argsman.AddCommand("encryptbackup", "Create an encrypted backup of wallet descriptors (outputs base64)");
argsman.AddCommand("decryptbackup", "Decrypt an encrypted backup (reads base64 from stdin, requires -pubkey)");
argsman.AddCommand("inspectbackup", "Show unencrypted metadata from a backup (reads base64 from stdin)");
}

static std::optional<int> WalletAppInit(ArgsManager& args, int argc, char* argv[])
Expand Down
37 changes: 37 additions & 0 deletions src/crypto/chacha20poly1305.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<const std::byte, NONCE_SIZE> 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<std::byte, NONCE_SIZE> 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.
Expand Down
5 changes: 5 additions & 0 deletions src/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ include(TargetDataSources)
target_json_data_sources(test_bitcoin
data/base58_encode_decode.json
data/bip341_wallet_vectors.json
data/bip_encrypted_backup_content_type.json
data/bip_encrypted_backup_derivation_path.json
data/bip_encrypted_backup_encryption_secret.json
data/bip_encrypted_backup_individual_secrets.json
data/bip_encrypted_backup_keys_types.json
data/blockfilters.json
data/key_io_invalid.json
data/key_io_valid.json
Expand Down
29 changes: 29 additions & 0 deletions src/test/crypto_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <util/strencodings.h>

#include <algorithm>
#include <ranges>
#include <vector>

#include <boost/test/unit_test.hpp>
Expand Down Expand Up @@ -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<std::byte>("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f");
auto nonce_bytes = ParseHex<std::byte>("000000000001020304050607");

// Convert bytes to Nonce96
auto nonce = AEADChaCha20Poly1305::NonceFromBytes(std::span<const std::byte, 12>{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<std::byte, 12> 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<std::byte>("aabbccdd11223344556677ff");
auto nonce2 = AEADChaCha20Poly1305::NonceFromBytes(std::span<const std::byte, 12>{nonce_bytes2.data(), 12});
BOOST_CHECK_EQUAL(nonce2.first, 0xaabbccddU);
BOOST_CHECK_EQUAL(nonce2.second, 0x11223344556677ffULL);

std::array<std::byte, 12> 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)
Expand Down
77 changes: 77 additions & 0 deletions src/test/data/bip_encrypted_backup_content_type.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
[
{
"description": "TYPE 0x00 is reserved",
"valid": false,
"content": "00"
},
{
"description": "BIP 380",
"valid": true,
"content": "01017c"
},
{
"description": "BIP 388",
"valid": true,
"content": "010184"
},
{
"description": "BIP 329",
"valid": true,
"content": "010149"
},
{
"description": "BIP 999",
"valid": true,
"content": "0103e7"
},
{
"description": "BIP max (65535)",
"valid": true,
"content": "01ffff"
},
{
"description": "BIP min (0)",
"valid": true,
"content": "010000"
},
{
"description": "Vendor-specific 00010203",
"valid": true,
"content": "020400010203"
},
{
"description": "Vendor-specific empty",
"valid": true,
"content": "0200"
},
{
"description": "TYPE 0x01 incomplete (missing DATA)",
"valid": false,
"content": "01"
},
{
"description": "TYPE 0x01 incomplete (only 1 byte of DATA)",
"valid": false,
"content": "0100"
},
{
"description": "TYPE 0x02 LENGTH exceeds remaining bytes",
"valid": false,
"content": "0205aabbcc"
},
{
"description": "TYPE >= 0x80 stops parsing",
"valid": false,
"content": "80"
},
{
"description": "TYPE >= 0x80 stops parsing (0xff)",
"valid": false,
"content": "ff"
},
{
"description": "Unknown TYPE < 0x80 with valid LENGTH skipped",
"valid": true,
"content": "0302aabb"
}
]
141 changes: 141 additions & 0 deletions src/test/data/bip_encrypted_backup_derivation_path.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
[
{
"description": "Empty derivation paths",
"paths": [],
"expected": "00"
},
{
"description": "Single path with one child: m/0",
"paths": ["m/0"],
"expected": "010100000000"
},
{
"description": "Single path with hardened child: m/44'",
"paths": ["m/44'"],
"expected": "01018000002c"
},
{
"description": "Standard BIP-84 path: m/84'/0'/0'",
"paths": ["m/84'/0'/0'"],
"expected": "0103800000548000000080000000"
},
{
"description": "Mixed hardened and normal: m/0/1'/2/3'",
"paths": ["m/0/1'/2/3'"],
"expected": "010400000000800000010000000280000003"
},
{
"description": "Multiple paths: m/0/1'/2/3' and m/84'/0'/0'/2'",
"paths": ["m/0/1'/2/3'", "m/84'/0'/0'/2'"],
"expected": "0204000000008000000100000002800000030480000054800000008000000080000002"
},
{
"description": "Path with large indices: m/2147483647'/2147483646",
"paths": ["m/2147483647'/2147483646"],
"expected": "0102ffffffff7ffffffe"
},
{
"description": "Single child path: m/1",
"paths": ["m/1"],
"expected": "010100000001"
},
{
"description": "Path with max normal index: m/2147483647",
"paths": ["m/2147483647"],
"expected": "01017fffffff"
},
{
"description": "Path with multiple normal indices: m/0/1/2/3/4",
"paths": ["m/0/1/2/3/4"],
"expected": "01050000000000000001000000020000000300000004"
},
{
"description": "Path with all hardened: m/0'/1'/2'",
"paths": ["m/0'/1'/2'"],
"expected": "0103800000008000000180000002"
},
{
"description": "Two different single-child paths: m/0 and m/1",
"paths": ["m/0", "m/1"],
"expected": "0201000000000100000001"
},
{
"description": "BIP-44 account 0: m/44'/0'/0'",
"paths": ["m/44'/0'/0'"],
"expected": "01038000002c8000000080000000"
},
{
"description": "BIP-49 account 0: m/49'/0'/0'",
"paths": ["m/49'/0'/0'"],
"expected": "0103800000318000000080000000"
},
{
"description": "256 paths should fail (exceeds u8::MAX)",
"paths": [
"m/0", "m/1", "m/2", "m/3", "m/4", "m/5", "m/6", "m/7", "m/8", "m/9",
"m/10", "m/11", "m/12", "m/13", "m/14", "m/15", "m/16", "m/17", "m/18", "m/19",
"m/20", "m/21", "m/22", "m/23", "m/24", "m/25", "m/26", "m/27", "m/28", "m/29",
"m/30", "m/31", "m/32", "m/33", "m/34", "m/35", "m/36", "m/37", "m/38", "m/39",
"m/40", "m/41", "m/42", "m/43", "m/44", "m/45", "m/46", "m/47", "m/48", "m/49",
"m/50", "m/51", "m/52", "m/53", "m/54", "m/55", "m/56", "m/57", "m/58", "m/59",
"m/60", "m/61", "m/62", "m/63", "m/64", "m/65", "m/66", "m/67", "m/68", "m/69",
"m/70", "m/71", "m/72", "m/73", "m/74", "m/75", "m/76", "m/77", "m/78", "m/79",
"m/80", "m/81", "m/82", "m/83", "m/84", "m/85", "m/86", "m/87", "m/88", "m/89",
"m/90", "m/91", "m/92", "m/93", "m/94", "m/95", "m/96", "m/97", "m/98", "m/99",
"m/100", "m/101", "m/102", "m/103", "m/104", "m/105", "m/106", "m/107", "m/108", "m/109",
"m/110", "m/111", "m/112", "m/113", "m/114", "m/115", "m/116", "m/117", "m/118", "m/119",
"m/120", "m/121", "m/122", "m/123", "m/124", "m/125", "m/126", "m/127", "m/128", "m/129",
"m/130", "m/131", "m/132", "m/133", "m/134", "m/135", "m/136", "m/137", "m/138", "m/139",
"m/140", "m/141", "m/142", "m/143", "m/144", "m/145", "m/146", "m/147", "m/148", "m/149",
"m/150", "m/151", "m/152", "m/153", "m/154", "m/155", "m/156", "m/157", "m/158", "m/159",
"m/160", "m/161", "m/162", "m/163", "m/164", "m/165", "m/166", "m/167", "m/168", "m/169",
"m/170", "m/171", "m/172", "m/173", "m/174", "m/175", "m/176", "m/177", "m/178", "m/179",
"m/180", "m/181", "m/182", "m/183", "m/184", "m/185", "m/186", "m/187", "m/188", "m/189",
"m/190", "m/191", "m/192", "m/193", "m/194", "m/195", "m/196", "m/197", "m/198", "m/199",
"m/200", "m/201", "m/202", "m/203", "m/204", "m/205", "m/206", "m/207", "m/208", "m/209",
"m/210", "m/211", "m/212", "m/213", "m/214", "m/215", "m/216", "m/217", "m/218", "m/219",
"m/220", "m/221", "m/222", "m/223", "m/224", "m/225", "m/226", "m/227", "m/228", "m/229",
"m/230", "m/231", "m/232", "m/233", "m/234", "m/235", "m/236", "m/237", "m/238", "m/239",
"m/240", "m/241", "m/242", "m/243", "m/244", "m/245", "m/246", "m/247", "m/248", "m/249",
"m/250", "m/251", "m/252", "m/253", "m/254", "m/255"
],
"expected": null
},
{
"description": "Path with 256 children should fail (exceeds u8::MAX)",
"paths": ["m/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0/0"],
"expected": null
},
{
"description": "255 paths should succeed (maximum allowed)",
"paths": [
"m/0", "m/1", "m/2", "m/3", "m/4", "m/5", "m/6", "m/7", "m/8", "m/9",
"m/10", "m/11", "m/12", "m/13", "m/14", "m/15", "m/16", "m/17", "m/18", "m/19",
"m/20", "m/21", "m/22", "m/23", "m/24", "m/25", "m/26", "m/27", "m/28", "m/29",
"m/30", "m/31", "m/32", "m/33", "m/34", "m/35", "m/36", "m/37", "m/38", "m/39",
"m/40", "m/41", "m/42", "m/43", "m/44", "m/45", "m/46", "m/47", "m/48", "m/49",
"m/50", "m/51", "m/52", "m/53", "m/54", "m/55", "m/56", "m/57", "m/58", "m/59",
"m/60", "m/61", "m/62", "m/63", "m/64", "m/65", "m/66", "m/67", "m/68", "m/69",
"m/70", "m/71", "m/72", "m/73", "m/74", "m/75", "m/76", "m/77", "m/78", "m/79",
"m/80", "m/81", "m/82", "m/83", "m/84", "m/85", "m/86", "m/87", "m/88", "m/89",
"m/90", "m/91", "m/92", "m/93", "m/94", "m/95", "m/96", "m/97", "m/98", "m/99",
"m/100", "m/101", "m/102", "m/103", "m/104", "m/105", "m/106", "m/107", "m/108", "m/109",
"m/110", "m/111", "m/112", "m/113", "m/114", "m/115", "m/116", "m/117", "m/118", "m/119",
"m/120", "m/121", "m/122", "m/123", "m/124", "m/125", "m/126", "m/127", "m/128", "m/129",
"m/130", "m/131", "m/132", "m/133", "m/134", "m/135", "m/136", "m/137", "m/138", "m/139",
"m/140", "m/141", "m/142", "m/143", "m/144", "m/145", "m/146", "m/147", "m/148", "m/149",
"m/150", "m/151", "m/152", "m/153", "m/154", "m/155", "m/156", "m/157", "m/158", "m/159",
"m/160", "m/161", "m/162", "m/163", "m/164", "m/165", "m/166", "m/167", "m/168", "m/169",
"m/170", "m/171", "m/172", "m/173", "m/174", "m/175", "m/176", "m/177", "m/178", "m/179",
"m/180", "m/181", "m/182", "m/183", "m/184", "m/185", "m/186", "m/187", "m/188", "m/189",
"m/190", "m/191", "m/192", "m/193", "m/194", "m/195", "m/196", "m/197", "m/198", "m/199",
"m/200", "m/201", "m/202", "m/203", "m/204", "m/205", "m/206", "m/207", "m/208", "m/209",
"m/210", "m/211", "m/212", "m/213", "m/214", "m/215", "m/216", "m/217", "m/218", "m/219",
"m/220", "m/221", "m/222", "m/223", "m/224", "m/225", "m/226", "m/227", "m/228", "m/229",
"m/230", "m/231", "m/232", "m/233", "m/234", "m/235", "m/236", "m/237", "m/238", "m/239",
"m/240", "m/241", "m/242", "m/243", "m/244", "m/245", "m/246", "m/247", "m/248", "m/249",
"m/250", "m/251", "m/252", "m/253", "m/254"
],
"expected": "ff0100000000010000000101000000020100000003010000000401000000050100000006010000000701000000080100000009010000000a010000000b010000000c010000000d010000000e010000000f0100000010010000001101000000120100000013010000001401000000150100000016010000001701000000180100000019010000001a010000001b010000001c010000001d010000001e010000001f0100000020010000002101000000220100000023010000002401000000250100000026010000002701000000280100000029010000002a010000002b010000002c010000002d010000002e010000002f0100000030010000003101000000320100000033010000003401000000350100000036010000003701000000380100000039010000003a010000003b010000003c010000003d010000003e010000003f0100000040010000004101000000420100000043010000004401000000450100000046010000004701000000480100000049010000004a010000004b010000004c010000004d010000004e010000004f0100000050010000005101000000520100000053010000005401000000550100000056010000005701000000580100000059010000005a010000005b010000005c010000005d010000005e010000005f0100000060010000006101000000620100000063010000006401000000650100000066010000006701000000680100000069010000006a010000006b010000006c010000006d010000006e010000006f0100000070010000007101000000720100000073010000007401000000750100000076010000007701000000780100000079010000007a010000007b010000007c010000007d010000007e010000007f0100000080010000008101000000820100000083010000008401000000850100000086010000008701000000880100000089010000008a010000008b010000008c010000008d010000008e010000008f0100000090010000009101000000920100000093010000009401000000950100000096010000009701000000980100000099010000009a010000009b010000009c010000009d010000009e010000009f01000000a001000000a101000000a201000000a301000000a401000000a501000000a601000000a701000000a801000000a901000000aa01000000ab01000000ac01000000ad01000000ae01000000af01000000b001000000b101000000b201000000b301000000b401000000b501000000b601000000b701000000b801000000b901000000ba01000000bb01000000bc01000000bd01000000be01000000bf01000000c001000000c101000000c201000000c301000000c401000000c501000000c601000000c701000000c801000000c901000000ca01000000cb01000000cc01000000cd01000000ce01000000cf01000000d001000000d101000000d201000000d301000000d401000000d501000000d601000000d701000000d801000000d901000000da01000000db01000000dc01000000dd01000000de01000000df01000000e001000000e101000000e201000000e301000000e401000000e501000000e601000000e701000000e801000000e901000000ea01000000eb01000000ec01000000ed01000000ee01000000ef01000000f001000000f101000000f201000000f301000000f401000000f501000000f601000000f701000000f801000000f901000000fa01000000fb01000000fc01000000fd01000000fe"
}
]
Loading
Loading