Files
ObsidianDragon/src/util/seed_phrase.h
DanS a1d3964e34 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>
2026-08-09 01:32:40 -05:00

39 lines
1.9 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// seed_phrase.h — shared, pure helpers for validating a pasted BIP39 recovery phrase.
// One source of truth for the seed-length contract so the lite-restore gates (first-run
// wizard + Settings) cannot drift apart. No I/O, no secrets retained — safe for both variants.
#pragma once
#include <string>
namespace dragonx {
namespace util {
// Normalize a pasted recovery phrase for consistent word counting AND backend submission:
// - every run of Unicode/ASCII whitespace (incl. NBSP U+00A0, en/em spaces U+2000200A,
// U+202F, U+205F, ideographic U+3000, NEL, tab/newline) collapses to a single ASCII space,
// - zero-width marks (U+200B/C/D, U+FEFF BOM) are stripped,
// - leading/trailing space is trimmed.
// Word bytes are copied verbatim — only these exact whitespace byte-sequences are substituted.
// The lite backend (tiny-bip39) splits on the literal ASCII space and does NO Unicode folding, so
// a phrase pasted with NBSPs (common from PDFs/note apps) is otherwise unrestorable; normalizing
// before submit makes the words space-separated and recoverable.
std::string normalizeSeedPhrase(const std::string& raw);
// Count ASCII-whitespace-separated words. Pair with normalizeSeedPhrase so exotic spacing counts right.
int seedPhraseWordCount(const std::string& phrase);
// True if `words` is a complete recovery phrase the DragonX backends accept. DragonX seeds are
// 24-word / 256-bit / 32-byte-entropy ONLY: the SDXL lite backend's LightWallet::new copies the
// phrase entropy into a fixed [u8;32] (a shorter valid-BIP39 phrase — 12/15/18/21 words — makes it
// panic, uncaught, across the restore FFI), and the full-node daemon likewise generates 24 words.
// Both lite-restore gates MUST use this so a crash-inducing length is refused client-side.
bool isCompleteRecoveryPhrase(int words);
} // namespace util
} // namespace dragonx