What changed, and why it matters
This commit adds a new utility module for handling Electrum-style cryptocurrency seed phrases, plus tests. It does not fix any bug or change existing behavior; it is a work-in-progress feature addition. There is nothing in the commit that suggests a security vulnerability or a security patch.
No security action required. Review the new seed handling code for correctness and side-channel safety before it is used in production wallet flows.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces lib/utilities/electrum_seed_utils.dart implementing Electrum seed normalization, version detection, and PBKDF2-based seed derivation using HMAC-SHA512 with the Electrum salt. It also adds unorm_dart as a direct dependency and a test file with known test vectors from Electrum. The code is a new feature (WIP) and does not alter existing wallet logic or address any disclosed vulnerability.
Changed components
lib/utilities/electrum_seed_utils.dartpubspec.lockscripts/app_config/templates/pubspec.template.yamltest/utilities/electrum_seed_utils_test.dartInspect captured patch +1402 / −1
diff --git a/lib/utilities/electrum_seed_utils.dart b/lib/utilities/electrum_seed_utils.dart
new file mode 100644
index 0000000..337987a
--- /dev/null
+++ b/lib/utilities/electrum_seed_utils.dart
@@ -0,0 +1,1098 @@
+import 'dart:typed_data';
+
+import 'package:pointycastle/export.dart';
+import 'package:unorm_dart/unorm_dart.dart';
+
+import 'extensions/extensions.dart';
+
+abstract class ElectrumSeedUtils {
+ static const kSeedPrefix = "01"; // standard
+ static const kSeedPrefixSegwit = "100"; // segwit
+ static const kSeedPrefix2fa = "101"; // 2FA standard
+ static const kSeedPrefix2faSegwit = "102"; // 2FA segwit
+
+ static Uint8List electrumMnemonicToSeedBytes(
+ final String mnemonic, {
+ final String passphrase = "",
+ }) {
+ final salt = Uint8List.fromList([
+ ..."electrum".toUint8ListFromUtf8,
+ ...normalize(passphrase).toUint8ListFromUtf8,
+ ]);
+
+ final kdf = PBKDF2KeyDerivator(HMac.withDigest(SHA512Digest()))
+ ..init(Pbkdf2Parameters(salt, 2048, 64));
+
+ return kdf.process(normalize(mnemonic).toUint8ListFromUtf8);
+ }
+
+ // based on https://electrum.readthedocs.io/en/latest/seedphrase.html#version-number
+ static String electrumMnemonicVersion(
+ final String mnemonic, {
+ final String passphrase = "",
+ }) {
+ final normalized = normalize(mnemonic).toUint8ListFromUtf8;
+
+ final hash = _hmacHex(normalized);
+
+ final length = int.parse(hash[0], radix: 16) + 2;
+
+ return hash.substring(0, length);
+ }
+
+ static bool isNewSeed(final String mnemonic, {String prefix = kSeedPrefix}) {
+ final normalized = normalize(mnemonic).toUint8ListFromUtf8;
+ final hash = _hmacHex(normalized);
+ return hash.startsWith(prefix);
+ }
+
+ static String normalize(final String mnemonic) {
+ final characters = String.fromCharCodes(
+ nfkd(
+ mnemonic,
+ ).toLowerCase().runes.where((e) => !_kNonZeroCCCCodeUnits.contains(e)),
+ ).split(RegExp(r"\s+")).join(" ").trim().split("");
+
+ final buffer = StringBuffer();
+
+ for (int i = 0; i < characters.length; i++) {
+ final char = characters[i];
+ final isSpace = RegExp(r"\s").hasMatch(char);
+ assert(char.runes.length == 1);
+
+ if (isSpace && i > 0 && i < characters.length - 1) {
+ final prev = characters[i - 1];
+ final next = characters[i + 1];
+ if (_isCJK(prev.runes.first) && _isCJK(next.runes.first)) {
+ continue;
+ }
+ }
+
+ buffer.write(char);
+ }
+
+ return buffer.toString();
+ }
+
+ static String _hmacHex(Uint8List message) =>
+ (HMac.withDigest(SHA512Digest())
+ ..init(KeyParameter("Seed version".toUint8ListFromUtf8)))
+ .process(message)
+ .toHex;
+
+ static bool _isCJK(int code) {
+ for (final (min, max, _) in _kCjkIntervals) {
+ if (min <= code && code <= max) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
+// https://www.unicode.org/reports/tr44/tr44-34.html#Canonical_Combining_Class_Values
+// generated from https://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt
+const _kNonZeroCCCCodeUnits = {
+ 768,
+ 769,
+ 770,
+ 771,
+ 772,
+ 773,
+ 774,
+ 775,
+ 776,
+ 777,
+ 778,
+ 779,
+ 780,
+ 781,
+ 782,
+ 783,
+ 784,
+ 785,
+ 786,
+ 787,
+ 788,
+ 789,
+ 790,
+ 791,
+ 792,
+ 793,
+ 794,
+ 795,
+ 796,
+ 797,
+ 798,
+ 799,
+ 800,
+ 801,
+ 802,
+ 803,
+ 804,
+ 805,
+ 806,
+ 807,
+ 808,
+ 809,
+ 810,
+ 811,
+ 812,
+ 813,
+ 814,
+ 815,
+ 816,
+ 817,
+ 818,
+ 819,
+ 820,
+ 821,
+ 822,
+ 823,
+ 824,
+ 825,
+ 826,
+ 827,
+ 828,
+ 829,
+ 830,
+ 831,
+ 832,
+ 833,
+ 834,
+ 835,
+ 836,
+ 837,
+ 838,
+ 839,
+ 840,
+ 841,
+ 842,
+ 843,
+ 844,
+ 845,
+ 846,
+ 848,
+ 849,
+ 850,
+ 851,
+ 852,
+ 853,
+ 854,
+ 855,
+ 856,
+ 857,
+ 858,
+ 859,
+ 860,
+ 861,
+ 862,
+ 863,
+ 864,
+ 865,
+ 866,
+ 867,
+ 868,
+ 869,
+ 870,
+ 871,
+ 872,
+ 873,
+ 874,
+ 875,
+ 876,
+ 877,
+ 878,
+ 879,
+ 1155,
+ 1156,
+ 1157,
+ 1158,
+ 1159,
+ 1425,
+ 1426,
+ 1427,
+ 1428,
+ 1429,
+ 1430,
+ 1431,
+ 1432,
+ 1433,
+ 1434,
+ 1435,
+ 1436,
+ 1437,
+ 1438,
+ 1439,
+ 1440,
+ 1441,
+ 1442,
+ 1443,
+ 1444,
+ 1445,
+ 1446,
+ 1447,
+ 1448,
+ 1449,
+ 1450,
+ 1451,
+ 1452,
+ 1453,
+ 1454,
+ 1455,
+ 1456,
+ 1457,
+ 1458,
+ 1459,
+ 1460,
+ 1461,
+ 1462,
+ 1463,
+ 1464,
+ 1465,
+ 1466,
+ 1467,
+ 1468,
+ 1469,
+ 1471,
+ 1473,
+ 1474,
+ 1476,
+ 1477,
+ 1479,
+ 1552,
+ 1553,
+ 1554,
+ 1555,
+ 1556,
+ 1557,
+ 1558,
+ 1559,
+ 1560,
+ 1561,
+ 1562,
+ 1611,
+ 1612,
+ 1613,
+ 1614,
+ 1615,
+ 1616,
+ 1617,
+ 1618,
+ 1619,
+ 1620,
+ 1621,
+ 1622,
+ 1623,
+ 1624,
+ 1625,
+ 1626,
+ 1627,
+ 1628,
+ 1629,
+ 1630,
+ 1631,
+ 1648,
+ 1750,
+ 1751,
+ 1752,
+ 1753,
+ 1754,
+ 1755,
+ 1756,
+ 1759,
+ 1760,
+ 1761,
+ 1762,
+ 1763,
+ 1764,
+ 1767,
+ 1768,
+ 1770,
+ 1771,
+ 1772,
+ 1773,
+ 1809,
+ 1840,
+ 1841,
+ 1842,
+ 1843,
+ 1844,
+ 1845,
+ 1846,
+ 1847,
+ 1848,
+ 1849,
+ 1850,
+ 1851,
+ 1852,
+ 1853,
+ 1854,
+ 1855,
+ 1856,
+ 1857,
+ 1858,
+ 1859,
+ 1860,
+ 1861,
+ 1862,
+ 1863,
+ 1864,
+ 1865,
+ 1866,
+ 2027,
+ 2028,
+ 2029,
+ 2030,
+ 2031,
+ 2032,
+ 2033,
+ 2034,
+ 2035,
+ 2045,
+ 2070,
+ 2071,
+ 2072,
+ 2073,
+ 2075,
+ 2076,
+ 2077,
+ 2078,
+ 2079,
+ 2080,
+ 2081,
+ 2082,
+ 2083,
+ 2085,
+ 2086,
+ 2087,
+ 2089,
+ 2090,
+ 2091,
+ 2092,
+ 2093,
+ 2137,
+ 2138,
+ 2139,
+ 2199,
+ 2200,
+ 2201,
+ 2202,
+ 2203,
+ 2204,
+ 2205,
+ 2206,
+ 2207,
+ 2250,
+ 2251,
+ 2252,
+ 2253,
+ 2254,
+ 2255,
+ 2256,
+ 2257,
+ 2258,
+ 2259,
+ 2260,
+ 2261,
+ 2262,
+ 2263,
+ 2264,
+ 2265,
+ 2266,
+ 2267,
+ 2268,
+ 2269,
+ 2270,
+ 2271,
+ 2272,
+ 2273,
+ 2275,
+ 2276,
+ 2277,
+ 2278,
+ 2279,
+ 2280,
+ 2281,
+ 2282,
+ 2283,
+ 2284,
+ 2285,
+ 2286,
+ 2287,
+ 2288,
+ 2289,
+ 2290,
+ 2291,
+ 2292,
+ 2293,
+ 2294,
+ 2295,
+ 2296,
+ 2297,
+ 2298,
+ 2299,
+ 2300,
+ 2301,
+ 2302,
+ 2303,
+ 2364,
+ 2381,
+ 2385,
+ 2386,
+ 2387,
+ 2388,
+ 2492,
+ 2509,
+ 2558,
+ 2620,
+ 2637,
+ 2748,
+ 2765,
+ 2876,
+ 2893,
+ 3021,
+ 3132,
+ 3149,
+ 3157,
+ 3158,
+ 3260,
+ 3277,
+ 3387,
+ 3388,
+ 3405,
+ 3530,
+ 3640,
+ 3641,
+ 3642,
+ 3656,
+ 3657,
+ 3658,
+ 3659,
+ 3768,
+ 3769,
+ 3770,
+ 3784,
+ 3785,
+ 3786,
+ 3787,
+ 3864,
+ 3865,
+ 3893,
+ 3895,
+ 3897,
+ 3953,
+ 3954,
+ 3956,
+ 3962,
+ 3963,
+ 3964,
+ 3965,
+ 3968,
+ 3970,
+ 3971,
+ 3972,
+ 3974,
+ 3975,
+ 4038,
+ 4151,
+ 4153,
+ 4154,
+ 4237,
+ 4957,
+ 4958,
+ 4959,
+ 5908,
+ 5909,
+ 5940,
+ 6098,
+ 6109,
+ 6313,
+ 6457,
+ 6458,
+ 6459,
+ 6679,
+ 6680,
+ 6752,
+ 6773,
+ 6774,
+ 6775,
+ 6776,
+ 6777,
+ 6778,
+ 6779,
+ 6780,
+ 6783,
+ 6832,
+ 6833,
+ 6834,
+ 6835,
+ 6836,
+ 6837,
+ 6838,
+ 6839,
+ 6840,
+ 6841,
+ 6842,
+ 6843,
+ 6844,
+ 6845,
+ 6847,
+ 6848,
+ 6849,
+ 6850,
+ 6851,
+ 6852,
+ 6853,
+ 6854,
+ 6855,
+ 6856,
+ 6857,
+ 6858,
+ 6859,
+ 6860,
+ 6861,
+ 6862,
+ 6863,
+ 6864,
+ 6865,
+ 6866,
+ 6867,
+ 6868,
+ 6869,
+ 6870,
+ 6871,
+ 6872,
+ 6873,
+ 6874,
+ 6875,
+ 6876,
+ 6877,
+ 6880,
+ 6881,
+ 6882,
+ 6883,
+ 6884,
+ 6885,
+ 6886,
+ 6887,
+ 6888,
+ 6889,
+ 6890,
+ 6891,
+ 6964,
+ 6980,
+ 7019,
+ 7020,
+ 7021,
+ 7022,
+ 7023,
+ 7024,
+ 7025,
+ 7026,
+ 7027,
+ 7082,
+ 7083,
+ 7142,
+ 7154,
+ 7155,
+ 7223,
+ 7376,
+ 7377,
+ 7378,
+ 7380,
+ 7381,
+ 7382,
+ 7383,
+ 7384,
+ 7385,
+ 7386,
+ 7387,
+ 7388,
+ 7389,
+ 7390,
+ 7391,
+ 7392,
+ 7394,
+ 7395,
+ 7396,
+ 7397,
+ 7398,
+ 7399,
+ 7400,
+ 7405,
+ 7412,
+ 7416,
+ 7417,
+ 7616,
+ 7617,
+ 7618,
+ 7619,
+ 7620,
+ 7621,
+ 7622,
+ 7623,
+ 7624,
+ 7625,
+ 7626,
+ 7627,
+ 7628,
+ 7629,
+ 7630,
+ 7631,
+ 7632,
+ 7633,
+ 7634,
+ 7635,
+ 7636,
+ 7637,
+ 7638,
+ 7639,
+ 7640,
+ 7641,
+ 7642,
+ 7643,
+ 7644,
+ 7645,
+ 7646,
+ 7647,
+ 7648,
+ 7649,
+ 7650,
+ 7651,
+ 7652,
+ 7653,
+ 7654,
+ 7655,
+ 7656,
+ 7657,
+ 7658,
+ 7659,
+ 7660,
+ 7661,
+ 7662,
+ 7663,
+ 7664,
+ 7665,
+ 7666,
+ 7667,
+ 7668,
+ 7669,
+ 7670,
+ 7671,
+ 7672,
+ 7673,
+ 7674,
+ 7675,
+ 7676,
+ 7677,
+ 7678,
+ 7679,
+ 8400,
+ 8401,
+ 8402,
+ 8403,
+ 8404,
+ 8405,
+ 8406,
+ 8407,
+ 8408,
+ 8409,
+ 8410,
+ 8411,
+ 8412,
+ 8417,
+ 8421,
+ 8422,
+ 8423,
+ 8424,
+ 8425,
+ 8426,
+ 8427,
+ 8428,
+ 8429,
+ 8430,
+ 8431,
+ 8432,
+ 11503,
+ 11504,
+ 11505,
+ 11647,
+ 11744,
+ 11745,
+ 11746,
+ 11747,
+ 11748,
+ 11749,
+ 11750,
+ 11751,
+ 11752,
+ 11753,
+ 11754,
+ 11755,
+ 11756,
+ 11757,
+ 11758,
+ 11759,
+ 11760,
+ 11761,
+ 11762,
+ 11763,
+ 11764,
+ 11765,
+ 11766,
+ 11767,
+ 11768,
+ 11769,
+ 11770,
+ 11771,
+ 11772,
+ 11773,
+ 11774,
+ 11775,
+ 12330,
+ 12331,
+ 12332,
+ 12333,
+ 12334,
+ 12335,
+ 12441,
+ 12442,
+ 42607,
+ 42612,
+ 42613,
+ 42614,
+ 42615,
+ 42616,
+ 42617,
+ 42618,
+ 42619,
+ 42620,
+ 42621,
+ 42654,
+ 42655,
+ 42736,
+ 42737,
+ 43014,
+ 43052,
+ 43204,
+ 43232,
+ 43233,
+ 43234,
+ 43235,
+ 43236,
+ 43237,
+ 43238,
+ 43239,
+ 43240,
+ 43241,
+ 43242,
+ 43243,
+ 43244,
+ 43245,
+ 43246,
+ 43247,
+ 43248,
+ 43249,
+ 43307,
+ 43308,
+ 43309,
+ 43347,
+ 43443,
+ 43456,
+ 43696,
+ 43698,
+ 43699,
+ 43700,
+ 43703,
+ 43704,
+ 43710,
+ 43711,
+ 43713,
+ 43766,
+ 44013,
+ 64286,
+ 65056,
+ 65057,
+ 65058,
+ 65059,
+ 65060,
+ 65061,
+ 65062,
+ 65063,
+ 65064,
+ 65065,
+ 65066,
+ 65067,
+ 65068,
+ 65069,
+ 65070,
+ 65071,
+ 66045,
+ 66272,
+ 66422,
+ 66423,
+ 66424,
+ 66425,
+ 66426,
+ 68109,
+ 68111,
+ 68152,
+ 68153,
+ 68154,
+ 68159,
+ 68325,
+ 68326,
+ 68900,
+ 68901,
+ 68902,
+ 68903,
+ 68969,
+ 68970,
+ 68971,
+ 68972,
+ 68973,
+ 69291,
+ 69292,
+ 69370,
+ 69371,
+ 69373,
+ 69374,
+ 69375,
+ 69446,
+ 69447,
+ 69448,
+ 69449,
+ 69450,
+ 69451,
+ 69452,
+ 69453,
+ 69454,
+ 69455,
+ 69456,
+ 69506,
+ 69507,
+ 69508,
+ 69509,
+ 69702,
+ 69744,
+ 69759,
+ 69817,
+ 69818,
+ 69888,
+ 69889,
+ 69890,
+ 69939,
+ 69940,
+ 70003,
+ 70080,
+ 70090,
+ 70197,
+ 70198,
+ 70377,
+ 70378,
+ 70459,
+ 70460,
+ 70477,
+ 70502,
+ 70503,
+ 70504,
+ 70505,
+ 70506,
+ 70507,
+ 70508,
+ 70512,
+ 70513,
+ 70514,
+ 70515,
+ 70516,
+ 70606,
+ 70607,
+ 70608,
+ 70722,
+ 70726,
+ 70750,
+ 70850,
+ 70851,
+ 71103,
+ 71104,
+ 71231,
+ 71350,
+ 71351,
+ 71467,
+ 71737,
+ 71738,
+ 71997,
+ 71998,
+ 72003,
+ 72160,
+ 72244,
+ 72263,
+ 72345,
+ 72767,
+ 73026,
+ 73028,
+ 73029,
+ 73111,
+ 73537,
+ 73538,
+ 90415,
+ 92912,
+ 92913,
+ 92914,
+ 92915,
+ 92916,
+ 92976,
+ 92977,
+ 92978,
+ 92979,
+ 92980,
+ 92981,
+ 92982,
+ 94192,
+ 94193,
+ 113822,
+ 119141,
+ 119142,
+ 119143,
+ 119144,
+ 119145,
+ 119149,
+ 119150,
+ 119151,
+ 119152,
+ 119153,
+ 119154,
+ 119163,
+ 119164,
+ 119165,
+ 119166,
+ 119167,
+ 119168,
+ 119169,
+ 119170,
+ 119173,
+ 119174,
+ 119175,
+ 119176,
+ 119177,
+ 119178,
+ 119179,
+ 119210,
+ 119211,
+ 119212,
+ 119213,
+ 119362,
+ 119363,
+ 119364,
+ 122880,
+ 122881,
+ 122882,
+ 122883,
+ 122884,
+ 122885,
+ 122886,
+ 122888,
+ 122889,
+ 122890,
+ 122891,
+ 122892,
+ 122893,
+ 122894,
+ 122895,
+ 122896,
+ 122897,
+ 122898,
+ 122899,
+ 122900,
+ 122901,
+ 122902,
+ 122903,
+ 122904,
+ 122907,
+ 122908,
+ 122909,
+ 122910,
+ 122911,
+ 122912,
+ 122913,
+ 122915,
+ 122916,
+ 122918,
+ 122919,
+ 122920,
+ 122921,
+ 122922,
+ 123023,
+ 123184,
+ 123185,
+ 123186,
+ 123187,
+ 123188,
+ 123189,
+ 123190,
+ 123566,
+ 123628,
+ 123629,
+ 123630,
+ 123631,
+ 124140,
+ 124141,
+ 124142,
+ 124143,
+ 124398,
+ 124399,
+ 124643,
+ 124646,
+ 124654,
+ 124655,
+ 124661,
+ 125136,
+ 125137,
+ 125138,
+ 125139,
+ 125140,
+ 125141,
+ 125142,
+ 125252,
+ 125253,
+ 125254,
+ 125255,
+ 125256,
+ 125257,
+ 125258,
+};
+
+// see https://github.com/spesmilo/electrum/blob/master/electrum/mnemonic.py#L39-L70
+// which references http://www.asahi-net.or.jp/~ax2s-kmtn/ref/unicode/e_asia.html
+const _kCjkIntervals = [
+ (0x4E00, 0x9FFF, "CJK Unified Ideographs"),
+ (0x3400, 0x4DBF, "CJK Unified Ideographs Extension A"),
+ (0x20000, 0x2A6DF, "CJK Unified Ideographs Extension B"),
+ (0x2A700, 0x2B73F, "CJK Unified Ideographs Extension C"),
+ (0x2B740, 0x2B81F, "CJK Unified Ideographs Extension D"),
+ (0xF900, 0xFAFF, "CJK Compatibility Ideographs"),
+ (0x2F800, 0x2FA1D, "CJK Compatibility Ideographs Supplement"),
+ (0x3190, 0x319F, "Kanbun"),
+ (0x2E80, 0x2EFF, "CJK Radicals Supplement"),
+ (0x2F00, 0x2FDF, "CJK Radicals"),
+ (0x31C0, 0x31EF, "CJK Strokes"),
+ (0x2FF0, 0x2FFF, "Ideographic Description Characters"),
+ (0xE0100, 0xE01EF, "Variation Selectors Supplement"),
+ (0x3100, 0x312F, "Bopomofo"),
+ (0x31A0, 0x31BF, "Bopomofo Extended"),
+ (0xFF00, 0xFFEF, "Halfwidth and Fullwidth Forms"),
+ (0x3040, 0x309F, "Hiragana"),
+ (0x30A0, 0x30FF, "Katakana"),
+ (0x31F0, 0x31FF, "Katakana Phonetic Extensions"),
+ (0x1B000, 0x1B0FF, "Kana Supplement"),
+ (0xAC00, 0xD7AF, "Hangul Syllables"),
+ (0x1100, 0x11FF, "Hangul Jamo"),
+ (0xA960, 0xA97F, "Hangul Jamo Extended A"),
+ (0xD7B0, 0xD7FF, "Hangul Jamo Extended B"),
+ (0x3130, 0x318F, "Hangul Compatibility Jamo"),
+ (0xA4D0, 0xA4FF, "Lisu"),
+ (0x16F00, 0x16F9F, "Miao"),
+ (0xA000, 0xA48F, "Yi Syllables"),
+ (0xA490, 0xA4CF, "Yi Radicals"),
+];
diff --git a/pubspec.lock b/pubspec.lock
index bcd7d7b..3a81a8c 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -2254,7 +2254,7 @@ packages:
source: hosted
version: "2.2.2"
unorm_dart:
- dependency: transitive
+ dependency: "direct main"
description:
name: unorm_dart
sha256: "5b35bff83fce4d76467641438f9e867dc9bcfdb8c1694854f230579d68cd8f4b"
diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml
index d29dea1..742f44e 100644
--- a/scripts/app_config/templates/pubspec.template.yaml
+++ b/scripts/app_config/templates/pubspec.template.yaml
@@ -245,6 +245,7 @@ dependencies:
fixnum: ^1.1.1
saf_util: ^0.11.0
saf_stream: ^0.12.3
+ unorm_dart: ^0.2.0
dev_dependencies:
flutter_test:
diff --git a/test/utilities/electrum_seed_utils_test.dart b/test/utilities/electrum_seed_utils_test.dart
new file mode 100644
index 0000000..a726cbb
--- /dev/null
+++ b/test/utilities/electrum_seed_utils_test.dart
@@ -0,0 +1,302 @@
+import 'package:coinlib_flutter/coinlib_flutter.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:stackwallet/utilities/electrum_seed_utils.dart';
+import 'package:stackwallet/utilities/extensions/extensions.dart';
+
+class _TestCase {
+ final String words, bip32Seed, seedVersion;
+ final String? lang, wordsHex, passphrase, passphraseHex;
+
+ const _TestCase({
+ required this.words,
+ required this.bip32Seed,
+ this.seedVersion = ElectrumSeedUtils.kSeedPrefix,
+ this.lang,
+ this.wordsHex,
+ this.passphrase,
+ this.passphraseHex,
+ });
+}
+
+// horror data sourced from https://github.com/spesmilo/electrum/blob/master/tests/test_wallet_vertical.py#L31-L33
+const kUnicodeHorror =
+ "₿ 😀 😈 う けたま わる w͢͢͝h͡o͢͡ ̸͢k̵͟n̴͘ǫw̸̛s͘ ̀́w͘͢ḩ̵a҉̡͢t ̧̕h́o̵r͏̵rors̡ ̶͡͠lį̶e͟͟ ̶͝in͢ ͏t̕h̷̡͟e ͟͟d̛a͜r̕͡k̢̨ ͡h̴e͏a̷̢̡rt́͏ ̴̷͠ò̵̶f̸ u̧͘ní̛͜c͢͏o̷͏d̸͢e̡͝?͞";
+const kUnicodeHorrorHex =
+ "e282bf20f09f988020f09f98882020202020e3818620e38191e3819fe381be20e3828fe382"
+ "8b2077cda2cda2cd9d68cda16fcda2cda120ccb8cda26bccb5cd9f6eccb4cd98c7ab77ccb8"
+ "cc9b73cd9820cc80cc8177cd98cda2e1b8a9ccb561d289cca1cda27420cca7cc9568cc816f"
+ "ccb572cd8fccb5726f7273cca120ccb6cda1cda06cc4afccb665cd9fcd9f20ccb6cd9d696e"
+ "cda220cd8f74cc9568ccb7cca1cd9f6520cd9fcd9f64cc9b61cd9c72cc95cda16bcca2cca8"
+ "20cda168ccb465cd8f61ccb7cca2cca17274cc81cd8f20ccb4ccb7cda0c3b2ccb5ccb666cc"
+ "b82075cca7cd986ec3adcc9bcd9c63cda2cd8f6fccb7cd8f64ccb8cda265cca1cd9d3fcd9e";
+
+// test cases sourced from https://github.com/spesmilo/electrum/blob/master/tests/test_mnemonic.py
+const kTestCases = {
+ "english": _TestCase(
+ words:
+ "wild father tree among universe such"
+ " mobile favorite target dynamic credit identify",
+ seedVersion: ElectrumSeedUtils.kSeedPrefixSegwit,
+ bip32Seed:
+ "aac2a6302e48577ab4b46f23dbae0774e2e62c796f797d0a1b5faeb528301e3064342d"
+ "afb79069e7c4c6b8c38ae11d7a973bec0d4f70626f8cc5184a8d0b0756",
+ ),
+ "english_with_passphrase": _TestCase(
+ words:
+ "wild father tree among universe such"
+ " mobile favorite target dynamic credit identify",
+ seedVersion: ElectrumSeedUtils.kSeedPrefixSegwit,
+ passphrase: "Did you ever hear the tragedy of Darth Plagueis the Wise?",
+ bip32Seed:
+ "4aa29f2aeb0127efb55138ab9e7be83b36750358751906f86c662b21a1ea1370f949e6"
+ "d1a12fa56d3d93cadda93038c76ac8118597364e46f5156fde6183c82f",
+ ),
+ "japanese": _TestCase(
+ lang: "ja",
+ words: "なのか ひろい しなん まなぶ つぶす さがす おしゃれ かわく おいかける けさき かいとう さたん",
+ wordsHex:
+ "e381aae381aee3818b20e381b2e3828de3818420e38197e381aae3829320e381bee381"
+ "aae381b5e3829920e381a4e381b5e38299e3819920e38195e3818be38299e38199"
+ "20e3818ae38197e38283e3828c20e3818be3828fe3818f20e3818ae38184e3818b"
+ "e38191e3828b20e38191e38195e3818d20e3818be38184e381a8e3818620e38195"
+ "e3819fe38293",
+ bip32Seed:
+ "d3eaf0e44ddae3a5769cb08a26918e8b308258bcb057bb704c6f69713245c0b35cb92c"
+ "03df9c9ece5eff826091b4e74041e010b701d44d610976ce8bfb66a8ad",
+ ),
+ "japanese_with_passphrase": _TestCase(
+ lang: "ja",
+ words: "なのか ひろい しなん まなぶ つぶす さがす おしゃれ かわく おいかける けさき かいとう さたん",
+ wordsHex:
+ "e381aae381aee3818b20e381b2e3828de3818420e38197e381aae3829320e381bee381"
+ "aae381b5e3829920e381a4e381b5e38299e3819920e38195e3818be38299e38199"
+ "20e3818ae38197e38283e3828c20e3818be3828fe3818f20e3818ae38184e3818b"
+ "e38191e3828b20e38191e38195e3818d20e3818be38184e381a8e3818620e38195"
+ "e3819fe38293",
+ passphrase: kUnicodeHorror,
+ passphraseHex: kUnicodeHorrorHex,
+ bip32Seed:
+ "251ee6b45b38ba0849e8f40794540f7e2c6d9d604c31d68d3ac50c034f8b64e4bc037c"
+ "5e1e985a2fed8aad23560e690b03b120daf2e84dceb1d7857dda042457",
+ ),
+ "chinese": _TestCase(
+ lang: "zh",
+ words: "眼 悲 叛 改 节 跃 衡 响 疆 股 遂 冬",
+ wordsHex:
+ "e79cbc20e682b220e58f9b20e694b920e88a8220e8b78320e8a1a120e5938d20e79686"
+ "20e882a120e9818220e586ac",
+ seedVersion: ElectrumSeedUtils.kSeedPrefixSegwit,
+ bip32Seed:
+ "0b9077db7b5a50dbb6f61821e2d35e255068a5847e221138048a20e12d80b673ce306b"
+ "6fe7ac174ebc6751e11b7037be6ee9f17db8040bb44f8466d519ce2abf",
+ ),
+ "chinese_with_passphrase": _TestCase(
+ lang: "zh",
+ words: "眼 悲 叛 改 节 跃 衡 响 疆 股 遂 冬",
+ wordsHex:
+ "e79cbc20e682b220e58f9b20e694b920e88a8220e8b78320e8a1a120e5938d20e79686"
+ "20e882a120e9818220e586ac",
+ seedVersion: ElectrumSeedUtils.kSeedPrefixSegwit,
+ passphrase: "给我一些测试向量谷歌",
+ passphraseHex:
+ "e7bb99e68891e4b880e4ba9be6b58be8af95e59091e9878fe8b0b7e6ad8c",
+ bip32Seed:
+ "6c03dd0615cf59963620c0af6840b52e867468cc64f20a1f4c8155705738e87b8edb0f"
+ "c8a6cee4085776cb3a629ff88bb1a38f37085efdbf11ce9ec5a7fa5f71",
+ ),
+ "spanish": _TestCase(
+ lang: "es",
+ words:
+ "almíbar tibio superar vencer hacha peatón"
+ " príncipe matar consejo polen vehículo odisea",
+ wordsHex:
+ "616c6d69cc8162617220746962696f20737570657261722076656e6365722068616368"
+ "6120706561746fcc816e20707269cc816e63697065206d6174617220636f6e7365"
+ "6a6f20706f6c656e2076656869cc8163756c6f206f6469736561",
+ bip32Seed:
+ "18bffd573a960cc775bbd80ed60b7dc00bc8796a186edebe7fc7cf1f316da0fe937852"
+ "a969c5c79ded8255cdf54409537a16339fbe33fb9161af793ea47faa7a",
+ ),
+ "spanish_with_passphrase": _TestCase(
+ lang: "es",
+ words:
+ "almíbar tibio superar vencer hacha peatón "
+ "príncipe matar consejo polen vehículo odisea",
+ wordsHex:
+ "616c6d69cc8162617220746962696f20737570657261722076656e6365722068616368"
+ "6120706561746fcc816e20707269cc816e63697065206d6174617220636f6e73656a6f"
+ "20706f6c656e2076656869cc8163756c6f206f6469736561",
+ passphrase: "araña difícil solución término cárcel",
+ passphraseHex:
+ "6172616ecc83612064696669cc8163696c20736f6c7563696fcc816e207465cc81726d"
+ "696e6f206361cc817263656c",
+ bip32Seed:
+ "363dec0e575b887cfccebee4c84fca5a3a6bed9d0e099c061fa6b85020b031f8fe3636"
+ "d9af187bf432d451273c625e20f24f651ada41aae2c4ea62d87e9fa44c",
+ ),
+ "spanish2": _TestCase(
+ lang: "es",
+ words:
+ "equipo fiar auge langosta hacha calor "
+ "trance cubrir carro pulmón oro áspero",
+ wordsHex:
+ "65717569706f20666961722061756765206c616e676f7374612068616368612063616c"
+ "6f72207472616e63652063756272697220636172726f2070756c6d6fcc816e206f"
+ "726f2061cc81737065726f",
+ seedVersion: ElectrumSeedUtils.kSeedPrefixSegwit,
+ bip32Seed:
+ "001ebce6bfde5851f28a0d44aae5ae0c762b600daf3b33fc8fc630aee0d207646b6f98"
+ "b18e17dfe3be0a5efe2753c7cdad95860adbbb62cecad4dedb88e02a64",
+ ),
+ "spanish3": _TestCase(
+ lang: "es",
+ words:
+ "vidrio jabón muestra pájaro capucha"
+ " eludir feliz rotar fogata pez rezar oír",
+ wordsHex:
+ "76696472696f206a61626fcc816e206d756573747261207061cc816a61726f20636170"
+ "7563686120656c756469722066656c697a20726f74617220666f67617461207065"
+ "7a2072657a6172206f69cc8172",
+ seedVersion: ElectrumSeedUtils.kSeedPrefixSegwit,
+ passphrase:
+ "¡Viva España! repiten veinte pueblos y al hablar dan fe "
+ "del ánimo español... ¡Marquen arado martillo y clarín",
+ passphraseHex:
+ "c2a1566976612045737061c3b16121207265706974656e207665696e74652070756562"
+ "6c6f73207920616c206861626c61722064616e2066652064656c20c3a16e696d6f"
+ "2065737061c3b16f6c2e2e2e20c2a14d61727175656e20617261646f206d617274"
+ "696c6c6f207920636c6172c3ad6e",
+ bip32Seed:
+ "c274665e5453c72f82b8444e293e048d700c59bf000cacfba597629d202dcf3aab1cf9"
+ "c00ba8d3456b7943428541fed714d01d8a0a4028fc3a9bb33d981cb49f",
+ ),
+};
+
+void main() {
+ const kElectrumMnemonic =
+ "party reward jealous build maze tunnel eternal candy recipe february kid animal";
+
+ test(
+ "standard seed prefix",
+ () => expect(ElectrumSeedUtils.kSeedPrefix, "01"),
+ );
+ test(
+ "segwit seed prefix",
+ () => expect(ElectrumSeedUtils.kSeedPrefixSegwit, "100"),
+ );
+ test(
+ "2fa standard seed prefix",
+ () => expect(ElectrumSeedUtils.kSeedPrefix2fa, "101"),
+ );
+ test(
+ "2fa segwit seed prefix",
+ () => expect(ElectrumSeedUtils.kSeedPrefix2faSegwit, "102"),
+ );
+
+ group("electrum mnemonic to seed tests", () {
+ for (final entry in kTestCases.entries) {
+ final name = entry.key;
+ final testCase = entry.value;
+
+ if (testCase.wordsHex != null) {
+ test("$name: mnemonic to bytes to hex", () {
+ expect(testCase.wordsHex, testCase.words.toUint8ListFromUtf8.toHex);
+ });
+ }
+
+ if (testCase.passphraseHex != null && testCase.passphrase != null) {
+ test("$name: passphrase to bytes to hex", () {
+ expect(
+ testCase.passphraseHex,
+ testCase.passphrase!.toUint8ListFromUtf8.toHex,
+ );
+ });
+ }
+
+ test("$name: isNewSeed", () {
+ expect(
+ ElectrumSeedUtils.isNewSeed(
+ testCase.words,
+ prefix: testCase.seedVersion,
+ ),
+ true,
+ );
+ });
+ test("$name: electrumMnemonicToSeedBytes", () {
+ expect(
+ ElectrumSeedUtils.electrumMnemonicToSeedBytes(
+ testCase.words,
+ passphrase: testCase.passphrase ?? "",
+ ).toHex,
+ testCase.bip32Seed,
+ );
+ });
+ }
+ });
+
+ test("test segwit version", () async {
+ expect(
+ ElectrumSeedUtils.electrumMnemonicVersion(kElectrumMnemonic),
+ ElectrumSeedUtils.kSeedPrefixSegwit,
+ );
+ });
+
+ group("test group requires coinlib", () {
+ setUpAll(() => loadCoinlib());
+
+ test("test master electrum fingerprint", () async {
+ final bytes = ElectrumSeedUtils.electrumMnemonicToSeedBytes(
+ kElectrumMnemonic,
+ );
+ final hd = HDPrivateKey.fromSeed(bytes);
+ expect(BigInt.from(hd.fingerprint).toHex, "ec8d82aa");
+ });
+
+ test("test root zpub", () async {
+ final bytes = ElectrumSeedUtils.electrumMnemonicToSeedBytes(
+ kElectrumMnemonic,
+ );
+ final hd = HDPrivateKey.fromSeed(bytes);
+ final master = hd.derivePath("m/0'");
+
+ const zpubHDVersion =
+ 0x04b24746; // https://github.com/satoshilabs/slips/blob/master/slip-0132.md
+ expect(
+ master.hdPublicKey.encode(zpubHDVersion),
+ "zpub6oHsSqJH7vSzDJTFB8NR4YpzFU13XRmkJaVW9jQTePrnf5BPHHAQXxBMiBot12Z7DqfuTykmyPxGowrQfNa7M8xiAdEvQG47V5jhx5Tk158",
+ );
+ });
+
+ test("test first receiving address", () async {
+ final bytes = ElectrumSeedUtils.electrumMnemonicToSeedBytes(
+ kElectrumMnemonic,
+ );
+ final hd = HDPrivateKey.fromSeed(bytes);
+ final master = hd.derivePath("m/0'");
+
+ expect(
+ P2WPKHAddress.fromHash(
+ hash160(master.derivePath("0/0").publicKey.data),
+ hrp: "bc",
+ ).toString(),
+ "bc1qgfjuzurxzhl9vdalmjgw68s680lj5q933k37h5",
+ );
+ });
+
+ test("test 9th change address", () async {
+ final bytes = ElectrumSeedUtils.electrumMnemonicToSeedBytes(
+ kElectrumMnemonic,
+ );
+ final hd = HDPrivateKey.fromSeed(bytes);
+ final master = hd.derivePath("m/0'");
+
+ expect(
+ P2WPKHAddress.fromHash(
+ hash160(master.derivePath("1/8").publicKey.data),
+ hrp: "bc",
+ ).toString(),
+ "bc1qzz0mvhza5sdd2fy77klh3w8h5z238avztvqjdx",
+ );
+ });
+ });
+}
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.