From: Chris Duncan Date: Wed, 16 Jul 2025 06:03:39 +0000 (-0700) Subject: Refactor account class. X-Git-Tag: v0.10.5~56^2~31 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=b996b4c8eecd44122a4c619139b5f6fa55c88907;p=libnemo.git Refactor account class. --- diff --git a/src/lib/account.ts b/src/lib/account.ts index f85699d..7a167af 100644 --- a/src/lib/account.ts +++ b/src/lib/account.ts @@ -18,9 +18,8 @@ export class Account { static #isInternal: boolean = false #address: string - #locked: boolean - #pub: string - #prv: Uint8Array + #publicKey: Uint8Array + #privateKey: Uint8Array #balance?: bigint #frontier?: string @@ -30,10 +29,10 @@ export class Account { #weight?: bigint get address () { return `${PREFIX}${this.#address}` } - get isLocked () { return this.#locked } - get isUnlocked () { return !this.#locked } - get publicKey () { return this.#pub } - get privateKey () { return bytes.toHex(this.#prv) } + get isLocked () { return this.#privateKey.buffer.detached } + get isUnlocked () { return !this.isLocked } + get publicKey () { return bytes.toHex(this.#publicKey) } + get privateKey () { return bytes.toHex(this.#privateKey) } get balance () { return this.#balance } get frontier () { return this.#frontier } @@ -56,7 +55,7 @@ export class Account { } set weight (v) { this.#weight = v ? BigInt(v) : undefined } - constructor (address: string, publicKey: string, privateKey: Uint8Array, index?: number) { + constructor (address: string, publicKey: Uint8Array, index?: number) { if (!Account.#isInternal) { throw new Error(`Account cannot be instantiated directly. Use factory methods instead.`) } @@ -67,9 +66,9 @@ export class Account { .replace(PREFIX, '') .replace(PREFIX_LEGACY, '') this.#index = index - this.#locked = false - this.#pub = publicKey - this.#prv = privateKey + this.#publicKey = publicKey + this.#privateKey = new Uint8Array(0) + bytes.erase(this.#privateKey) } /** @@ -77,10 +76,10 @@ export class Account { * allow garbage collection. */ async destroy (): Promise { - bytes.erase(this.#prv) + bytes.erase(this.#privateKey) await SafeWorker.add({ method: 'destroy', - name: this.#pub + name: this.#publicKey }) this.#index = undefined this.#frontier = undefined @@ -101,23 +100,23 @@ export class Account { this.#isInternal = true this.validate(address) const publicKey = this.#addressToKey(address) - const account = new this(address, publicKey, new Uint8Array(32), index) - return account + return new this(address, publicKey, index) } /** - * Instantiates an Account object from its public key. + * Instantiates an Account object from its public key. It is unable to sign + * blocks or messages since it has no private key. * - * @param {string} publicKey - Public key of the account + * @param {(string|Uint8Array)} publicKey - Public key of the account * @param {number} [index] - Account number used when deriving the key * @returns {Account} The instantiated Account object */ - static fromPublicKey (publicKey: string, index?: number): Account { - this.#isInternal = true + static fromPublicKey (publicKey: string | Uint8Array, index?: number): Account { this.#validateKey(publicKey) + if (typeof publicKey === 'string') publicKey = hex.toBytes(publicKey) const address = this.#keyToAddress(publicKey) - const account = new this(address, publicKey, new Uint8Array(32), index) - return account + this.#isInternal = true + return new this(address, publicKey, index) } /** @@ -129,9 +128,8 @@ export class Account { * @returns {Account} A new Account object */ static async fromPrivateKey (privateKey: string | Uint8Array, index?: number): Promise { - if (typeof privateKey === 'string') privateKey = hex.toBytes(privateKey) - this.#isInternal = true this.#validateKey(privateKey) + if (typeof privateKey === 'string') privateKey = hex.toBytes(privateKey) let publicKey: string try { const headers = { @@ -144,9 +142,13 @@ export class Account { } catch (err) { throw new Error(`Failed to derive public key from private key`, { cause: err }) } - const address = this.#keyToAddress(publicKey) - const account = new this(address, publicKey, privateKey, index) - return account + try { + const self = await this.fromPublicKey(publicKey, index) + self.#privateKey = privateKey + return self + } catch (err) { + throw new Error(`Failed to lock new Account`, { cause: err }) + } } /** @@ -156,33 +158,29 @@ export class Account { * @returns True if successfully locked */ async lock (password: string | Uint8Array): Promise { + if (this.isLocked) { + throw new Error(`Account ${this.address} is already locked`) + } if (typeof password === 'string') password = utf8.toBytes(password) if (password == null || !(password instanceof Uint8Array)) { - throw new Error('Failed to lock account') + throw new Error(`Failed to lock Account ${this.address}`) } try { const headers = { method: 'set', - name: this.#pub + name: this.publicKey } const data = { password: password.buffer, - id: hex.toBytes(this.#pub).buffer, - privateKey: this.#prv.buffer - } - const success = await SafeWorker.add(headers, data) - if (!success) { - throw null + id: new Uint8Array(this.#publicKey).buffer, + privateKey: this.#privateKey.buffer } + return await SafeWorker.add(headers, data) } catch (err) { - console.error(`Failed to lock account ${this.address}`, err) - return false + throw new Error(`Failed to lock Account ${this.address}`, { cause: err }) } finally { bytes.erase(password) } - bytes.erase(this.#prv) - this.#locked = true - return true } /** @@ -218,23 +216,28 @@ export class Account { } /** - * Signs a block using the private key of the account. + * Signs a block using the private key of the account. The signature is + * appended to the signature field of the block before being returned. * - * @param {(ChangeBlock|ReceiveBlock|SendBlock)} block - + * @param {(string|Uint8Array)} password - Required to decrypt the private key for signing + * @param {(ChangeBlock|ReceiveBlock|SendBlock)} block - The block data to be hashed and signed + * @returns {Promise} Hexadecimal-formatted 64-byte signature */ async sign (block: ChangeBlock | ReceiveBlock | SendBlock): Promise { - if (this.isLocked || this.#prv.buffer.detached) { - throw new Error('Failed to sign block with locked Account') + if (this.isLocked) { + throw new Error(`Account ${this.address} must be unlocked prior to signing`) } try { const headers = { method: 'detached' } const data = { - privateKey: new Uint8Array(this.#prv).buffer, + privateKey: new Uint8Array(this.#privateKey).buffer, msg: hex.toBytes(block.hash).buffer } - return await NanoNaClWorker.add(headers, data) + const result = await NanoNaClWorker.add(headers, data) + block.signature = result + return result } catch (err) { throw new Error(`Failed to sign block`, { cause: err }) } @@ -249,12 +252,12 @@ export class Account { async unlock (password: string | Uint8Array): Promise { if (typeof password === 'string') password = utf8.toBytes(password) if (password == null || !(password instanceof Uint8Array)) { - throw new Error('Failed to unlock account') + throw new Error('Password must be string or bytes') } try { const headers = { method: 'get', - name: this.#pub + name: this.publicKey } const data = { password: password.buffer @@ -268,15 +271,13 @@ export class Account { if (id !== this.publicKey) { throw null } - this.#prv = new Uint8Array(privateKey as ArrayBuffer) + this.#privateKey = new Uint8Array(privateKey as ArrayBuffer) } catch (err) { - console.error(`Failed to unlock account ${this.address}`, err) - return false + throw new Error(`Failed to export private key for Account ${this.address}`, { cause: err }) } finally { bytes.erase(password) + return this.isUnlocked } - this.#locked = false - return true } /** @@ -310,21 +311,20 @@ export class Account { } } - static #addressToKey (v: string): string { - const publicKeyBytes = base32.toBytes(v.slice(-60, -8)) - const checksumBytes = base32.toBytes(v.slice(-8)) - const rechecksumBytes = new Blake2b(5).update(publicKeyBytes).digest().reverse() - if (bytes.toHex(checksumBytes) !== bytes.toHex(rechecksumBytes)) { + static #addressToKey (address: string): Uint8Array { + const publicKey = base32.toBytes(address.slice(-60, -8)) + const checksum = base32.toBytes(address.slice(-8)) + const rechecksum = new Blake2b(5).update(publicKey).digest().reverse() + if (bytes.toHex(checksum) !== bytes.toHex(rechecksum)) { throw new Error('Checksum mismatch in address') } - return bytes.toHex(publicKeyBytes) + return publicKey } - static #keyToAddress (publicKey: string): string { - const publicKeyBytes = hex.toBytes(publicKey) - const checksumBytes = new Blake2b(5).update(publicKeyBytes).digest().reverse() - const encodedPublicKey = bytes.toBase32(publicKeyBytes) - const encodedChecksum = bytes.toBase32(checksumBytes) + static #keyToAddress (publicKey: Uint8Array): string { + const checksum = new Blake2b(5).update(publicKey).digest().reverse() + const encodedPublicKey = bytes.toBase32(publicKey) + const encodedChecksum = bytes.toBase32(checksum) return `${PREFIX}${encodedPublicKey}${encodedChecksum}` }