Skip to content

Commit

Permalink
feat: Support reading mnemonic or private key from file (#970)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pxrl authored Oct 9, 2023
1 parent 1650bfb commit e92573d
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 24 deletions.
27 changes: 17 additions & 10 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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 <secret|mnemonic|privateKey>
#
# --wallet <mnemonic|privateKey>
# 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...
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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], ...]
# FORCE_PROPOSAL_BUNDLE_RANGE = [[1, 2], [1, 3], ...]
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
.*
node_modules
.env
coverage
coverage.json
typechain*
.DS_Store
dump.rdb*
.idea

# Hardhat files
cache
Expand Down
6 changes: 3 additions & 3 deletions src/utils/CLIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function retrieveSignerFromCLIArgs(): Promise<Wallet> {
// 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.
Expand All @@ -30,6 +30,6 @@ export function retrieveSignerFromCLIArgs(): Promise<Wallet> {
* @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);
}
34 changes: 26 additions & 8 deletions src/utils/SignerUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { readFile } from "fs/promises";
import { typeguards } from "@across-protocol/sdk-v2";
import { Wallet, retrieveGckmsKeys, getGckmsConfig, isDefined } from "./";

/**
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<Wallet> {
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]);
}

0 comments on commit e92573d

Please sign in to comment.