Skip to content

feat: use keystore to store private keys#235

Merged
cristiam86 merged 12 commits into
mainfrom
dxp-450-use-keystore-to-store-private-keys
Jul 9, 2025
Merged

feat: use keystore to store private keys#235
cristiam86 merged 12 commits into
mainfrom
dxp-450-use-keystore-to-store-private-keys

Conversation

@epsjunior

@epsjunior epsjunior commented Jul 8, 2025

Copy link
Copy Markdown
Contributor

🔐 Implement Encrypted Keystore Security

Overview

Replaces the insecure keypairManager system with enterprise-grade encrypted keystore functionality using ethers.js HDNodeWallet encryption. Centralizes all keystore logic in BaseAction to eliminate code duplication and prevent cyclic import issues across commands.

🚀 Key Features

Security Enhancements

  • Encrypted Storage: All private keys now stored encrypted using ethers.js wallet encryption
  • 3-Tries Security: Maximum 3 password attempts before lockout with progressive messaging
  • Password Validation: Minimum 8 characters + confirmation matching required

Architecture Improvements

  • Centralized Logic: All keystore operations consolidated in BaseAction class
  • Eliminated Duplication: Single source of truth for private key management across all commands
  • Resolved Cyclic Imports: Commands now inherit keystore functionality instead of importing separate managers
  • Clean Error Patterns: Eliminated fragile string matching (error.message.includes())
  • Method Organization: Private methods first, then protected (proper encapsulation)
  • Recursive Retry Logic: Clean password retry implementation without string parsing
  • Type Safety: Proper KeystoreData interface with validation

UX Improvements

  • Spinner Feedback: Replaced raw exceptions with clean spinner fail messages
  • Optimized Flow: Eliminated redundant password prompts after keystore creation
  • Progressive Messaging: Clear attempt counters ("Attempt 2/3") for password retries
  • Graceful Recovery: Automatic new keypair creation on file corruption/wrong format

🛠️ Technical Implementation

Centralized BaseAction Design

// All commands inherit keystore functionality
export class BaseAction extends ConfigFileManager {
  // Private keystore methods (internal use only)
  private async decryptKeystore(keystoreData, attempt): Promise<string>
  private isValidKeystoreFormat(data): boolean
  
  // Protected methods (available to all commands)
  protected async getPrivateKey(): Promise<string>
  protected async createKeypair(outputPath, overwrite): Promise<string>
  protected async promptPassword(message): Promise<string>
}

Benefits of Centralization

  • No Code Duplication: All commands use same keystore logic via inheritance
  • Consistent Security: Same 3-tries mechanism across all commands
  • Maintainability: Single location for keystore updates
  • Import Simplicity: Commands extend BaseAction instead of importing multiple managers
  • Testing: Unified test suite for all keystore operations

Security Features

// Progressive password attempts with clear messaging
const message = attempt === 1 
  ? "Enter password to decrypt keystore:" 
  : `Invalid password. Attempt ${attempt}/3 - Enter password to decrypt keystore:`;

// Lockout after max attempts
if (attempt >= 3) {
  this.failSpinner("Maximum password attempts exceeded (3/3).");
  process.exit(1);
}

Keystore Format

{
  "version": 1,
  "encrypted": "ethers-encrypted-json-wallet",
  "address": "0x..."
}

📋 Breaking Changes

  • Removed: keypairManager class and all references
  • Centralized: All keystore logic moved to BaseAction
  • Modified: getPrivateKey() now handles encrypted keystore directly
  • Modified: createKeypair() returns private key instead of void
  • File Format: New encrypted keystore format (auto-migration on corruption)

🧪 Testing Coverage

  • Password Handling: promptPassword, validation, retry mechanisms
  • Keystore Operations: read/write/decrypt operations with error scenarios
  • 3-Tries Security: attempt counting, progressive messaging, lockout behavior
  • File System: corruption handling, overwrite protection, path validation
  • Error Scenarios: invalid formats, wrong passwords, file conflicts
  • Security Validation: format checking, version validation, null/undefined handling

🔄 Migration Path

  • Existing plain text keypairs automatically trigger "create new keypair" flow
  • Users prompted to create encrypted keystore on first use
  • Corrupted files gracefully handled with new keypair creation option
  • No data loss - users can recover by creating new keypair

📈 Benefits

  • Security: Private keys now encrypted at rest with user passwords
  • Architecture: Centralized logic eliminates duplication and cyclic imports
  • UX: Clean error messages and progressive feedback instead of raw exceptions
  • Reliability: Proper error handling patterns without fragile string matching
  • Maintainability: Single source of truth for all keystore operations
  • Testability: Comprehensive test coverage for all security scenarios

Files Changed:

  • src/lib/actions/BaseAction.ts - Centralized keystore implementation
  • src/commands/keygen/create.ts - Updated integration
  • src/lib/interfaces/KeystoreData.ts - New type definitions
  • tests/libs/baseAction.test.ts - Comprehensive test coverage

Files Removed:

  • src/lib/accounts/KeypairManager.ts - Logic moved to BaseAction
  • tests/libs/accounts/KeypairManager.test.ts - Tests consolidated

Security Review: ✅ All private keys encrypted, proper attempt limiting, no plain text storage

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced encrypted keystore support for storing private keys, enhancing security with password protection.
    • Added user prompts for password entry and confirmation during keystore creation and decryption.
  • Refactor

    • Replaced the previous keypair management system with a new keystore-based approach.
    • Updated command descriptions and messages to reflect the use of encrypted keystores.
  • Bug Fixes

    • Improved error handling and user feedback during keystore creation, decryption, and file validation.
  • Tests

    • Updated and expanded test coverage to validate keystore creation, decryption, password handling, and error scenarios.
    • Removed outdated tests related to the old keypair management system.

@coderabbitai

coderabbitai Bot commented Jul 8, 2025

Copy link
Copy Markdown

Walkthrough

The changes transition the application's key management from handling raw private keys and keypairs to using encrypted keystore files with password protection. This involves removing the KeypairManager class, updating the BaseAction class to support keystore encryption/decryption, introducing a new KeystoreData interface, modifying command descriptions and handlers, and updating or removing related tests.

Changes

File(s) Change Summary
src/lib/accounts/KeypairManager.ts, tests/libs/accounts/KeypairManager.test.ts Removed the KeypairManager class and its associated tests.
src/lib/actions/BaseAction.ts, tests/libs/baseAction.test.ts Refactored to support encrypted keystore files, added password prompts, keystore validation, and updated tests.
src/lib/interfaces/KeystoreData.ts Added a new KeystoreData interface defining the structure of keystore files.
src/commands/keygen/create.ts, src/commands/keygen/index.ts Updated command descriptions, spinner messages, and made command actions asynchronous.
tests/actions/create.test.ts Updated tests to mock internal async methods and use new terminology for keystores.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI
    participant BaseAction
    participant FileSystem
    participant ethers

    User->>CLI: Run keygen create command
    CLI->>BaseAction: createKeypairAction(options)
    BaseAction->>BaseAction: Prompt user for password (twice)
    BaseAction->>ethers: Create random wallet
    BaseAction->>ethers: Encrypt wallet with password
    ethers-->>BaseAction: Encrypted keystore JSON
    BaseAction->>FileSystem: Write keystore JSON to file
    BaseAction->>CLI: Show success message
Loading
sequenceDiagram
    participant User
    participant CLI
    participant BaseAction
    participant FileSystem
    participant ethers

    User->>CLI: Run command needing private key
    CLI->>BaseAction: getPrivateKey()
    BaseAction->>FileSystem: Check for keystore file
    alt Keystore exists and valid
        BaseAction->>User: Prompt for password (up to 3 tries)
        User-->>BaseAction: Enter password
        BaseAction->>ethers: Decrypt keystore with password
        ethers-->>BaseAction: Private key
    else Keystore missing/invalid
        BaseAction->>User: Prompt to create new keypair
        User-->>BaseAction: Confirm
        BaseAction->>BaseAction: createKeypair(...)
        BaseAction->>ethers: Create and encrypt new wallet
        ethers-->>BaseAction: Encrypted keystore JSON
        BaseAction->>FileSystem: Write keystore JSON to file
        BaseAction->>ethers: Return new private key
    end
    BaseAction-->>CLI: Return private key
Loading

Poem

In the warren where secrets are stored,
Now keystores are locked, private keys ignored.
With passwords we hop, through prompts we leap,
Encrypted and safe, our secrets we keep.
Out with the old, in with the new—
A keystore adventure for the rabbit crew!
🐇🔐

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

npm error Exit handler never called!
npm error This is an error with npm itself. Please report this error at:
npm error https://github.com/npm/cli/issues
npm error A complete log of this run can be found in: /.npm/_logs/2025-07-09T13_24_50_994Z-debug-0.log


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75447dc and 9205040.

📒 Files selected for processing (1)
  • src/lib/actions/BaseAction.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/actions/BaseAction.ts
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/lib/actions/BaseAction.ts (2)

39-46: Consider enhancing keystore format validation.

While the current validation is good, consider adding a check to ensure the encrypted field contains valid JSON that ethers.js expects.

 private isValidKeystoreFormat(data: any): data is KeystoreData {
   return (
     data && 
     data.version === 1 && 
     typeof data.encrypted === "string" && 
-    typeof data.address === "string"
+    typeof data.address === "string" &&
+    data.encrypted.length > 0 &&
+    (() => {
+      try {
+        JSON.parse(data.encrypted);
+        return true;
+      } catch {
+        return false;
+      }
+    })()
   );
 }

93-129: Good implementation with room for UX improvements.

The keystore creation follows security best practices. Consider these enhancements:

  1. Show progress during encryption as it can be slow
  2. Consider adding password strength requirements beyond length
  3. Display success message with the address after creation
 protected async createKeypair(outputPath: string, overwrite: boolean): Promise<string> {
   const finalOutputPath = this.getFilePath(outputPath);
   this.stopSpinner();
 
   if (existsSync(finalOutputPath) && !overwrite) {
     this.failSpinner(`The file at ${finalOutputPath} already exists. Use the '--overwrite' option to replace it.`);
     process.exit(1);
   }
 
   const wallet = ethers.Wallet.createRandom();
   
   const password = await this.promptPassword("Enter password to encrypt your keystore:");
   const confirmPassword = await this.promptPassword("Confirm password:");
 
   if (password !== confirmPassword) {
     this.failSpinner("Passwords do not match");
     process.exit(1);
   }
 
   if (password.length < 8) {
     this.failSpinner("Password must be at least 8 characters long");
     process.exit(1);
   }
 
+  this.startSpinner("Encrypting keystore...");
   const encryptedJson = await wallet.encrypt(password);
+  this.stopSpinner();
   
   const keystoreData: KeystoreData = {
     version: 1,
     encrypted: encryptedJson,
     address: wallet.address,
   };
 
   writeFileSync(finalOutputPath, JSON.stringify(keystoreData, null, 2));
   this.writeConfig('keyPairPath', finalOutputPath);
   
+  this.logSuccess(`Keystore created successfully at ${finalOutputPath}`);
+  this.logInfo(`Address: ${wallet.address}`);
+  
   return wallet.privateKey;
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a17392f and 17f6252.

📒 Files selected for processing (8)
  • src/commands/keygen/create.ts (1 hunks)
  • src/commands/keygen/index.ts (1 hunks)
  • src/lib/accounts/KeypairManager.ts (0 hunks)
  • src/lib/actions/BaseAction.ts (2 hunks)
  • src/lib/interfaces/KeystoreData.ts (1 hunks)
  • tests/actions/create.test.ts (1 hunks)
  • tests/libs/accounts/KeypairManager.test.ts (0 hunks)
  • tests/libs/baseAction.test.ts (3 hunks)
💤 Files with no reviewable changes (2)
  • src/lib/accounts/KeypairManager.ts
  • tests/libs/accounts/KeypairManager.test.ts
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/commands/keygen/index.ts (1)
src/commands/keygen/create.ts (2)
  • CreateKeypairOptions (3-6)
  • KeypairCreator (8-23)
tests/libs/baseAction.test.ts (1)
src/lib/actions/BaseAction.ts (1)
  • BaseAction (13-214)
src/lib/actions/BaseAction.ts (2)
src/lib/config/ConfigFileManager.ts (1)
  • ConfigFileManager (5-51)
src/lib/interfaces/KeystoreData.ts (1)
  • KeystoreData (1-5)
🪛 Gitleaks (8.26.0)
tests/libs/baseAction.test.ts

29-29: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (9)
src/lib/interfaces/KeystoreData.ts (1)

1-5: LGTM!

The KeystoreData interface is well-structured and aligns with the encrypted keystore format requirements outlined in the PR objectives.

src/commands/keygen/index.ts (1)

12-17: Command updates are consistent with keystore implementation.

The terminology changes and async handler conversion properly support the new encrypted keystore functionality.

tests/actions/create.test.ts (1)

10-44: Test refactoring aligns well with the new async implementation.

The tests properly mock internal methods and validate the expected behavior for both success and error scenarios with appropriate spinner messages.

src/commands/keygen/create.ts (1)

13-22: Async implementation correctly handles keystore creation.

The method properly awaits the keystore creation process and provides clear user feedback through spinner messages.

tests/libs/baseAction.test.ts (2)

29-29: Test mock data is appropriate.

The static analysis warning is a false positive - this is clearly a mock private key value used for testing purposes.


198-371: Comprehensive test coverage for keystore operations.

The test suite thoroughly covers all aspects of the new keystore functionality including:

  • Password prompting and validation
  • Keystore decryption with retry logic
  • Error handling and edge cases
  • File operations and overwrite scenarios

Excellent coverage of the security-critical components!

src/lib/actions/BaseAction.ts (3)

9-11: LGTM! Appropriate imports for keystore functionality.

The addition of ethers for wallet encryption and specific fs imports for file operations aligns well with the new keystore implementation.


55-66: LGTM! Proper integration with async private key retrieval.

The method correctly awaits the new async getPrivateKey() method, maintaining compatibility with the encrypted keystore approach.


131-147: LGTM! Secure password prompting implementation.

Good use of inquirer with password masking and validation. The empty password check prevents accidental security issues.

Comment thread tests/libs/baseAction.test.ts Outdated
Comment thread src/lib/actions/BaseAction.ts
Comment thread src/lib/actions/BaseAction.ts
@epsjunior epsjunior closed this Jul 8, 2025
@epsjunior epsjunior reopened this Jul 8, 2025
@epsjunior epsjunior requested a review from cristiam86 July 8, 2025 23:54
Comment thread src/lib/actions/BaseAction.ts Outdated

const wallet = ethers.Wallet.createRandom();

const password = await this.promptPassword("Enter password to encrypt your keystore:");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@epsjunior here we must explain the password requirements

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

return wallet.privateKey;
} catch (error) {
if (attempt >= 3) {
this.failSpinner("Maximum password attempts exceeded (3/3).");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@epsjunior how is this preventing a bad actor to try again? Shouldn't the file be removed or something after x unsuccessful attempts?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hi @cristiam86,

Since we're using ethers.js, the wallet is encrypted using the Ethereum keystore V3 format with scrypt as the KDF (N = 131072 by default). That makes each decryption attempt slow (~0.5–1s), which is the real protection against brute-force attacks.

Deleting the file after failed attempts wouldn’t prevent a bad actor — they could just copy the keystore file and run the command again. So limiting to 3 retries and exiting is mostly a UX choice, not a security layer.

@cristiam86 cristiam86 merged commit f18f2af into main Jul 9, 2025
2 checks passed
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.

2 participants