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>
78 lines
2.9 KiB
C++
78 lines
2.9 KiB
C++
// DragonX Wallet - ImGui Edition
|
||
// Copyright 2024-2026 The Hush Developers
|
||
// Released under the GPLv3
|
||
|
||
#include "seed_phrase.h"
|
||
|
||
#include <cctype>
|
||
|
||
namespace dragonx {
|
||
namespace util {
|
||
|
||
std::string normalizeSeedPhrase(const std::string& raw)
|
||
{
|
||
// Unicode whitespace encoded as UTF-8, each mapped to a single ASCII space; and zero-width marks
|
||
// to strip. We substitute only these EXACT byte sequences, so ordinary (ASCII) word bytes are
|
||
// never touched — the common all-ASCII phrase just gets its spacing collapsed and trimmed.
|
||
static const char* const kSpaces[] = {
|
||
"\xC2\xA0", // U+00A0 NBSP
|
||
"\xC2\x85", // U+0085 NEL
|
||
"\xE1\x9A\x80", // U+1680 ogham space
|
||
"\xE2\x80\x80", "\xE2\x80\x81", "\xE2\x80\x82", "\xE2\x80\x83", // U+2000–2003
|
||
"\xE2\x80\x84", "\xE2\x80\x85", "\xE2\x80\x86", "\xE2\x80\x87", // U+2004–2007
|
||
"\xE2\x80\x88", "\xE2\x80\x89", "\xE2\x80\x8A", // U+2008–200A
|
||
"\xE2\x80\xAF", // U+202F narrow NBSP
|
||
"\xE2\x81\x9F", // U+205F math space
|
||
"\xE3\x80\x80", // U+3000 ideographic
|
||
};
|
||
static const char* const kZeroWidth[] = {
|
||
"\xE2\x80\x8B", "\xE2\x80\x8C", "\xE2\x80\x8D", // U+200B/C/D
|
||
"\xEF\xBB\xBF", // U+FEFF BOM / ZWNBSP
|
||
};
|
||
|
||
std::string s = raw;
|
||
auto replaceAll = [&s](const std::string& from, const std::string& to) {
|
||
if (from.empty()) return;
|
||
std::size_t pos = 0;
|
||
while ((pos = s.find(from, pos)) != std::string::npos) {
|
||
s.replace(pos, from.size(), to);
|
||
pos += to.size();
|
||
}
|
||
};
|
||
for (const char* zw : kZeroWidth) replaceAll(zw, "");
|
||
for (const char* sp : kSpaces) replaceAll(sp, " ");
|
||
|
||
// Collapse ASCII whitespace runs to a single space and trim ends.
|
||
std::string out;
|
||
out.reserve(s.size());
|
||
bool pendingSpace = false;
|
||
bool sawWord = false;
|
||
for (unsigned char c : s) {
|
||
if (std::isspace(c)) { pendingSpace = sawWord; continue; }
|
||
if (pendingSpace) { out.push_back(' '); pendingSpace = false; }
|
||
out.push_back(static_cast<char>(c));
|
||
sawWord = true;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
int seedPhraseWordCount(const std::string& phrase)
|
||
{
|
||
int words = 0;
|
||
bool inWord = false;
|
||
for (unsigned char c : phrase) {
|
||
const bool space = std::isspace(c) != 0;
|
||
if (space) inWord = false;
|
||
else if (!inWord) { inWord = true; ++words; }
|
||
}
|
||
return words;
|
||
}
|
||
|
||
bool isCompleteRecoveryPhrase(int words)
|
||
{
|
||
return words == 24;
|
||
}
|
||
|
||
} // namespace util
|
||
} // namespace dragonx
|