fix(lite): require a confirmed 8+ character passphrase to encrypt
The Lite "Encrypt wallet" field and the Create/Restore passphrase had no confirmation and no minimum length; only an empty passphrase was refused. Encrypting locks the wallet at once, and a locked wallet cannot reveal its seed, so a typo there could cost a user without a seed backup their funds. The full-node encrypt dialog already requires 8 characters and a confirmation. This applies to new passphrases only. Opening and unlocking are unchanged: an existing wallet with a shorter passphrase still opens and unlocks. - A pure liteEncryptPassphraseValid(pass, confirm): at least 8 bytes and equal to the confirmation. It takes string_views, so the page's secret buffers are never copied. - liteLifecyclePassphraseAcceptable(op, pass, confirm) gates the Settings create/open/restore request. Open is never checked, because its passphrase field also unlocks an existing wallet. Create and Restore are checked only when a passphrase was entered; an empty one still creates an unencrypted wallet. - Encrypt gets a Confirm field. The button stays disabled until the passphrase is valid, and a hint says why (too short, with the count, or not matching). - Create/Restore show a Confirm row once a passphrase is typed, with the same hint. Validate stays disabled while the passphrase is invalid, so a click never wipes a typed seed over a passphrase typo. The request builder re-checks the rule. - The confirmation buffers are wiped with their passphrase: after a run or an encrypt, and when the Confirm row is hidden. - Once the wallet is closed, the Settings page wipes the Encrypt, Confirm and Decrypt passphrase buffers whole every time, also when no secret was revealed; before, closing the wallet wiped them only while clearing a revealed backup or secret. They are zeroed in full because an emptied field can still hold the old tail past its first NUL. - New copy under Encrypt: "Anyone with the wallet file can try passphrases offline. Back up your seed phrase first and use a long passphrase." - i18n: three new keys (lite_confirm_label, tt_lite_confirm_pass, lite_encrypt_offline_warning) in i18n.cpp and the 8 language files, added additively. The hints reuse the wizard's wiz_pass_too_short and wiz_pass_mismatch. No new CJK glyphs, so the subset font is unchanged. Tests: the validator rejects empty, short (7) and mismatched or unconfirmed passphrases, and accepts 8 or more with a match. The request gate always accepts Open, even "abc" with no confirmation, accepts Create/Restore without a passphrase, and applies the rule to them otherwise. An encrypted, locked wallet still opens and unlocks through the controller with the 3-character passphrase "abc". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -87,6 +87,7 @@
|
||||
#include "wallet/lite_owned_string.h"
|
||||
#include "wallet/lite_rollout_policy.h"
|
||||
#include "wallet/lite_wallet_controller.h"
|
||||
#include "wallet/lite_wallet_lifecycle_ui_adapter.h"
|
||||
#include "wallet/lite_wallet_gateway.h"
|
||||
#include "wallet/lite_wallet_state_mapper.h"
|
||||
#include "config/settings.h"
|
||||
@@ -8783,6 +8784,90 @@ void testLiteWalletControllerAsyncAppliesPassphrase()
|
||||
dragonx::test::g_liteFakeWarmupServerSubstr.clear();
|
||||
}
|
||||
|
||||
// C2 (s1-D8, X7): a NEW Lite passphrase (Settings "Encrypt wallet", or Create/Restore with one entered)
|
||||
// must be 8+ characters and confirmed. Opening and unlocking an existing wallet never apply the rule, so
|
||||
// a wallet encrypted earlier with a short passphrase still opens and unlocks.
|
||||
void testLiteNewPassphraseRules()
|
||||
{
|
||||
using namespace dragonx::wallet;
|
||||
using Op = LiteWalletLifecycleOperation;
|
||||
|
||||
EXPECT_EQ(kLiteMinNewPassphraseLength, static_cast<std::size_t>(8));
|
||||
|
||||
// The validator: empty, short or mismatched is rejected; 8+ and confirmed is accepted.
|
||||
EXPECT_FALSE(liteEncryptPassphraseValid("", ""));
|
||||
EXPECT_FALSE(liteEncryptPassphraseValid("a", "a"));
|
||||
EXPECT_FALSE(liteEncryptPassphraseValid("1234567", "1234567")); // 7 < 8
|
||||
EXPECT_FALSE(liteEncryptPassphraseValid("", "12345678"));
|
||||
EXPECT_FALSE(liteEncryptPassphraseValid("12345678", "")); // not confirmed
|
||||
EXPECT_FALSE(liteEncryptPassphraseValid("12345678", "12345679")); // mismatch
|
||||
EXPECT_FALSE(liteEncryptPassphraseValid("12345678", "12345678 ")); // a trailing space is a mismatch
|
||||
EXPECT_TRUE(liteEncryptPassphraseValid("12345678", "12345678")); // exactly 8
|
||||
EXPECT_TRUE(liteEncryptPassphraseValid("correct horse battery staple", "correct horse battery staple"));
|
||||
{
|
||||
// The page passes its fixed char buffers straight in (no copy of the secret).
|
||||
char pass[128] = "hunter2hunter2";
|
||||
char confirm[128] = "hunter2hunter2";
|
||||
EXPECT_TRUE(liteEncryptPassphraseValid(pass, confirm));
|
||||
confirm[0] = 'H';
|
||||
EXPECT_FALSE(liteEncryptPassphraseValid(pass, confirm));
|
||||
}
|
||||
|
||||
// The Settings request gate. OpenExisting is never validated: a short or unconfirmed passphrase
|
||||
// for an existing wallet passes.
|
||||
EXPECT_TRUE(liteLifecyclePassphraseAcceptable(Op::OpenExisting, "abc", ""));
|
||||
EXPECT_TRUE(liteLifecyclePassphraseAcceptable(Op::OpenExisting, "hunter2", "different"));
|
||||
EXPECT_TRUE(liteLifecyclePassphraseAcceptable(Op::OpenExisting, "", ""));
|
||||
// Create/Restore without a passphrase make an unencrypted wallet: nothing to confirm.
|
||||
EXPECT_TRUE(liteLifecyclePassphraseAcceptable(Op::CreateNew, "", ""));
|
||||
EXPECT_TRUE(liteLifecyclePassphraseAcceptable(Op::RestoreFromSeed, "", ""));
|
||||
// With a passphrase they set a NEW one: 8+ characters and confirmed.
|
||||
for (const Op op : {Op::CreateNew, Op::RestoreFromSeed}) {
|
||||
EXPECT_FALSE(liteLifecyclePassphraseAcceptable(op, "abc", "abc"));
|
||||
EXPECT_FALSE(liteLifecyclePassphraseAcceptable(op, "1234567", "1234567"));
|
||||
EXPECT_FALSE(liteLifecyclePassphraseAcceptable(op, "12345678", ""));
|
||||
EXPECT_FALSE(liteLifecyclePassphraseAcceptable(op, "12345678", "1234567X"));
|
||||
EXPECT_TRUE(liteLifecyclePassphraseAcceptable(op, "12345678", "12345678"));
|
||||
}
|
||||
|
||||
// The controller's open and unlock paths take an existing short passphrase as is: an encrypted,
|
||||
// locked wallet opens and unlocks with "abc" (as the Settings Open request sends it), and unlocks
|
||||
// with it again after a lock.
|
||||
{
|
||||
const auto liteCaps = makeWalletCapabilities(WalletBuildKind::Lite, false, true);
|
||||
LiteConnectionSettings conn;
|
||||
conn.chainName = "main";
|
||||
conn.servers = { LiteServerEndpoint{"https://good.example", "Good", true} };
|
||||
conn.selectionMode = LiteServerSelectionMode::Sticky;
|
||||
conn.stickyServerUrl = "https://good.example";
|
||||
|
||||
dragonx::test::resetLiteFakeCounters();
|
||||
dragonx::test::g_liteFakeWalletExists = true;
|
||||
dragonx::test::g_liteFakeEncrypted = true;
|
||||
dragonx::test::g_liteFakeLocked = true;
|
||||
LiteWalletController controller(liteCaps, conn,
|
||||
LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||
LiteWalletOpenRequest req;
|
||||
req.passphrase = "abc";
|
||||
EXPECT_TRUE(liteLifecyclePassphraseAcceptable(Op::OpenExisting, req.passphrase, ""));
|
||||
EXPECT_TRUE(controller.beginOpenWalletAsync(req));
|
||||
for (int i = 0; i < 400 && controller.lifecycleRequestInProgress(); ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
controller.pumpLifecycleResult();
|
||||
EXPECT_TRUE(controller.walletOpen());
|
||||
EXPECT_TRUE(controller.encryptionStatus().encrypted);
|
||||
EXPECT_FALSE(controller.encryptionStatus().locked); // the short passphrase unlocked it
|
||||
EXPECT_TRUE(controller.lockWallet());
|
||||
EXPECT_TRUE(controller.encryptionStatus().locked);
|
||||
EXPECT_TRUE(controller.unlockWallet("abc"));
|
||||
EXPECT_FALSE(controller.encryptionStatus().locked);
|
||||
}
|
||||
|
||||
dragonx::test::g_liteFakeWalletExists = false;
|
||||
dragonx::test::g_liteFakeEncrypted = false;
|
||||
dragonx::test::g_liteFakeLocked = false;
|
||||
}
|
||||
|
||||
// WalletState::tx_revision (the History / Mining caches key on it): sameTransactionInfo covers every
|
||||
// TransactionInfo field and compares `amount` bit-exactly; setTransactionsIfChanged bumps only on a real change
|
||||
// and otherwise keeps the old list.
|
||||
@@ -15713,6 +15798,7 @@ int main()
|
||||
testLiteWalletControllerEncryption();
|
||||
testLiteWalletControllerCreateEncryptsWithPassphrase();
|
||||
testLiteWalletControllerAsyncAppliesPassphrase();
|
||||
testLiteNewPassphraseRules();
|
||||
testLiteChainNameMigration();
|
||||
testLiteRefreshModelAppliesToWalletState();
|
||||
testSameTransactionList();
|
||||
|
||||
Reference in New Issue
Block a user