]> git.codecow.com Git - libnemo.git/commitdiff
Reprivatize Vault class members and instantiate worker script with new instead of...
authorChris Duncan <chris@zoso.dev>
Sat, 30 Aug 2025 17:24:51 +0000 (10:24 -0700)
committerChris Duncan <chris@zoso.dev>
Sat, 30 Aug 2025 17:24:51 +0000 (10:24 -0700)
src/lib/vault/index.ts
src/lib/vault/vault-worker.ts

index 4d107cdc261a86386871838fff11416a3e0a4237..3a2e7b66f5e2fab662c2bbca6b1ef87c3440989a 100644 (file)
@@ -30,7 +30,7 @@ export class Vault {
                        const NanoNaCl = ${NanoNaCl}
                        const VaultTimer = ${VaultTimer}
                        const VaultWorker = ${VaultWorker}
-                       VaultWorker.init()
+                       const v = new VaultWorker()
                `
        }
        static #instances: Vault[] = []
index 48f6bf3dcbfe0ba16ba9b763e45e3f937746ee16..c1f74e4f6f47f74ccccd333538a089a59a77f931 100644 (file)
@@ -12,22 +12,22 @@ import { VaultTimer } from './vault-timer'
 * Cross-platform worker for managing wallet secrets.
 */
 export class VaultWorker {
-       static locked: boolean
-       static timeout: VaultTimer
-       static type?: 'BIP-44' | 'BLAKE2b'
-       static seed?: ArrayBuffer
-       static mnemonic?: ArrayBuffer
-       static parentPort?: any
+       #locked: boolean
+       #timeout: VaultTimer
+       #type?: 'BIP-44' | 'BLAKE2b'
+       #seed?: ArrayBuffer
+       #mnemonic?: ArrayBuffer
+       #parentPort?: any
 
-       static init (): void {
-               this.locked = true
-               this.timeout = new VaultTimer(() => { }, 0)
-               this.type = undefined
-               this.seed = undefined
-               this.mnemonic = undefined
-               NODE: this.parentPort = parentPort
+       constructor () {
+               this.#locked = true
+               this.#timeout = new VaultTimer(() => { }, 0)
+               this.#type = undefined
+               this.#seed = undefined
+               this.#mnemonic = undefined
+               NODE: this.#parentPort = parentPort
                const listener = (message: MessageEvent<any>): Promise<void> => {
-                       return this._extractData(message.data)
+                       return this.#extractData(message.data)
                                .then(extracted => {
                                        const {
                                                action,
@@ -57,7 +57,7 @@ export class VaultWorker {
                                                        break
                                                }
                                                case 'load': {
-                                                       result = this.load(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt)
+                                                       result = this.#load(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt)
                                                        break
                                                }
                                                case 'lock': {
@@ -110,23 +110,23 @@ export class VaultWorker {
                                })
                }
                BROWSER: addEventListener('message', listener)
-               NODE: this.parentPort?.on('message', listener)
+               NODE: this.#parentPort?.on('message', listener)
        }
 
        /**
        * Generates a new mnemonic and seed and then returns the initialization vector
        * vector, salt, and encrypted data representing the wallet in a locked state.
        */
-       static create (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
+       create (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
                try {
                        const entropy = crypto.getRandomValues(new Uint8Array(32))
                        return Bip39.fromEntropy(entropy)
-                               .then(bip39 => this._load(type, key, keySalt, bip39.phrase, mnemonicSalt))
+                               .then(bip39 => this.#load(type, key, keySalt, bip39.phrase, mnemonicSalt))
                                .then(({ iv, salt, encrypted }) => {
-                                       if (this.seed == null || this.mnemonic == null) {
+                                       if (this.#seed == null || this.#mnemonic == null) {
                                                throw new Error('Failed to generate seed and mnemonic')
                                        }
-                                       return { iv, salt, encrypted, seed: this.seed.slice(), mnemonic: this.mnemonic.slice() }
+                                       return { iv, salt, encrypted, seed: this.#seed.slice(), mnemonic: this.#mnemonic.slice() }
                                })
                } catch (err) {
                        console.error(err)
@@ -141,32 +141,32 @@ export class VaultWorker {
        * wallet seed at a specified index and then returns the public key. The wallet
        * must be unlocked prior to derivation.
        */
-       static derive (index?: number): Promise<NamedData<number | ArrayBuffer>> {
+       derive (index?: number): Promise<NamedData<number | ArrayBuffer>> {
                try {
-                       this.timeout.pause()
-                       if (this.locked) {
+                       this.#timeout.pause()
+                       if (this.#locked) {
                                throw new Error('Wallet is locked')
                        }
-                       if (this.seed == null) {
+                       if (this.#seed == null) {
                                throw new Error('Wallet seed not found')
                        }
-                       if (this.type !== 'BIP-44' && this.type !== 'BLAKE2b') {
+                       if (this.#type !== 'BIP-44' && this.#type !== 'BLAKE2b') {
                                throw new Error('Invalid wallet type')
                        }
                        if (typeof index !== 'number') {
                                throw new Error('Invalid wallet account index')
                        }
-                       const derive = this.type === 'BIP-44'
-                               ? Bip44.ckd(this.seed, BIP44_COIN_NANO, index)
-                               : Promise.resolve(this._deriveBlake2bPrivateKey(this.seed, index))
+                       const derive = this.#type === 'BIP-44'
+                               ? Bip44.ckd(this.#seed, BIP44_COIN_NANO, index)
+                               : Promise.resolve(this.#deriveBlake2bPrivateKey(this.#seed, index))
                        return derive.then(prv => {
                                const pub = NanoNaCl.convert(new Uint8Array(prv))
-                               this.timeout = new VaultTimer(() => this.lock(), 120000)
+                               this.#timeout = new VaultTimer(() => this.lock(), 120000)
                                return { index, publicKey: pub.buffer }
                        })
                } catch (err) {
                        console.error(err)
-                       this.timeout.resume()
+                       this.#timeout.resume()
                        throw new Error('Failed to derive account', { cause: err })
                }
        }
@@ -175,10 +175,10 @@ export class VaultWorker {
        * Encrypts an existing seed or mnemonic+salt and returns the initialization
        * vector, salt, and encrypted data representing the wallet in a locked state.
        */
-       static load (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
-               return this._load(type, key, keySalt, secret, mnemonicSalt)
+       load (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
+               return this.#load(type, key, keySalt, secret, mnemonicSalt)
                        .then(record => {
-                               if (this.seed == null) {
+                               if (this.#seed == null) {
                                        throw new Error('Wallet seed not found')
                                }
                                return record
@@ -190,25 +190,25 @@ export class VaultWorker {
                        .finally(() => this.lock())
        }
 
-       static lock (): NamedData<boolean> {
-               this.mnemonic = undefined
-               this.seed = undefined
-               this.locked = true
-               this.timeout?.pause()
-               return { isLocked: this.locked }
+       lock (): NamedData<boolean> {
+               this.#mnemonic = undefined
+               this.#seed = undefined
+               this.#locked = true
+               this.#timeout?.pause()
+               return { isLocked: this.#locked }
        }
 
        /**
        * Derives the account private key at a specified index, signs the input data,
        * and returns a signature. The wallet must be unlocked prior to verification.
        */
-       static sign (index?: number, data?: ArrayBuffer): Promise<NamedData<ArrayBuffer>> {
+       sign (index?: number, data?: ArrayBuffer): Promise<NamedData<ArrayBuffer>> {
                try {
-                       this.timeout.pause()
-                       if (this.locked) {
+                       this.#timeout.pause()
+                       if (this.#locked) {
                                throw new Error('Wallet is locked')
                        }
-                       if (this.seed == null) {
+                       if (this.#seed == null) {
                                throw new Error('Wallet seed not found')
                        }
                        if (index == null) {
@@ -217,17 +217,17 @@ export class VaultWorker {
                        if (data == null) {
                                throw new Error('Data to sign not found')
                        }
-                       const derive = this.type === 'BIP-44'
-                               ? Bip44.ckd(this.seed, BIP44_COIN_NANO, index)
-                               : Promise.resolve(this._deriveBlake2bPrivateKey(this.seed, index))
+                       const derive = this.#type === 'BIP-44'
+                               ? Bip44.ckd(this.#seed, BIP44_COIN_NANO, index)
+                               : Promise.resolve(this.#deriveBlake2bPrivateKey(this.#seed, index))
                        return derive.then(prv => {
                                const sig = NanoNaCl.detached(new Uint8Array(data), new Uint8Array(prv))
-                               this.timeout = new VaultTimer(() => this.lock(), 120000)
+                               this.#timeout = new VaultTimer(() => this.lock(), 120000)
                                return { signature: sig.buffer }
                        })
                } catch (err) {
                        console.error(err)
-                       this.timeout.resume()
+                       this.#timeout.resume()
                        throw new Error('Failed to sign message', { cause: err })
                }
        }
@@ -235,7 +235,7 @@ export class VaultWorker {
        /**
        * Decrypts the input and sets the seed and, if it is included, the mnemonic.
        */
-       static unlock (type?: string, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise<NamedData<boolean>> {
+       unlock (type?: string, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise<NamedData<boolean>> {
                if (type == null) {
                        throw new TypeError('Wallet type is required')
                }
@@ -248,22 +248,22 @@ export class VaultWorker {
                if (encrypted == null) {
                        throw new TypeError('Wallet encrypted data is required')
                }
-               this.timeout?.pause()
-               return this._decryptWallet(type, key, iv, encrypted)
+               this.#timeout?.pause()
+               return this.#decryptWallet(type, key, iv, encrypted)
                        .then(() => {
-                               if (!(this.seed instanceof ArrayBuffer)) {
+                               if (!(this.#seed instanceof ArrayBuffer)) {
                                        throw new TypeError('Invalid seed')
                                }
-                               if (this.mnemonic != null && !(this.mnemonic instanceof ArrayBuffer)) {
+                               if (this.#mnemonic != null && !(this.#mnemonic instanceof ArrayBuffer)) {
                                        throw new TypeError('Invalid mnemonic')
                                }
-                               this.locked = false
-                               this.timeout = new VaultTimer(() => this.lock(), 120000)
-                               return { isUnlocked: !this.locked }
+                               this.#locked = false
+                               this.#timeout = new VaultTimer(() => this.lock(), 120000)
+                               return { isUnlocked: !this.#locked }
                        })
                        .catch(err => {
                                console.error(err)
-                               this.timeout?.resume()
+                               this.#timeout?.resume()
                                throw new Error('Failed to unlock wallet', { cause: err })
                        })
        }
@@ -271,26 +271,26 @@ export class VaultWorker {
        /**
        * Re-encrypts the wallet with a new password.
        */
-       static update (key?: CryptoKey, keySalt?: ArrayBuffer): Promise<NamedData<ArrayBuffer>> {
+       update (key?: CryptoKey, keySalt?: ArrayBuffer): Promise<NamedData<ArrayBuffer>> {
                try {
-                       this.timeout.pause()
-                       if (this.locked) {
+                       this.#timeout.pause()
+                       if (this.#locked) {
                                throw new Error('Wallet is locked')
                        }
-                       if (this.seed == null) {
+                       if (this.#seed == null) {
                                throw new Error('Wallet seed not found')
                        }
                        if (key == null || keySalt == null) {
                                throw new TypeError('Wallet password is required')
                        }
-                       return this._encryptWallet(key)
+                       return this.#encryptWallet(key)
                                .then(({ iv, encrypted }) => {
-                                       this.timeout = new VaultTimer(() => this.lock(), 120000)
+                                       this.#timeout = new VaultTimer(() => this.lock(), 120000)
                                        return { iv, salt: keySalt, encrypted }
                                })
                } catch (err) {
                        console.error(err)
-                       this.timeout.resume()
+                       this.#timeout.resume()
                        throw new Error('Failed to update wallet password', { cause: err })
                }
        }
@@ -299,12 +299,12 @@ export class VaultWorker {
        * Checks the seed and, if it exists, the mnemonic against input. The wallet
        * must be unlocked prior to verification.
        */
-       static verify (seed?: ArrayBuffer, mnemonicPhrase?: string): NamedData<boolean> {
+       verify (seed?: ArrayBuffer, mnemonicPhrase?: string): NamedData<boolean> {
                try {
-                       if (this.locked) {
+                       if (this.#locked) {
                                throw new Error('Wallet is locked')
                        }
-                       if (this.seed == null) {
+                       if (this.#seed == null) {
                                throw new Error('Wallet seed not found')
                        }
                        if (seed == null && mnemonicPhrase == null) {
@@ -317,7 +317,7 @@ export class VaultWorker {
                        if (seed != null) {
                                let diff = 0
                                const userSeed = new Uint8Array(seed)
-                               const thisSeed = new Uint8Array(this.seed)
+                               const thisSeed = new Uint8Array(this.#seed)
                                for (let i = 0; i < userSeed.byteLength; i++) {
                                        diff |= userSeed[i] ^ thisSeed[i]
                                }
@@ -326,7 +326,7 @@ export class VaultWorker {
                        if (mnemonicPhrase != null) {
                                let diff = 0
                                const userMnemonic = utf8.toBytes(mnemonicPhrase)
-                               const thisMnemonic = new Uint8Array(this.mnemonic ?? [])
+                               const thisMnemonic = new Uint8Array(this.#mnemonic ?? [])
                                for (let i = 0; i < userMnemonic.byteLength; i++) {
                                        diff |= userMnemonic[i] ^ thisMnemonic[i]
                                }
@@ -339,7 +339,7 @@ export class VaultWorker {
                }
        }
 
-       static _createAesKey (purpose: 'encrypt' | 'decrypt', keySalt: ArrayBuffer, password?: ArrayBuffer): Promise<CryptoKey | undefined> {
+       #createAesKey (purpose: 'encrypt' | 'decrypt', keySalt: ArrayBuffer, password?: ArrayBuffer): Promise<CryptoKey | undefined> {
                if (password == null) {
                        return Promise.resolve(undefined)
                }
@@ -366,14 +366,14 @@ export class VaultWorker {
                        })
        }
 
-       static _decryptWallet (type: string, key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise<void> {
+       #decryptWallet (type: string, key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise<void> {
                const seedLength = type === 'BIP-44' ? 64 : 32
                const additionalData = utf8.toBytes(type)
                return crypto.subtle
                        .decrypt({ name: 'AES-GCM', iv, additionalData }, key, encrypted)
                        .then(decrypted => {
-                               this.seed = decrypted.slice(0, seedLength)
-                               this.mnemonic = decrypted.slice(seedLength)
+                               this.#seed = decrypted.slice(0, seedLength)
+                               this.#mnemonic = decrypted.slice(seedLength)
                                new Uint8Array(decrypted).fill(0)
                        })
        }
@@ -391,7 +391,7 @@ export class VaultWorker {
        * @param {number} index - 4-byte index of account to derive
        * @returns {ArrayBuffer} Private key for the account
        */
-       static _deriveBlake2bPrivateKey (seed: ArrayBuffer, index: number): ArrayBuffer {
+       #deriveBlake2bPrivateKey (seed: ArrayBuffer, index: number): ArrayBuffer {
                const b = new ArrayBuffer(4)
                new DataView(b).setUint32(0, index, false)
                const s = new Uint8Array(seed)
@@ -399,18 +399,18 @@ export class VaultWorker {
                return new Blake2b(32).update(s).update(i).digest().buffer
        }
 
-       static _encryptWallet (key: CryptoKey): Promise<NamedData<ArrayBuffer>> {
-               if (this.type == null) {
+       #encryptWallet (key: CryptoKey): Promise<NamedData<ArrayBuffer>> {
+               if (this.#type == null) {
                        throw new Error('Invalid wallet type')
                }
-               if (this.seed == null) {
+               if (this.#seed == null) {
                        throw new Error('Wallet seed not found')
                }
-               const seed = new Uint8Array(this.seed)
-               const mnemonic = new Uint8Array(this.mnemonic ?? [])
+               const seed = new Uint8Array(this.#seed)
+               const mnemonic = new Uint8Array(this.#mnemonic ?? [])
                // restrict iv to 96 bits per GCM best practice
                const iv = crypto.getRandomValues(new Uint8Array(12)).buffer
-               const additionalData = utf8.toBytes(this.type)
+               const additionalData = utf8.toBytes(this.#type)
                const encoded = new Uint8Array([...seed, ...mnemonic])
                return crypto.subtle
                        .encrypt({ name: 'AES-GCM', iv, additionalData }, key, encoded)
@@ -423,7 +423,7 @@ export class VaultWorker {
        /**
        * Parse inbound message from main thread into typechecked variables.
        */
-       static _extractData (message: unknown) {
+       #extractData (message: unknown) {
                try {
                        // Message itself
                        if (message == null) {
@@ -478,7 +478,7 @@ export class VaultWorker {
                                : crypto.getRandomValues(new Uint8Array(32)).buffer
 
                        // CryptoKey from password, decryption key if unlocking else encryption key
-                       return this._createAesKey(action === 'unlock' ? 'decrypt' : 'encrypt', keySalt, password)
+                       return this.#createAesKey(action === 'unlock' ? 'decrypt' : 'encrypt', keySalt, password)
                                .then(key => {
                                        if (password?.detached === false) {
                                                new Uint8Array(password).fill(0)
@@ -581,9 +581,9 @@ export class VaultWorker {
        * Encrypts an existing seed or mnemonic+salt and returns the initialization
        * vector, salt, and encrypted data representing the wallet in a locked state.
        */
-       static _load (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
+       #load (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
                try {
-                       if (!this.locked) {
+                       if (!this.#locked) {
                                throw new Error('Wallet is in use')
                        }
                        if (key == null || keySalt == null) {
@@ -611,13 +611,13 @@ export class VaultWorker {
                                        throw new RangeError('Seed for BLAKE2b wallet must be 32 bytes')
                                }
                        }
-                       this.type = type
+                       this.#type = type
                        let seed: Promise<ArrayBuffer>
                        if (secret instanceof ArrayBuffer) {
                                if (type === 'BLAKE2b') {
                                        seed = Bip39.fromEntropy(new Uint8Array(secret))
                                                .then(bip39 => {
-                                                       this.mnemonic = utf8.toBuffer(bip39.phrase ?? '')
+                                                       this.#mnemonic = utf8.toBuffer(bip39.phrase ?? '')
                                                        return secret
                                                })
                                } else {
@@ -626,7 +626,7 @@ export class VaultWorker {
                        } else {
                                seed = Bip39.fromPhrase(secret)
                                        .then(bip39 => {
-                                               this.mnemonic = utf8.toBuffer(bip39.phrase ?? '')
+                                               this.#mnemonic = utf8.toBuffer(bip39.phrase ?? '')
                                                const derive = type === 'BIP-44'
                                                        ? bip39.toBip39Seed(mnemonicSalt ?? '')
                                                        : Promise.resolve(bip39.toBlake2bSeed())
@@ -634,8 +634,8 @@ export class VaultWorker {
                                        })
                        }
                        return seed.then(seed => {
-                               this.seed = seed
-                               return this._encryptWallet(key)
+                               this.#seed = seed
+                               return this.#encryptWallet(key)
                                        .then(({ iv, encrypted }) => ({ iv, salt: keySalt, encrypted }))
                        })
                } catch (err) {