From de56a799920ba239c899acb5282609e49c05b5bf Mon Sep 17 00:00:00 2001 From: Kamil Sobonski <39964811+SucharMistrz@users.noreply.github.com> Date: Wed, 26 Feb 2025 11:26:39 +0100 Subject: [PATCH] Feature: Reimplement mnemonic validation The purpose of this branch is to introduce a proper method of validating mnemonics, discarded in previous versions. List of changes: - updated mnemonic.dart with a new isValidMnemonic() method of validating mnemonics without creating a Mnemonic object. Previously implemented validation in Mnemonic object constructor has been updated to work with the converted static methods but otherwise remained unchanged as it might later be used for displaying more specific reasons on why a mnemonic is invalid. - updated mnemonic_test.dart to include tests for the new isValidMnemonic() method --- lib/src/bip/bip39/mnemonic.dart | 49 +++++++++++++++++++++---------- pubspec.yaml | 2 +- test/bip/bip39/mnemonic_test.dart | 47 +++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 16 deletions(-) diff --git a/lib/src/bip/bip39/mnemonic.dart b/lib/src/bip/bip39/mnemonic.dart index 99fe3057..7bc5d2be 100644 --- a/lib/src/bip/bip39/mnemonic.dart +++ b/lib/src/bip/bip39/mnemonic.dart @@ -13,16 +13,16 @@ class Mnemonic extends Equatable { final List mnemonicList; // Validation of a mnemonic phrase requires to check its length, checksum and words. - // To do this before the object is created - in the factory constructor, all public methods contained in this class must be static, - // Such a solution would be less readable and less intuitive. For that reason, the validation and exception throwing are done in the constructor. + // To do this before a Mnemonic object is created, you can use the isValidMnemonic static method. + // Exceptions are left in the constructor mainly in case we want to tell a user the reason why a mnemonic is invalid. Mnemonic(this.mnemonicList) { if (mnemonicList.length % 3 != 0 || mnemonicList.isEmpty) { throw const MnemonicException(MnemonicExceptionType.invalidLength); - } else if (_mnemonicListIndexes.contains(_mnemonicWordNotFoundInDictionary)) { + } else if (_parseWordsToWordIndexes(mnemonicList).contains(_mnemonicWordNotFoundInDictionary)) { throw const MnemonicException(MnemonicExceptionType.invalidWord); } - String extractedChecksum = _extractChecksum(); + String extractedChecksum = _extractChecksum(mnemonicList); String calculatedChecksum = _calculateChecksum(entropy); if (extractedChecksum != calculatedChecksum) { @@ -57,9 +57,30 @@ class Mnemonic extends Equatable { return Mnemonic(mnemonicList); } + /// Validates a mnemonic phrase without creating an object of the Mnemonic class. + static bool isValidMnemonic(List mnemonicList) { + Uint8List entropy = _calcEntropy(mnemonicList); + + String extractedChecksum = _extractChecksum(mnemonicList); + String calculatedChecksum = _calculateChecksum(entropy); + + if (extractedChecksum != calculatedChecksum || extractedChecksum == '0') { + return false; + } + + return true; + } + + @override + String toString() { + return mnemonicList.join(' '); + } + + Uint8List get entropy => _calcEntropy(mnemonicList); + /// Returns the entropy of the mnemonic phrase. - Uint8List get entropy { - String mnemonicBits = _mnemonicBits; + static Uint8List _calcEntropy(List mnemonicList) { + String mnemonicBits = _parseWordsToBits(mnemonicList); int entropyStartIndex = 0; int entropyEndIndex = (mnemonicBits.length / 33).floor() * 32; @@ -81,8 +102,8 @@ class Mnemonic extends Equatable { } /// Returns binary checksum included in the last word of the mnemonic phrase. - String _extractChecksum() { - String mnemonicBits = _mnemonicBits; + static String _extractChecksum(List mnemonicList) { + String mnemonicBits = _parseWordsToBits(mnemonicList); int checksumStartIndex = (mnemonicBits.length / 33).floor() * 32; int checksumEndIndex = mnemonicBits.length; @@ -93,8 +114,9 @@ class Mnemonic extends Equatable { } /// Returns the combined mnemonic words in binary format. - String get _mnemonicBits { - List mnemonicListBinaries = _mnemonicListIndexes.map((int index) => BinaryUtils.intToBinary(index, padding: 11)).toList(); + static String _parseWordsToBits(List mnemonicList) { + List mnemonicListIndexes = mnemonicList.map(MnemonicDictionary.english.indexOf).toList(); + List mnemonicListBinaries = mnemonicListIndexes.map((int index) => BinaryUtils.intToBinary(index, padding: 11)).toList(); String mnemonicBits = mnemonicListBinaries.join(''); return mnemonicBits; @@ -102,11 +124,8 @@ class Mnemonic extends Equatable { /// Returns dictionary indexes of the mnemonic words. /// If a word is not found in the dictionary, it will be represented as -1. - List get _mnemonicListIndexes => mnemonicList.map(MnemonicDictionary.english.indexOf).toList(); - - @override - String toString() { - return mnemonicList.join(' '); + static List _parseWordsToWordIndexes(List mnemonicList) { + return mnemonicList.map(MnemonicDictionary.english.indexOf).toList(); } @override diff --git a/pubspec.yaml b/pubspec.yaml index 91c7be80..226a99e7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: cryptography_utils description: "Dart package containing utility methods for common cryptographic and blockchain-specific operations" publish_to: none -version: 0.0.21 +version: 0.0.22 environment: sdk: ">=3.2.6" diff --git a/test/bip/bip39/mnemonic_test.dart b/test/bip/bip39/mnemonic_test.dart index 8c0ff903..9039996a 100644 --- a/test/bip/bip39/mnemonic_test.dart +++ b/test/bip/bip39/mnemonic_test.dart @@ -8,7 +8,9 @@ void main() { group('Tests of Mnemonic() constructor', () { test('Should [return Mnemonic] from given mnemonic phrase', () { // Arrange + // @formatter:off List actualMnemonicList = ['catalog', 'letter', 'frown', 'ramp', 'chest', 'van', 'pole', 'unfold', 'sound', 'unable', 'cool', 'endorse']; + // @formatter:on // Act Mnemonic actualMnemonic = Mnemonic(actualMnemonicList); @@ -224,6 +226,51 @@ void main() { }); }); + group('Tests of Mnemonic.isValidMnemonic()', () { + test('Should [return TRUE] for a valid mnemonic', () { + // Arrange + // @formatter:off + List mnemonicList = ['catalog', 'letter', 'frown', 'ramp', 'chest', 'van', 'pole', 'unfold', 'sound', 'unable', 'cool', 'endorse']; + // @formatter:on + + // Act + bool actualValidBool = Mnemonic.isValidMnemonic(mnemonicList); + + // Assert + bool expectedValidBool = true; + + expect(actualValidBool, expectedValidBool); + }); + + test('Should [return FALSE] if mnemonic phrase has invalid length', () { + // Arrange + List mnemonicList = ['catalog', 'letter', 'frown']; + + // Act + bool actualValidBool = Mnemonic.isValidMnemonic(mnemonicList); + + // Assert + bool expectedValidBool = false; + + expect(actualValidBool, expectedValidBool); + }); + + test('Should [return FALSE] if mnemonic phrase has invalid checksum', () { + // Arrange + // @formatter:off + List mnemonicList = ['attend', 'piano', 'mail', 'clap', 'argue', 'square', 'effort', 'cause', 'cook', 'onion', 'mouse', 'delay' ]; + // @formatter:on + + // Act + bool actualValidBool = Mnemonic.isValidMnemonic(mnemonicList); + + // Assert + bool expectedValidBool = false; + + expect(actualValidBool, expectedValidBool); + }); + }); + group('Tests of Mnemonic.entropy getter', () { test('Should [return entropy] from [12-word Mnemonic]', () { // Arrange