From: Chris Duncan Date: Sun, 6 Jul 2025 07:04:22 +0000 (-0700) Subject: Use bytes to store account private key instead of string. Rename private class proper... X-Git-Tag: v0.10.5~106 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=8232698313cfd7e98870f58a18e5a60d120d9498;p=libnemo.git Use bytes to store account private key instead of string. Rename private class properties to be more descriptive. --- diff --git a/src/lib/account.ts b/src/lib/account.ts index 2e6432e..a8ce5f5 100644 --- a/src/lib/account.ts +++ b/src/lib/account.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later import { Blake2b } from './blake2b' -import { ACCOUNT_KEY_LENGTH, ALPHABET, PREFIX, PREFIX_LEGACY } from './constants' +import { ACCOUNT_KEY_BYTE_LENGTH, ACCOUNT_KEY_HEX_LENGTH, ALPHABET, PREFIX, PREFIX_LEGACY } from './constants' import { base32, bytes, hex, utf8 } from './convert' import { Pool } from './pool' import { Rpc } from './rpc' @@ -17,53 +17,60 @@ import { NanoNaCl, SafeWorker } from '#workers' export class Account { static #isInternal: boolean = false static #poolSafe: Pool - #a: string + + #address: string + #locked: boolean #pub: string - #prv: string | null - #i?: number - #f?: string - #b?: bigint - #r?: bigint - #rep?: Account - #w?: bigint + #prv: Uint8Array + + #balance?: bigint + #frontier?: string + #index?: number + #receivable?: bigint + #representative?: Account + #weight?: bigint - get address () { return `${PREFIX}${this.#a}` } + get address () { return `${PREFIX}${this.#address}` } + get isLocked () { return this.#locked } + get isUnLocked () { return !this.#locked } get publicKey () { return this.#pub } - get privateKey () { return this.#prv } - get index () { return this.#i } - get frontier () { return this.#f } - get balance () { return this.#b } - get receivable () { return this.#r } - get representative () { return this.#rep } - get weight () { return this.#w } + get privateKey () { return bytes.toHex(this.#prv) } - set frontier (v) { this.#f = v } - set balance (v) { this.#b = v ? BigInt(v) : undefined } - set receivable (v) { this.#r = v ? BigInt(v) : undefined } + get balance () { return this.#balance } + get frontier () { return this.#frontier } + get index () { return this.#index } + get receivable () { return this.#receivable } + get representative () { return this.#representative } + get weight () { return this.#weight } + + set balance (v) { this.#balance = v ? BigInt(v) : undefined } + set frontier (v) { this.#frontier = v } + set receivable (v) { this.#receivable = v ? BigInt(v) : undefined } set representative (v) { if (v?.constructor === Account) { - this.#rep = v + this.#representative = v } else if (typeof v === 'string') { - this.#rep = Account.fromAddress(v) + this.#representative = Account.fromAddress(v) } else { throw new TypeError(`Invalid argument for account representative: ${v}`) } } - set weight (v) { this.#w = v ? BigInt(v) : undefined } + set weight (v) { this.#weight = v ? BigInt(v) : undefined } - constructor (address: string, publicKey: string, privateKey?: string, index?: number) { + constructor (address: string, publicKey: string, privateKey: Uint8Array, index?: number) { if (!Account.#isInternal) { throw new Error(`Account cannot be instantiated directly. Use factory methods instead.`) } if (index !== undefined && typeof index !== 'number') { throw new TypeError(`Invalid index ${index} when creating Account ${address}`) } - this.#a = address + this.#address = address .replace(PREFIX, '') .replace(PREFIX_LEGACY, '') + this.#index = index + this.#locked = false this.#pub = publicKey - this.#prv = privateKey ?? null - this.#i = index + this.#prv = privateKey Account.#poolSafe ??= new Pool(SafeWorker) Account.#isInternal = false } @@ -73,17 +80,17 @@ export class Account { * allow garbage collection. */ async destroy (): Promise { + this.#prv.fill(0) await Account.#poolSafe.assign({ method: 'destroy', name: this.#pub }) - this.#prv = null - this.#i = undefined - this.#f = undefined - this.#b = undefined - this.#r = undefined - this.#rep = undefined - this.#w = undefined + this.#index = undefined + this.#frontier = undefined + this.#balance = undefined + this.#receivable = undefined + this.#representative = undefined + this.#weight = undefined } /** @@ -97,7 +104,7 @@ export class Account { Account.#isInternal = true Account.validate(address) const publicKey = Account.#addressToKey(address) - const account = new this(address, publicKey, undefined, index) + const account = new this(address, publicKey, new Uint8Array(32), index) return account } @@ -112,27 +119,34 @@ export class Account { Account.#isInternal = true Account.#validateKey(publicKey) const address = Account.#keyToAddress(publicKey) - const account = new this(address, publicKey, undefined, index) + const account = new this(address, publicKey, new Uint8Array(32), index) return account } /** - * Instantiates an Account object from its private key. The - * corresponding public key will automatically be derived and saved. + * Instantiates an Account object from its private key. The corresponding + * public key will automatically be derived and saved. * * @param {string} privateKey - Private key of the account * @param {number} [index] - Account number used when deriving the key * @returns {Account} A new Account object */ - static fromPrivateKey (privateKey: string, index?: number): Account { + static fromPrivateKey (privateKey: string | Uint8Array, index?: number): Account { + if (typeof privateKey === 'string') privateKey = hex.toBytes(privateKey) Account.#isInternal = true Account.#validateKey(privateKey) const publicKey = NanoNaCl.convert(privateKey) - const account = Account.fromPublicKey(publicKey, index) - account.#prv = privateKey.toUpperCase() + const address = Account.#keyToAddress(publicKey) + const account = new this(address, publicKey, privateKey, index) return account } + /** + * Locks the account with a password that will be needed to unlock it later. + * + * @param {(string|Uint8Array)} password Used to lock the account + * @returns True if successfully locked + */ async lock (password: string | Uint8Array): Promise { if (typeof password === 'string') { password = utf8.toBytes(password) @@ -141,20 +155,17 @@ export class Account { throw new Error('Failed to unlock wallet') } try { - const data: { id: string, privateKey: string | null } = { + const data: { id: string, privateKey: Uint8Array } = { id: this.#pub, - privateKey: null - } - if (this.#prv != null) { - data.privateKey = this.#prv + privateKey: this.#prv } const response = (await Account.#poolSafe.assign({ - method: 'put', + method: 'set', name: this.#pub, password, data }))[0] - const success = response.result + const success = response?.result if (!success) { throw null } @@ -164,10 +175,17 @@ export class Account { } finally { password.fill(0) } - this.#prv = null + this.#prv.fill(0) + this.#locked = true return true } + /** + * Unlocks the account using the same password as used prior to lock it. + * + * @param {(string|Uint8Array)} password Used previously to lock the account + * @returns True if successfully unlocked + */ async unlock (password: string | Uint8Array): Promise { if (typeof password === 'string') { password = utf8.toBytes(password) @@ -181,19 +199,18 @@ export class Account { name: this.#pub, password }))[0] - const { id, privateKey } = response.result - if (id !== this.#pub) { + const { id, privateKey } = response?.result + if (id == null || id !== this.#pub) { throw null } - if (privateKey != null) { - this.#prv = privateKey - } + this.#prv.set(privateKey) } catch (err) { console.error(`Failed to unlock account ${this.address}`, err) return false } finally { password.fill(0) } + this.#locked = false return true } @@ -254,11 +271,11 @@ export class Account { if (frontier == null) { throw new Error('Account not found') } - this.#b = BigInt(balance) - this.#f = frontier - this.#r = BigInt(receivable) - this.#rep = Account.fromAddress(representative) - this.#w = BigInt(weight) + this.#balance = BigInt(balance) + this.#frontier = frontier + this.#receivable = BigInt(receivable) + this.#representative = Account.fromAddress(representative) + this.#weight = BigInt(weight) } static #addressToKey (v: string): string { @@ -279,18 +296,28 @@ export class Account { return `${PREFIX}${encodedPublicKey}${encodedChecksum}` } - static #validateKey (key: string): void { + static #validateKey (key: unknown): asserts key is (string | Uint8Array) { if (key === undefined) { throw new TypeError(`Key is undefined`) } - if (typeof key !== 'string') { - throw new TypeError(`Key must be a string`) + if (typeof key !== 'string' && !(key instanceof Uint8Array)) { + throw new TypeError(`Key must be a string or Uint8Array`) } - if (key.length !== ACCOUNT_KEY_LENGTH) { - throw new TypeError(`Key must be ${ACCOUNT_KEY_LENGTH} characters`) + if (typeof key === 'string') { + if (key.length !== ACCOUNT_KEY_HEX_LENGTH) { + throw new TypeError(`Key must be ${ACCOUNT_KEY_HEX_LENGTH} characters`) + } + if (!/^[A-Fa-f0-9]{64}$/i.test(key)) { + throw new RangeError(`Key is not a valid hexadecimal value`) + } } - if (!/^[0-9a-fA-F]+$/i.test(key)) { - throw new RangeError('Key is not a valid hexadecimal value') + if (key instanceof Uint8Array) { + if (key.byteLength !== ACCOUNT_KEY_BYTE_LENGTH) { + throw new TypeError(`Key must be ${ACCOUNT_KEY_BYTE_LENGTH} BYTES`) + } + if (key.every(v => v === 0)) { + throw new TypeError(`Key is not a valid byte array`) + } } } }