Files
easy-wallet/tests/test_crypto.py
Davide Grilli c98d4db711 test: expand test suite to 194 tests across all modules
Add tests/test_crypto.py (22 tests) and tests/test_single_wallet.py
(46 tests) for previously uncovered modules; extend existing test files
with WIF format checks, error/validation cases, BIP39 passphrase,
account index, words_num, and redeem script structure tests
2026-03-10 09:40:01 +01:00

157 lines
4.7 KiB
Python

import sys
import os
import base64
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
import pytest
from src.crypto import pw_encode, pw_decode, _derive_key
SECRETSCAN_URL = "https://secretscan.org/Bitcoin?address={}"
PASSWORD = "correct_horse_battery_staple"
PLAINTEXT = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
# --- Key derivation ---
def test_derive_key_is_32_bytes():
key = _derive_key(PASSWORD)
assert len(key) == 32
def test_derive_key_deterministic():
assert _derive_key(PASSWORD) == _derive_key(PASSWORD)
def test_derive_key_different_passwords_differ():
assert _derive_key("password1") != _derive_key("password2")
def test_derive_key_returns_bytes():
assert isinstance(_derive_key(PASSWORD), bytes)
# --- pw_encode output format ---
def test_pw_encode_returns_string():
result = pw_encode(PLAINTEXT, PASSWORD)
assert isinstance(result, str)
def test_pw_encode_is_valid_base64():
result = pw_encode(PLAINTEXT, PASSWORD)
raw = base64.b64decode(result)
# Must contain at least 16 bytes IV + 16 bytes (one AES block)
assert len(raw) >= 32
def test_pw_encode_includes_iv():
# Encoded payload = IV (16 bytes) + ciphertext; base64 of 32+ bytes
result = pw_encode(PLAINTEXT, PASSWORD)
raw = base64.b64decode(result)
assert len(raw) >= 16
# --- Random IV: same inputs produce different ciphertext each call ---
def test_pw_encode_random_iv_produces_unique_ciphertext():
c1 = pw_encode(PLAINTEXT, PASSWORD)
c2 = pw_encode(PLAINTEXT, PASSWORD)
assert c1 != c2
def test_pw_encode_random_iv_different_on_three_calls():
outputs = {pw_encode(PLAINTEXT, PASSWORD) for _ in range(5)}
assert len(outputs) == 5
# --- Round-trip ---
def test_pw_round_trip_basic():
ciphertext = pw_encode(PLAINTEXT, PASSWORD)
assert pw_decode(ciphertext, PASSWORD) == PLAINTEXT
def test_pw_round_trip_empty_string():
ciphertext = pw_encode("", PASSWORD)
assert pw_decode(ciphertext, PASSWORD) == ""
def test_pw_round_trip_unicode():
text = "passphrase: résumé café 日本語 🔑"
ciphertext = pw_encode(text, PASSWORD)
assert pw_decode(ciphertext, PASSWORD) == text
def test_pw_round_trip_long_text():
text = "a" * 1000
ciphertext = pw_encode(text, PASSWORD)
assert pw_decode(ciphertext, PASSWORD) == text
def test_pw_round_trip_special_characters():
text = '!@#$%^&*()_+-=[]{}|;\':",./<>?`~\\'
ciphertext = pw_encode(text, PASSWORD)
assert pw_decode(ciphertext, PASSWORD) == text
def test_pw_round_trip_private_key_wif():
# Simulate encrypting a real WIF-looking private key
wif = "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU74NMTptX4"
ciphertext = pw_encode(wif, PASSWORD)
assert pw_decode(ciphertext, PASSWORD) == wif
# --- Wrong password / corrupted data ---
def test_pw_decode_wrong_password_raises_value_error():
ciphertext = pw_encode(PLAINTEXT, PASSWORD)
with pytest.raises(ValueError):
pw_decode(ciphertext, "wrong_password")
def test_pw_decode_wrong_password_error_message():
ciphertext = pw_encode(PLAINTEXT, PASSWORD)
with pytest.raises(ValueError, match="Incorrect password or corrupted data"):
pw_decode(ciphertext, "wrong_password")
def test_pw_decode_empty_password_still_encrypts():
# Empty password is technically valid; round-trip must work
ciphertext = pw_encode(PLAINTEXT, "")
assert pw_decode(ciphertext, "") == PLAINTEXT
def test_pw_decode_corrupted_not_base64_raises():
with pytest.raises(ValueError):
pw_decode("!!!not_valid_base64!!!", PASSWORD)
def test_pw_decode_too_short_data_raises():
# 15 bytes = less than an IV (16 bytes), cannot contain any ciphertext
short = base64.b64encode(b"x" * 15).decode()
with pytest.raises(ValueError):
pw_decode(short, PASSWORD)
def test_pw_decode_only_iv_no_ciphertext_raises():
# Exactly 16 bytes: IV with zero ciphertext → unpadding must fail
iv_only = base64.b64encode(b"A" * 16).decode()
with pytest.raises(ValueError):
pw_decode(iv_only, PASSWORD)
# --- SecretScan integration note ---
def test_crypto_print_secretscan(capsys):
"""Document that encrypted wallet seeds can be verified via SecretScan after decryption."""
ciphertext = pw_encode(PLAINTEXT, PASSWORD)
decrypted = pw_decode(ciphertext, PASSWORD)
# Once decrypted, the seed can be used to derive addresses verifiable on SecretScan
url = SECRETSCAN_URL.format("bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu")
print(f"\n[Crypto] Encrypted seed round-trip OK")
print(f"[Crypto] Derived addresses verifiable at: {url}")
captured = capsys.readouterr()
assert "secretscan.org" in captured.out
assert decrypted == PLAINTEXT