From e92573d8083ba993a7ca50e1a7ac58e84097f760 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 9 Oct 2023 17:24:52 +0200 Subject: [PATCH] feat: Support reading mnemonic or private key from file (#970) Rather than read the mnemonic or private key directly from .env, specify the location of a separate file via the SECRET env var. Then, open that file and read the mnemonic or key from there. Global fs scope is applied to the location, so it can be located in a totally different path, and can even have more restrictive permissions. This significantly reduces the chance of leaking critical secrets - i.e. when sharing screen. A simple regex is applied to determine whether the format matches a private key, and if not, it's assumed to be a mnemonic. This should make it easier to manage for operators. --- .env.example | 27 +++++++++++++++++---------- .gitignore | 4 +--- src/utils/CLIUtils.ts | 6 +++--- src/utils/SignerUtils.ts | 34 ++++++++++++++++++++++++++-------- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/.env.example b/.env.example index df9cc7f98..8e4173f4e 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,16 @@ -# Wallet details. Exercise *extreme* caution with these, and *never* share these -# lines from the configuration. Key theft will likely result in loss of funds. -# Uncomment and the configure desired variable, and use the following runtime -# argument to specify which should be used: +# Wallet configuration is controlled by the runtime argument: +# --wallet # -# --wallet +# SECRET identifies a file containing a mnemonic or private key. The file can +# reside anywhere in the accessible filesystem, and may have more restrictive +# permissions. This is the preferred method of configuring a wallet. +#SECRET="./secret" + +# MNEMONIC or PRIVATE_KEY can be specified directly in the .env file. Exercise +# *extreme* caution with these, and *never* share these lines from the +# configuration. Key theft will likely result in loss of funds. Uncomment and +# the configure desired variable, and use the following runtime argument to +# specify which should be used: # #MNEMONIC="your twelve or twenty four word seed phrase..." #PRIVATE_KEY=0xabc123... @@ -231,9 +238,9 @@ RELAYER_TOKENS='["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "0xA0b86991c6218b ################################################################################ # Note: This section is intended for advanced users only. It ONLY serves to aid -# developers in testing the relayer bot. It is NOT intended for any relayer -# or dataworker operator to use in production. It's recommended to consult -# the #relayers channel within the Across Discord server before making any +# developers in testing the relayer bot. It is NOT intended for any relayer +# or dataworker operator to use in production. It's recommended to consult +# the #relayers channel within the Across Discord server before making any # changes to this section. See https://discord.across.to. # # Note: PLEASE DO NOT USE THIS SECTION IN PRODUCTION. IT IS FOR TESTING ONLY. @@ -247,7 +254,7 @@ RELAYER_TOKENS='["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "0xA0b86991c6218b # Config Store. #INJECT_CHAIN_ID_INCLUSION='{"blockNumber":17876743,"chainId":8453}' -# Used to force a proposal to be attempted regardless of whether there is a +# Used to force a proposal to be attempted regardless of whether there is a # pending proposal. This is useful for testing the proposal logic. # Note: This logic ONLY works if `SEND_PROPOSALS` is set to false. #FORCE_PROPOSAL=false @@ -257,4 +264,4 @@ RELAYER_TOKENS='["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "0xA0b86991c6218b # [number, number][] where the two numbers are the start and end bundle ranges and the array # represents the bundle ranges that will be proposed per the chain id indices. # Note: This logic ONLY works if `SEND_PROPOSALS` and `SEND_DISPUTES` are BOTH set to false. -# FORCE_PROPOSAL_BUNDLE_RANGE = [[1, 2], [1, 3], ...] \ No newline at end of file +# FORCE_PROPOSAL_BUNDLE_RANGE = [[1, 2], [1, 3], ...] diff --git a/.gitignore b/.gitignore index e90d1435a..cf02508ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,9 @@ +.* node_modules -.env coverage coverage.json typechain* -.DS_Store dump.rdb* -.idea # Hardhat files cache diff --git a/src/utils/CLIUtils.ts b/src/utils/CLIUtils.ts index d0aa9f9db..91ad7317d 100644 --- a/src/utils/CLIUtils.ts +++ b/src/utils/CLIUtils.ts @@ -12,7 +12,7 @@ export function retrieveSignerFromCLIArgs(): Promise { // Resolve the wallet type & verify that it is valid. const keyType = (args.wallet as string) ?? "mnemonic"; if (!isValidKeyType(keyType)) { - throw new Error(`Unsupported key type (${keyType}); expected "mnemonic", "privateKey" or "gckms"`); + throw new Error(`Unsupported key type (${keyType}); expected "secret", "mnemonic", "privateKey" or "gckms"`); } // Build out the signer options to pass to the signer utils. @@ -30,6 +30,6 @@ export function retrieveSignerFromCLIArgs(): Promise { * @param keyType The key type to check. * @returns True if the key type is valid, false otherwise. */ -function isValidKeyType(keyType: unknown): keyType is "mnemonic" | "privateKey" | "gckms" { - return ["mnemonic", "privateKey", "gckms"].includes(keyType as string); +function isValidKeyType(keyType: unknown): keyType is "secret" | "mnemonic" | "privateKey" | "gckms" { + return ["secret", "mnemonic", "privateKey", "gckms"].includes(keyType as string); } diff --git a/src/utils/SignerUtils.ts b/src/utils/SignerUtils.ts index 530b84841..c529e4f72 100644 --- a/src/utils/SignerUtils.ts +++ b/src/utils/SignerUtils.ts @@ -1,3 +1,5 @@ +import { readFile } from "fs/promises"; +import { typeguards } from "@across-protocol/sdk-v2"; import { Wallet, retrieveGckmsKeys, getGckmsConfig, isDefined } from "./"; /** @@ -41,11 +43,14 @@ export async function getSigner({ keyType, gckmsKeys, cleanEnv }: SignerOptions) case "gckms": wallet = await getGckmsSigner(gckmsKeys); break; + case "secret": + wallet = await getSecretSigner(); + break; default: throw new Error(`getSigner: Unsupported key type (${keyType})`); } if (!wallet) { - throw new Error("Must define mnemonic, privatekey or gckms for wallet"); + throw new Error("Must define secret, mnemonic, privateKey or gckms for wallet"); } if (cleanEnv) { cleanKeysFromEnvironment(); @@ -91,13 +96,26 @@ function getMnemonicSigner(): Wallet { } /** - * Clears the mnemonic and private key from the env. + * Retrieves a signer based on the secret stored in ./.secret. + * @returns An ethers Signer object. + * @throws If a valid secret could not be read. */ -function cleanKeysFromEnvironment(): void { - if (process.env.MNEMONIC) { - delete process.env.MNEMONIC; - } - if (process.env.PRIVATE_KEY) { - delete process.env.PRIVATE_KEY; +async function getSecretSigner(): Promise { + const { SECRET = "./.secret" } = process.env; + let secret: string; + try { + secret = await readFile(SECRET, { encoding: "utf8" }); + secret = secret.trim().replace("\n", ""); + return /^0x[0-9a-f]{64}$/.test(secret) ? new Wallet(secret) : Wallet.fromMnemonic(secret); + } catch (err) { + const msg = typeguards.isError(err) ? err.message : "unknown error"; + throw new Error(`Unable to load secret (${SECRET}: ${msg})`); } } + +/** + * Clears any instances of MNEMONIC, PRIVATE_KEY or SECRET from the env. + */ +function cleanKeysFromEnvironment(): void { + ["MNEMONIC", "PRIVATE_KEY", "SECRET"].forEach((config) => delete process.env[config]); +}