From 72ddb8636665e373ad14e789e2423cd8a46d9793 Mon Sep 17 00:00:00 2001 From: dynst <148708712+dynst@users.noreply.github.com> Date: Tue, 14 Oct 2025 00:33:42 +0000 Subject: [PATCH 1/4] crypto: use @scure/bip39 --- .pnp.cjs | 23 + ...hashes-npm-2.0.1-3deaaa8c92-e81769ce21.zip | 3 + ...-bip39-npm-2.0.1-715af0e367-ed8a0788bc.zip | 3 + packages/crypto/package.json | 1 + packages/crypto/src/bip39.spec.ts | 42 +- packages/crypto/src/bip39.ts | 2177 +---------------- yarn.lock | 26 +- 7 files changed, 60 insertions(+), 2215 deletions(-) create mode 100644 .yarn/cache/@noble-hashes-npm-2.0.1-3deaaa8c92-e81769ce21.zip create mode 100644 .yarn/cache/@scure-bip39-npm-2.0.1-715af0e367-ed8a0788bc.zip diff --git a/.pnp.cjs b/.pnp.cjs index 47870d1b14..ecae736bb5 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -497,6 +497,10 @@ const RAW_RUNTIME_STATE = "@scure/base",\ "npm:2.0.0"\ ],\ + [\ + "@scure/bip39",\ + "npm:2.0.1"\ + ],\ [\ "@shikijs/engine-oniguruma",\ "npm:3.6.0"\ @@ -3503,6 +3507,7 @@ const RAW_RUNTIME_STATE = ["@noble/ciphers", "npm:1.3.0"],\ ["@noble/curves", "npm:1.9.2"],\ ["@noble/hashes", "npm:1.8.0"],\ + ["@scure/bip39", "npm:2.0.1"],\ ["@types/jasmine", "npm:4.6.1"],\ ["@types/karma-firefox-launcher", "npm:2.1.0"],\ ["@types/karma-jasmine", "npm:4.0.2"],\ @@ -4500,6 +4505,13 @@ const RAW_RUNTIME_STATE = ["@noble/hashes", "npm:1.8.0"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:2.0.1", {\ + "packageLocation": "./.yarn/cache/@noble-hashes-npm-2.0.1-3deaaa8c92-e81769ce21.zip/node_modules/@noble/hashes/",\ + "packageDependencies": [\ + ["@noble/hashes", "npm:2.0.1"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@nodelib/fs.scandir", [\ @@ -4678,6 +4690,17 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@scure/bip39", [\ + ["npm:2.0.1", {\ + "packageLocation": "./.yarn/cache/@scure-bip39-npm-2.0.1-715af0e367-ed8a0788bc.zip/node_modules/@scure/bip39/",\ + "packageDependencies": [\ + ["@noble/hashes", "npm:2.0.1"],\ + ["@scure/base", "npm:2.0.0"],\ + ["@scure/bip39", "npm:2.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@shikijs/engine-oniguruma", [\ ["npm:3.6.0", {\ "packageLocation": "./.yarn/cache/@shikijs-engine-oniguruma-npm-3.6.0-2a2a8bbf52-2e3a1fb02d.zip/node_modules/@shikijs/engine-oniguruma/",\ diff --git a/.yarn/cache/@noble-hashes-npm-2.0.1-3deaaa8c92-e81769ce21.zip b/.yarn/cache/@noble-hashes-npm-2.0.1-3deaaa8c92-e81769ce21.zip new file mode 100644 index 0000000000..65cb332929 --- /dev/null +++ b/.yarn/cache/@noble-hashes-npm-2.0.1-3deaaa8c92-e81769ce21.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1418d1af628db45a47de1fe940d1cc2f6cb36733e9c9b51ae5931108d6cf76b +size 685555 diff --git a/.yarn/cache/@scure-bip39-npm-2.0.1-715af0e367-ed8a0788bc.zip b/.yarn/cache/@scure-bip39-npm-2.0.1-715af0e367-ed8a0788bc.zip new file mode 100644 index 0000000000..b0daee687d --- /dev/null +++ b/.yarn/cache/@scure-bip39-npm-2.0.1-715af0e367-ed8a0788bc.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a411036cb2d0f07e6b72a92a3138d67813c96fa31f190cfb3a16d6190ecff74 +size 202966 diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 7ca5183c82..d7d1fc3487 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -45,6 +45,7 @@ "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.2", "@noble/hashes": "^1.8.0", + "@scure/bip39": "^2.0.1", "hash-wasm": "^4.12.0" }, "devDependencies": { diff --git a/packages/crypto/src/bip39.spec.ts b/packages/crypto/src/bip39.spec.ts index 72bf615eeb..458a0a034c 100644 --- a/packages/crypto/src/bip39.spec.ts +++ b/packages/crypto/src/bip39.spec.ts @@ -1,50 +1,10 @@ import { fromAscii, fromBase64, fromHex } from "@cosmjs/encoding"; -import { Bip39, EnglishMnemonic, entropyToMnemonic, mnemonicToEntropy } from "./bip39"; +import { Bip39, EnglishMnemonic } from "./bip39"; import { sha256 } from "./sha"; import bip39Vectors from "./testdata/bip39.json"; import wordlists from "./testdata/bip39_wordlists.json"; -describe("entropyToMnemonic", () => { - it("works", () => { - // From https://iancoleman.io/bip39/ - expect(entropyToMnemonic(fromHex("a323224e6b13d31942509dc4e2e579be3d5bb7f2"))).toEqual( - "permit boil near stomach diamond million announce beauty shaft blame fury ladder stick swim slab", - ); - }); - - it("works for all the test vectors", () => { - // Test vectors from https://github.com/trezor/python-mnemonic/blob/b502451a33a440783926e04428115e0bed87d01f/vectors.json - // plus similar vectors generated for the missing lengths 15 and 21 words - const { "12": vec12, "15": vec15, "18": vec18, "21": vec21, "24": vec24 } = bip39Vectors.encoding; - for (const vectors of [vec12, vec15, vec18, vec21, vec24]) { - for (const { entropy, mnemonic } of vectors) { - expect(entropyToMnemonic(fromHex(entropy))).toEqual(mnemonic); - } - } - }); -}); - -describe("mnemonicToEntropy", () => { - it("works", () => { - // From https://iancoleman.io/bip39/ - expect( - mnemonicToEntropy( - "permit boil near stomach diamond million announce beauty shaft blame fury ladder stick swim slab", - ), - ).toEqual(fromHex("a323224e6b13d31942509dc4e2e579be3d5bb7f2")); - }); - - it("works for all the test vectors", () => { - const { "12": vec12, "15": vec15, "18": vec18, "21": vec21, "24": vec24 } = bip39Vectors.encoding; - for (const vectors of [vec12, vec15, vec18, vec21, vec24]) { - for (const { entropy, mnemonic } of vectors) { - expect(mnemonicToEntropy(mnemonic)).toEqual(fromHex(entropy)); - } - } - }); -}); - describe("Bip39", () => { describe("encode", () => { it("can encode to mnemonic", () => { diff --git a/packages/crypto/src/bip39.ts b/packages/crypto/src/bip39.ts index 7e3b9a46c1..389aa1254e 100644 --- a/packages/crypto/src/bip39.ts +++ b/packages/crypto/src/bip39.ts @@ -1,2174 +1,14 @@ -import { toUtf8 } from "@cosmjs/encoding"; - -import { pbkdf2Sha512 } from "./pbkdf2"; -import { sha256 } from "./sha"; - -const wordlist = [ - "abandon", - "ability", - "able", - "about", - "above", - "absent", - "absorb", - "abstract", - "absurd", - "abuse", - "access", - "accident", - "account", - "accuse", - "achieve", - "acid", - "acoustic", - "acquire", - "across", - "act", - "action", - "actor", - "actress", - "actual", - "adapt", - "add", - "addict", - "address", - "adjust", - "admit", - "adult", - "advance", - "advice", - "aerobic", - "affair", - "afford", - "afraid", - "again", - "age", - "agent", - "agree", - "ahead", - "aim", - "air", - "airport", - "aisle", - "alarm", - "album", - "alcohol", - "alert", - "alien", - "all", - "alley", - "allow", - "almost", - "alone", - "alpha", - "already", - "also", - "alter", - "always", - "amateur", - "amazing", - "among", - "amount", - "amused", - "analyst", - "anchor", - "ancient", - "anger", - "angle", - "angry", - "animal", - "ankle", - "announce", - "annual", - "another", - "answer", - "antenna", - "antique", - "anxiety", - "any", - "apart", - "apology", - "appear", - "apple", - "approve", - "april", - "arch", - "arctic", - "area", - "arena", - "argue", - "arm", - "armed", - "armor", - "army", - "around", - "arrange", - "arrest", - "arrive", - "arrow", - "art", - "artefact", - "artist", - "artwork", - "ask", - "aspect", - "assault", - "asset", - "assist", - "assume", - "asthma", - "athlete", - "atom", - "attack", - "attend", - "attitude", - "attract", - "auction", - "audit", - "august", - "aunt", - "author", - "auto", - "autumn", - "average", - "avocado", - "avoid", - "awake", - "aware", - "away", - "awesome", - "awful", - "awkward", - "axis", - "baby", - "bachelor", - "bacon", - "badge", - "bag", - "balance", - "balcony", - "ball", - "bamboo", - "banana", - "banner", - "bar", - "barely", - "bargain", - "barrel", - "base", - "basic", - "basket", - "battle", - "beach", - "bean", - "beauty", - "because", - "become", - "beef", - "before", - "begin", - "behave", - "behind", - "believe", - "below", - "belt", - "bench", - "benefit", - "best", - "betray", - "better", - "between", - "beyond", - "bicycle", - "bid", - "bike", - "bind", - "biology", - "bird", - "birth", - "bitter", - "black", - "blade", - "blame", - "blanket", - "blast", - "bleak", - "bless", - "blind", - "blood", - "blossom", - "blouse", - "blue", - "blur", - "blush", - "board", - "boat", - "body", - "boil", - "bomb", - "bone", - "bonus", - "book", - "boost", - "border", - "boring", - "borrow", - "boss", - "bottom", - "bounce", - "box", - "boy", - "bracket", - "brain", - "brand", - "brass", - "brave", - "bread", - "breeze", - "brick", - "bridge", - "brief", - "bright", - "bring", - "brisk", - "broccoli", - "broken", - "bronze", - "broom", - "brother", - "brown", - "brush", - "bubble", - "buddy", - "budget", - "buffalo", - "build", - "bulb", - "bulk", - "bullet", - "bundle", - "bunker", - "burden", - "burger", - "burst", - "bus", - "business", - "busy", - "butter", - "buyer", - "buzz", - "cabbage", - "cabin", - "cable", - "cactus", - "cage", - "cake", - "call", - "calm", - "camera", - "camp", - "can", - "canal", - "cancel", - "candy", - "cannon", - "canoe", - "canvas", - "canyon", - "capable", - "capital", - "captain", - "car", - "carbon", - "card", - "cargo", - "carpet", - "carry", - "cart", - "case", - "cash", - "casino", - "castle", - "casual", - "cat", - "catalog", - "catch", - "category", - "cattle", - "caught", - "cause", - "caution", - "cave", - "ceiling", - "celery", - "cement", - "census", - "century", - "cereal", - "certain", - "chair", - "chalk", - "champion", - "change", - "chaos", - "chapter", - "charge", - "chase", - "chat", - "cheap", - "check", - "cheese", - "chef", - "cherry", - "chest", - "chicken", - "chief", - "child", - "chimney", - "choice", - "choose", - "chronic", - "chuckle", - "chunk", - "churn", - "cigar", - "cinnamon", - "circle", - "citizen", - "city", - "civil", - "claim", - "clap", - "clarify", - "claw", - "clay", - "clean", - "clerk", - "clever", - "click", - "client", - "cliff", - "climb", - "clinic", - "clip", - "clock", - "clog", - "close", - "cloth", - "cloud", - "clown", - "club", - "clump", - "cluster", - "clutch", - "coach", - "coast", - "coconut", - "code", - "coffee", - "coil", - "coin", - "collect", - "color", - "column", - "combine", - "come", - "comfort", - "comic", - "common", - "company", - "concert", - "conduct", - "confirm", - "congress", - "connect", - "consider", - "control", - "convince", - "cook", - "cool", - "copper", - "copy", - "coral", - "core", - "corn", - "correct", - "cost", - "cotton", - "couch", - "country", - "couple", - "course", - "cousin", - "cover", - "coyote", - "crack", - "cradle", - "craft", - "cram", - "crane", - "crash", - "crater", - "crawl", - "crazy", - "cream", - "credit", - "creek", - "crew", - "cricket", - "crime", - "crisp", - "critic", - "crop", - "cross", - "crouch", - "crowd", - "crucial", - "cruel", - "cruise", - "crumble", - "crunch", - "crush", - "cry", - "crystal", - "cube", - "culture", - "cup", - "cupboard", - "curious", - "current", - "curtain", - "curve", - "cushion", - "custom", - "cute", - "cycle", - "dad", - "damage", - "damp", - "dance", - "danger", - "daring", - "dash", - "daughter", - "dawn", - "day", - "deal", - "debate", - "debris", - "decade", - "december", - "decide", - "decline", - "decorate", - "decrease", - "deer", - "defense", - "define", - "defy", - "degree", - "delay", - "deliver", - "demand", - "demise", - "denial", - "dentist", - "deny", - "depart", - "depend", - "deposit", - "depth", - "deputy", - "derive", - "describe", - "desert", - "design", - "desk", - "despair", - "destroy", - "detail", - "detect", - "develop", - "device", - "devote", - "diagram", - "dial", - "diamond", - "diary", - "dice", - "diesel", - "diet", - "differ", - "digital", - "dignity", - "dilemma", - "dinner", - "dinosaur", - "direct", - "dirt", - "disagree", - "discover", - "disease", - "dish", - "dismiss", - "disorder", - "display", - "distance", - "divert", - "divide", - "divorce", - "dizzy", - "doctor", - "document", - "dog", - "doll", - "dolphin", - "domain", - "donate", - "donkey", - "donor", - "door", - "dose", - "double", - "dove", - "draft", - "dragon", - "drama", - "drastic", - "draw", - "dream", - "dress", - "drift", - "drill", - "drink", - "drip", - "drive", - "drop", - "drum", - "dry", - "duck", - "dumb", - "dune", - "during", - "dust", - "dutch", - "duty", - "dwarf", - "dynamic", - "eager", - "eagle", - "early", - "earn", - "earth", - "easily", - "east", - "easy", - "echo", - "ecology", - "economy", - "edge", - "edit", - "educate", - "effort", - "egg", - "eight", - "either", - "elbow", - "elder", - "electric", - "elegant", - "element", - "elephant", - "elevator", - "elite", - "else", - "embark", - "embody", - "embrace", - "emerge", - "emotion", - "employ", - "empower", - "empty", - "enable", - "enact", - "end", - "endless", - "endorse", - "enemy", - "energy", - "enforce", - "engage", - "engine", - "enhance", - "enjoy", - "enlist", - "enough", - "enrich", - "enroll", - "ensure", - "enter", - "entire", - "entry", - "envelope", - "episode", - "equal", - "equip", - "era", - "erase", - "erode", - "erosion", - "error", - "erupt", - "escape", - "essay", - "essence", - "estate", - "eternal", - "ethics", - "evidence", - "evil", - "evoke", - "evolve", - "exact", - "example", - "excess", - "exchange", - "excite", - "exclude", - "excuse", - "execute", - "exercise", - "exhaust", - "exhibit", - "exile", - "exist", - "exit", - "exotic", - "expand", - "expect", - "expire", - "explain", - "expose", - "express", - "extend", - "extra", - "eye", - "eyebrow", - "fabric", - "face", - "faculty", - "fade", - "faint", - "faith", - "fall", - "false", - "fame", - "family", - "famous", - "fan", - "fancy", - "fantasy", - "farm", - "fashion", - "fat", - "fatal", - "father", - "fatigue", - "fault", - "favorite", - "feature", - "february", - "federal", - "fee", - "feed", - "feel", - "female", - "fence", - "festival", - "fetch", - "fever", - "few", - "fiber", - "fiction", - "field", - "figure", - "file", - "film", - "filter", - "final", - "find", - "fine", - "finger", - "finish", - "fire", - "firm", - "first", - "fiscal", - "fish", - "fit", - "fitness", - "fix", - "flag", - "flame", - "flash", - "flat", - "flavor", - "flee", - "flight", - "flip", - "float", - "flock", - "floor", - "flower", - "fluid", - "flush", - "fly", - "foam", - "focus", - "fog", - "foil", - "fold", - "follow", - "food", - "foot", - "force", - "forest", - "forget", - "fork", - "fortune", - "forum", - "forward", - "fossil", - "foster", - "found", - "fox", - "fragile", - "frame", - "frequent", - "fresh", - "friend", - "fringe", - "frog", - "front", - "frost", - "frown", - "frozen", - "fruit", - "fuel", - "fun", - "funny", - "furnace", - "fury", - "future", - "gadget", - "gain", - "galaxy", - "gallery", - "game", - "gap", - "garage", - "garbage", - "garden", - "garlic", - "garment", - "gas", - "gasp", - "gate", - "gather", - "gauge", - "gaze", - "general", - "genius", - "genre", - "gentle", - "genuine", - "gesture", - "ghost", - "giant", - "gift", - "giggle", - "ginger", - "giraffe", - "girl", - "give", - "glad", - "glance", - "glare", - "glass", - "glide", - "glimpse", - "globe", - "gloom", - "glory", - "glove", - "glow", - "glue", - "goat", - "goddess", - "gold", - "good", - "goose", - "gorilla", - "gospel", - "gossip", - "govern", - "gown", - "grab", - "grace", - "grain", - "grant", - "grape", - "grass", - "gravity", - "great", - "green", - "grid", - "grief", - "grit", - "grocery", - "group", - "grow", - "grunt", - "guard", - "guess", - "guide", - "guilt", - "guitar", - "gun", - "gym", - "habit", - "hair", - "half", - "hammer", - "hamster", - "hand", - "happy", - "harbor", - "hard", - "harsh", - "harvest", - "hat", - "have", - "hawk", - "hazard", - "head", - "health", - "heart", - "heavy", - "hedgehog", - "height", - "hello", - "helmet", - "help", - "hen", - "hero", - "hidden", - "high", - "hill", - "hint", - "hip", - "hire", - "history", - "hobby", - "hockey", - "hold", - "hole", - "holiday", - "hollow", - "home", - "honey", - "hood", - "hope", - "horn", - "horror", - "horse", - "hospital", - "host", - "hotel", - "hour", - "hover", - "hub", - "huge", - "human", - "humble", - "humor", - "hundred", - "hungry", - "hunt", - "hurdle", - "hurry", - "hurt", - "husband", - "hybrid", - "ice", - "icon", - "idea", - "identify", - "idle", - "ignore", - "ill", - "illegal", - "illness", - "image", - "imitate", - "immense", - "immune", - "impact", - "impose", - "improve", - "impulse", - "inch", - "include", - "income", - "increase", - "index", - "indicate", - "indoor", - "industry", - "infant", - "inflict", - "inform", - "inhale", - "inherit", - "initial", - "inject", - "injury", - "inmate", - "inner", - "innocent", - "input", - "inquiry", - "insane", - "insect", - "inside", - "inspire", - "install", - "intact", - "interest", - "into", - "invest", - "invite", - "involve", - "iron", - "island", - "isolate", - "issue", - "item", - "ivory", - "jacket", - "jaguar", - "jar", - "jazz", - "jealous", - "jeans", - "jelly", - "jewel", - "job", - "join", - "joke", - "journey", - "joy", - "judge", - "juice", - "jump", - "jungle", - "junior", - "junk", - "just", - "kangaroo", - "keen", - "keep", - "ketchup", - "key", - "kick", - "kid", - "kidney", - "kind", - "kingdom", - "kiss", - "kit", - "kitchen", - "kite", - "kitten", - "kiwi", - "knee", - "knife", - "knock", - "know", - "lab", - "label", - "labor", - "ladder", - "lady", - "lake", - "lamp", - "language", - "laptop", - "large", - "later", - "latin", - "laugh", - "laundry", - "lava", - "law", - "lawn", - "lawsuit", - "layer", - "lazy", - "leader", - "leaf", - "learn", - "leave", - "lecture", - "left", - "leg", - "legal", - "legend", - "leisure", - "lemon", - "lend", - "length", - "lens", - "leopard", - "lesson", - "letter", - "level", - "liar", - "liberty", - "library", - "license", - "life", - "lift", - "light", - "like", - "limb", - "limit", - "link", - "lion", - "liquid", - "list", - "little", - "live", - "lizard", - "load", - "loan", - "lobster", - "local", - "lock", - "logic", - "lonely", - "long", - "loop", - "lottery", - "loud", - "lounge", - "love", - "loyal", - "lucky", - "luggage", - "lumber", - "lunar", - "lunch", - "luxury", - "lyrics", - "machine", - "mad", - "magic", - "magnet", - "maid", - "mail", - "main", - "major", - "make", - "mammal", - "man", - "manage", - "mandate", - "mango", - "mansion", - "manual", - "maple", - "marble", - "march", - "margin", - "marine", - "market", - "marriage", - "mask", - "mass", - "master", - "match", - "material", - "math", - "matrix", - "matter", - "maximum", - "maze", - "meadow", - "mean", - "measure", - "meat", - "mechanic", - "medal", - "media", - "melody", - "melt", - "member", - "memory", - "mention", - "menu", - "mercy", - "merge", - "merit", - "merry", - "mesh", - "message", - "metal", - "method", - "middle", - "midnight", - "milk", - "million", - "mimic", - "mind", - "minimum", - "minor", - "minute", - "miracle", - "mirror", - "misery", - "miss", - "mistake", - "mix", - "mixed", - "mixture", - "mobile", - "model", - "modify", - "mom", - "moment", - "monitor", - "monkey", - "monster", - "month", - "moon", - "moral", - "more", - "morning", - "mosquito", - "mother", - "motion", - "motor", - "mountain", - "mouse", - "move", - "movie", - "much", - "muffin", - "mule", - "multiply", - "muscle", - "museum", - "mushroom", - "music", - "must", - "mutual", - "myself", - "mystery", - "myth", - "naive", - "name", - "napkin", - "narrow", - "nasty", - "nation", - "nature", - "near", - "neck", - "need", - "negative", - "neglect", - "neither", - "nephew", - "nerve", - "nest", - "net", - "network", - "neutral", - "never", - "news", - "next", - "nice", - "night", - "noble", - "noise", - "nominee", - "noodle", - "normal", - "north", - "nose", - "notable", - "note", - "nothing", - "notice", - "novel", - "now", - "nuclear", - "number", - "nurse", - "nut", - "oak", - "obey", - "object", - "oblige", - "obscure", - "observe", - "obtain", - "obvious", - "occur", - "ocean", - "october", - "odor", - "off", - "offer", - "office", - "often", - "oil", - "okay", - "old", - "olive", - "olympic", - "omit", - "once", - "one", - "onion", - "online", - "only", - "open", - "opera", - "opinion", - "oppose", - "option", - "orange", - "orbit", - "orchard", - "order", - "ordinary", - "organ", - "orient", - "original", - "orphan", - "ostrich", - "other", - "outdoor", - "outer", - "output", - "outside", - "oval", - "oven", - "over", - "own", - "owner", - "oxygen", - "oyster", - "ozone", - "pact", - "paddle", - "page", - "pair", - "palace", - "palm", - "panda", - "panel", - "panic", - "panther", - "paper", - "parade", - "parent", - "park", - "parrot", - "party", - "pass", - "patch", - "path", - "patient", - "patrol", - "pattern", - "pause", - "pave", - "payment", - "peace", - "peanut", - "pear", - "peasant", - "pelican", - "pen", - "penalty", - "pencil", - "people", - "pepper", - "perfect", - "permit", - "person", - "pet", - "phone", - "photo", - "phrase", - "physical", - "piano", - "picnic", - "picture", - "piece", - "pig", - "pigeon", - "pill", - "pilot", - "pink", - "pioneer", - "pipe", - "pistol", - "pitch", - "pizza", - "place", - "planet", - "plastic", - "plate", - "play", - "please", - "pledge", - "pluck", - "plug", - "plunge", - "poem", - "poet", - "point", - "polar", - "pole", - "police", - "pond", - "pony", - "pool", - "popular", - "portion", - "position", - "possible", - "post", - "potato", - "pottery", - "poverty", - "powder", - "power", - "practice", - "praise", - "predict", - "prefer", - "prepare", - "present", - "pretty", - "prevent", - "price", - "pride", - "primary", - "print", - "priority", - "prison", - "private", - "prize", - "problem", - "process", - "produce", - "profit", - "program", - "project", - "promote", - "proof", - "property", - "prosper", - "protect", - "proud", - "provide", - "public", - "pudding", - "pull", - "pulp", - "pulse", - "pumpkin", - "punch", - "pupil", - "puppy", - "purchase", - "purity", - "purpose", - "purse", - "push", - "put", - "puzzle", - "pyramid", - "quality", - "quantum", - "quarter", - "question", - "quick", - "quit", - "quiz", - "quote", - "rabbit", - "raccoon", - "race", - "rack", - "radar", - "radio", - "rail", - "rain", - "raise", - "rally", - "ramp", - "ranch", - "random", - "range", - "rapid", - "rare", - "rate", - "rather", - "raven", - "raw", - "razor", - "ready", - "real", - "reason", - "rebel", - "rebuild", - "recall", - "receive", - "recipe", - "record", - "recycle", - "reduce", - "reflect", - "reform", - "refuse", - "region", - "regret", - "regular", - "reject", - "relax", - "release", - "relief", - "rely", - "remain", - "remember", - "remind", - "remove", - "render", - "renew", - "rent", - "reopen", - "repair", - "repeat", - "replace", - "report", - "require", - "rescue", - "resemble", - "resist", - "resource", - "response", - "result", - "retire", - "retreat", - "return", - "reunion", - "reveal", - "review", - "reward", - "rhythm", - "rib", - "ribbon", - "rice", - "rich", - "ride", - "ridge", - "rifle", - "right", - "rigid", - "ring", - "riot", - "ripple", - "risk", - "ritual", - "rival", - "river", - "road", - "roast", - "robot", - "robust", - "rocket", - "romance", - "roof", - "rookie", - "room", - "rose", - "rotate", - "rough", - "round", - "route", - "royal", - "rubber", - "rude", - "rug", - "rule", - "run", - "runway", - "rural", - "sad", - "saddle", - "sadness", - "safe", - "sail", - "salad", - "salmon", - "salon", - "salt", - "salute", - "same", - "sample", - "sand", - "satisfy", - "satoshi", - "sauce", - "sausage", - "save", - "say", - "scale", - "scan", - "scare", - "scatter", - "scene", - "scheme", - "school", - "science", - "scissors", - "scorpion", - "scout", - "scrap", - "screen", - "script", - "scrub", - "sea", - "search", - "season", - "seat", - "second", - "secret", - "section", - "security", - "seed", - "seek", - "segment", - "select", - "sell", - "seminar", - "senior", - "sense", - "sentence", - "series", - "service", - "session", - "settle", - "setup", - "seven", - "shadow", - "shaft", - "shallow", - "share", - "shed", - "shell", - "sheriff", - "shield", - "shift", - "shine", - "ship", - "shiver", - "shock", - "shoe", - "shoot", - "shop", - "short", - "shoulder", - "shove", - "shrimp", - "shrug", - "shuffle", - "shy", - "sibling", - "sick", - "side", - "siege", - "sight", - "sign", - "silent", - "silk", - "silly", - "silver", - "similar", - "simple", - "since", - "sing", - "siren", - "sister", - "situate", - "six", - "size", - "skate", - "sketch", - "ski", - "skill", - "skin", - "skirt", - "skull", - "slab", - "slam", - "sleep", - "slender", - "slice", - "slide", - "slight", - "slim", - "slogan", - "slot", - "slow", - "slush", - "small", - "smart", - "smile", - "smoke", - "smooth", - "snack", - "snake", - "snap", - "sniff", - "snow", - "soap", - "soccer", - "social", - "sock", - "soda", - "soft", - "solar", - "soldier", - "solid", - "solution", - "solve", - "someone", - "song", - "soon", - "sorry", - "sort", - "soul", - "sound", - "soup", - "source", - "south", - "space", - "spare", - "spatial", - "spawn", - "speak", - "special", - "speed", - "spell", - "spend", - "sphere", - "spice", - "spider", - "spike", - "spin", - "spirit", - "split", - "spoil", - "sponsor", - "spoon", - "sport", - "spot", - "spray", - "spread", - "spring", - "spy", - "square", - "squeeze", - "squirrel", - "stable", - "stadium", - "staff", - "stage", - "stairs", - "stamp", - "stand", - "start", - "state", - "stay", - "steak", - "steel", - "stem", - "step", - "stereo", - "stick", - "still", - "sting", - "stock", - "stomach", - "stone", - "stool", - "story", - "stove", - "strategy", - "street", - "strike", - "strong", - "struggle", - "student", - "stuff", - "stumble", - "style", - "subject", - "submit", - "subway", - "success", - "such", - "sudden", - "suffer", - "sugar", - "suggest", - "suit", - "summer", - "sun", - "sunny", - "sunset", - "super", - "supply", - "supreme", - "sure", - "surface", - "surge", - "surprise", - "surround", - "survey", - "suspect", - "sustain", - "swallow", - "swamp", - "swap", - "swarm", - "swear", - "sweet", - "swift", - "swim", - "swing", - "switch", - "sword", - "symbol", - "symptom", - "syrup", - "system", - "table", - "tackle", - "tag", - "tail", - "talent", - "talk", - "tank", - "tape", - "target", - "task", - "taste", - "tattoo", - "taxi", - "teach", - "team", - "tell", - "ten", - "tenant", - "tennis", - "tent", - "term", - "test", - "text", - "thank", - "that", - "theme", - "then", - "theory", - "there", - "they", - "thing", - "this", - "thought", - "three", - "thrive", - "throw", - "thumb", - "thunder", - "ticket", - "tide", - "tiger", - "tilt", - "timber", - "time", - "tiny", - "tip", - "tired", - "tissue", - "title", - "toast", - "tobacco", - "today", - "toddler", - "toe", - "together", - "toilet", - "token", - "tomato", - "tomorrow", - "tone", - "tongue", - "tonight", - "tool", - "tooth", - "top", - "topic", - "topple", - "torch", - "tornado", - "tortoise", - "toss", - "total", - "tourist", - "toward", - "tower", - "town", - "toy", - "track", - "trade", - "traffic", - "tragic", - "train", - "transfer", - "trap", - "trash", - "travel", - "tray", - "treat", - "tree", - "trend", - "trial", - "tribe", - "trick", - "trigger", - "trim", - "trip", - "trophy", - "trouble", - "truck", - "true", - "truly", - "trumpet", - "trust", - "truth", - "try", - "tube", - "tuition", - "tumble", - "tuna", - "tunnel", - "turkey", - "turn", - "turtle", - "twelve", - "twenty", - "twice", - "twin", - "twist", - "two", - "type", - "typical", - "ugly", - "umbrella", - "unable", - "unaware", - "uncle", - "uncover", - "under", - "undo", - "unfair", - "unfold", - "unhappy", - "uniform", - "unique", - "unit", - "universe", - "unknown", - "unlock", - "until", - "unusual", - "unveil", - "update", - "upgrade", - "uphold", - "upon", - "upper", - "upset", - "urban", - "urge", - "usage", - "use", - "used", - "useful", - "useless", - "usual", - "utility", - "vacant", - "vacuum", - "vague", - "valid", - "valley", - "valve", - "van", - "vanish", - "vapor", - "various", - "vast", - "vault", - "vehicle", - "velvet", - "vendor", - "venture", - "venue", - "verb", - "verify", - "version", - "very", - "vessel", - "veteran", - "viable", - "vibrant", - "vicious", - "victory", - "video", - "view", - "village", - "vintage", - "violin", - "virtual", - "virus", - "visa", - "visit", - "visual", - "vital", - "vivid", - "vocal", - "voice", - "void", - "volcano", - "volume", - "vote", - "voyage", - "wage", - "wagon", - "wait", - "walk", - "wall", - "walnut", - "want", - "warfare", - "warm", - "warrior", - "wash", - "wasp", - "waste", - "water", - "wave", - "way", - "wealth", - "weapon", - "wear", - "weasel", - "weather", - "web", - "wedding", - "weekend", - "weird", - "welcome", - "west", - "wet", - "whale", - "what", - "wheat", - "wheel", - "when", - "where", - "whip", - "whisper", - "wide", - "width", - "wife", - "wild", - "will", - "win", - "window", - "wine", - "wing", - "wink", - "winner", - "winter", - "wire", - "wisdom", - "wise", - "wish", - "witness", - "wolf", - "woman", - "wonder", - "wood", - "wool", - "word", - "work", - "world", - "worry", - "worth", - "wrap", - "wreck", - "wrestle", - "wrist", - "write", - "wrong", - "yard", - "year", - "yellow", - "you", - "young", - "youth", - "zebra", - "zero", - "zone", - "zoo", -]; - -function bytesToBitstring(bytes: ArrayLike): string { - return Array.from(bytes) - .map((byte: number): string => byte.toString(2).padStart(8, "0")) - .join(""); -} - -function deriveChecksumBits(entropy: Uint8Array): string { - const entropyLengthBits = entropy.length * 8; // "ENT" (in bits) - const checksumLengthBits = entropyLengthBits / 32; // "CS" (in bits) - const hash = sha256(entropy); - return bytesToBitstring(hash).slice(0, checksumLengthBits); -} - -function bitstringToByte(bin: string): number { - return parseInt(bin, 2); -} - -const allowedEntropyLengths: readonly number[] = [16, 20, 24, 28, 32]; -const allowedWordLengths: readonly number[] = [12, 15, 18, 21, 24]; - -export function entropyToMnemonic(entropy: Uint8Array): string { - if (allowedEntropyLengths.indexOf(entropy.length) === -1) { - throw new Error("invalid input length"); - } - - const entropyBits = bytesToBitstring(entropy); - const checksumBits = deriveChecksumBits(entropy); - - const bits = entropyBits + checksumBits; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const chunks = bits.match(/(.{11})/g)!; - const words = chunks.map((binary: string): string => { - const index = bitstringToByte(binary); - return wordlist[index]; - }); - - return words.join(" "); -} - -const invalidNumberOfWords = "Invalid number of words"; -const wordNotInWordlist = "Found word that is not in the wordlist"; -const invalidEntropy = "Invalid entropy"; -const invalidChecksum = "Invalid mnemonic checksum"; - -function normalize(str: string): string { - return str.normalize("NFKD"); -} - -export function mnemonicToEntropy(mnemonic: string): Uint8Array { - const words = normalize(mnemonic).split(" "); - if (!allowedWordLengths.includes(words.length)) { - throw new Error(invalidNumberOfWords); - } - - // convert word indices to 11 bit binary strings - const bits = words - .map((word: string): string => { - const index = wordlist.indexOf(word); - if (index === -1) { - throw new Error(wordNotInWordlist); - } - return index.toString(2).padStart(11, "0"); - }) - .join(""); - - // split the binary string into ENT/CS - const dividerIndex = Math.floor(bits.length / 33) * 32; - const entropyBits = bits.slice(0, dividerIndex); - const checksumBits = bits.slice(dividerIndex); - - // calculate the checksum and compare - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const entropyBytes = entropyBits.match(/(.{1,8})/g)!.map(bitstringToByte); - if (entropyBytes.length < 16 || entropyBytes.length > 32 || entropyBytes.length % 4 !== 0) { - throw new Error(invalidEntropy); - } - - const entropy = Uint8Array.from(entropyBytes); - const newChecksum = deriveChecksumBits(entropy); - if (newChecksum !== checksumBits) { - throw new Error(invalidChecksum); - } - - return entropy; -} +import { entropyToMnemonic, mnemonicToEntropy, mnemonicToSeed } from "@scure/bip39"; +import { wordlist } from "@scure/bip39/wordlists/english.js"; export class EnglishMnemonic { public static readonly wordlist: readonly string[] = wordlist; - // list of space separated lower case words (1 or more) - private static readonly mnemonicMatcher = /^[a-z]+( [a-z]+)*$/; - private readonly data: string; public constructor(mnemonic: string) { - if (!EnglishMnemonic.mnemonicMatcher.test(mnemonic)) { - throw new Error("Invalid mnemonic format"); - } - - const words = mnemonic.split(" "); - const allowedWordsLengths: readonly number[] = [12, 15, 18, 21, 24]; - if (allowedWordsLengths.indexOf(words.length) === -1) { - throw new Error( - `Invalid word count in mnemonic (allowed: ${allowedWordsLengths} got: ${words.length})`, - ); - } - - for (const word of words) { - if (EnglishMnemonic.wordlist.indexOf(word) === -1) { - throw new Error("Mnemonic contains invalid word"); - } - } - - // Throws with informative error message if mnemonic is not valid - mnemonicToEntropy(mnemonic); + // throws an error if mnemonic is invalid + const _ = mnemonicToEntropy(mnemonic, wordlist); this.data = mnemonic; } @@ -2195,17 +35,14 @@ export class Bip39 { * @param entropy The entropy to be encoded. This must be cryptographically secure. */ public static encode(entropy: Uint8Array): EnglishMnemonic { - return new EnglishMnemonic(entropyToMnemonic(entropy)); + return new EnglishMnemonic(entropyToMnemonic(entropy, wordlist)); } public static decode(mnemonic: EnglishMnemonic): Uint8Array { - return mnemonicToEntropy(mnemonic.toString()); + return mnemonicToEntropy(mnemonic.toString(), wordlist); } public static async mnemonicToSeed(mnemonic: EnglishMnemonic, password?: string): Promise { - const mnemonicBytes = toUtf8(normalize(mnemonic.toString())); - const salt = "mnemonic" + (password ? normalize(password) : ""); - const saltBytes = toUtf8(salt); - return pbkdf2Sha512(mnemonicBytes, saltBytes, 2048, 64); + return await mnemonicToSeed(mnemonic.toString(), password); } } diff --git a/yarn.lock b/yarn.lock index bbdf3b57b4..90752b2f7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -325,6 +325,7 @@ __metadata: "@noble/ciphers": "npm:^1.3.0" "@noble/curves": "npm:^1.9.2" "@noble/hashes": "npm:^1.8.0" + "@scure/bip39": "npm:^2.0.1" "@types/jasmine": "npm:^4" "@types/karma-firefox-launcher": "npm:^2" "@types/karma-jasmine": "npm:^4" @@ -1192,6 +1193,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:2.0.1": + version: 2.0.1 + resolution: "@noble/hashes@npm:2.0.1" + checksum: 10c0/e81769ce21c3b1c80141a3b99bd001f17edea09879aa936692ae39525477386d696101cd573928a304806efb2b9fa751e1dd83241c67d0c84d30091e85c79bdb + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -1322,6 +1330,13 @@ __metadata: languageName: node linkType: hard +"@scure/base@npm:2.0.0, @scure/base@npm:^2.0.0": + version: 2.0.0 + resolution: "@scure/base@npm:2.0.0" + checksum: 10c0/7d999c7bebf053bb49cb706fdc6c5366737cff0f7f7518f52d32d7f7ad7b898904f03673648a2af5c4f22396f5c05f1d8bddbf010d6595052d07ba8163d506ad + languageName: node + linkType: hard + "@scure/base@npm:^1.2.4": version: 1.2.6 resolution: "@scure/base@npm:1.2.6" @@ -1329,10 +1344,13 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:^2.0.0": - version: 2.0.0 - resolution: "@scure/base@npm:2.0.0" - checksum: 10c0/7d999c7bebf053bb49cb706fdc6c5366737cff0f7f7518f52d32d7f7ad7b898904f03673648a2af5c4f22396f5c05f1d8bddbf010d6595052d07ba8163d506ad +"@scure/bip39@npm:^2.0.1": + version: 2.0.1 + resolution: "@scure/bip39@npm:2.0.1" + dependencies: + "@noble/hashes": "npm:2.0.1" + "@scure/base": "npm:2.0.0" + checksum: 10c0/ed8a0788bca006a6e4a647ed67c4c973b1deeaee5d62ddc168c9521c33e3a66cf5707c8aadcd0b6f9e3e41c3f763a985d913f4abc3813963497238e73ce166b6 languageName: node linkType: hard From 75d4650cd9d67f666d12ba840e96a9772bf16b2a Mon Sep 17 00:00:00 2001 From: dynst <148708712+dynst@users.noreply.github.com> Date: Tue, 14 Oct 2025 00:57:29 +0000 Subject: [PATCH 2/4] crypto: bip39 tests --- packages/crypto/src/bip39.spec.ts | 72 +++++++++++++++---------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/crypto/src/bip39.spec.ts b/packages/crypto/src/bip39.spec.ts index 458a0a034c..200b3a19f8 100644 --- a/packages/crypto/src/bip39.spec.ts +++ b/packages/crypto/src/bip39.spec.ts @@ -20,26 +20,26 @@ describe("Bip39", () => { it("throws for invalid input", () => { // invalid input length - expect(() => Bip39.encode(fromHex(""))).toThrowError(/invalid input length/); - expect(() => Bip39.encode(fromHex("00"))).toThrowError(/invalid input length/); + expect(() => Bip39.encode(fromHex(""))).toThrowError(/invalid entropy length/); + expect(() => Bip39.encode(fromHex("00"))).toThrowError(/invalid entropy length/); expect(() => Bip39.encode(fromHex("000000000000000000000000000000"))).toThrowError( - /invalid input length/, + /invalid entropy length/, ); expect(() => Bip39.encode(fromHex("0000000000000000000000000000000000"))).toThrowError( - /invalid input length/, + /invalid entropy length/, ); expect(() => Bip39.encode(fromHex("0000000000000000000000000000000000000000000000"))).toThrowError( - /invalid input length/, + /invalid entropy length/, ); expect(() => Bip39.encode(fromHex("00000000000000000000000000000000000000000000000000"))).toThrowError( - /invalid input length/, + /invalid entropy length/, ); expect(() => Bip39.encode(fromHex("00000000000000000000000000000000000000000000000000000000000000")), - ).toThrowError(/invalid input length/); + ).toThrowError(/invalid entropy length/); expect(() => Bip39.encode(fromHex("000000000000000000000000000000000000000000000000000000000000000000")), - ).toThrowError(/invalid input length/); + ).toThrowError(/invalid entropy length/); }); }); @@ -469,19 +469,19 @@ describe("EnglishMnemonic", () => { new EnglishMnemonic( " abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", ), - ).toThrowError(/invalid mnemonic format/i); + ).toThrowError(/invalid mnemonic/i); expect( () => new EnglishMnemonic( "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", ), - ).toThrowError(/invalid mnemonic format/i); + ).toThrowError(/invalid mnemonic/i); expect( () => new EnglishMnemonic( "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about ", ), - ).toThrowError(/invalid mnemonic format/i); + ).toThrowError(/invalid mnemonic/i); // newline, tab expect( @@ -489,13 +489,13 @@ describe("EnglishMnemonic", () => { new EnglishMnemonic( "abandon\nabandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", ), - ).toThrowError(/invalid mnemonic format/i); + ).toThrowError(/invalid mnemonic/i); expect( () => new EnglishMnemonic( "abandon\tabandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", ), - ).toThrowError(/invalid mnemonic format/i); + ).toThrowError(/invalid mnemonic/i); }); it("rejects disallowed letters", () => { @@ -505,37 +505,37 @@ describe("EnglishMnemonic", () => { new EnglishMnemonic( "Abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", ), - ).toThrowError(/invalid mnemonic format/i); + ).toThrowError(/unknown letter/i); expect( () => new EnglishMnemonic( "abandon abandon Abandon abandon abandon abandon abandon abandon abandon abandon abandon about", ), - ).toThrowError(/invalid mnemonic format/i); + ).toThrowError(/unknown letter/i); expect( () => new EnglishMnemonic( "route66 abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", ), - ).toThrowError(/invalid mnemonic format/i); + ).toThrowError(/unknown letter/i); expect( () => new EnglishMnemonic( "abandon abandon route66 abandon abandon abandon abandon abandon abandon abandon abandon about", ), - ).toThrowError(/invalid mnemonic format/i); + ).toThrowError(/unknown letter/i); expect( () => new EnglishMnemonic( "lötkolben abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", ), - ).toThrowError(/invalid mnemonic format/i); + ).toThrowError(/unknown letter/i); expect( () => new EnglishMnemonic( "abandon abandon lötkolben abandon abandon abandon abandon abandon abandon abandon abandon about", ), - ).toThrowError(/invalid mnemonic format/i); + ).toThrowError(/unknown letter/i); }); it("word counts other than 12, 15, 18, 21, 24", () => { @@ -545,62 +545,62 @@ describe("EnglishMnemonic", () => { new EnglishMnemonic( "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", ), - ).toThrowError(/invalid word count(.*)got: 11/i); + ).toThrowError(/invalid/i); expect( () => new EnglishMnemonic( "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", ), - ).toThrowError(/invalid word count(.*)got: 13/i); + ).toThrowError(/invalid/i); expect( () => new EnglishMnemonic( "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent", ), - ).toThrowError(/invalid word count(.*)got: 17/i); + ).toThrowError(/invalid/i); expect( () => new EnglishMnemonic( "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent", ), - ).toThrowError(/invalid word count(.*)got: 19/i); + ).toThrowError(/invalid/i); expect( () => new EnglishMnemonic( "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art", ), - ).toThrowError(/invalid word count(.*)got: 23/i); + ).toThrowError(/invalid/i); expect( () => new EnglishMnemonic( "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art", ), - ).toThrowError(/invalid word count(.*)got: 25/i); + ).toThrowError(/invalid/i); }); it("rejects invalid checksums", () => { // 12x, 15x, 18x, 21x, 24x "zoo" expect(() => new EnglishMnemonic("zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo")).toThrowError( - /invalid mnemonic checksum/i, + /invalid checksum/i, ); expect( () => new EnglishMnemonic("zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo"), - ).toThrowError(/invalid mnemonic checksum/i); + ).toThrowError(/invalid checksum/i); expect( () => new EnglishMnemonic("zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo"), - ).toThrowError(/invalid mnemonic checksum/i); + ).toThrowError(/invalid checksum/i); expect( () => new EnglishMnemonic( "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo", ), - ).toThrowError(/invalid mnemonic checksum/i); + ).toThrowError(/invalid checksum/i); expect( () => new EnglishMnemonic( "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo", ), - ).toThrowError(/invalid mnemonic checksum/i); + ).toThrowError(/invalid checksum/i); }); it("rejects valid mnemonics of other languages", () => { @@ -610,37 +610,37 @@ describe("EnglishMnemonic", () => { new EnglishMnemonic( "humo odio oriente colina taco fingir salto geranio glaciar academia suave vigor", ), - ).toThrowError(/contains invalid word/i); + ).toThrowError(/unknown letter: "humo". allowed: abandon,ability,able,/i); expect( () => new EnglishMnemonic( "yema folleto tos llave obtener natural fruta deseo laico sopa novato lazo imponer afinar vena hoja zarza cama", ), - ).toThrowError(/contains invalid word/i); + ).toThrowError(/unknown letter: "yema"/i); expect( () => new EnglishMnemonic( "burla plaza arroz ronda pregunta vacuna veloz boina retiro exento prensa tortuga cabeza pilar anual molino molde fiesta masivo jefe leve fatiga clase plomo", ), - ).toThrowError(/contains invalid word/i); + ).toThrowError(/unknown letter/i); expect( () => new EnglishMnemonic( "braccio trincea armonia emiro svedese lepre stridulo metallo baldo rasente potassio rilassato", ), - ).toThrowError(/contains invalid word/i); + ).toThrowError(/unknown letter/i); expect( () => new EnglishMnemonic( "riparato arrosto globulo singolo bozzolo roba pirolisi ultimato padrone munto leggero avanzato monetario guanto lorenzo latino inoltrare modulo", ), - ).toThrowError(/contains invalid word/i); + ).toThrowError(/unknown letter/i); expect( () => new EnglishMnemonic( "promessa mercurio spessore snodo trave risata mecenate vichingo ceto orecchino vissuto risultato canino scarso futile fune epilogo uovo inedito apatico folata egoismo rifugio coma", ), - ).toThrowError(/contains invalid word/i); + ).toThrowError(/unknown letter/i); }); describe("toString", () => { From 9d9c76933675690bf5f5622a0479587ecc57cb87 Mon Sep 17 00:00:00 2001 From: dynst <148708712+dynst@users.noreply.github.com> Date: Tue, 14 Oct 2025 02:18:10 +0000 Subject: [PATCH 3/4] crypto: use fast WebCrypto native code --- packages/crypto/src/bip39.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/crypto/src/bip39.ts b/packages/crypto/src/bip39.ts index 389aa1254e..4fd637817e 100644 --- a/packages/crypto/src/bip39.ts +++ b/packages/crypto/src/bip39.ts @@ -1,4 +1,4 @@ -import { entropyToMnemonic, mnemonicToEntropy, mnemonicToSeed } from "@scure/bip39"; +import { entropyToMnemonic, mnemonicToEntropy, mnemonicToSeedWebcrypto } from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english.js"; export class EnglishMnemonic { @@ -43,6 +43,6 @@ export class Bip39 { } public static async mnemonicToSeed(mnemonic: EnglishMnemonic, password?: string): Promise { - return await mnemonicToSeed(mnemonic.toString(), password); + return await mnemonicToSeedWebcrypto(mnemonic.toString(), password); } } From 242673f5b0e0cfbe06cf8628ef079e8812687bf6 Mon Sep 17 00:00:00 2001 From: dynst <148708712+dynst@users.noreply.github.com> Date: Tue, 14 Oct 2025 02:21:35 +0000 Subject: [PATCH 4/4] downgrade to @scure/bip39 1.6 CJS package needed to work around node20 failing in CI. --- .pnp.cjs | 21 ++++------ ...hashes-npm-2.0.1-3deaaa8c92-e81769ce21.zip | 3 -- ...-bip39-npm-1.6.0-63a27ac0b7-73a54b5566.zip | 3 ++ ...-bip39-npm-2.0.1-715af0e367-ed8a0788bc.zip | 3 -- packages/crypto/package.json | 2 +- packages/crypto/src/bip39.spec.ts | 16 ++++---- packages/crypto/src/bip39.ts | 6 +-- yarn.lock | 39 ++++++++----------- 8 files changed, 38 insertions(+), 55 deletions(-) delete mode 100644 .yarn/cache/@noble-hashes-npm-2.0.1-3deaaa8c92-e81769ce21.zip create mode 100644 .yarn/cache/@scure-bip39-npm-1.6.0-63a27ac0b7-73a54b5566.zip delete mode 100644 .yarn/cache/@scure-bip39-npm-2.0.1-715af0e367-ed8a0788bc.zip diff --git a/.pnp.cjs b/.pnp.cjs index ecae736bb5..8289a59551 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -499,7 +499,7 @@ const RAW_RUNTIME_STATE = ],\ [\ "@scure/bip39",\ - "npm:2.0.1"\ + "npm:1.6.0"\ ],\ [\ "@shikijs/engine-oniguruma",\ @@ -3507,7 +3507,7 @@ const RAW_RUNTIME_STATE = ["@noble/ciphers", "npm:1.3.0"],\ ["@noble/curves", "npm:1.9.2"],\ ["@noble/hashes", "npm:1.8.0"],\ - ["@scure/bip39", "npm:2.0.1"],\ + ["@scure/bip39", "npm:1.6.0"],\ ["@types/jasmine", "npm:4.6.1"],\ ["@types/karma-firefox-launcher", "npm:2.1.0"],\ ["@types/karma-jasmine", "npm:4.0.2"],\ @@ -4505,13 +4505,6 @@ const RAW_RUNTIME_STATE = ["@noble/hashes", "npm:1.8.0"]\ ],\ "linkType": "HARD"\ - }],\ - ["npm:2.0.1", {\ - "packageLocation": "./.yarn/cache/@noble-hashes-npm-2.0.1-3deaaa8c92-e81769ce21.zip/node_modules/@noble/hashes/",\ - "packageDependencies": [\ - ["@noble/hashes", "npm:2.0.1"]\ - ],\ - "linkType": "HARD"\ }]\ ]],\ ["@nodelib/fs.scandir", [\ @@ -4691,12 +4684,12 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["@scure/bip39", [\ - ["npm:2.0.1", {\ - "packageLocation": "./.yarn/cache/@scure-bip39-npm-2.0.1-715af0e367-ed8a0788bc.zip/node_modules/@scure/bip39/",\ + ["npm:1.6.0", {\ + "packageLocation": "./.yarn/cache/@scure-bip39-npm-1.6.0-63a27ac0b7-73a54b5566.zip/node_modules/@scure/bip39/",\ "packageDependencies": [\ - ["@noble/hashes", "npm:2.0.1"],\ - ["@scure/base", "npm:2.0.0"],\ - ["@scure/bip39", "npm:2.0.1"]\ + ["@noble/hashes", "npm:1.8.0"],\ + ["@scure/base", "npm:1.2.6"],\ + ["@scure/bip39", "npm:1.6.0"]\ ],\ "linkType": "HARD"\ }]\ diff --git a/.yarn/cache/@noble-hashes-npm-2.0.1-3deaaa8c92-e81769ce21.zip b/.yarn/cache/@noble-hashes-npm-2.0.1-3deaaa8c92-e81769ce21.zip deleted file mode 100644 index 65cb332929..0000000000 --- a/.yarn/cache/@noble-hashes-npm-2.0.1-3deaaa8c92-e81769ce21.zip +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c1418d1af628db45a47de1fe940d1cc2f6cb36733e9c9b51ae5931108d6cf76b -size 685555 diff --git a/.yarn/cache/@scure-bip39-npm-1.6.0-63a27ac0b7-73a54b5566.zip b/.yarn/cache/@scure-bip39-npm-1.6.0-63a27ac0b7-73a54b5566.zip new file mode 100644 index 0000000000..f0cf40ad47 --- /dev/null +++ b/.yarn/cache/@scure-bip39-npm-1.6.0-63a27ac0b7-73a54b5566.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b8d39184f678d684942a2be88ae029bd70ff315fc0f3b15d359220b29b551c3 +size 393414 diff --git a/.yarn/cache/@scure-bip39-npm-2.0.1-715af0e367-ed8a0788bc.zip b/.yarn/cache/@scure-bip39-npm-2.0.1-715af0e367-ed8a0788bc.zip deleted file mode 100644 index b0daee687d..0000000000 --- a/.yarn/cache/@scure-bip39-npm-2.0.1-715af0e367-ed8a0788bc.zip +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4a411036cb2d0f07e6b72a92a3138d67813c96fa31f190cfb3a16d6190ecff74 -size 202966 diff --git a/packages/crypto/package.json b/packages/crypto/package.json index d7d1fc3487..d1fbbdcea7 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -45,7 +45,7 @@ "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.2", "@noble/hashes": "^1.8.0", - "@scure/bip39": "^2.0.1", + "@scure/bip39": "^1.6.0", "hash-wasm": "^4.12.0" }, "devDependencies": { diff --git a/packages/crypto/src/bip39.spec.ts b/packages/crypto/src/bip39.spec.ts index 200b3a19f8..01bed59866 100644 --- a/packages/crypto/src/bip39.spec.ts +++ b/packages/crypto/src/bip39.spec.ts @@ -20,26 +20,26 @@ describe("Bip39", () => { it("throws for invalid input", () => { // invalid input length - expect(() => Bip39.encode(fromHex(""))).toThrowError(/invalid entropy length/); - expect(() => Bip39.encode(fromHex("00"))).toThrowError(/invalid entropy length/); + expect(() => Bip39.encode(fromHex(""))).toThrowError(/got length/); + expect(() => Bip39.encode(fromHex("00"))).toThrowError(/got length/); expect(() => Bip39.encode(fromHex("000000000000000000000000000000"))).toThrowError( - /invalid entropy length/, + /expected of length .*, got length=15/, ); expect(() => Bip39.encode(fromHex("0000000000000000000000000000000000"))).toThrowError( - /invalid entropy length/, + /expected of length .*, got length=17/, ); expect(() => Bip39.encode(fromHex("0000000000000000000000000000000000000000000000"))).toThrowError( - /invalid entropy length/, + /got length/, ); expect(() => Bip39.encode(fromHex("00000000000000000000000000000000000000000000000000"))).toThrowError( - /invalid entropy length/, + /got length/, ); expect(() => Bip39.encode(fromHex("00000000000000000000000000000000000000000000000000000000000000")), - ).toThrowError(/invalid entropy length/); + ).toThrowError(/got length/); expect(() => Bip39.encode(fromHex("000000000000000000000000000000000000000000000000000000000000000000")), - ).toThrowError(/invalid entropy length/); + ).toThrowError(/got length/); }); }); diff --git a/packages/crypto/src/bip39.ts b/packages/crypto/src/bip39.ts index 4fd637817e..1cf21613fd 100644 --- a/packages/crypto/src/bip39.ts +++ b/packages/crypto/src/bip39.ts @@ -1,5 +1,5 @@ -import { entropyToMnemonic, mnemonicToEntropy, mnemonicToSeedWebcrypto } from "@scure/bip39"; -import { wordlist } from "@scure/bip39/wordlists/english.js"; +import { entropyToMnemonic, mnemonicToEntropy, mnemonicToSeed } from "@scure/bip39"; +import { wordlist } from "@scure/bip39/wordlists/english"; export class EnglishMnemonic { public static readonly wordlist: readonly string[] = wordlist; @@ -43,6 +43,6 @@ export class Bip39 { } public static async mnemonicToSeed(mnemonic: EnglishMnemonic, password?: string): Promise { - return await mnemonicToSeedWebcrypto(mnemonic.toString(), password); + return await mnemonicToSeed(mnemonic.toString(), password); } } diff --git a/yarn.lock b/yarn.lock index 90752b2f7b..78e6423e46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -325,7 +325,7 @@ __metadata: "@noble/ciphers": "npm:^1.3.0" "@noble/curves": "npm:^1.9.2" "@noble/hashes": "npm:^1.8.0" - "@scure/bip39": "npm:^2.0.1" + "@scure/bip39": "npm:^1.6.0" "@types/jasmine": "npm:^4" "@types/karma-firefox-launcher": "npm:^2" "@types/karma-jasmine": "npm:^4" @@ -1186,20 +1186,13 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.8.0, @noble/hashes@npm:^1.7.1, @noble/hashes@npm:^1.8.0": +"@noble/hashes@npm:1.8.0, @noble/hashes@npm:^1.7.1, @noble/hashes@npm:^1.8.0, @noble/hashes@npm:~1.8.0": version: 1.8.0 resolution: "@noble/hashes@npm:1.8.0" checksum: 10c0/06a0b52c81a6fa7f04d67762e08b2c476a00285858150caeaaff4037356dd5e119f45b2a530f638b77a5eeca013168ec1b655db41bae3236cb2e9d511484fc77 languageName: node linkType: hard -"@noble/hashes@npm:2.0.1": - version: 2.0.1 - resolution: "@noble/hashes@npm:2.0.1" - checksum: 10c0/e81769ce21c3b1c80141a3b99bd001f17edea09879aa936692ae39525477386d696101cd573928a304806efb2b9fa751e1dd83241c67d0c84d30091e85c79bdb - languageName: node - linkType: hard - "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -1330,27 +1323,27 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:2.0.0, @scure/base@npm:^2.0.0": - version: 2.0.0 - resolution: "@scure/base@npm:2.0.0" - checksum: 10c0/7d999c7bebf053bb49cb706fdc6c5366737cff0f7f7518f52d32d7f7ad7b898904f03673648a2af5c4f22396f5c05f1d8bddbf010d6595052d07ba8163d506ad - languageName: node - linkType: hard - -"@scure/base@npm:^1.2.4": +"@scure/base@npm:^1.2.4, @scure/base@npm:~1.2.5": version: 1.2.6 resolution: "@scure/base@npm:1.2.6" checksum: 10c0/49bd5293371c4e062cb6ba689c8fe3ea3981b7bb9c000400dc4eafa29f56814cdcdd27c04311c2fec34de26bc373c593a1d6ca6d754398a488d587943b7c128a languageName: node linkType: hard -"@scure/bip39@npm:^2.0.1": - version: 2.0.1 - resolution: "@scure/bip39@npm:2.0.1" +"@scure/base@npm:^2.0.0": + version: 2.0.0 + resolution: "@scure/base@npm:2.0.0" + checksum: 10c0/7d999c7bebf053bb49cb706fdc6c5366737cff0f7f7518f52d32d7f7ad7b898904f03673648a2af5c4f22396f5c05f1d8bddbf010d6595052d07ba8163d506ad + languageName: node + linkType: hard + +"@scure/bip39@npm:^1.6.0": + version: 1.6.0 + resolution: "@scure/bip39@npm:1.6.0" dependencies: - "@noble/hashes": "npm:2.0.1" - "@scure/base": "npm:2.0.0" - checksum: 10c0/ed8a0788bca006a6e4a647ed67c4c973b1deeaee5d62ddc168c9521c33e3a66cf5707c8aadcd0b6f9e3e41c3f763a985d913f4abc3813963497238e73ce166b6 + "@noble/hashes": "npm:~1.8.0" + "@scure/base": "npm:~1.2.5" + checksum: 10c0/73a54b5566a50a3f8348a5cfd74d2092efeefc485efbed83d7a7374ffd9a75defddf446e8e5ea0385e4adb49a94b8ae83c5bad3e16333af400e932f7da3aaff8 languageName: node linkType: hard