fix(crypto): Bip39.TryParse must not throw on unrecognisable text

Wordlist.AutoDetect lands on NBitcoin's internal "Unknown" language for
text resembling no wordlist and throws NotSupportedException instead
of failing gracefully — found by fuzzing the restore-mnemonic input.
TryParse now treats that (and the same failure mode from the Mnemonic
constructor) as "not a valid mnemonic", consistent with every other
rejection path.
This commit is contained in:
2026-07-07 21:51:22 +02:00
parent d8917fbd9a
commit c868c17505
2 changed files with 42 additions and 14 deletions
+31 -14
View File
@@ -58,28 +58,45 @@ public static class Bip39
// ideographic spaces — the text must not be altered further (§4.1). // ideographic spaces — the text must not be altered further (§4.1).
text = text.Trim(); text = text.Trim();
IEnumerable<Wordlist> candidates = language is not null Wordlist wordlist;
? [ToWordlist(language.Value)] if (language is not null)
: [Wordlist.AutoDetect(text)]; {
wordlist = ToWordlist(language.Value);
foreach (var wordlist in candidates) }
else
{ {
try try
{ {
var parsed = new Mnemonic(text, wordlist); wordlist = Wordlist.AutoDetect(text);
// The constructor does NOT verify the checksum — explicit check is mandatory.
if (parsed.IsValidChecksum && parsed.Words.Length is 12 or 15 or 18 or 21 or 24)
{
mnemonic = parsed;
return true;
}
} }
catch (FormatException) catch (NotSupportedException)
{ {
// Words outside the wordlist or invalid count — try the next candidate. // AutoDetect lands on NBitcoin's internal "Unknown" language for
// text resembling no wordlist and throws instead of returning a
// default (found by fuzzing) — not a valid mnemonic, plain and simple.
return false;
} }
} }
try
{
var parsed = new Mnemonic(text, wordlist);
// The constructor does NOT verify the checksum — explicit check is mandatory.
if (parsed.IsValidChecksum && parsed.Words.Length is 12 or 15 or 18 or 21 or 24)
{
mnemonic = parsed;
return true;
}
}
catch (FormatException)
{
// Words outside the wordlist or invalid count.
}
catch (NotSupportedException)
{
// Defensive: same NBitcoin failure mode surfacing from the constructor.
}
return false; return false;
} }
@@ -126,4 +126,15 @@ public class Bip39Tests
Assert.Throws<ArgumentOutOfRangeException>( Assert.Throws<ArgumentOutOfRangeException>(
() => Bip39.Generate(MnemonicLength.Twelve, (MnemonicLanguage)99)); () => Bip39.Generate(MnemonicLength.Twelve, (MnemonicLanguage)99));
} }
[Fact]
public void Testo_che_non_somiglia_a_nessuna_wordlist_viene_rifiutato_senza_eccezioni()
{
// NBitcoin's Wordlist.AutoDetect throws NotSupportedException("Unknown")
// for text resembling no wordlist instead of failing gracefully (found by
// fuzzing): TryParse must swallow it and return false — this string reaches
// TryParse straight from the user's restore input.
Assert.False(Bip39.TryParse("ábÊco ábaco0ábaco ábÊ", out var mnemonic));
Assert.Null(mnemonic);
}
} }