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