paritytech / paritytech/polkadot-cli

Encrypted Private Key Storage + ENV Var Support

Open
#39 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
10
Forks
2
Avg merge
12h 35m
Merged PRs (30d)
4

Description

Context

Private keys (BIP39 mnemonics / hex seeds) are currently stored as plaintext in ~/.polkadot/accounts.json. The README already warns against using this for mainnet. This plan adds optional password-based encryption for stored keys, plus support for ephemeral keys via environment variables for CI/CD workflows.

Key finding: @noble/hashes (already a dependency) provides scrypt, pbkdf2, and argon2id. Combined with the Web Crypto API (available in both Bun and Node.js) for AES-256-GCM, we need zero new dependencies.


Design Decisions

Decision Choice Rationale
KDF scrypt (N=2^15, r=8, p=1) Already used in Polkadot.js ecosystem; ~100ms interactive UX; @noble/hashes/scrypt already available
Cipher AES-256-GCM via Web Crypto API Zero deps; authenticated encryption; works in Node 18+ and Bun
Passwords Per-account Simple, no global state, different security levels per account
Default Unencrypted (opt-in encryption) Backward compatible, no surprise prompts
Password prompt node:readline with muted output stream No new dependency; works in both Bun and Node.js

Storage Format

The secret field becomes a union — plain string (backward compat) or an encryption envelope:

interface EncryptedSecret {
  version: 1;
  kdf: "scrypt";
  kdfParams: { N: number; r: number; p: number };
  salt: string;       // 16 bytes, hex
  nonce: string;      // 12 bytes, hex
  ciphertext: string; // AES-256-GCM output + auth tag, hex
}

interface StoredAccount {
  name: string;
  secret: string | EncryptedSecret;  // ← union
  publicKey: string;
  derivationPath: string;
}

Detection: typeof secret === "string" → plaintext, typeof secret === "object" → encrypted.


New CLI Commands & Flags

dot account create <name> --encrypt [--password <p>]   # create with encryption
dot account import <name> --secret <s> --encrypt       # import with encryption
dot account encrypt <name> [--password <p>]             # encrypt existing account
dot account decrypt <name> [--password <p>]             # decrypt back to plaintext
dot account list                                        # shows "(encrypted)" badge

dot tx ... --from <name> [--password <p>]               # password for encrypted accounts

Password resolution order (when account is encrypted):

  1. --password flag
  2. DOT_PASSWORD env var
  3. Interactive TTY prompt
  4. Error with guidance message

ENV Var Support

DOT_PASSWORD — password for decrypting stored encrypted accounts:

DOT_PASSWORD=secret dot tx System.remark 0xdead --from my-validator

DOT_SECRET_<NAME> — ephemeral in-memory secret (never touches disk):

DOT_SECRET_DEPLOYER="word1 word2 ..." dot tx System.remark 0xdead --from deployer

Name matching: uppercase, hyphens replaced with underscores (e.g., my-validatorDOT_SECRET_MY_VALIDATOR).

Resolution order in resolveAccountSigner:

  1. Dev accounts (alice, bob, etc.)
  2. DOT_SECRET_<NAME> env var
  3. Stored accounts in accounts.json

File Changes

New files
File Purpose
src/core/crypto.ts encryptSecret(), decryptSecret(), isEncrypted() — scrypt + AES-256-GCM
src/core/crypto.test.ts Round-trip, wrong password, structure validation tests
src/core/prompt.ts readPassword(), readPasswordConfirmed() — TTY password input via node:readline
Modified files
File Changes
src/config/accounts-types.ts Add EncryptedSecret interface; update StoredAccount.secret to union type
src/core/accounts.ts Refactor resolveAccountSigner(): add ResolveOptions param, env var lookup (DOT_SECRET_<NAME>), decrypt-if-needed flow, resolvePassword() helper
src/commands/account.ts Add --encrypt/--password flags; add encrypt/decrypt subcommands; update list to show encryption badge; update help text
src/commands/tx.ts Add --password option; pass through to resolveAccountSigner()
Not changed
  • src/config/accounts-store.ts — JSON load/save is already type-agnostic
  • src/cli.ts — options registered per-command, no structural change

Implementation Order

  1. Typessrc/config/accounts-types.ts (add EncryptedSecret, update union)
  2. Cryptosrc/core/crypto.ts + tests (encrypt/decrypt with scrypt + AES-256-GCM)
  3. Promptsrc/core/prompt.ts (TTY password reading)
  4. Accounts coresrc/core/accounts.ts (refactor resolveAccountSigner, add env var support)
  5. Account commandssrc/commands/account.ts (new flags, encrypt/decrypt subcommands)
  6. TX commandsrc/commands/tx.ts (wire --password through)
  7. Tests — update existing + add new tests
  8. Help text / warnings — update README note and CLI help

Drawbacks & Trade-offs

  • --password visible in shell history/ps — warn in help text, recommend DOT_PASSWORD env var for CI
  • No password caching — must enter password on every sign operation (acceptable for CLI; avoids complexity)
  • JS can't zero memory — plaintext secret is in memory during signing (inherent platform limitation)
  • scrypt N=2^15 is moderate — faster UX (~100ms) but lower than maximum paranoia; adequate for dev CLI
  • Per-account passwords = more typing — but simpler than managing a master password + session
  • Env vars aren't fully secure — visible via /proc, inherited by children; documented as CI convenience, not gold standard

Verification

  1. bun test — all existing + new tests pass
  2. Manual: dot account create test-enc --encrypt → prompts for password, stores encrypted envelope in accounts.json
  3. Manual: dot tx System.remark 0xdead --from test-enc → prompts for password, signs successfully
  4. Manual: DOT_PASSWORD=pass dot tx ... → signs without prompt
  5. Manual: DOT_SECRET_TEMP="mnemonic..." dot account list → shows ephemeral account (note: env var secrets only work for signing via --from, not in account list)
  6. Manual: dot account encrypt <existing> / dot account decrypt <existing> → toggles encryption
  7. Verify backward compat: existing plaintext accounts still work without any flags

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the implementation order in the issue: src/config/accounts-types.ts, then src/core/crypto.ts and src/core/crypto.test.ts, followed by src/core/prompt.ts and src/core/accounts.ts. Review the account and transaction command entry points in src/commands/account.ts and src/commands/tx.ts, then run bun test. Done means encrypted and plaintext accounts, password and environment-variable resolution, CLI commands, and backward compatibility all work as described.

Written by the indexing model from the issue text.

Assessment

Tech stack
bun, node.js, typescript
Domain
cli, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.