feat: add Electrum-compatible wallet encryption
- Add src/crypto.py with AES-256-CBC pw_encode/pw_decode using double SHA256 key derivation (Electrum standard) - Add encrypt_wallet() and decrypt_wallet() to src/hd_wallet.py - Prompt for encryption password when saving HD wallet to file - Add 13 encryption/decryption tests to tests/test_hd_wallet.py - Add pycryptodome to requirements.txt
This commit is contained in:
@@ -2,7 +2,8 @@ import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from src.hd_wallet import generate_hd_wallet
|
||||
from src.hd_wallet import generate_hd_wallet, encrypt_wallet, decrypt_wallet
|
||||
from src.crypto import pw_decode
|
||||
|
||||
SECRETSCAN_URL = "https://secretscan.org/Bitcoin?address={}"
|
||||
|
||||
@@ -222,6 +223,103 @@ def test_hd_generates_mnemonic_when_none():
|
||||
assert len(result['keystore']['seed'].split()) == 12
|
||||
|
||||
|
||||
# --- Encryption ---
|
||||
|
||||
TEST_PASSWORD = "correct_horse_battery_staple"
|
||||
|
||||
def test_hd_no_encryption_by_default():
|
||||
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
|
||||
assert result['use_encryption'] is False
|
||||
|
||||
|
||||
def test_hd_encryption_sets_flag():
|
||||
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
|
||||
encrypted = encrypt_wallet(result, TEST_PASSWORD)
|
||||
assert encrypted['use_encryption'] is True
|
||||
|
||||
|
||||
def test_hd_encryption_encrypts_seed():
|
||||
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
|
||||
encrypted = encrypt_wallet(result, TEST_PASSWORD)
|
||||
assert encrypted['keystore']['seed'] != TEST_MNEMONIC
|
||||
|
||||
|
||||
def test_hd_encryption_encrypts_xprv():
|
||||
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
|
||||
original_xprv = result['keystore']['xprv']
|
||||
encrypted = encrypt_wallet(result, TEST_PASSWORD)
|
||||
assert encrypted['keystore']['xprv'] != original_xprv
|
||||
|
||||
|
||||
def test_hd_encryption_leaves_xpub_plaintext():
|
||||
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
|
||||
encrypted = encrypt_wallet(result, TEST_PASSWORD)
|
||||
assert encrypted['keystore']['xpub'] == result['keystore']['xpub']
|
||||
|
||||
|
||||
def test_hd_encryption_leaves_addresses_plaintext():
|
||||
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
|
||||
encrypted = encrypt_wallet(result, TEST_PASSWORD)
|
||||
assert encrypted['addresses']['receiving'][0]['address'] == result['addresses']['receiving'][0]['address']
|
||||
assert encrypted['addresses']['receiving'][0]['public_key'] == result['addresses']['receiving'][0]['public_key']
|
||||
|
||||
|
||||
def test_hd_encryption_encrypts_private_keys():
|
||||
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=2)
|
||||
encrypted = encrypt_wallet(result, TEST_PASSWORD)
|
||||
for i in range(2):
|
||||
orig = result['addresses']['receiving'][i]
|
||||
enc = encrypted['addresses']['receiving'][i]
|
||||
assert enc['private_key_hex'] != orig['private_key_hex']
|
||||
assert enc['private_key_wif'] != orig['private_key_wif']
|
||||
|
||||
|
||||
def test_hd_encryption_round_trip_seed():
|
||||
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
|
||||
encrypted = encrypt_wallet(result, TEST_PASSWORD)
|
||||
decrypted = decrypt_wallet(encrypted, TEST_PASSWORD)
|
||||
assert decrypted['keystore']['seed'] == TEST_MNEMONIC
|
||||
|
||||
|
||||
def test_hd_encryption_round_trip_xprv():
|
||||
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
|
||||
encrypted = encrypt_wallet(result, TEST_PASSWORD)
|
||||
decrypted = decrypt_wallet(encrypted, TEST_PASSWORD)
|
||||
assert decrypted['keystore']['xprv'] == result['keystore']['xprv']
|
||||
|
||||
|
||||
def test_hd_encryption_round_trip_private_keys():
|
||||
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=3)
|
||||
encrypted = encrypt_wallet(result, TEST_PASSWORD)
|
||||
decrypted = decrypt_wallet(encrypted, TEST_PASSWORD)
|
||||
for i in range(3):
|
||||
assert decrypted['addresses']['receiving'][i]['private_key_hex'] == result['addresses']['receiving'][i]['private_key_hex']
|
||||
assert decrypted['addresses']['receiving'][i]['private_key_wif'] == result['addresses']['receiving'][i]['private_key_wif']
|
||||
|
||||
|
||||
def test_hd_decrypt_wrong_password():
|
||||
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
|
||||
encrypted = encrypt_wallet(result, TEST_PASSWORD)
|
||||
try:
|
||||
decrypt_wallet(encrypted, "wrong_password")
|
||||
assert False, "Should have raised ValueError"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def test_hd_encrypt_does_not_mutate_original():
|
||||
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
|
||||
original_seed = result['keystore']['seed']
|
||||
encrypt_wallet(result, TEST_PASSWORD)
|
||||
assert result['keystore']['seed'] == original_seed
|
||||
|
||||
|
||||
def test_hd_decrypt_unencrypted_is_noop():
|
||||
result = generate_hd_wallet('mainnet', 'bip84', TEST_MNEMONIC, num_addresses=1)
|
||||
decrypted = decrypt_wallet(result, TEST_PASSWORD)
|
||||
assert decrypted['keystore']['seed'] == result['keystore']['seed']
|
||||
|
||||
|
||||
# --- SecretScan ---
|
||||
|
||||
def test_hd_print_secretscan(capsys):
|
||||
|
||||
Reference in New Issue
Block a user