From eb7b780c972b6b0d78632ce1389efd32f145452a Mon Sep 17 00:00:00 2001 From: Julian Gruber Date: Thu, 1 Aug 2024 13:09:18 +0200 Subject: [PATCH 1/5] Add `--recreateStationIdOnError` --- README.md | 10 ++++++---- bin/station.js | 6 +++++- commands/station.js | 4 ++-- lib/station-id.js | 6 ++++-- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 28b5877f..77bd240b 100644 --- a/README.md +++ b/README.md @@ -140,10 +140,12 @@ $ station --help Usage: station [options] Options: - -j, --json Output JSON [boolean] - --experimental Also run experimental modules [boolean] - -v, --version Show version number [boolean] - -h, --help Show help [boolean] + -j, --json Output JSON [boolean] + --experimental Also run experimental modules [boolean] + --recreateStationIdOnError Recreate Station ID if it is corrupted + [boolean] + -v, --version Show version number [boolean] + -h, --help Show help [boolean] ``` ### `$ station --version` diff --git a/bin/station.js b/bin/station.js index e4465ed7..8fc7eebb 100755 --- a/bin/station.js +++ b/bin/station.js @@ -31,8 +31,12 @@ yargs(hideBin(process.argv)) .option('experimental', { type: 'boolean', description: 'Also run experimental modules' + }) + .option('recreateStationIdOnError', { + type: 'boolean', + description: 'Recreate Station ID if it is corrupted' }), - ({ json, experimental }) => station({ json, experimental }) + ({ json, experimental, recreateStationIdOnError }) => station({ json, experimental, recreateStationIdOnError }) ) .version(`${pkg.name}: ${pkg.version}`) .alias('v', 'version') diff --git a/commands/station.js b/commands/station.js index 226e6aa6..b0a733e7 100644 --- a/commands/station.js +++ b/commands/station.js @@ -31,7 +31,7 @@ const panic = (msg, exitCode = 1) => { process.exit(exitCode) } -export const station = async ({ json, experimental }) => { +export const station = async ({ json, recreateStationIdOnError, experimental }) => { if (!FIL_WALLET_ADDRESS) panic('FIL_WALLET_ADDRESS required') if (FIL_WALLET_ADDRESS.startsWith('f1')) { panic('Invalid FIL_WALLET_ADDRESS: f1 addresses are currently not supported. Please use an f4 or 0x address.') @@ -46,7 +46,7 @@ export const station = async ({ json, experimental }) => { panic('Invalid FIL_WALLET_ADDRESS ethereum address', 2) } - const keypair = await getStationId({ secretsDir: paths.secrets, passphrase: PASSPHRASE }) + const keypair = await getStationId({ secretsDir: paths.secrets, passphrase: PASSPHRASE, recreateOnError: recreateStationIdOnError }) const STATION_ID = keypair.publicKey const fetchRes = await pRetry( diff --git a/lib/station-id.js b/lib/station-id.js index f788755c..54dfa2c5 100644 --- a/lib/station-id.js +++ b/lib/station-id.js @@ -7,10 +7,11 @@ import { subtle, getRandomValues } from 'node:crypto' * @param {object} args * @param {string} args.secretsDir * @param {string} args.passphrase + * @param {boolean} [args.recreateOnError] * @param {import('node:console')} [args.log] * @returns {Promise<{publicKey: string, privateKey: string}>} */ -export async function getStationId ({ secretsDir, passphrase, log = console }) { +export async function getStationId ({ secretsDir, passphrase, recreateOnError = false, log = console }) { assert.strictEqual(typeof secretsDir, 'string', 'secretsDir must be a string') await fs.mkdir(secretsDir, { recursive: true }) @@ -21,7 +22,8 @@ export async function getStationId ({ secretsDir, passphrase, log = console }) { log.error('Loaded Station ID: %s', keypair.publicKey) return keypair } catch (err) { - if (err.code === 'ENOENT' && err.path === keystore) { + if (recreateOnError || (err.code === 'ENOENT' && err.path === keystore)) { + if (recreateOnError) console.error(err) // the keystore file does not exist, create a new key return await generateKeypair(keystore, passphrase, { log }) } else { From d248386668a985d1719b355bd8f901e09e712449 Mon Sep 17 00:00:00 2001 From: Julian Gruber Date: Fri, 2 Aug 2024 07:21:00 +0200 Subject: [PATCH 2/5] add passing test --- test/station-id.test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/station-id.test.js b/test/station-id.test.js index 5d2f6c86..2f44732e 100644 --- a/test/station-id.test.js +++ b/test/station-id.test.js @@ -55,6 +55,12 @@ describe('station-id', () => { ) }) + it('recreates unreadable station ids on demand', async () => { + const secretsDir = getUniqueTempDir() + await getStationId({ secretsDir, passphrase: 'secret', log }) + await getStationId({ secretsDir, passphrase: 'new pass', recreateOnError: true, log }) + }) + it('encrypts plaintext station_id file when PASSPHRASE is provided', async () => { const secretsDir = getUniqueTempDir() const generated = await getStationId({ secretsDir, passphrase: '', log }) From 0c13786fff2f2e94bb08cfd275fff70613d51e84 Mon Sep 17 00:00:00 2001 From: Julian Gruber Date: Fri, 2 Aug 2024 07:36:13 +0200 Subject: [PATCH 3/5] add passing test --- test/cli.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/cli.js b/test/cli.js index 12705297..6d12b007 100644 --- a/test/cli.js +++ b/test/cli.js @@ -64,6 +64,33 @@ describe('CLI', () => { await once(ps.stdout, 'data') ps.kill() }) + it('fails with the wrong pass phrase', async () => { + const STATE_ROOT = getUniqueTempDir() + { + const ps = execa(station, { + env: { + STATE_ROOT, + FIL_WALLET_ADDRESS, + PASSPHRASE + } + }) + await once(ps.stdout, 'data') + ps.kill() + } + try { + await execa(station, { + env: { + STATE_ROOT, + FIL_WALLET_ADDRESS, + PASSPHRASE: `${PASSPHRASE}x` + } + }) + } catch (err) { + assert.strictEqual(err.exitCode, 1) + return + } + assert.fail('Expected Station Core to return a non-zero exit code') + }) }) describe('--version', () => { From e0da346763da6c01f5c613bb080233b90f1edde0 Mon Sep 17 00:00:00 2001 From: Julian Gruber Date: Fri, 2 Aug 2024 07:39:29 +0200 Subject: [PATCH 4/5] add passing test, fix style --- test/cli.js | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/test/cli.js b/test/cli.js index 6d12b007..c93e2bb4 100644 --- a/test/cli.js +++ b/test/cli.js @@ -65,6 +65,34 @@ describe('CLI', () => { ps.kill() }) it('fails with the wrong pass phrase', async () => { + const STATE_ROOT = getUniqueTempDir() + const ps = execa(station, { + env: { + STATE_ROOT, + FIL_WALLET_ADDRESS, + PASSPHRASE + } + }) + await once(ps.stdout, 'data') + ps.kill() + try { + await execa(station, { + env: { + STATE_ROOT, + FIL_WALLET_ADDRESS, + PASSPHRASE: `${PASSPHRASE}x` + } + }) + } catch (err) { + assert.strictEqual(err.exitCode, 1) + return + } + assert.fail('Expected Station Core to return a non-zero exit code') + }) + }) + + describe('--recreateStationIdOnError', () => { + it('recreates the station id on demand', async () => { const STATE_ROOT = getUniqueTempDir() { const ps = execa(station, { @@ -77,19 +105,17 @@ describe('CLI', () => { await once(ps.stdout, 'data') ps.kill() } - try { - await execa(station, { + { + const ps = execa(station, ['--recreateStationIdOnError'], { env: { STATE_ROOT, FIL_WALLET_ADDRESS, PASSPHRASE: `${PASSPHRASE}x` } }) - } catch (err) { - assert.strictEqual(err.exitCode, 1) - return + await once(ps.stdout, 'data') + ps.kill() } - assert.fail('Expected Station Core to return a non-zero exit code') }) }) From 33e4f0c490da7f1be2162ad4f45f0acef4598ab2 Mon Sep 17 00:00:00 2001 From: Julian Gruber Date: Fri, 2 Aug 2024 11:25:27 +0200 Subject: [PATCH 5/5] improve error message --- lib/station-id.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/station-id.js b/lib/station-id.js index 54dfa2c5..9c266753 100644 --- a/lib/station-id.js +++ b/lib/station-id.js @@ -63,7 +63,7 @@ async function loadKeypair (keystore, passphrase, { log }) { plaintext = await decrypt(passphrase, ciphertext) } catch (err) { throw new Error( - 'Cannot decrypt Station ID file. Did you configure the correct PASSPHRASE?', + 'Cannot decrypt Station ID file. Did you configure the correct PASSPHRASE? Alternatively overwrite it using `--recreateStationIdOnError`', { cause: err } ) }