@scure/bip39
Advanced tools
+0
-1
@@ -130,2 +130,1 @@ import { type TArg, type TRet } from '@noble/hashes/utils.js'; | ||
| export declare function mnemonicToSeedWebcrypto(mnemonic: string, passphrase?: string): Promise<TRet<Uint8Array>>; | ||
| //# sourceMappingURL=index.d.ts.map |
+61
-10
@@ -6,3 +6,2 @@ /*! scure-bip39 - MIT License (c) 2022 Patricio Palladino, Paul Miller (paulmillr.com) */ | ||
| import { pbkdf2 as pbkdf2web, sha512 as sha512web } from '@noble/hashes/webcrypto.js'; | ||
| import { utils as baseUtils } from '@scure/base'; | ||
| // Japanese wordlist | ||
@@ -60,2 +59,4 @@ // The canonical BIP-39 Japanese wordlist starts with あいこくしん. | ||
| } | ||
| // BIP-39 checksum is the first ENT/32 bits of SHA-256(entropy). | ||
| // Returned as a byte with checksum bits on the left and zeroes on the right. | ||
| const calcChecksum = (entropy) => { | ||
@@ -66,5 +67,5 @@ // Checksum is ent.length/4 bits long | ||
| // For example: bitsLeft=4 val=10111101 -> 10110000 | ||
| return new Uint8Array([(sha256(entropy)[0] >> bitsLeft) << bitsLeft]); | ||
| return (sha256(entropy)[0] >> bitsLeft) << bitsLeft; | ||
| }; | ||
| function getCoder(wordlist) { | ||
| function awordlist(wordlist) { | ||
| if (!Array.isArray(wordlist) || wordlist.length !== 2048 || typeof wordlist[0] !== 'string') | ||
@@ -76,6 +77,53 @@ throw new TypeError('Wordlist: expected array of 2048 strings'); | ||
| }); | ||
| // BIP-39 appends checksum bits to entropy. | ||
| // It then splits the bitstream into 11-bit indexes for a 2048-word list. | ||
| return baseUtils.chain(baseUtils.checksum(1, calcChecksum), baseUtils.radix2(11, true), baseUtils.alphabet(wordlist)); | ||
| } | ||
| // BIP-39 appends checksum bits to entropy, | ||
| // then splits the bitstream into 11-bit indexes for a 2048-word list. | ||
| function encodeWords(entropy, wordlist) { | ||
| awordlist(wordlist); | ||
| const bytes = new Uint8Array(entropy.length + 1); // entropy || checksum byte | ||
| bytes.set(entropy); | ||
| bytes[entropy.length] = calcChecksum(entropy); | ||
| const words = []; | ||
| let carry = 0; // bit accumulator, holds < 19 bits | ||
| let bits = 0; | ||
| for (const byte of bytes) { | ||
| carry = (carry << 8) | byte; | ||
| bits += 8; | ||
| if (bits >= 11) { | ||
| bits -= 11; | ||
| words.push(wordlist[(carry >>> bits) & 0x7ff]); | ||
| carry &= (1 << bits) - 1; | ||
| } | ||
| } | ||
| // Bits still in carry are the zero right bits of the checksum byte; drop them. | ||
| return words; | ||
| } | ||
| // Reverse of encodeWords: repacks 11-bit indexes into bytes and verifies checksum. | ||
| function decodeWords(words, wordlist) { | ||
| awordlist(wordlist); | ||
| const entLen = (words.length / 3) * 4; // every 3 words hold 32 entropy bits + 1 checksum bit | ||
| const bytes = new Uint8Array(entLen + 1); // entropy || checksum byte | ||
| let carry = 0; // bit accumulator, holds < 19 bits | ||
| let bits = 0; | ||
| let pos = 0; | ||
| for (const word of words) { | ||
| const index = wordlist.indexOf(word); | ||
| if (index === -1) | ||
| throw new Error('Unknown word: ' + word); | ||
| carry = (carry << 11) | index; | ||
| bits += 11; | ||
| while (bits >= 8) { | ||
| bits -= 8; | ||
| bytes[pos++] = (carry >>> bits) & 0xff; | ||
| } | ||
| carry &= (1 << bits) - 1; | ||
| } | ||
| // Left-align leftover checksum bits, matching calcChecksum output. | ||
| if (bits > 0) | ||
| bytes[pos] = carry << (8 - bits); | ||
| const entropy = bytes.subarray(0, entLen); | ||
| if (bytes[entLen] !== calcChecksum(entropy)) | ||
| throw new Error('Invalid checksum'); | ||
| return Uint8Array.from(entropy); | ||
| } | ||
| /** | ||
@@ -105,3 +153,3 @@ * Reversible: Converts mnemonic string to raw entropy in form of byte array. | ||
| const { words } = normalize(mnemonic); | ||
| const entropy = getCoder(wordlist).decode(words); | ||
| const entropy = decodeWords(words, wordlist); | ||
| aentropy(entropy); | ||
@@ -132,3 +180,3 @@ return entropy; | ||
| aentropy(entropy); | ||
| const words = getCoder(wordlist).encode(entropy); | ||
| const words = encodeWords(entropy, wordlist); | ||
| return words.join(isJapanese(wordlist) ? '\u3000' : ' '); | ||
@@ -163,3 +211,7 @@ } | ||
| // BIP-39 salts PBKDF2 with the UTF-8 NFKD string "mnemonic" + passphrase. | ||
| const psalt = (passphrase) => nfkd('mnemonic' + passphrase); | ||
| const psalt = (passphrase) => { | ||
| if (typeof passphrase !== 'string') | ||
| throw new TypeError('invalid passphrase type: ' + typeof passphrase); | ||
| return nfkd('mnemonic' + passphrase); | ||
| }; | ||
| /** | ||
@@ -231,2 +283,1 @@ * Irreversible: Uses KDF to derive 64 bytes of key data from mnemonic + optional password. | ||
| } | ||
| //# sourceMappingURL=index.js.map |
+7
-14
| { | ||
| "name": "@scure/bip39", | ||
| "version": "2.2.0", | ||
| "version": "2.3.0", | ||
| "description": "Secure, audited & minimal implementation of BIP39 mnemonic phrases", | ||
@@ -13,7 +13,6 @@ "files": [ | ||
| "dependencies": { | ||
| "@noble/hashes": "2.2.0", | ||
| "@scure/base": "2.2.0" | ||
| "@noble/hashes": "2.3.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@paulmillr/jsbt": "0.5.0", | ||
| "@paulmillr/jsbt": "0.6.5", | ||
| "prettier": "3.6.2", | ||
@@ -23,14 +22,8 @@ "typescript": "6.0.2" | ||
| "scripts": { | ||
| "benchmark": "node test/benchmark.js", | ||
| "benchmark:size": "npx bismar@0.1.3 -s", | ||
| "build": "tsc", | ||
| "build:release": "npx --no @paulmillr/jsbt esbuild test/build", | ||
| "check": "npx --no @paulmillr/jsbt check package.json", | ||
| "check:readme": "npx --no @paulmillr/jsbt readme package.json", | ||
| "check:treeshake": "npx --no @paulmillr/jsbt treeshake package.json test/build/out-treeshake", | ||
| "check:jsdoc": "npx --no @paulmillr/jsbt tsdoc package.json", | ||
| "check": "jsbt-check", | ||
| "format": "prettier --write 'src/**/*.ts' 'test/*.test.ts' 'test/scripts/*.js'", | ||
| "bench": "node test/benchmark.js", | ||
| "test": "node --experimental-strip-types --no-warnings test/index.ts", | ||
| "test:bun": "bun test/index.ts", | ||
| "test:deno": "deno --allow-env --allow-read test/index.js", | ||
| "test:node20": "cd test; npx tsc; node compiled/test/index.js", | ||
| "test": "node test/index.ts", | ||
| "fetch-wordlist": "./test/scripts/fetch-wordlist.js" | ||
@@ -37,0 +30,0 @@ }, |
+3
-10
@@ -8,4 +8,4 @@ # scure-bip39 | ||
| - 🥈 Two implementations: pure JS or friendly WebCrypto wrapper | ||
| - ➰ Only 2 audited dependencies by the same author: | ||
| [noble-hashes](https://github.com/paulmillr/noble-hashes) and [scure-base](https://github.com/paulmillr/scure-base) | ||
| - ➰ Only 1 audited dependency by the same author: | ||
| [noble-hashes](https://github.com/paulmillr/noble-hashes) | ||
| - 🪶 14KB (gzipped) with one wordlist, 79KB with all of them: much smaller than similar libraries | ||
@@ -125,15 +125,8 @@ | ||
| For this package, there are 2 dependencies; and a few dev dependencies: | ||
| For this package, there is 1 dependency; and a few dev dependencies: | ||
| - [noble-hashes](https://github.com/paulmillr/noble-hashes) provides cryptographic hashing functionality | ||
| - [scure-base](https://github.com/paulmillr/scure-base) provides low-level wordlist utilities | ||
| - jsbt is used for benchmarking / testing / build tooling and developed by the same author | ||
| - prettier, fast-check and typescript are used for code quality / test generation / ts compilation | ||
| ## Contributing & testing | ||
| - `npm install && npm run build && npm test` will build the code and run tests. | ||
| - `npm run lint` / `npm run format` will run linter / fix linter issues. | ||
| - `npm run build:release` will build single file | ||
| ## License | ||
@@ -140,0 +133,0 @@ |
+61
-14
@@ -6,3 +6,2 @@ /*! scure-bip39 - MIT License (c) 2022 Patricio Palladino, Paul Miller (paulmillr.com) */ | ||
| import { pbkdf2 as pbkdf2web, sha512 as sha512web } from '@noble/hashes/webcrypto.js'; | ||
| import { utils as baseUtils } from '@scure/base'; | ||
@@ -62,3 +61,5 @@ // Japanese wordlist | ||
| const calcChecksum = (entropy: TArg<Uint8Array>) => { | ||
| // BIP-39 checksum is the first ENT/32 bits of SHA-256(entropy). | ||
| // Returned as a byte with checksum bits on the left and zeroes on the right. | ||
| const calcChecksum = (entropy: Uint8Array) => { | ||
| // Checksum is ent.length/4 bits long | ||
@@ -68,6 +69,6 @@ const bitsLeft = 8 - entropy.length / 4; | ||
| // For example: bitsLeft=4 val=10111101 -> 10110000 | ||
| return new Uint8Array([(sha256(entropy)[0]! >> bitsLeft) << bitsLeft]); | ||
| return (sha256(entropy)[0]! >> bitsLeft) << bitsLeft; | ||
| }; | ||
| function getCoder(wordlist: string[]) { | ||
| function awordlist(wordlist: string[]) { | ||
| if (!Array.isArray(wordlist) || wordlist.length !== 2048 || typeof wordlist[0] !== 'string') | ||
@@ -78,11 +79,53 @@ throw new TypeError('Wordlist: expected array of 2048 strings'); | ||
| }); | ||
| // BIP-39 appends checksum bits to entropy. | ||
| // It then splits the bitstream into 11-bit indexes for a 2048-word list. | ||
| return baseUtils.chain( | ||
| baseUtils.checksum(1, calcChecksum), | ||
| baseUtils.radix2(11, true), | ||
| baseUtils.alphabet(wordlist) | ||
| ); | ||
| } | ||
| // BIP-39 appends checksum bits to entropy, | ||
| // then splits the bitstream into 11-bit indexes for a 2048-word list. | ||
| function encodeWords(entropy: Uint8Array, wordlist: string[]): string[] { | ||
| awordlist(wordlist); | ||
| const bytes = new Uint8Array(entropy.length + 1); // entropy || checksum byte | ||
| bytes.set(entropy); | ||
| bytes[entropy.length] = calcChecksum(entropy); | ||
| const words: string[] = []; | ||
| let carry = 0; // bit accumulator, holds < 19 bits | ||
| let bits = 0; | ||
| for (const byte of bytes) { | ||
| carry = (carry << 8) | byte; | ||
| bits += 8; | ||
| if (bits >= 11) { | ||
| bits -= 11; | ||
| words.push(wordlist[(carry >>> bits) & 0x7ff]!); | ||
| carry &= (1 << bits) - 1; | ||
| } | ||
| } | ||
| // Bits still in carry are the zero right bits of the checksum byte; drop them. | ||
| return words; | ||
| } | ||
| // Reverse of encodeWords: repacks 11-bit indexes into bytes and verifies checksum. | ||
| function decodeWords(words: string[], wordlist: string[]): Uint8Array { | ||
| awordlist(wordlist); | ||
| const entLen = (words.length / 3) * 4; // every 3 words hold 32 entropy bits + 1 checksum bit | ||
| const bytes = new Uint8Array(entLen + 1); // entropy || checksum byte | ||
| let carry = 0; // bit accumulator, holds < 19 bits | ||
| let bits = 0; | ||
| let pos = 0; | ||
| for (const word of words) { | ||
| const index = wordlist.indexOf(word); | ||
| if (index === -1) throw new Error('Unknown word: ' + word); | ||
| carry = (carry << 11) | index; | ||
| bits += 11; | ||
| while (bits >= 8) { | ||
| bits -= 8; | ||
| bytes[pos++] = (carry >>> bits) & 0xff; | ||
| } | ||
| carry &= (1 << bits) - 1; | ||
| } | ||
| // Left-align leftover checksum bits, matching calcChecksum output. | ||
| if (bits > 0) bytes[pos] = carry << (8 - bits); | ||
| const entropy = bytes.subarray(0, entLen); | ||
| if (bytes[entLen] !== calcChecksum(entropy)) throw new Error('Invalid checksum'); | ||
| return Uint8Array.from(entropy); | ||
| } | ||
| /** | ||
@@ -112,3 +155,3 @@ * Reversible: Converts mnemonic string to raw entropy in form of byte array. | ||
| const { words } = normalize(mnemonic); | ||
| const entropy = getCoder(wordlist).decode(words); | ||
| const entropy = decodeWords(words, wordlist); | ||
| aentropy(entropy); | ||
@@ -140,3 +183,3 @@ return entropy as TRet<Uint8Array>; | ||
| aentropy(entropy); | ||
| const words = getCoder(wordlist).encode(entropy); | ||
| const words = encodeWords(entropy as Uint8Array, wordlist); | ||
| return words.join(isJapanese(wordlist) ? '\u3000' : ' '); | ||
@@ -172,3 +215,7 @@ } | ||
| // BIP-39 salts PBKDF2 with the UTF-8 NFKD string "mnemonic" + passphrase. | ||
| const psalt = (passphrase: string) => nfkd('mnemonic' + passphrase); | ||
| const psalt = (passphrase: string) => { | ||
| if (typeof passphrase !== 'string') | ||
| throw new TypeError('invalid passphrase type: ' + typeof passphrase); | ||
| return nfkd('mnemonic' + passphrase); | ||
| }; | ||
@@ -175,0 +222,0 @@ /** |
| /** Czech BIP39 wordlist. */ | ||
| export declare const wordlist: string[]; | ||
| //# sourceMappingURL=czech.d.ts.map |
@@ -2050,2 +2050,1 @@ /** Czech BIP39 wordlist. */ | ||
| zvyk`.split('\n')); | ||
| //# sourceMappingURL=czech.js.map |
| /** English BIP39 wordlist. */ | ||
| export declare const wordlist: string[]; | ||
| //# sourceMappingURL=english.d.ts.map |
@@ -2050,2 +2050,1 @@ /** English BIP39 wordlist. */ | ||
| zoo`.split('\n')); | ||
| //# sourceMappingURL=english.js.map |
| /** French BIP39 wordlist. */ | ||
| export declare const wordlist: string[]; | ||
| //# sourceMappingURL=french.d.ts.map |
@@ -2050,2 +2050,1 @@ /** French BIP39 wordlist. */ | ||
| zoologie`.split('\n')); | ||
| //# sourceMappingURL=french.js.map |
| /** Italian BIP39 wordlist. */ | ||
| export declare const wordlist: string[]; | ||
| //# sourceMappingURL=italian.d.ts.map |
@@ -2050,2 +2050,1 @@ /** Italian BIP39 wordlist. */ | ||
| zuppa`.split('\n')); | ||
| //# sourceMappingURL=italian.js.map |
| /** Japanese BIP39 wordlist. */ | ||
| export declare const wordlist: string[]; | ||
| //# sourceMappingURL=japanese.d.ts.map |
@@ -2050,2 +2050,1 @@ /** Japanese BIP39 wordlist. */ | ||
| われる`.split('\n')); | ||
| //# sourceMappingURL=japanese.js.map |
| /** Korean BIP39 wordlist. */ | ||
| export declare const wordlist: string[]; | ||
| //# sourceMappingURL=korean.d.ts.map |
@@ -2050,2 +2050,1 @@ /** Korean BIP39 wordlist. */ | ||
| 힘껏`.split('\n')); | ||
| //# sourceMappingURL=korean.js.map |
| /** Portuguese BIP39 wordlist. */ | ||
| export declare const wordlist: string[]; | ||
| //# sourceMappingURL=portuguese.d.ts.map |
@@ -2050,2 +2050,1 @@ /** Portuguese BIP39 wordlist. */ | ||
| zumbido`.split('\n')); | ||
| //# sourceMappingURL=portuguese.js.map |
| /** Simplified Chinese BIP39 wordlist. */ | ||
| export declare const wordlist: string[]; | ||
| //# sourceMappingURL=simplified-chinese.d.ts.map |
@@ -2050,2 +2050,1 @@ /** Simplified Chinese BIP39 wordlist. */ | ||
| 歇`.split('\n')); | ||
| //# sourceMappingURL=simplified-chinese.js.map |
| /** Spanish BIP39 wordlist. */ | ||
| export declare const wordlist: string[]; | ||
| //# sourceMappingURL=spanish.d.ts.map |
@@ -2050,2 +2050,1 @@ /** Spanish BIP39 wordlist. */ | ||
| zurdo`.split('\n')); | ||
| //# sourceMappingURL=spanish.js.map |
| /** Traditional Chinese BIP39 wordlist. */ | ||
| export declare const wordlist: string[]; | ||
| //# sourceMappingURL=traditional-chinese.d.ts.map |
@@ -2050,2 +2050,1 @@ /** Traditional Chinese BIP39 wordlist. */ | ||
| 歇`.split('\n')); | ||
| //# sourceMappingURL=traditional-chinese.js.map |
211133
0.88%1
-50%21183
0.46%135
-4.93%+ Added
- Removed
- Removed
- Removed
Updated