test: add wallet-encryption (CCrypter) coverage

crypter.cpp had zero tests despite guarding every encrypted wallet. Add
8 cases: passphrase round-trip for both KDFs (sha512 method 0 and scrypt
method 1), wrong-passphrase rejection, salt-affects-key, KDF determinism,
bad-parameter rejection (zero rounds / short salt / encrypt-before-key),
the EncryptSecret/DecryptSecret private-key path with a uint256 IV, and
ciphertext-tamper rejection. Round-trip/negative style, no brittle hard-coded
ciphertext. No implementation change (crypter.cpp is correct).

Note captured in the test: the wallet uses a uint256 as the AES IV but
AES-256-CBC consumes only the first 16 (little-endian) memory bytes -- a
subtlety worth remembering for anyone touching the key-encryption path.
This commit is contained in:
Krystie
2026-07-04 19:35:00 -07:00
parent bfe4681d97
commit 43eab5f8cd
+160
View File
@@ -0,0 +1,160 @@
// Wallet-encryption (CCrypter) tests. Added 2026-07-04 during the test audit.
// crypter.cpp had ZERO coverage despite guarding every encrypted wallet: a
// bug here corrupts keys or weakens protection. These are round-trip,
// negative, and determinism checks (no brittle hard-coded ciphertext).
#include <boost/test/unit_test.hpp>
#include "../crypter.h"
#include "../key.h"
#include <string>
#include <vector>
BOOST_AUTO_TEST_SUITE(crypter_tests)
static std::vector<unsigned char> Salt8(unsigned char seed)
{
return std::vector<unsigned char>(WALLET_CRYPTO_SALT_SIZE, seed);
}
static CKeyingMaterial MakePlain(const std::string& s)
{
return CKeyingMaterial(s.begin(), s.end());
}
// sha512 KDF (method 0): passphrase -> encrypt -> decrypt round-trips.
BOOST_AUTO_TEST_CASE(passphrase_roundtrip_sha512)
{
CCrypter c;
BOOST_REQUIRE(c.SetKeyFromPassphrase(SecureString("correct horse"), Salt8(0x11), 1000, 0));
CKeyingMaterial plain = MakePlain("a 32-byte secret payload here!!");
std::vector<unsigned char> cipher;
BOOST_REQUIRE(c.Encrypt(plain, cipher));
BOOST_CHECK(cipher.size() >= plain.size());
BOOST_CHECK(cipher != std::vector<unsigned char>(plain.begin(), plain.end()));
CKeyingMaterial out;
BOOST_REQUIRE(c.Decrypt(cipher, out));
BOOST_CHECK(out == plain);
}
// scrypt KDF (method 1) round-trips too.
BOOST_AUTO_TEST_CASE(passphrase_roundtrip_scrypt)
{
CCrypter c;
BOOST_REQUIRE(c.SetKeyFromPassphrase(SecureString("correct horse"), Salt8(0x22), 100, 1));
CKeyingMaterial plain = MakePlain("scrypt-derived key path payload");
std::vector<unsigned char> cipher;
BOOST_REQUIRE(c.Encrypt(plain, cipher));
CKeyingMaterial out;
BOOST_REQUIRE(c.Decrypt(cipher, out));
BOOST_CHECK(out == plain);
}
// A different passphrase derives a different key: decryption must NOT recover
// the plaintext (AES-CBC padding check rejects the wrong key).
BOOST_AUTO_TEST_CASE(wrong_passphrase_fails)
{
std::vector<unsigned char> salt = Salt8(0x33);
CCrypter good;
BOOST_REQUIRE(good.SetKeyFromPassphrase(SecureString("right pass"), salt, 1000, 0));
CKeyingMaterial plain = MakePlain("top secret wallet material x");
std::vector<unsigned char> cipher;
BOOST_REQUIRE(good.Encrypt(plain, cipher));
CCrypter bad;
BOOST_REQUIRE(bad.SetKeyFromPassphrase(SecureString("wrong pass"), salt, 1000, 0));
CKeyingMaterial out;
bool ok = bad.Decrypt(cipher, out);
// Either the padding check fails outright, or (rarely) it "succeeds" with
// garbage — in no case may it recover the real plaintext.
BOOST_CHECK(!ok || out != plain);
}
// Different salt => different derived key => different ciphertext.
BOOST_AUTO_TEST_CASE(salt_affects_key)
{
CKeyingMaterial plain = MakePlain("same plaintext, two salts here");
CCrypter a, b;
BOOST_REQUIRE(a.SetKeyFromPassphrase(SecureString("pw"), Salt8(0x01), 1000, 0));
BOOST_REQUIRE(b.SetKeyFromPassphrase(SecureString("pw"), Salt8(0x02), 1000, 0));
std::vector<unsigned char> ca, cb;
BOOST_REQUIRE(a.Encrypt(plain, ca));
BOOST_REQUIRE(b.Encrypt(plain, cb));
BOOST_CHECK(ca != cb);
}
// Same passphrase+salt+rounds is deterministic (fixed key+IV, AES-CBC).
BOOST_AUTO_TEST_CASE(derivation_is_deterministic)
{
CKeyingMaterial plain = MakePlain("deterministic check payload!!");
CCrypter a, b;
BOOST_REQUIRE(a.SetKeyFromPassphrase(SecureString("pw"), Salt8(0x44), 2000, 0));
BOOST_REQUIRE(b.SetKeyFromPassphrase(SecureString("pw"), Salt8(0x44), 2000, 0));
std::vector<unsigned char> ca, cb;
BOOST_REQUIRE(a.Encrypt(plain, ca));
BOOST_REQUIRE(b.Encrypt(plain, cb));
BOOST_CHECK(ca == cb);
}
// Bad parameters are rejected: zero rounds and wrong salt length.
BOOST_AUTO_TEST_CASE(bad_params_rejected)
{
CCrypter c;
BOOST_CHECK(!c.SetKeyFromPassphrase(SecureString("pw"), Salt8(0x55), 0, 0));
std::vector<unsigned char> shortSalt(WALLET_CRYPTO_SALT_SIZE - 1, 0x00);
BOOST_CHECK(!c.SetKeyFromPassphrase(SecureString("pw"), shortSalt, 1000, 0));
// Encrypt before any key is set must fail.
CCrypter unset;
std::vector<unsigned char> cipher;
BOOST_CHECK(!unset.Encrypt(MakePlain("x"), cipher));
}
// The actual wallet key-encryption path: EncryptSecret/DecryptSecret with a
// 32-byte master key and a uint256 IV round-trips a private-key-sized secret.
BOOST_AUTO_TEST_CASE(encrypt_secret_roundtrip)
{
CKeyingMaterial master(WALLET_CRYPTO_KEY_SIZE, 0xAB);
uint256 iv("0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20");
CSecret secret;
for (int i = 0; i < 32; i++) secret.push_back((unsigned char)(i * 7 + 1));
std::vector<unsigned char> cipher;
BOOST_REQUIRE(EncryptSecret(master, secret, iv, cipher));
BOOST_CHECK(cipher.size() >= secret.size());
CSecret recovered;
BOOST_REQUIRE(DecryptSecret(master, cipher, iv, recovered));
BOOST_CHECK(recovered == secret);
// Wrong IV must not recover the secret. NOTE: uint256 hex is big-endian
// for display but little-endian in memory, and AES-256-CBC uses only the
// FIRST 16 memory bytes as the IV. So we must perturb a low-order byte
// (the trailing hex pair), which maps to memory byte 0 -- inside the AES
// IV window. A wrong IV corrupts the first plaintext block, so the full
// 32-byte secret cannot be recovered intact.
uint256 iv2("0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f21");
CSecret wrong;
bool ok = DecryptSecret(master, cipher, iv2, wrong);
BOOST_CHECK(!ok || wrong != secret);
}
// Flipping a ciphertext byte must break decryption (padding/integrity).
BOOST_AUTO_TEST_CASE(tampered_ciphertext_fails)
{
CCrypter c;
BOOST_REQUIRE(c.SetKeyFromPassphrase(SecureString("pw"), Salt8(0x66), 1000, 0));
CKeyingMaterial plain = MakePlain("integrity of this block matters");
std::vector<unsigned char> cipher;
BOOST_REQUIRE(c.Encrypt(plain, cipher));
cipher[cipher.size() - 1] ^= 0x01; // corrupt last block
CKeyingMaterial out;
bool ok = c.Decrypt(cipher, out);
BOOST_CHECK(!ok || out != plain);
}
BOOST_AUTO_TEST_SUITE_END()