Skip to main content

Using Keystores Library for Local Key Management

· 4 min read
Josh Crites

Introduction to the keystores library and how to use it for local key management.

Getting Started

This is a JavaScript library that provides functions for creating and interacting with encrypted keystores for private key management. To do this, this library wraps the existing ethereumjs-wallet library, which is a standard library for managing keystores according to the Web3 Secret Storage Definition. As specified, secrets are encrypted using the Scrypt KDF (Key Derivation Function); in this case, the private key is encrypted with a passphrase (that should be kept secret) and can be decrypted later by the same phrase. Note that a keystore generated for the same (private key, passphrase) multiple times will not yield the same output due to how the KDF works. Keystore files generated by a geth node can be decrypted and accessed with this library, and vice versa.

Note that keystore files generated by this library do not contain BLS public keys, meaning that these should not be used for validator signer keys used in consensus.

The components of the library are roughly as follows:

  • KeystoreBase which wraps the functionality of ethereumjs-wallet and exposes functions to:
    • import PKs (into encrypted keystores)
    • decrypt and get a PK from an encrypted keystore
    • change the passphrase on a keystore
  • FileKeystore, InMemoryKeystore which specifiy the IO in addition to the above base class
  • KeystoreWalletWrapper: (not stable; likely to structurally change) a very simple wrapper for a Keystore and LocalWallet, which allows a user to decrypt a keystore and pass the key to the LocalWallet in order to sign transactions.

Usage

warning

For accounts containing significant funds or otherwise requiring a high degree of security, we do not recommend this keystore library! This is only for managing keys for low-risk hot wallets and signers.

For more stringent security requirements, check out the guide to Choosing a Wallet.

Depending on your use case, you can either interact directly with the FileKeystore (purely for creating and interacting with keystore files, importing or accessing private keys) or else use the KeystoreWalletWrapper (combines the keystore functionality with convenient access to the LocalWallet for signing tranactions).

Using the FileKeystore

Create new keystore and import private key

This snippet will create a keystore directory in the parentDirectory and create an encrypted file in the keystore directory containing the private key. Note that you can only create a new encrypted file for a private key if there is not already an existing file for that private key. If it already exists, you can change the passphrase (see below), but you may not have multiple files for the same private key in the same keystore directory.

import * as readline from "readline";
import { FileKeystore } from "@celo/keystores";

// This is the directory that will contain a "keystore" directory
const parentDirectory = "<INSERT_PATH_HERE>";
// This creates a "keystore" directory if one does not already exist in the parentDirectory
const keystore = new FileKeystore(parentDirectory);

// Prompt to enter private key and passphrase on the command line
let rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const privateKey: string = await new Promise((resolve) =>
rl.question("Enter private key:", (answer) => {
resolve(answer);
})
);
const passphrase: string = await new Promise((resolve) =>
rl.question("Enter secret passphrase:", (answer) => {
rl.close();
resolve(answer);
})
);
// Import private key into the keystore, which is then stored as an encrypted file
// Should create a file with a name like `UTC-<DATETIME>-<ACCOUNT_ADDRESS>`
await keystore.importPrivateKey(privateKey, passphrase);
// Retrieve all addresses contained in the keystore
console.log("Addresses in keystore: ", await keystore.listKeystoreAddresses());

Accessing an existing keystore file

// Keystore already exists
const parentDirectory = '<INSERT_PATH_HERE>'
const keystore = new FileKeystore(parentDirectory)
const address = '<YOUR_ADDRESS_HERE>'
const oldPassphrase = '<OLD_PASSPHRASE>'

// Decrypt file and retrieve private key
await keystore.getPrivateKey(address, oldPassphrase)

// Change the passphrase encrypting the file
const newPassphrase = '<NEW_PASSPHRASE>'
await.keystore.changeKeystorePassphrase(address, oldPassphrase, newPassphrase)

// Decrypt file and retrieve private key using new passphrase
console.log(await keystore.getPrivateKey(address, newPassphrase))

Remove (delete) a keystore file for a particular address

const parentDirectory = "<INSERT_PATH_HERE>";
const keystore = new FileKeystore(parentDirectory);
const address = "<YOUR_ADDRESS_HERE>";

// When you know the address
// Get the filename (keystore name)
const keystoreName = await keystore.getKeystoreName(address);
await keystore.removeKeystore(keystoreName);

// Alternatively, you can do this by passing in the filename directly
keystore.removeKeystore("<KEYSTORE_FILENAME_TO_DELETE>");

Using the KeystoreWalletWrapper

This example will instantiate a KeystoreWalletWrapper, import a private key, and use the inner LocalWallet within the wrapper to sign and send a transaction with ContractKit.

import { newKit } from "@celo/contractkit";
import { FileKeystore, KeystoreWalletWrapper } from "@celo/keystores";

// This is the directory that will contain a "keystore" directory
const parentDirectory =
"/celo/celo-monorepo/packages/sdk/wallets/wallet-keystore/test-keystore-dir";
// Instantiate a KeystoreWalletWrapper using a FileKeystore
const keystoreWalletWrapper = new KeystoreWalletWrapper(
new FileKeystore(parentDirectory)
);
// Make sure to not commit this if using real funds!
// You can also get this as input on the command-line using `readline`
// as in the example above for FileKeystore
const privateKey = "YOUR_TEST_PRIVATE_KEY";
const passphrase = "test-passphrase1! ";

// Import private key or unlock account
await keystoreWalletWrapper.importPrivateKey(privateKey, passphrase);
// If the keystore file already exists for an address, simply unlock:
// const address = 'YOUR_TEST_ADDRESS'
// await keystoreWalletWrapper.unlockAccount(address, passphrase)

// Get the wrapper's `LocalWallet` instance and pass this into ContractKit
const wallet = keystoreWalletWrapper.getLocalWallet();
const kit = newKit("https://alfajores-forno.celo-testnet.org", wallet);
const [from] = wallet.getAccounts();

// Send a test transaction
const gold = await kit.contracts.getGoldToken();
await gold
.transfer("0x22579ca45ee22e2e16ddf72d955d6cf4c767b0ef", "1")
.sendAndWaitForReceipt({ from });
console.log("Transaction sent!");