From 64d60c7083975dad055fcd3cb9096fea9d2a5870 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 26 Jan 2022 23:52:28 +0100 Subject: [PATCH] Re-implement bip39 --- .pnp.cjs | 41 +- ...node-npm-11.11.6-40abad0842-075f1c011c.zip | 3 - ...pbkdf2-npm-3.1.0-9fa74ff7fb-d15024b195.zip | 3 + .../bip39-npm-3.0.4-7c69c9182f-79ce1600a0.zip | 3 - NOTICE | 3 + packages/crypto/package.json | 5 +- packages/crypto/src/bip39.spec.ts | 284 ++- packages/crypto/src/bip39.ts | 2215 ++++++++++++++++- packages/crypto/src/englishmnemonic.spec.ts | 242 -- packages/crypto/src/englishmnemonic.ts | 39 - packages/crypto/src/index.ts | 3 +- yarn.lock | 35 +- 12 files changed, 2523 insertions(+), 353 deletions(-) delete mode 100644 .yarn/cache/@types-node-npm-11.11.6-40abad0842-075f1c011c.zip create mode 100644 .yarn/cache/@types-pbkdf2-npm-3.1.0-9fa74ff7fb-d15024b195.zip delete mode 100644 .yarn/cache/bip39-npm-3.0.4-7c69c9182f-79ce1600a0.zip delete mode 100644 packages/crypto/src/englishmnemonic.spec.ts delete mode 100644 packages/crypto/src/englishmnemonic.ts diff --git a/.pnp.cjs b/.pnp.cjs index 55a0c6b86c..adf0899eeb 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -573,6 +573,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "@types/pako", "npm:1.0.1" ], + [ + "@types/pbkdf2", + "npm:3.1.0" + ], [ "@types/qs", "npm:6.9.6" @@ -841,10 +845,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "bindings", "npm:1.5.0" ], - [ - "bip39", - "npm:3.0.4" - ], [ "bl", "npm:4.1.0" @@ -3357,9 +3357,9 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@types/karma-jasmine-html-reporter", "npm:1.5.1"], ["@types/libsodium-wrappers", "npm:0.7.9"], ["@types/node", "npm:15.3.1"], + ["@types/pbkdf2", "npm:3.1.0"], ["@typescript-eslint/eslint-plugin", "virtual:4f1584ad4aba8733a24be7c8aebbffafef25607f2d00f4b314cf96717145c692763628a31c2b85d4686fbb091ff21ebffa3cc337399c042c19a32b9bdb786464#npm:4.28.4"], ["@typescript-eslint/parser", "virtual:4f1584ad4aba8733a24be7c8aebbffafef25607f2d00f4b314cf96717145c692763628a31c2b85d4686fbb091ff21ebffa3cc337399c042c19a32b9bdb786464#npm:4.28.4"], - ["bip39", "npm:3.0.4"], ["bn.js", "npm:5.2.0"], ["buffer", "npm:6.0.3"], ["elliptic", "npm:6.5.4"], @@ -3381,6 +3381,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["karma-jasmine-html-reporter", "virtual:4f1584ad4aba8733a24be7c8aebbffafef25607f2d00f4b314cf96717145c692763628a31c2b85d4686fbb091ff21ebffa3cc337399c042c19a32b9bdb786464#npm:1.6.0"], ["libsodium-wrappers", "npm:0.7.9"], ["nyc", "npm:15.1.0"], + ["pbkdf2", "npm:3.1.2"], ["prettier", "npm:2.4.1"], ["ses", "npm:0.11.1"], ["source-map-support", "npm:0.5.19"], @@ -4716,13 +4717,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }] ]], ["@types/node", [ - ["npm:11.11.6", { - "packageLocation": "./.yarn/cache/@types-node-npm-11.11.6-40abad0842-075f1c011c.zip/node_modules/@types/node/", - "packageDependencies": [ - ["@types/node", "npm:11.11.6"] - ], - "linkType": "HARD", - }], ["npm:13.13.52", { "packageLocation": "./.yarn/cache/@types-node-npm-13.13.52-95159539bb-8f1afff497.zip/node_modules/@types/node/", "packageDependencies": [ @@ -4764,6 +4758,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD", }] ]], + ["@types/pbkdf2", [ + ["npm:3.1.0", { + "packageLocation": "./.yarn/cache/@types-pbkdf2-npm-3.1.0-9fa74ff7fb-d15024b195.zip/node_modules/@types/pbkdf2/", + "packageDependencies": [ + ["@types/pbkdf2", "npm:3.1.0"], + ["@types/node", "npm:15.3.1"] + ], + "linkType": "HARD", + }] + ]], ["@types/qs", [ ["npm:6.9.6", { "packageLocation": "./.yarn/cache/@types-qs-npm-6.9.6-2fc5ce36d4-01871b1cf7.zip/node_modules/@types/qs/", @@ -6287,19 +6291,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD", }] ]], - ["bip39", [ - ["npm:3.0.4", { - "packageLocation": "./.yarn/cache/bip39-npm-3.0.4-7c69c9182f-79ce1600a0.zip/node_modules/bip39/", - "packageDependencies": [ - ["bip39", "npm:3.0.4"], - ["@types/node", "npm:11.11.6"], - ["create-hash", "npm:1.2.0"], - ["pbkdf2", "npm:3.1.2"], - ["randombytes", "npm:2.1.0"] - ], - "linkType": "HARD", - }] - ]], ["bl", [ ["npm:4.1.0", { "packageLocation": "./.yarn/cache/bl-npm-4.1.0-7f94cdcf3f-9e8521fa7e.zip/node_modules/bl/", diff --git a/.yarn/cache/@types-node-npm-11.11.6-40abad0842-075f1c011c.zip b/.yarn/cache/@types-node-npm-11.11.6-40abad0842-075f1c011c.zip deleted file mode 100644 index 20d411df59..0000000000 --- a/.yarn/cache/@types-node-npm-11.11.6-40abad0842-075f1c011c.zip +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:954ce971817fd8cedea69bf3799493b74563b2c846ad37d41c9468e877bd625a -size 99178 diff --git a/.yarn/cache/@types-pbkdf2-npm-3.1.0-9fa74ff7fb-d15024b195.zip b/.yarn/cache/@types-pbkdf2-npm-3.1.0-9fa74ff7fb-d15024b195.zip new file mode 100644 index 0000000000..0cfbd38445 --- /dev/null +++ b/.yarn/cache/@types-pbkdf2-npm-3.1.0-9fa74ff7fb-d15024b195.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bd7613f059879f62a9615d57f5acca76792006b7fc9eac1dd654c966ef9cb08 +size 2645 diff --git a/.yarn/cache/bip39-npm-3.0.4-7c69c9182f-79ce1600a0.zip b/.yarn/cache/bip39-npm-3.0.4-7c69c9182f-79ce1600a0.zip deleted file mode 100644 index b2e09997de..0000000000 --- a/.yarn/cache/bip39-npm-3.0.4-7c69c9182f-79ce1600a0.zip +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4effdee56187e2170492c8aef4184e8bcab1b81cd477a358c75903550e6d5172 -size 82326 diff --git a/NOTICE b/NOTICE index e41b761d6d..ac0c150d2a 100644 --- a/NOTICE +++ b/NOTICE @@ -23,6 +23,9 @@ The code in packages/json-rpc was forked from https://github.com/iov-one/iov-cor The code in packages/socket and scripts/socketserver was forked from the folders packages/iov-socket and scripts/socketserver respectively of https://github.com/iov-one/iov-core at tag v2.5.0 on 2020-06-24. +The code in packages/crypto/src/bip39.ts contains code forked from bitcoinjs/bip39 (https://github.com/bitcoinjs/bip39/blob/v3.0.4/LICENSE) +on 2021-12-16. Copyright (c) 2014, Wei Lu and Daniel Cousens. + Copyright 2018-2020 IOV SAS Copyright 2020-2021 Confio OÜ Copyright 2020 Simon Warta diff --git a/packages/crypto/package.json b/packages/crypto/package.json index c1719c3394..9e7140a574 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -45,10 +45,10 @@ "@cosmjs/math": "workspace:packages/math", "@cosmjs/utils": "workspace:packages/utils", "@noble/hashes": "^1", - "bip39": "^3.0.2", "bn.js": "^5.2.0", "elliptic": "^6.5.3", - "libsodium-wrappers": "^0.7.6" + "libsodium-wrappers": "^0.7.6", + "pbkdf2": "^3.1.2" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", @@ -61,6 +61,7 @@ "@types/karma-jasmine-html-reporter": "^1", "@types/libsodium-wrappers": "^0.7.7", "@types/node": "^15.0.1", + "@types/pbkdf2": "^3.1.0", "@typescript-eslint/eslint-plugin": "^4.28", "@typescript-eslint/parser": "^4.28", "buffer": "^6.0.3", diff --git a/packages/crypto/src/bip39.spec.ts b/packages/crypto/src/bip39.spec.ts index d77298eef7..81b1b71281 100644 --- a/packages/crypto/src/bip39.spec.ts +++ b/packages/crypto/src/bip39.spec.ts @@ -1,8 +1,49 @@ -import { fromHex } from "@cosmjs/encoding"; +import { fromAscii, fromBase64, fromHex } from "@cosmjs/encoding"; -import { Bip39 } from "./bip39"; -import { EnglishMnemonic } from "./englishmnemonic"; +import { Bip39, EnglishMnemonic, entropyToMnemonic, mnemonicToEntropy } 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)).toString()).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", () => { @@ -414,3 +455,240 @@ describe("Bip39", () => { }); }); }); + +describe("EnglishMnemonic", () => { + describe("wordlist", () => { + it("matches the words from the bitcoin/bips/bip-0039 spec", () => { + const lineFeed = 0x0a; + const bip39EnglishTxt = fromBase64(wordlists.english); + + // Ensure we loaded the correct english.txt from https://github.com/bitcoin/bips/tree/master/bip-0039 + const checksum = sha256(bip39EnglishTxt); + expect(checksum).toEqual(fromHex("2f5eed53a4727b4bf8880d8f3f199efc90e58503646d9ff8eff3a2ed3b24dbda")); + + const wordsFromSpec: string[] = []; + + let start = 0; // the start cursor marks the first byte of the word + let end = 0; // the end cursor marks the line feed byte + while (end < bip39EnglishTxt.length - 1) { + end = start; + while (bip39EnglishTxt[end] !== lineFeed) end++; + const slice = bip39EnglishTxt.slice(start, end); + wordsFromSpec.push(fromAscii(slice)); + start = end + 1; + } + + expect(EnglishMnemonic.wordlist).toEqual(wordsFromSpec); + }); + }); + + it("works for valid inputs", () => { + expect(() => { + new EnglishMnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + ); + new EnglishMnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon address", + ); + new EnglishMnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent", + ); + new EnglishMnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon admit", + ); + 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 art", + ); + }).not.toThrow(); + }); + + it("rejects invalid whitespacing", () => { + // extra space (leading, middle, trailing) + expect( + () => + new EnglishMnemonic( + " abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + ), + ).toThrowError(/invalid mnemonic format/i); + expect( + () => + new EnglishMnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + ), + ).toThrowError(/invalid mnemonic format/i); + expect( + () => + new EnglishMnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about ", + ), + ).toThrowError(/invalid mnemonic format/i); + + // newline, tab + expect( + () => + new EnglishMnemonic( + "abandon\nabandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + ), + ).toThrowError(/invalid mnemonic format/i); + expect( + () => + new EnglishMnemonic( + "abandon\tabandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + ), + ).toThrowError(/invalid mnemonic format/i); + }); + + it("rejects disallowed letters", () => { + // Disallowed letters in words (capital, number, special char) + expect( + () => + new EnglishMnemonic( + "Abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + ), + ).toThrowError(/invalid mnemonic format/i); + expect( + () => + new EnglishMnemonic( + "abandon abandon Abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + ), + ).toThrowError(/invalid mnemonic format/i); + expect( + () => + new EnglishMnemonic( + "route66 abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + ), + ).toThrowError(/invalid mnemonic format/i); + expect( + () => + new EnglishMnemonic( + "abandon abandon route66 abandon abandon abandon abandon abandon abandon abandon abandon about", + ), + ).toThrowError(/invalid mnemonic format/i); + expect( + () => + new EnglishMnemonic( + "lötkolben abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + ), + ).toThrowError(/invalid mnemonic format/i); + expect( + () => + new EnglishMnemonic( + "abandon abandon lötkolben abandon abandon abandon abandon abandon abandon abandon abandon about", + ), + ).toThrowError(/invalid mnemonic format/i); + }); + + it("word counts other than 12, 15, 18, 21, 24", () => { + // too few and too many words (11, 13, 17, 19, 23, 25) + expect( + () => + new EnglishMnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + ), + ).toThrowError(/invalid word count(.*)got: 11/i); + expect( + () => + new EnglishMnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + ), + ).toThrowError(/invalid word count(.*)got: 13/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); + 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); + 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); + 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); + }); + + 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, + ); + expect( + () => new EnglishMnemonic("zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo"), + ).toThrowError(/invalid mnemonic 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); + 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); + 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); + }); + + it("rejects valid mnemonics of other languages", () => { + // valid Spanish and Italian bip39 mnemonics + expect( + () => + new EnglishMnemonic( + "humo odio oriente colina taco fingir salto geranio glaciar academia suave vigor", + ), + ).toThrowError(/contains invalid word/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); + 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); + expect( + () => + new EnglishMnemonic( + "braccio trincea armonia emiro svedese lepre stridulo metallo baldo rasente potassio rilassato", + ), + ).toThrowError(/contains invalid word/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); + 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); + }); + + describe("toString", () => { + it("works", () => { + const original = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + const mnemonic = new EnglishMnemonic(original); + expect(mnemonic.toString()).toEqual(original); + }); + }); +}); diff --git a/packages/crypto/src/bip39.ts b/packages/crypto/src/bip39.ts index 634605b081..4b5f6d1275 100644 --- a/packages/crypto/src/bip39.ts +++ b/packages/crypto/src/bip39.ts @@ -1,7 +1,2182 @@ -import { fromHex, toHex } from "@cosmjs/encoding"; -import * as bip39 from "bip39"; +import { toUtf8 } from "@cosmjs/encoding"; +import { pbkdf2 } from "pbkdf2"; -import { EnglishMnemonic } from "./englishmnemonic"; +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 invalidNumberOfWorks = "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(invalidNumberOfWorks); + } + + // 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; +} + +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); + + this.data = mnemonic; + } + + public toString(): string { + return this.data; + } +} export class Bip39 { /** @@ -20,20 +2195,36 @@ export class Bip39 { * @param entropy The entropy to be encoded. This must be cryptographically secure. */ public static encode(entropy: Uint8Array): EnglishMnemonic { - const allowedEntropyLengths: readonly number[] = [16, 20, 24, 28, 32]; - - if (allowedEntropyLengths.indexOf(entropy.length) === -1) { - throw new Error("invalid input length"); - } - - return new EnglishMnemonic(bip39.entropyToMnemonic(toHex(entropy))); + return new EnglishMnemonic(entropyToMnemonic(entropy)); } public static decode(mnemonic: EnglishMnemonic): Uint8Array { - return fromHex(bip39.mnemonicToEntropy(mnemonic.toString())); + return mnemonicToEntropy(mnemonic.toString()); } public static async mnemonicToSeed(mnemonic: EnglishMnemonic, password?: string): Promise { - return new Uint8Array(await bip39.mnemonicToSeed(mnemonic.toString(), password)); + const mnemonicBytes = toUtf8(normalize(mnemonic.toString())); + const salt = "mnemonic" + (password ? normalize(password) : ""); + const saltBytes = toUtf8(salt); + return this.pbkdf2(mnemonicBytes, saltBytes, 2048, 64, "sha512"); + } + + // convert pbkdf2's callback interface to Promise interface + private static async pbkdf2( + secret: Uint8Array, + salt: Uint8Array, + iterations: number, + keylen: number, + digest: string, + ): Promise { + return new Promise((resolve, reject) => { + pbkdf2(secret, salt, iterations, keylen, digest, (err, derivedKey) => { + if (err) { + reject(err); + } else { + resolve(new Uint8Array(derivedKey)); + } + }); + }); } } diff --git a/packages/crypto/src/englishmnemonic.spec.ts b/packages/crypto/src/englishmnemonic.spec.ts deleted file mode 100644 index ec377d1b38..0000000000 --- a/packages/crypto/src/englishmnemonic.spec.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { fromAscii, fromBase64, fromHex } from "@cosmjs/encoding"; - -import { EnglishMnemonic } from "./englishmnemonic"; -import { sha256 } from "./sha"; -import wordlists from "./testdata/bip39_wordlists.json"; - -describe("EnglishMnemonic", () => { - describe("wordlist", () => { - it("matches the words from the bitcoin/bips/bip-0039 spec", () => { - const lineFeed = 0x0a; - const bip39EnglishTxt = fromBase64(wordlists.english); - - // Ensure we loaded the correct english.txt from https://github.com/bitcoin/bips/tree/master/bip-0039 - const checksum = sha256(bip39EnglishTxt); - expect(checksum).toEqual(fromHex("2f5eed53a4727b4bf8880d8f3f199efc90e58503646d9ff8eff3a2ed3b24dbda")); - - const wordsFromSpec: string[] = []; - - let start = 0; // the start cursor marks the first byte of the word - let end = 0; // the end cursor marks the line feed byte - while (end < bip39EnglishTxt.length - 1) { - end = start; - while (bip39EnglishTxt[end] !== lineFeed) end++; - const slice = bip39EnglishTxt.slice(start, end); - wordsFromSpec.push(fromAscii(slice)); - start = end + 1; - } - - expect(EnglishMnemonic.wordlist).toEqual(wordsFromSpec); - }); - }); - - it("works for valid inputs", () => { - expect(() => { - new EnglishMnemonic( - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - ); - new EnglishMnemonic( - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon address", - ); - new EnglishMnemonic( - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent", - ); - new EnglishMnemonic( - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon admit", - ); - 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 art", - ); - }).not.toThrow(); - }); - - it("rejects invalid whitespacing", () => { - // extra space (leading, middle, trailing) - expect( - () => - new EnglishMnemonic( - " abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - ), - ).toThrowError(/invalid mnemonic format/i); - expect( - () => - new EnglishMnemonic( - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - ), - ).toThrowError(/invalid mnemonic format/i); - expect( - () => - new EnglishMnemonic( - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about ", - ), - ).toThrowError(/invalid mnemonic format/i); - - // newline, tab - expect( - () => - new EnglishMnemonic( - "abandon\nabandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - ), - ).toThrowError(/invalid mnemonic format/i); - expect( - () => - new EnglishMnemonic( - "abandon\tabandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - ), - ).toThrowError(/invalid mnemonic format/i); - }); - - it("rejects disallowed letters", () => { - // Disallowed letters in words (capital, number, special char) - expect( - () => - new EnglishMnemonic( - "Abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - ), - ).toThrowError(/invalid mnemonic format/i); - expect( - () => - new EnglishMnemonic( - "abandon abandon Abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - ), - ).toThrowError(/invalid mnemonic format/i); - expect( - () => - new EnglishMnemonic( - "route66 abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - ), - ).toThrowError(/invalid mnemonic format/i); - expect( - () => - new EnglishMnemonic( - "abandon abandon route66 abandon abandon abandon abandon abandon abandon abandon abandon about", - ), - ).toThrowError(/invalid mnemonic format/i); - expect( - () => - new EnglishMnemonic( - "lötkolben abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - ), - ).toThrowError(/invalid mnemonic format/i); - expect( - () => - new EnglishMnemonic( - "abandon abandon lötkolben abandon abandon abandon abandon abandon abandon abandon abandon about", - ), - ).toThrowError(/invalid mnemonic format/i); - }); - - it("word counts other than 12, 15, 18, 21, 24", () => { - // too few and too many words (11, 13, 17, 19, 23, 25) - expect( - () => - new EnglishMnemonic( - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - ), - ).toThrowError(/invalid word count(.*)got: 11/i); - expect( - () => - new EnglishMnemonic( - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - ), - ).toThrowError(/invalid word count(.*)got: 13/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); - 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); - 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); - 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); - }); - - 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, - ); - expect( - () => new EnglishMnemonic("zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo"), - ).toThrowError(/invalid mnemonic 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); - 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); - 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); - }); - - it("rejects valid mnemonics of other languages", () => { - // valid Spanish and Italian bip39 mnemonics - expect( - () => - new EnglishMnemonic( - "humo odio oriente colina taco fingir salto geranio glaciar academia suave vigor", - ), - ).toThrowError(/contains invalid word/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); - 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); - expect( - () => - new EnglishMnemonic( - "braccio trincea armonia emiro svedese lepre stridulo metallo baldo rasente potassio rilassato", - ), - ).toThrowError(/contains invalid word/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); - 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); - }); - - describe("toString", () => { - it("works", () => { - const original = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - const mnemonic = new EnglishMnemonic(original); - expect(mnemonic.toString()).toEqual(original); - }); - }); -}); diff --git a/packages/crypto/src/englishmnemonic.ts b/packages/crypto/src/englishmnemonic.ts deleted file mode 100644 index cceb99f0cb..0000000000 --- a/packages/crypto/src/englishmnemonic.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as bip39 from "bip39"; - -export class EnglishMnemonic { - public static readonly wordlist: readonly string[] = bip39.wordlists.english; - - // 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 - bip39.mnemonicToEntropy(mnemonic); - - this.data = mnemonic; - } - - public toString(): string { - return this.data; - } -} diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts index 0f2c73edd5..73d9df616e 100644 --- a/packages/crypto/src/index.ts +++ b/packages/crypto/src/index.ts @@ -1,5 +1,4 @@ -export { Bip39 } from "./bip39"; -export { EnglishMnemonic } from "./englishmnemonic"; +export { Bip39, EnglishMnemonic } from "./bip39"; export { HashFunction } from "./hash"; export { Hmac } from "./hmac"; export { Keccak256, keccak256 } from "./keccak"; diff --git a/yarn.lock b/yarn.lock index ef16a1bf23..dbc86c3a7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -462,9 +462,9 @@ __metadata: "@types/karma-jasmine-html-reporter": ^1 "@types/libsodium-wrappers": ^0.7.7 "@types/node": ^15.0.1 + "@types/pbkdf2": ^3.1.0 "@typescript-eslint/eslint-plugin": ^4.28 "@typescript-eslint/parser": ^4.28 - bip39: ^3.0.2 bn.js: ^5.2.0 buffer: ^6.0.3 elliptic: ^6.5.3 @@ -486,6 +486,7 @@ __metadata: karma-jasmine-html-reporter: ^1.5.4 libsodium-wrappers: ^0.7.6 nyc: ^15.1.0 + pbkdf2: ^3.1.2 prettier: ^2.4.1 ses: ^0.11.0 source-map-support: ^0.5.19 @@ -1692,13 +1693,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:11.11.6": - version: 11.11.6 - resolution: "@types/node@npm:11.11.6" - checksum: 075f1c011cf568e49701419acbcb55c24906b3bb5a34d9412a3b88f228a7a78401a5ad4d3e1cd6855c99aaea5ef96e37fc86ca097e50f06da92cf822befc1fff - languageName: node - linkType: hard - "@types/node@npm:>=13.7.0": version: 15.9.0 resolution: "@types/node@npm:15.9.0" @@ -1720,6 +1714,15 @@ __metadata: languageName: node linkType: hard +"@types/pbkdf2@npm:^3.1.0": + version: 3.1.0 + resolution: "@types/pbkdf2@npm:3.1.0" + dependencies: + "@types/node": "*" + checksum: d15024b1957c21cf3b8887329d9bd8dfde754cf13a09d76ae25f1391cfc62bb8b8d7b760773c5dbaa748172fba8b3e0c3dbe962af6ccbd69b76df12a48dfba40 + languageName: node + linkType: hard + "@types/qs@npm:*": version: 6.9.6 resolution: "@types/qs@npm:6.9.6" @@ -2401,18 +2404,6 @@ __metadata: languageName: node linkType: hard -"bip39@npm:^3.0.2": - version: 3.0.4 - resolution: "bip39@npm:3.0.4" - dependencies: - "@types/node": 11.11.6 - create-hash: ^1.1.0 - pbkdf2: ^3.0.9 - randombytes: ^2.0.1 - checksum: 79ce1600a03d1ba5053bdd4e6323f9463ec340764c7e52918b6c6b9dca81221940f2d9a65656447f108f9bc2c8d9ae8df319cca83bbd1dad63f53ef2768d9bae - languageName: node - linkType: hard - "bl@npm:^4.0.3": version: 4.1.0 resolution: "bl@npm:4.1.0" @@ -5947,7 +5938,7 @@ __metadata: languageName: node linkType: hard -"pbkdf2@npm:^3.0.9": +"pbkdf2@npm:^3.1.2": version: 3.1.2 resolution: "pbkdf2@npm:3.1.2" dependencies: @@ -6210,7 +6201,7 @@ __metadata: languageName: node linkType: hard -"randombytes@npm:^2.0.1, randombytes@npm:^2.1.0": +"randombytes@npm:^2.1.0": version: 2.1.0 resolution: "randombytes@npm:2.1.0" dependencies: