Skip to content

feat(wallet): encrypt private keys at rest using eth-keystore#45

Open
Aboudjem wants to merge 5 commits intoPolymarket:mainfrom
Aboudjem:fix/encrypted-keystore
Open

feat(wallet): encrypt private keys at rest using eth-keystore#45
Aboudjem wants to merge 5 commits intoPolymarket:mainfrom
Aboudjem:fix/encrypted-keystore

Conversation

@Aboudjem
Copy link

@Aboudjem Aboudjem commented Mar 12, 2026

Summary

  • Private keys are now encrypted at rest using AES-128-CTR + scrypt (via Alloy's keystore)
  • Password prompt on wallet create/import, with POLYMARKET_PASSWORD env var for CI/automation
  • Backward compatible: detects plaintext keys and offers automatic migration
  • New wallet export subcommand to decrypt and display key when needed
  • Key resolution priority: --private-key flag → env var → auto-migrate → keystore prompt

Changes

File What
src/password.rs New module: password prompting with retries + env var fallback
src/config.rs Keystore encrypt/decrypt, migration logic
src/auth.rs Updated key resolution chain
src/commands/wallet.rs create/import use encryption, new export subcommand
src/commands/setup.rs Uses encrypted save path
Cargo.toml Added alloy/signer-keystore, rpassword, rand, secrecy

Test plan

  • cargo clippy -D warnings — clean
  • cargo fmt --check — clean
  • 84 unit + 49 integration tests passing
  • New tests: keystore round-trip (encrypt → decrypt same address) + wrong-password rejection

Fixes #18


Note

Medium Risk
Touches wallet key storage and authentication key-resolution paths; while changes are localized, mistakes could lock users out of keys or mishandle secrets during migration/export.

Overview
Private keys are no longer stored in plaintext config. Wallet creation/import now encrypts the key into a keystore.json (via Alloy keystore) and writes config.json with only non-sensitive settings.

Key resolution is updated to support password-protected keystores (with retry + POLYMARKET_PASSWORD env var) and includes auto-migration from legacy plaintext config on first use. Adds a new wallet export command to decrypt and print the private key, updates setup/wallet flows accordingly, and expands dependencies (alloy signer-keystore, secrecy, rpassword, rand) plus lockfile updates.

Written by Cursor Bugbot for commit 52452f6. This will update automatically on new commits. Configure here.

Aboudjem and others added 2 commits March 12, 2026 02:40
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…g keystore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

let password = crate::password::prompt_new_password()?;
config::migrate_to_encrypted(&password)?;
eprintln!("Wallet key encrypted successfully.");
return config::load_key_encrypted(password.expose_secret());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant costly scrypt decryption after migration

Medium Severity

After migration, resolve_key_string calls config::load_key_encrypted(password.expose_secret()) to decrypt the keystore that was just encrypted moments earlier. Since scrypt is intentionally slow (~1–2 seconds with default parameters), this doubles the wait time during migration. The plaintext key is already available inside migrate_to_encrypted (from load_config()) but is discarded. Having migrate_to_encrypted return the key would avoid the redundant scrypt computation.

Additional Locations (1)
Fix in Cursor Fix in Web

);
}
Ok((None, KeySource::None))
(None, KeySource::None)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Config file errors silently swallowed in resolve_key

Low Severity

resolve_key changed from returning Result<(Option<String>, KeySource)> to (Option<SecretString>, KeySource), silently discarding load_config() errors via if let Ok(Some(config)). A corrupted or unreadable config file is now indistinguishable from "no config" — callers like cmd_show and setup::execute will report "not configured" instead of surfacing the actual parse/IO error.

Additional Locations (1)
Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Private keys stored as plaintext in config file

1 participant