fix(lite): require exactly 24 words on first-run restore (crash on valid seed)
The lite first-run restore wizard enabled Restore for {12,15,18,21,24}-word
phrases, but the SDXL backend only accepts 24-word / 32-byte-entropy seeds:
LightWallet::new does copy_from_slice(&phrase.entropy()) into a [u8;32]
(lightwallet.rs:231), which panics on 16/20/24/28-byte entropy. Mnemonic::
from_phrase accepts the shorter valid phrase, and the restore FFI
litelib_initialize_new_from_phrase (lib.rs:127) has no catch_unwind (unlike
litelib_execute), so the panic unwinds across extern "C" -> process abort
(UB on the pinned rustc 1.63). A user restoring a legitimate 12-word seed
from another wallet crashed the app.
The Settings restore gate was already tightened to == 24 (6ff1fda) but the
first-run wizard gate (df14533) was never updated — same restore path, two
verdicts, crash only via the more-common first-run path.
Add shared util/seed_phrase.{h,cpp} as the single source of truth:
- normalizeSeedPhrase: fold NBSP/en/em/ideographic/narrow spaces to ASCII,
strip zero-width marks, collapse+trim (word bytes untouched)
- seedPhraseWordCount
- isCompleteRecoveryPhrase(int) == 24 (the sole SDXL contract)
Both restore gates now count via the normalizer and gate via
isCompleteRecoveryPhrase, and both submit the normalized phrase. This closes
the crash, reconciles the two gates so they can't drift again, and — because
tiny-bip39 splits on literal ASCII space with no NFKD — makes an NBSP-pasted
24-word seed (common from PDFs/note apps) restore correctly instead of being
undercounted and rejected.
Adds testSeedPhraseHelpers. Suite green (1/1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,7 @@
|
||||
#include "ui/notifications.h"
|
||||
#include "data/seed_migration_resume.h"
|
||||
#include "util/address_validation.h"
|
||||
#include "util/seed_phrase.h"
|
||||
#include "util/amount_format.h"
|
||||
#include "util/payment_uri.h"
|
||||
#include "util/platform.h"
|
||||
@@ -6044,6 +6045,47 @@ void testPrivateKeyImportRecognition()
|
||||
EXPECT_FALSE(WalletSecurityController::isRecognizedImportKey("hello world"));
|
||||
}
|
||||
|
||||
// Seed-phrase normalization + word count + the 24-word completeness gate. Guards the lite-restore
|
||||
// crash fix (only 24-word/32-byte-entropy seeds are safe for the SDXL backend) and the NBSP-paste
|
||||
// recovery fix. Both restore gates (first-run wizard + Settings) route through these.
|
||||
void testSeedPhraseHelpers()
|
||||
{
|
||||
using dragonx::util::normalizeSeedPhrase;
|
||||
using dragonx::util::seedPhraseWordCount;
|
||||
using dragonx::util::isCompleteRecoveryPhrase;
|
||||
|
||||
// --- completeness gate: 24 words only (12/15/18/21 valid-BIP39 lengths crash the backend) ---
|
||||
EXPECT_TRUE(isCompleteRecoveryPhrase(24));
|
||||
EXPECT_FALSE(isCompleteRecoveryPhrase(12));
|
||||
EXPECT_FALSE(isCompleteRecoveryPhrase(15));
|
||||
EXPECT_FALSE(isCompleteRecoveryPhrase(21));
|
||||
EXPECT_FALSE(isCompleteRecoveryPhrase(23));
|
||||
EXPECT_FALSE(isCompleteRecoveryPhrase(25));
|
||||
EXPECT_FALSE(isCompleteRecoveryPhrase(0));
|
||||
|
||||
// --- plain ASCII: trim, collapse runs, count exactly; the common case must be untouched otherwise ---
|
||||
EXPECT_EQ(normalizeSeedPhrase(" alpha beta\tgamma\ndelta "), std::string("alpha beta gamma delta"));
|
||||
EXPECT_EQ(seedPhraseWordCount(normalizeSeedPhrase("alpha beta gamma")), 3);
|
||||
EXPECT_EQ(seedPhraseWordCount(""), 0);
|
||||
EXPECT_EQ(seedPhraseWordCount(normalizeSeedPhrase(" \t \n ")), 0); // whitespace-only
|
||||
|
||||
// --- NBSP (U+00A0 = 0xC2 0xA0) between words must fold to a real space, not glue the words ---
|
||||
EXPECT_EQ(normalizeSeedPhrase("alpha\xC2\xA0" "beta"), std::string("alpha beta"));
|
||||
EXPECT_EQ(seedPhraseWordCount(normalizeSeedPhrase("alpha\xC2\xA0" "beta")), 2);
|
||||
// Other Unicode spaces: en space U+2002, ideographic U+3000, narrow NBSP U+202F.
|
||||
EXPECT_EQ(normalizeSeedPhrase("a\xE2\x80\x82" "b\xE3\x80\x80" "c\xE2\x80\xAF" "d"), std::string("a b c d"));
|
||||
// Zero-width chars (U+200B, U+FEFF BOM) are stripped, not treated as separators.
|
||||
EXPECT_EQ(normalizeSeedPhrase("\xEF\xBB\xBF" "alpha\xE2\x80\x8B beta"), std::string("alpha beta"));
|
||||
|
||||
// --- a full 24-word phrase pasted with NBSP separators counts as 24 (regression for the fix) ---
|
||||
std::string words24;
|
||||
for (int i = 0; i < 24; ++i) { if (i) words24 += "\xC2\xA0"; words24 += "word"; }
|
||||
EXPECT_EQ(seedPhraseWordCount(normalizeSeedPhrase(words24)), 24);
|
||||
EXPECT_TRUE(isCompleteRecoveryPhrase(seedPhraseWordCount(normalizeSeedPhrase(words24))));
|
||||
// The normalized form is plain single-space separated (what the backend's split(" ") needs).
|
||||
EXPECT_EQ(normalizeSeedPhrase(words24).find("\xC2\xA0"), std::string::npos);
|
||||
}
|
||||
|
||||
// Live probe of a real lite server (env-gated). Validates CONNECT_ONLY latency + IP capture.
|
||||
void testLiteServerProbeLive()
|
||||
{
|
||||
@@ -7286,6 +7328,7 @@ int main()
|
||||
testWalletFileProbe();
|
||||
testAddressChecksumValidation();
|
||||
testPrivateKeyImportRecognition();
|
||||
testSeedPhraseHelpers();
|
||||
testLiteServerProbeLive();
|
||||
testXmrigLiveInstall();
|
||||
testGeneratedResourceBehavior();
|
||||
|
||||
Reference in New Issue
Block a user