Skip to content

Commit 5d62909

Browse files
authored
Merge pull request #2551 from SatoshiPortal/last-mnemonic-word-prediction
feat(mnemonic): checksum-aware last word entry and leak hardening
2 parents 0e4bf66 + d9747f2 commit 5d62909

16 files changed

Lines changed: 1228 additions & 301 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import 'package:bb_mobile/core/failures/failure.dart';
2+
3+
/// Why a typed mnemonic could not be accepted.
4+
///
5+
/// No variant carries a word, on purpose. The sentence being validated *is* the
6+
/// secret, and [Failure.logMessage] is documented as going to logs and Sentry.
7+
/// `bip39_mnemonic` builds its exception messages out of the offending word
8+
/// (`Mnemonic word "$word" does not exist in $language`,
9+
/// `Mnemonic checksum ${words.last} is invalid`), so those strings must be
10+
/// dropped where the exception is caught and never forwarded into a failure.
11+
sealed class MnemonicEntryFailure extends Failure {
12+
const MnemonicEntryFailure([super.logMessage]);
13+
}
14+
15+
/// At least one field is still blank.
16+
final class MnemonicEntryIncompleteFailure extends MnemonicEntryFailure {
17+
const MnemonicEntryIncompleteFailure();
18+
}
19+
20+
/// Every word is in the wordlist, but the sentence does not close its checksum
21+
/// — the transcription error the checksum exists to catch.
22+
final class MnemonicEntryInvalidChecksumFailure extends MnemonicEntryFailure {
23+
const MnemonicEntryInvalidChecksumFailure();
24+
}
25+
26+
/// At least one word is not in the wordlist of the selected language.
27+
final class MnemonicEntryUnknownWordFailure extends MnemonicEntryFailure {
28+
const MnemonicEntryUnknownWordFailure();
29+
}
30+
31+
/// Catch-all. [logMessage] holds the exception's *runtime type only* — never
32+
/// its message, which would carry a word.
33+
final class MnemonicEntryUnexpectedFailure extends MnemonicEntryFailure {
34+
const MnemonicEntryUnexpectedFailure([super.logMessage]);
35+
}

lib/core/seed/data/repository/word_list_repository.dart

Lines changed: 0 additions & 10 deletions
This file was deleted.

lib/core/seed/domain/usecases/find_mnemonic_words_usecase.dart

Lines changed: 0 additions & 12 deletions
This file was deleted.

lib/core/seed/seed_locator.dart

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import 'package:bb_mobile/core/seed/data/datasources/seed_datasource.dart';
22
import 'package:bb_mobile/core/seed/data/repository/seed_repository.dart';
3-
import 'package:bb_mobile/core/seed/data/repository/word_list_repository.dart';
43
import 'package:bb_mobile/core/seed/data/services/mnemonic_generator.dart';
54
import 'package:bb_mobile/core/seed/domain/usecases/delete_seed_usecase.dart';
6-
import 'package:bb_mobile/core/seed/domain/usecases/find_mnemonic_words_usecase.dart';
75
import 'package:bb_mobile/core/seed/domain/usecases/get_default_seed_usecase.dart';
86
import 'package:bb_mobile/core/seed/domain/usecases/process_and_separate_seeds_usecase.dart';
97
import 'package:bb_mobile/core/storage/data/datasources/key_value_storage/key_value_storage_datasource.dart';
@@ -26,10 +24,6 @@ class SeedLocator {
2624
locator.registerLazySingleton<SeedRepository>(
2725
() => SeedRepository(source: locator<SeedDatasource>()),
2826
);
29-
30-
locator.registerLazySingleton<WordListRepository>(
31-
() => WordListRepository(),
32-
);
3327
}
3428

3529
static void registerServices(GetIt locator) {
@@ -39,12 +33,6 @@ class SeedLocator {
3933
}
4034

4135
static void registerUsecases(GetIt locator) {
42-
locator.registerFactory<FindMnemonicWordsUsecase>(
43-
() => FindMnemonicWordsUsecase(
44-
wordListRepository: locator<WordListRepository>(),
45-
),
46-
);
47-
4836
locator.registerFactory<GetDefaultSeedUsecase>(
4937
() => GetDefaultSeedUsecase(
5038
walletRepository: locator<WalletRepository>(),

lib/core/utils/bip39.dart

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
import 'package:bip39_mnemonic/bip39_mnemonic.dart' as bip39;
22

33
class Bip39WordList {
4-
static List<String> english() => bip39.Language.english.list;
4+
/// The words that give [words] a valid checksum once appended to it.
5+
///
6+
/// The last word of a sentence carries the whole checksum, so only a small
7+
/// share of the wordlist can ever close it: 128 words for a 12 word
8+
/// sentence, down to 8 for a 24 word one.
9+
///
10+
/// Returns null when the question does not apply — [words] must hold every
11+
/// word of the sentence but the last, and each of them must belong to
12+
/// [language].
13+
static List<String>? lastWordCandidates({
14+
required List<String> words,
15+
bip39.Language language = bip39.Language.english,
16+
}) {
17+
try {
18+
return bip39.Mnemonic.lastWordCandidates(
19+
words: words,
20+
language: language,
21+
);
22+
} on bip39.MnemonicException {
23+
return null;
24+
}
25+
}
526
}
File renamed without changes.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import 'package:bb_mobile/core/failures/mnemonic_entry_failure.dart';
2+
import 'package:bb_mobile/core/utils/build_context_x.dart';
3+
import 'package:flutter/widgets.dart';
4+
5+
/// The only place a [MnemonicEntryFailure] becomes a user-facing string, so the
6+
/// failure family itself stays Flutter-free. The `sealed` switch makes a missing
7+
/// message a compile error.
8+
extension MnemonicEntryFailureL10n on MnemonicEntryFailure {
9+
String toTranslated(BuildContext context) => switch (this) {
10+
MnemonicEntryIncompleteFailure() => context.loc.emptyMnemonicWordsError,
11+
MnemonicEntryInvalidChecksumFailure() =>
12+
context.loc.mnemonicInvalidChecksumError,
13+
MnemonicEntryUnknownWordFailure() => context.loc.mnemonicUnknownWordError,
14+
// Never `logMessage`: generic message only, per rule #11.
15+
MnemonicEntryUnexpectedFailure() => context.loc.oopsSomethingWentWrong,
16+
};
17+
}

0 commit comments

Comments
 (0)