From f16a7c340c643cc17329a3b6877351f98e3a86ed Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Tue, 19 Nov 2024 10:48:36 -0800 Subject: [PATCH] passes wallet passphrase from CLI to migrator (#5631) * passes wallet passphrase from CLI to migrator adds an error type, EncryptedWalletMigrationError, and catches errors of that type in the three commands that run migrations: 'migrations:start', 'migrations:revert', and 'start' prompts user to enter wallet passphrase and passes passphrase through to migrator to support passing the passphrase down to the migrator in the 'start' command we pass the passphrase through 'NodeUtils.waitForOpen' and the the node's 'openDB' method supports implementing database migrations that handle encrypted wallet logic. for example, throwing the EncryptedWalletMigrationError on an encrypted account and using the wallet passphrase passed to 'migrate' to decrypt and re-encrypt the wallet * removes passphrase prompt from start command displays error message to user instead removes logic plumbing passphrase through openDB to migrator --- .../src/commands/migrations/revert.ts | 37 ++++++++++++++++++- ironfish-cli/src/commands/migrations/start.ts | 31 +++++++++++++++- ironfish-cli/src/commands/start.ts | 22 ++++++++++- ironfish/src/migrations/errors.ts | 7 ++++ ironfish/src/migrations/index.ts | 1 + ironfish/src/migrations/migration.ts | 2 + ironfish/src/migrations/migrator.ts | 21 +++++++++-- ironfish/src/node.ts | 5 ++- ironfish/src/wallet/index.ts | 1 + 9 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 ironfish/src/migrations/errors.ts diff --git a/ironfish-cli/src/commands/migrations/revert.ts b/ironfish-cli/src/commands/migrations/revert.ts index 39bf7b3780..49b2e2ba42 100644 --- a/ironfish-cli/src/commands/migrations/revert.ts +++ b/ironfish-cli/src/commands/migrations/revert.ts @@ -1,17 +1,50 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { AccountDecryptionFailedError, EncryptedWalletMigrationError } from '@ironfish/sdk' +import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' +import { inputPrompt } from '../../ui' export class RevertCommand extends IronfishCommand { static description = `revert the last run migration` + static flags = { + passphrase: Flags.string({ + description: 'Passphrase to unlock the wallet database with', + }), + } + static hidden = true async start(): Promise { - await this.parse(RevertCommand) + const { flags } = await this.parse(RevertCommand) const node = await this.sdk.node() - await node.migrator.revert() + + let walletPassphrase = flags.passphrase + // eslint-disable-next-line no-constant-condition + while (true) { + try { + await node.migrator.revert({ walletPassphrase }) + break + } catch (e) { + if ( + e instanceof EncryptedWalletMigrationError || + e instanceof AccountDecryptionFailedError + ) { + this.logger.info(e.message) + walletPassphrase = await inputPrompt( + 'Enter your passphrase to unlock the wallet', + true, + { + password: true, + }, + ) + } else { + throw e + } + } + } } } diff --git a/ironfish-cli/src/commands/migrations/start.ts b/ironfish-cli/src/commands/migrations/start.ts index 125d7715e3..12d5db5455 100644 --- a/ironfish-cli/src/commands/migrations/start.ts +++ b/ironfish-cli/src/commands/migrations/start.ts @@ -1,8 +1,10 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { AccountDecryptionFailedError, EncryptedWalletMigrationError } from '@ironfish/sdk' import { Flags } from '@oclif/core' import { IronfishCommand } from '../../command' +import { inputPrompt } from '../../ui' export class StartCommand extends IronfishCommand { static description = `run migrations` @@ -17,12 +19,39 @@ export class StartCommand extends IronfishCommand { char: 'q', default: false, }), + passphrase: Flags.string({ + description: 'Passphrase to unlock the wallet database with', + }), } async start(): Promise { const { flags } = await this.parse(StartCommand) const node = await this.sdk.node() - await node.migrator.migrate({ quiet: flags.quiet, dryRun: flags.dry }) + + let walletPassphrase = flags.passphrase + // eslint-disable-next-line no-constant-condition + while (true) { + try { + await node.migrator.migrate({ quiet: flags.quiet, dryRun: flags.dry, walletPassphrase }) + break + } catch (e) { + if ( + e instanceof EncryptedWalletMigrationError || + e instanceof AccountDecryptionFailedError + ) { + this.logger.info(e.message) + walletPassphrase = await inputPrompt( + 'Enter your passphrase to unlock the wallet', + true, + { + password: true, + }, + ) + } else { + throw e + } + } + } } } diff --git a/ironfish-cli/src/commands/start.ts b/ironfish-cli/src/commands/start.ts index 847951f645..16f9d20f26 100644 --- a/ironfish-cli/src/commands/start.ts +++ b/ironfish-cli/src/commands/start.ts @@ -1,7 +1,13 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Assert, FullNode, NodeUtils, PromiseUtils } from '@ironfish/sdk' +import { + Assert, + EncryptedWalletMigrationError, + FullNode, + NodeUtils, + PromiseUtils, +} from '@ironfish/sdk' import { Flags } from '@oclif/core' import inspector from 'node:inspector' import { v4 as uuid } from 'uuid' @@ -223,7 +229,19 @@ export default class Start extends IronfishCommand { } this.log(` `) - await NodeUtils.waitForOpen(node, () => this.closing) + try { + await NodeUtils.waitForOpen(node, () => this.closing) + } catch (e) { + if (e instanceof EncryptedWalletMigrationError) { + this.logger.error(e.message) + this.logger.error( + 'Run `ironfish migrations:start` to enter wallet passphrase and migrate wallet databases', + ) + this.exit(1) + } else { + throw e + } + } if (this.closing) { return startDoneResolve() diff --git a/ironfish/src/migrations/errors.ts b/ironfish/src/migrations/errors.ts new file mode 100644 index 0000000000..baed8856e7 --- /dev/null +++ b/ironfish/src/migrations/errors.ts @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +export class EncryptedWalletMigrationError extends Error { + name = this.constructor.name +} diff --git a/ironfish/src/migrations/index.ts b/ironfish/src/migrations/index.ts index b696f4d3bd..00106cdebc 100644 --- a/ironfish/src/migrations/index.ts +++ b/ironfish/src/migrations/index.ts @@ -2,4 +2,5 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +export * from './errors' export { Migrator } from './migrator' diff --git a/ironfish/src/migrations/migration.ts b/ironfish/src/migrations/migration.ts index 7c9bcd17c1..52bade07b2 100644 --- a/ironfish/src/migrations/migration.ts +++ b/ironfish/src/migrations/migration.ts @@ -45,6 +45,7 @@ export abstract class Migration { tx: IDatabaseTransaction | undefined, logger: Logger, dryRun: boolean, + walletPassphrase: string | undefined, ): Promise abstract backward( @@ -53,5 +54,6 @@ export abstract class Migration { tx: IDatabaseTransaction | undefined, logger: Logger, dryRun: boolean, + walletPassphrase: string | undefined, ): Promise } diff --git a/ironfish/src/migrations/migrator.ts b/ironfish/src/migrations/migrator.ts index b8448fcf25..1f0a6fd7ed 100644 --- a/ironfish/src/migrations/migrator.ts +++ b/ironfish/src/migrations/migrator.ts @@ -69,7 +69,7 @@ export class Migrator { } } - async revert(options?: { dryRun?: boolean }): Promise { + async revert(options?: { dryRun?: boolean; walletPassphrase?: string }): Promise { const dryRun = options?.dryRun ?? false const migrations = this.migrations.slice().reverse() @@ -88,7 +88,14 @@ export class Migrator { await db.open() tx = db.transaction() - await migration.backward(this.context, db, tx, childLogger, dryRun) + await migration.backward( + this.context, + db, + tx, + childLogger, + dryRun, + options?.walletPassphrase, + ) await db.putVersion(migration.id - 1, tx) if (dryRun) { @@ -109,6 +116,7 @@ export class Migrator { quiet?: boolean quietNoop?: boolean dryRun?: boolean + walletPassphrase?: string }): Promise { const dryRun = options?.dryRun ?? false const logger = this.logger.create({}) @@ -154,7 +162,14 @@ export class Migrator { tx = db.transaction() } - await migration.forward(this.context, db, tx, childLogger, dryRun) + await migration.forward( + this.context, + db, + tx, + childLogger, + dryRun, + options?.walletPassphrase, + ) await db.putVersion(migration.id, tx) if (dryRun) { diff --git a/ironfish/src/node.ts b/ironfish/src/node.ts index 34e411e9a9..319ab9654c 100644 --- a/ironfish/src/node.ts +++ b/ironfish/src/node.ts @@ -359,7 +359,10 @@ export class FullNode { const initial = await this.migrator.isInitial() if (migrate || initial) { - await this.migrator.migrate({ quiet: !migrate, quietNoop: true }) + await this.migrator.migrate({ + quiet: !migrate, + quietNoop: true, + }) } try { diff --git a/ironfish/src/wallet/index.ts b/ironfish/src/wallet/index.ts index 63a341cf2e..7334e4fce0 100644 --- a/ironfish/src/wallet/index.ts +++ b/ironfish/src/wallet/index.ts @@ -10,3 +10,4 @@ export { JsonEncoder } from './exporter/encoders/json' export * from './validator' export * from './walletdb/walletdb' export * from './multisig' +export * from './errors'