Skip to content

Commit

Permalink
implement bip39
Browse files Browse the repository at this point in the history
  • Loading branch information
mistermoe committed Aug 5, 2024
1 parent 3a599c3 commit e03faaa
Show file tree
Hide file tree
Showing 10 changed files with 2,563 additions and 5 deletions.
1 change: 1 addition & 0 deletions lib/src/crypto.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export './crypto/secp256k1.dart';
export './crypto/key_manager.dart';
export './crypto/algorithm_id.dart';
export './crypto/in_memory_key_manager.dart';
export './crypto/bip39/bip39.dart';
136 changes: 136 additions & 0 deletions lib/src/crypto/bip39/bip39.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import 'dart:math';
import 'dart:typed_data';

import 'package:cryptography/dart.dart';
import 'package:web5/src/crypto/bip39/wordlists/english.dart';
import 'package:web5/src/crypto/bip39/pbkdf2.dart';

class Bip39 {
static const int _sizeByte = 255;
static const _sha256 = DartSha256();

static int _binaryToByte(String binary) => int.parse(binary, radix: 2);

static String _bytesToBinary(Uint8List bytes) =>
bytes.map((byte) => byte.toRadixString(2).padLeft(8, '0')).join('');

static _deriveChecksumBits(Uint8List entropy) {
final checksumLengthBits = (entropy.length * 8) ~/ 32;
final hash = Uint8List.fromList(_sha256.hashSync(entropy).bytes);

return _bytesToBinary(hash).substring(0, checksumLengthBits);
}

static Uint8List _defaultRandomBytes(int size) {
final rng = Random.secure();
return Uint8List.fromList(
List.generate(size, (_) => rng.nextInt(_sizeByte)),
);
}

static List<String> generateMnemonic({
int strength = 128,
Uint8List Function(int) randomBytes = _defaultRandomBytes,
}) {
assert(strength % 32 == 0);

final entropy = randomBytes(strength ~/ 8);
return entropyToMnemonic(entropy);
}

static List<String> entropyToMnemonic(Uint8List entropy) {
if (entropy.length < 16 || entropy.length > 32 || entropy.length % 4 != 0) {
throw InvalidEntropyException('entropy ');
}

final entropyBits = _bytesToBinary(Uint8List.fromList(entropy));
final checksumBits = _deriveChecksumBits(Uint8List.fromList(entropy));
final bits = '$entropyBits$checksumBits';

final regex = RegExp(r'.{1,11}');
final chunks = regex
.allMatches(bits)
.map((match) => match.group(0)!)
.toList(growable: false);

return chunks.map((binary) => WORDLIST[_binaryToByte(binary)]).toList();
}

static Future<Uint8List> mnemonicToSeed(
List<String> mnemonic, {
String passphrase = '',
int desiredKeyLength = 64,
}) async {
final pbkdf2 = Pbkdf2(desiredKeyLength: desiredKeyLength);
return pbkdf2.process(mnemonic.join(' '), passphrase: passphrase);
}

static bool validateMnemonic(List<String> mnemonic) {
try {
mnemonicToEntropy(mnemonic);
return true;
} catch (e) {
return false;
}
}

static Uint8List mnemonicToEntropy(List<String> mnemonic) {
if (mnemonic.length % 3 != 0) {
throw InvalidMnemonicException();
}

final bits = mnemonic.map((word) {
final index = WORDLIST.indexOf(word);
if (index == -1) {
throw InvalidMnemonicException();
}
return index.toRadixString(2).padLeft(11, '0');
}).join('');

final dividerIndex = (bits.length / 33).floor() * 32;
final entropyBits = bits.substring(0, dividerIndex);
final checksumBits = bits.substring(dividerIndex);

final regex = RegExp(r'.{1,8}');
final groupedBits = regex
.allMatches(entropyBits)
.map((match) => _binaryToByte(match.group(0)!))
.toList(growable: false);

final entropyBytes = Uint8List.fromList(groupedBits);

if (entropyBytes.length < 16 ||
entropyBytes.length > 32 ||
entropyBytes.length % 4 != 0) {
throw InvalidEntropyException();
}

final newChecksum = _deriveChecksumBits(entropyBytes);
if (newChecksum != checksumBits) {
throw InvalidChecksumException();
}

return entropyBytes;
}
}

class InvalidMnemonicException implements Exception {
final String message;
InvalidMnemonicException([this.message = 'Invalid mnemonic']);
@override
String toString() => 'InvalidMnemonicException: $message';
}

class InvalidEntropyException implements Exception {
final String message;
InvalidEntropyException([this.message = 'Invalid entropy']);
@override
String toString() => 'InvalidEntropyException: $message';
}

class InvalidChecksumException implements Exception {
final String message;
InvalidChecksumException([this.message = 'Invalid mnemonic checksum']);
@override
String toString() => 'InvalidChecksumException: $message';
}
34 changes: 34 additions & 0 deletions lib/src/crypto/bip39/pbkdf2.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:pointycastle/digests/sha512.dart';
import 'package:pointycastle/key_derivators/api.dart' show Pbkdf2Parameters;
import 'package:pointycastle/key_derivators/pbkdf2.dart';
import 'package:pointycastle/macs/hmac.dart';

class Pbkdf2 {
static const int defaultBlockLength = 128;
static const int defaultIterationCount = 2048;
static const int defaultDesiredKeyLength = 64;
static const String saltPrefix = 'mnemonic';

final int blockLength;
final int iterationCount;
final int desiredKeyLength;
late final PBKDF2KeyDerivator _derivator;

Pbkdf2({
this.blockLength = defaultBlockLength,
this.iterationCount = defaultIterationCount,
this.desiredKeyLength = defaultDesiredKeyLength,
}) {
_derivator = PBKDF2KeyDerivator(HMac(SHA512Digest(), blockLength));
}

Uint8List process(String mnemonic, {String passphrase = ''}) {
final salt = Uint8List.fromList(utf8.encode('$saltPrefix$passphrase'));
_derivator.reset();
_derivator.init(Pbkdf2Parameters(salt, iterationCount, desiredKeyLength));
return _derivator.process(Uint8List.fromList(utf8.encode(mnemonic)));
}
}
Loading

0 comments on commit e03faaa

Please sign in to comment.