mirror of https://github.com/CympleTech/ESSE.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
266 lines
6.4 KiB
266 lines
6.4 KiB
import 'dart:async'; |
|
import 'dart:math'; |
|
import 'dart:typed_data'; |
|
import 'dart:ui' show Locale; |
|
|
|
import 'package:flutter/services.dart' show rootBundle; |
|
|
|
import 'package:crypto/crypto.dart'; |
|
import 'package:unorm_dart/unorm_dart.dart'; |
|
|
|
enum MnemonicLang { |
|
NONE, |
|
CHINESE_SIMPLIFIED, |
|
CHINESE_TRADITIONAL, |
|
ENGLISH, |
|
FRENCH, |
|
ITALIAN, |
|
JAPANESE, |
|
KOREAN, |
|
SPANISH, |
|
} |
|
|
|
final MNEMONIC_LANGS = [ |
|
MnemonicLang.ENGLISH, |
|
MnemonicLang.CHINESE_SIMPLIFIED, |
|
]; |
|
|
|
final MNEMONIC_LANGS_NO_DEFAULT = [ |
|
MnemonicLang.NONE, |
|
MnemonicLang.ENGLISH, |
|
MnemonicLang.CHINESE_SIMPLIFIED, |
|
]; |
|
|
|
|
|
extension MnemonicLangExtension on MnemonicLang { |
|
String localizations() { |
|
switch (this) { |
|
case MnemonicLang.NONE: |
|
return '—'; |
|
case MnemonicLang.CHINESE_SIMPLIFIED: |
|
return '简体中文'; |
|
case MnemonicLang.ENGLISH: |
|
return 'English'; |
|
default: |
|
return 'English'; |
|
} |
|
} |
|
|
|
int toInt() { |
|
switch (this) { |
|
case MnemonicLang.ENGLISH: |
|
return 1; |
|
case MnemonicLang.CHINESE_SIMPLIFIED: |
|
return 2; |
|
default: |
|
return 0; |
|
} |
|
} |
|
|
|
static MnemonicLang fromInt(int a) { |
|
switch (a) { |
|
case 1: |
|
return MnemonicLang.ENGLISH; |
|
case 2: |
|
return MnemonicLang.CHINESE_SIMPLIFIED; |
|
default: |
|
return MnemonicLang.NONE; |
|
} |
|
} |
|
|
|
static MnemonicLang fromLocale(Locale locale) { |
|
switch (locale.languageCode) { |
|
case 'en': |
|
return MnemonicLang.ENGLISH; |
|
case 'zh': |
|
return MnemonicLang.CHINESE_SIMPLIFIED; |
|
default: |
|
return MnemonicLang.ENGLISH; |
|
} |
|
} |
|
} |
|
|
|
final _langCache = Map<MnemonicLang, List<String>>(); |
|
|
|
const MnemonicLang _DEFAULT_LANG = MnemonicLang.ENGLISH; |
|
|
|
const int _SIZE_8BITS = 255; |
|
const String _INVALID_ENTROPY = 'Invalid entropy'; |
|
const String _INVALID_MNEMONIC = 'Invalid mnemonic'; |
|
const String _INVALID_CHECKSUM = 'Invalid checksum'; |
|
|
|
String _getMnemonicLangName(MnemonicLang lang) { |
|
switch (lang) { |
|
case MnemonicLang.CHINESE_SIMPLIFIED: |
|
return 'chinese_simplified'; |
|
case MnemonicLang.CHINESE_TRADITIONAL: |
|
return 'chinese_traditional'; |
|
case MnemonicLang.ENGLISH: |
|
return 'english'; |
|
case MnemonicLang.FRENCH: |
|
return 'french'; |
|
case MnemonicLang.ITALIAN: |
|
return 'italian'; |
|
case MnemonicLang.JAPANESE: |
|
return 'japanese'; |
|
case MnemonicLang.KOREAN: |
|
return 'korean'; |
|
case MnemonicLang.SPANISH: |
|
return 'spanish'; |
|
default: |
|
return 'english'; |
|
} |
|
} |
|
|
|
int _binaryToByte(String binary) { |
|
return int.parse(binary, radix: 2); |
|
} |
|
|
|
String _bytesToBinary(Uint8List bytes) { |
|
return bytes.map((byte) => byte.toRadixString(2).padLeft(8, '0')).join(''); |
|
} |
|
|
|
String _deriveChecksumBits(Uint8List entropy) { |
|
final ENT = entropy.length * 8; |
|
final CS = ENT ~/ 32; |
|
|
|
final hash = sha256.convert(entropy); |
|
return _bytesToBinary(Uint8List.fromList(hash.bytes)).substring(0, CS); |
|
} |
|
|
|
typedef Uint8List RandomBytes(int size); |
|
|
|
Uint8List _nextBytes(int size) { |
|
final rnd = Random.secure(); |
|
final bytes = Uint8List(size); |
|
for (var i = 0; i < size; i++) { |
|
bytes[i] = rnd.nextInt(_SIZE_8BITS); |
|
} |
|
return bytes; |
|
} |
|
|
|
/// Converts [mnemonic] code to entropy. |
|
Future<Uint8List> mnemonicToEntropy(String mnemonic, [MnemonicLang lang = _DEFAULT_LANG]) async { |
|
if (lang == MnemonicLang.NONE) { |
|
return Uint8List(0); |
|
} |
|
|
|
final wordRes = await _loadMnemonicLang(lang); |
|
final words = nfkd(mnemonic).split(' '); |
|
|
|
if (words.length % 3 != 0) { |
|
throw new ArgumentError(_INVALID_MNEMONIC); |
|
} |
|
|
|
// convert word indices to 11bit binary strings |
|
final bits = words.map((word) { |
|
final index = wordRes.indexOf(word); |
|
if (index == -1) { |
|
throw ArgumentError(_INVALID_MNEMONIC); |
|
} |
|
|
|
return index.toRadixString(2).padLeft(11, '0'); |
|
}).join(''); |
|
|
|
// split the binary string into ENT/CS |
|
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 entropyBytes = Uint8List.fromList(regex |
|
.allMatches(entropyBits) |
|
.map((match) => _binaryToByte(match.group(0)!)) |
|
.toList(growable: false)); |
|
if (entropyBytes.length < 16) { |
|
throw StateError(_INVALID_ENTROPY); |
|
} |
|
if (entropyBytes.length > 32) { |
|
throw StateError(_INVALID_ENTROPY); |
|
} |
|
if (entropyBytes.length % 4 != 0) { |
|
throw StateError(_INVALID_ENTROPY); |
|
} |
|
|
|
final newCheckSum = _deriveChecksumBits(entropyBytes); |
|
if (newCheckSum != checksumBits) { |
|
throw StateError(_INVALID_CHECKSUM); |
|
} |
|
|
|
return entropyBytes; |
|
} |
|
|
|
/// Converts [entropy] to mnemonic code. |
|
Future<String> entropyToMnemonic(Uint8List entropy, |
|
[MnemonicLang lang = _DEFAULT_LANG]) async { |
|
if (lang == MnemonicLang.NONE) { |
|
return ""; |
|
} |
|
|
|
if (entropy.length < 16) { |
|
throw ArgumentError(_INVALID_ENTROPY); |
|
} |
|
if (entropy.length > 32) { |
|
throw ArgumentError(_INVALID_ENTROPY); |
|
} |
|
if (entropy.length % 4 != 0) { |
|
throw ArgumentError(_INVALID_ENTROPY); |
|
} |
|
|
|
final entropyBits = _bytesToBinary(entropy); |
|
final checksumBits = _deriveChecksumBits(entropy); |
|
|
|
final bits = entropyBits + checksumBits; |
|
|
|
final regex = new RegExp(r".{1,11}", caseSensitive: false, multiLine: false); |
|
final chunks = regex |
|
.allMatches(bits) |
|
.map((match) => match.group(0)) |
|
.toList(growable: false); |
|
|
|
final wordRes = await _loadMnemonicLang(lang); |
|
|
|
return chunks |
|
.map((binary) => wordRes[_binaryToByte(binary!)]) |
|
.join(lang == MnemonicLang.JAPANESE ? '\u3000' : ' '); |
|
} |
|
|
|
|
|
/// Generates a random mnemonic. |
|
/// |
|
/// Defaults to 128-bits of entropy. |
|
/// By default it uses [Random.secure()] under the food to get random bytes, |
|
/// but you can swap RNG by providing [randomBytes]. |
|
/// Default lang is English, but you can use different lang by providing [lang]. |
|
Future<String> generateMnemonic({ |
|
int strength = 128, |
|
RandomBytes randomBytes = _nextBytes, |
|
MnemonicLang lang = _DEFAULT_LANG, |
|
}) async { |
|
if (lang == MnemonicLang.NONE) { |
|
return ""; |
|
} |
|
|
|
assert(strength % 32 == 0); |
|
|
|
final entropy = randomBytes(strength ~/ 8); |
|
|
|
return await entropyToMnemonic(entropy, lang); |
|
} |
|
|
|
Future<List<String>> _loadMnemonicLang(MnemonicLang lang) async { |
|
if (_langCache.containsKey(lang)) { |
|
return _langCache[lang]!; |
|
} else { |
|
final rawWords = await rootBundle |
|
.loadString('assets/mnemonic/${_getMnemonicLangName(lang)}.txt'); |
|
final result = rawWords |
|
.split('\n') |
|
.map((s) => s.trim()) |
|
.where((s) => s.isNotEmpty) |
|
.toList(growable: false); |
|
_langCache[lang] = result; |
|
return result; |
|
} |
|
}
|
|
|