]> git.codecow.com Git - libnemo.git/commitdiff
Replace remaining usage of Entropy with plain crypto calls.
authorChris Duncan <chris@zoso.dev>
Fri, 8 Aug 2025 03:26:00 +0000 (20:26 -0700)
committerChris Duncan <chris@zoso.dev>
Fri, 8 Aug 2025 03:26:00 +0000 (20:26 -0700)
README.md
src/lib/bip39.ts
src/lib/safe.ts

index 126fa525f5175f157399807dc7c9928d17c25620..8b574c7f181e5e9ee6cb449f44cec180164b520c 100644 (file)
--- a/README.md
+++ b/README.md
@@ -291,11 +291,11 @@ import { Tools } from 'libnemo'
 
 const address = 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d'
 const privateKey = '3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143'
-const randomData = new Entropy().hex
+const randomData = crypto.getRandomValues(new Uint8Array(32))
 
-const signature = await Tools.sign(privateKey, randomData)
+const signature = await Tools.sign(privateKey, ...randomData)
 const publicKey = new Account(address).publicKey
-const isValid = await Tools.verify(publicKey, signature, randomData)
+const isValid = await Tools.verify(publicKey, signature, ...randomData)
 ```
 
 #### Validate a Nano account address
index a4002fd9ab468eb08897d1e85172d93713ba11b7..e2a5485fa9c9ba2251e6ab880fb6412906c83ac4 100644 (file)
@@ -3,7 +3,6 @@
 \r
 import { BIP39_ITERATIONS } from './constants'\r
 import { bytes, utf8 } from './convert'\r
-import { Entropy } from './entropy'\r
 \r
 /**\r
 * Represents a mnemonic phrase that identifies a wallet as defined by BIP-39.\r
@@ -37,16 +36,28 @@ export class Bip39 {
        * the limit of 128-256 bits defined in BIP-39. Typically, wallets use the\r
        * maximum entropy allowed.\r
        *\r
-       * @param {(string|ArrayBuffer|Uint8Array<ArrayBuffer>)} entropy - Cryptographically secure random value\r
+       * @param {(ArrayBuffer|Uint8Array<ArrayBuffer>)} entropy - Cryptographically secure random value\r
        * @returns {string} Mnemonic phrase created using the BIP-39 wordlist\r
        */\r
-       static async fromEntropy (entropy: string | ArrayBuffer | Uint8Array<ArrayBuffer>): Promise<Bip39> {\r
-               const e = new Entropy(entropy)\r
-               const phraseLength = 0.75 * e.byteLength\r
-               const checksum = await this.#checksum(e.bytes)\r
+       static async fromEntropy (entropy: ArrayBuffer | Uint8Array<ArrayBuffer>): Promise<Bip39> {\r
+               if (entropy instanceof ArrayBuffer) {\r
+                       entropy = new Uint8Array(entropy)\r
+               }\r
+               if (!(entropy instanceof Uint8Array)) {\r
+                       throw new TypeError('Invalid entropy')\r
+               }\r
+               if (entropy.byteLength < 16 || entropy.byteLength > 32) {\r
+                       throw new RangeError(`Entropy must be 16-32 bytes`)\r
+               }\r
+               if (entropy.byteLength % 4 !== 0) {\r
+                       throw new RangeError(`Entropy must be a multiple of 4 bytes`)\r
+               }\r
+\r
+               const phraseLength = 0.75 * entropy.byteLength\r
+               const checksum = await this.#checksum(entropy)\r
+               const bigEntropy = entropy.reduce((a, b) => a = (a << 8n) | BigInt(b), 0n)\r
 \r
-               let concatenation: bigint = (e.bigint << BigInt(e.byteLength) / 4n) | checksum\r
-               e.destroy()\r
+               let concatenation: bigint = (bigEntropy << BigInt(entropy.byteLength) / 4n) | checksum\r
                const words: string[] = []\r
                for (let i = 0; i < phraseLength; i++) {\r
                        const wordBits = concatenation & 2047n\r
index e1f7c437f3e0572503d82def1dab8b43ef5a901d..55c8b29769c79a1d602a84e99cdc8d427176e7f2 100644 (file)
@@ -9,7 +9,6 @@ import { Bip44 } from './bip44'
 import { Blake2b } from './blake2b'
 import { default as Constants, BIP44_COIN_NANO } from './constants'
 import { default as Convert, bytes, hex, utf8 } from './convert'
-import { Entropy } from './entropy'
 import { NanoNaCl } from './nano-nacl'
 import { NamedData } from '#types'
 
@@ -20,7 +19,7 @@ export class Safe {
        static #locked: boolean = true
        static #type?: 'BIP-44' | 'BLAKE2b'
        static #seed?: ArrayBuffer
-       static #mnemonic?: Bip39
+       static #mnemonic?: string
        static #parentPort?: any
        static {
                NODE: this.#parentPort = parentPort
@@ -111,13 +110,13 @@ export class Safe {
        */
        static async create (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
                try {
-                       const entropy = new Entropy()
-                       const { phrase: mnemonicPhrase } = await Bip39.fromEntropy(entropy.bytes)
+                       const entropy = crypto.getRandomValues(new Uint8Array(32))
+                       const { phrase: mnemonicPhrase } = await Bip39.fromEntropy(entropy)
                        const record = await this.import(type, key, keySalt, mnemonicPhrase, mnemonicSalt)
-                       if (this.#seed == null || this.#mnemonic?.phrase == null) {
+                       if (this.#seed == null || this.#mnemonic == null) {
                                throw new Error('Failed to generate seed and mnemonic')
                        }
-                       return { ...record, seed: this.#seed.slice(), mnemonic: utf8.toBuffer(this.#mnemonic.phrase) }
+                       return { ...record, seed: this.#seed.slice(), mnemonic: utf8.toBuffer(this.#mnemonic) }
                } catch (err) {
                        throw new Error('Failed to create wallet', { cause: err })
                }
@@ -190,13 +189,14 @@ export class Safe {
                        if (secret instanceof ArrayBuffer) {
                                this.#seed = secret
                                if (type === 'BLAKE2b') {
-                                       this.#mnemonic = await Bip39.fromEntropy(new Uint8Array(secret))
+                                       this.#mnemonic = (await Bip39.fromEntropy(new Uint8Array(secret))).phrase
                                }
                        } else {
-                               this.#mnemonic = await Bip39.fromPhrase(secret)
+                               const mnemonic = await Bip39.fromPhrase(secret)
+                               this.#mnemonic = mnemonic.phrase
                                this.#seed = type === 'BIP-44'
-                                       ? (await this.#mnemonic.toBip39Seed(mnemonicSalt ?? '')).buffer
-                                       : (await this.#mnemonic.toBlake2bSeed()).buffer
+                                       ? (await mnemonic.toBip39Seed(mnemonicSalt ?? '')).buffer
+                                       : (await mnemonic.toBlake2bSeed()).buffer
                        }
                        const { iv, encrypted } = await this.#encryptWallet(key)
                        return { iv, salt: keySalt, encrypted }
@@ -261,7 +261,7 @@ export class Safe {
                                throw new TypeError('Invalid seed')
                        }
                        this.#seed = seed
-                       if (mnemonic != null) this.#mnemonic = await Bip39.fromPhrase(mnemonic)
+                       if (mnemonic != null) this.#mnemonic = (await Bip39.fromPhrase(mnemonic)).phrase
                        this.#locked = false
                        return { isUnlocked: !this.#locked }
                } catch (err) {
@@ -326,7 +326,7 @@ export class Safe {
                                }
                        }
                        if (mnemonicPhrase != null) {
-                               if (mnemonicPhrase === this.#mnemonic?.phrase) {
+                               if (mnemonicPhrase === this.#mnemonic) {
                                        isVerified = true
                                }
                        }
@@ -388,8 +388,8 @@ export class Safe {
                const data: NamedData<string> = {
                        seed: bytes.toHex(new Uint8Array(this.#seed))
                }
-               if (this.#mnemonic?.phrase != null) data.mnemonic = this.#mnemonic.phrase
-               const iv = new Entropy().buffer
+               if (this.#mnemonic != null) data.mnemonic = this.#mnemonic
+               const iv = crypto.getRandomValues(new Uint8Array(32)).buffer
                const encoded = utf8.toBytes(JSON.stringify(data))
                const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded)
                return { iv, encrypted }
@@ -436,7 +436,7 @@ export class Safe {
                }
                const iv: ArrayBuffer = action === 'unlock' && messageData.iv instanceof ArrayBuffer
                        ? messageData.iv
-                       : new Entropy().buffer
+                       : crypto.getRandomValues(new Uint8Array(32)).buffer
 
                // Salt for decryption key to unlock
                if (action === 'unlock' && !(messageData.keySalt instanceof ArrayBuffer)) {
@@ -444,7 +444,7 @@ export class Safe {
                }
                const keySalt: ArrayBuffer = action === 'unlock' && messageData.keySalt instanceof ArrayBuffer
                        ? messageData.keySalt
-                       : new Entropy().buffer
+                       : crypto.getRandomValues(new Uint8Array(32)).buffer
 
                // CryptoKey from password, decryption key if unlocking else encryption key
                const key = password instanceof ArrayBuffer
@@ -533,7 +533,6 @@ export default `
        const Bip39 = ${Bip39}
        const Bip44 = ${Bip44}
        const Blake2b = ${Blake2b}
-       const Entropy = ${Entropy}
        const NanoNaCl = ${NanoNaCl}
        const Safe = ${Safe}
 `