paritytech / paritytech/polkadot-cli
Encrypted Private Key Storage + ENV Var Support
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):
--passwordflagDOT_PASSWORDenv var- Interactive TTY prompt
- 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-validator → DOT_SECRET_MY_VALIDATOR).
Resolution order in resolveAccountSigner:
- Dev accounts (alice, bob, etc.)
DOT_SECRET_<NAME>env var- 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-agnosticsrc/cli.ts— options registered per-command, no structural change
Implementation Order
- Types —
src/config/accounts-types.ts(addEncryptedSecret, update union) - Crypto —
src/core/crypto.ts+ tests (encrypt/decrypt with scrypt + AES-256-GCM) - Prompt —
src/core/prompt.ts(TTY password reading) - Accounts core —
src/core/accounts.ts(refactorresolveAccountSigner, add env var support) - Account commands —
src/commands/account.ts(new flags, encrypt/decrypt subcommands) - TX command —
src/commands/tx.ts(wire--passwordthrough) - Tests — update existing + add new tests
- Help text / warnings — update README note and CLI help
Drawbacks & Trade-offs
--passwordvisible in shell history/ps— warn in help text, recommendDOT_PASSWORDenv 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
bun test— all existing + new tests pass- Manual:
dot account create test-enc --encrypt→ prompts for password, stores encrypted envelope inaccounts.json - Manual:
dot tx System.remark 0xdead --from test-enc→ prompts for password, signs successfully - Manual:
DOT_PASSWORD=pass dot tx ...→ signs without prompt - Manual:
DOT_SECRET_TEMP="mnemonic..." dot account list→ shows ephemeral account (note: env var secrets only work for signing via--from, not inaccount list) - Manual:
dot account encrypt <existing>/dot account decrypt <existing>→ toggles encryption - Verify backward compat: existing plaintext accounts still work without any flags
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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