Skip to content

Commit

Permalink
add batch key create utility (#35)
Browse files Browse the repository at this point in the history
* add batch key utility

* fmt

* fix lint

* add util to read the generated folder

* fix golint error

* addressed comments

* add command descriptions

* add go install

* add better descriptions

* update readme

* update readme

* moved the readutil to crypto util

* move types too

* lint

* password without symbols for testing

* keep 3 test files
  • Loading branch information
shrimalmadhur authored Oct 17, 2023
1 parent 8a60549 commit 9a026d0
Show file tree
Hide file tree
Showing 18 changed files with 447 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

GO_LINES_IGNORED_DIRS=contracts
GO_PACKAGES=./chainio/... ./crypto/... ./logging/... \
./types/... ./utils/... ./signer/...
./types/... ./utils/... ./signer/... ./cmd/...
GO_FOLDERS=$(shell echo ${GO_PACKAGES} | sed -e "s/\.\///g" | sed -e "s/\/\.\.\.//g")
help:
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
Expand Down
29 changes: 29 additions & 0 deletions cmd/egnkey/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## egnkey
This tool is used to manage keys for AVS development purpose

Features:
- [Generate ecdsa or bls key in batches](#generate-ecdsa-or-bls-key-in-batches)

### How to install
#### Install from source
```bash
go install github.com/Layr-Labs/eigensdk-go/cmd/egnkey@latest
```

#### Build from source
Navigate to [egnkey](../egnkey/) directory and run
```bash
go install
```

### Generate ecdsa or bls key in batches

To create in a random folder
```bash
egnkey generate --key-type ecdsa --num-keys <num_key>
```

To create in specific folder
```bash
egnkey generate --key-type ecdsa --num-keys <num_key> --output-dir <path_to_folder>
```
208 changes: 208 additions & 0 deletions cmd/egnkey/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package main

import (
"encoding/hex"
"fmt"
"math/rand"
"os"
"path/filepath"
"time"

"github.com/Layr-Labs/eigensdk-go/crypto/bls"

"github.com/Layr-Labs/eigensdk-go/crypto/ecdsa"
"github.com/ethereum/go-ethereum/crypto"
"github.com/google/uuid"
"github.com/urfave/cli/v2"
)

const (
DefaultKeyFolder = "keys"

PasswordFile = "password.txt"
PrivateKeyHexFile = "private_key_hex.txt"
)

var (
KeyTypeFlag = &cli.StringFlag{
Name: "key-type",
Usage: "key type to create (ecdsa or bls)",
Required: true,
}
NumKeysFlag = &cli.IntFlag{
Name: "num-keys",
Usage: "number of keys to create",
Required: true,
DefaultText: "1",
}
OutputDirFlag = &cli.StringFlag{
Name: "output-dir",
Usage: "folder to store keys",
Required: false,
}
)

var commandGenerate = &cli.Command{
Name: "generate",
Aliases: []string{"g"},
Description: `Generate keys for testing purpose.
This command creates ecdsa or bls key pair for testing purposes. It generates
all the relevant files for reading and keys and decrypted it and also gets
you the private keys in plaintext.
It creates the following artifacts based on arguments
- passwords.txt - contains all passwords to decrypt keys
- private_key_hex.txt - will create plaintext private keys
- keys/* - create all the encrypted json files in this folder
`,
Action: generate,
Flags: []cli.Flag{
KeyTypeFlag,
NumKeysFlag,
OutputDirFlag,
},
}

func generate(c *cli.Context) error {
keyType := c.String(KeyTypeFlag.Name)
if keyType != "ecdsa" && keyType != "bls" {
return cli.Exit("Invalid key type", 1)
}
numKeys := c.Int(NumKeysFlag.Name)
if numKeys < 1 {
return cli.Exit("Invalid number of keys", 1)
}

folder := c.String(OutputDirFlag.Name)
if folder == "" {
id, err := uuid.NewUUID()
if err != nil {
return cli.Exit("Failed to generate UUID", 1)
}
folder = id.String()
}

_, err := os.Stat(folder)
if !os.IsNotExist(err) {
return cli.Exit("Folder already exists, choose a different folder or delete the existing folder", 1)
}

err = os.MkdirAll(folder+"/"+DefaultKeyFolder, 0755)
if err != nil {
return err
}

passwordFile, err := os.Create(filepath.Clean(folder + "/" + PasswordFile))
if err != nil {
return err
}
privateKeyFile, err := os.Create(filepath.Clean(folder + "/" + PrivateKeyHexFile))
if err != nil {
return err
}

if keyType == "ecdsa" {
err := generateECDSAKeys(numKeys, folder, passwordFile, privateKeyFile)
if err != nil {
return err
}
} else if keyType == "bls" {
err := generateBlsKeys(numKeys, folder, passwordFile, privateKeyFile)
if err != nil {
return err
}
} else {
return cli.Exit("Invalid key type", 1)
}

return nil
}

func generateBlsKeys(numKeys int, path string, passwordFile, privateKeyFile *os.File) error {
for i := 0; i < numKeys; i++ {
key, err := bls.GenRandomBlsKeys()
if err != nil {
return err
}

password := generateRandomPassword()
if err != nil {
return err
}

privateKeyHex := key.PrivKey.String()
fileName := fmt.Sprintf("%d.bls.key.json", i+1)
err = key.SaveToFile(filepath.Clean(path+"/"+DefaultKeyFolder+"/"+fileName), password)
if err != nil {
return err
}

_, err = passwordFile.WriteString(password + "\n")
if err != nil {
return err
}

_, err = privateKeyFile.WriteString(privateKeyHex + "\n")
if err != nil {
return err
}
}
return nil
}

func generateECDSAKeys(numKeys int, path string, passwordFile, privateKeyFile *os.File) error {
for i := 0; i < numKeys; i++ {
key, err := crypto.GenerateKey()
if err != nil {
return err
}

password := generateRandomPassword()
if err != nil {
return err
}

privateKeyHex := hex.EncodeToString(key.D.Bytes())
fileName := fmt.Sprintf("%d.ecdsa.key.json", i+1)
err = ecdsa.WriteKey(filepath.Clean(path+"/"+DefaultKeyFolder+"/"+fileName), key, password)
if err != nil {
return err
}

_, err = passwordFile.WriteString(password + "\n")
if err != nil {
return err
}

_, err = privateKeyFile.WriteString("0x" + privateKeyHex + "\n")
if err != nil {
return err
}
}
return nil
}

func generateRandomPassword() string {
// Seed the random number generator
random := rand.New(rand.NewSource(time.Now().UnixNano()))

// Define character sets for the password
uppercaseLetters := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lowercaseLetters := "abcdefghijklmnopqrstuvwxyz"
digits := "0123456789"
//specialSymbols := "!@#$%^&*()-_+=[]{}|;:,.<>?/\\"

// Combine character sets into one
//allCharacters := uppercaseLetters + lowercaseLetters + digits + specialSymbols
allCharacters := uppercaseLetters + lowercaseLetters + digits

// Length of the password you want
passwordLength := 20

// Generate the password
password := make([]byte, passwordLength)
for i := range password {
password[i] = allCharacters[random.Intn(len(allCharacters))]
}
return string(password)
}
25 changes: 25 additions & 0 deletions cmd/egnkey/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import (
"fmt"
"os"

"github.com/urfave/cli/v2"
)

func main() {
app := cli.NewApp()
app.Name = "egnkey"
app.Description = "Eigenlayer batch keys manager"
app.Commands = []*cli.Command{
commandGenerate,
}

app.Usage = "Used to manage batch keys for testing"

if err := app.Run(os.Args); err != nil {
fmt.Println("Error: ", err)
os.Exit(1)
}

}
97 changes: 97 additions & 0 deletions crypto/utils/batch_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package utils

import (
"bufio"
"fmt"
"os"
"path/filepath"
)

const (
// TODO: move to a common constants file which
// can be used by utils and cmd
DefaultKeyFolder = "keys"
PasswordFile = "password.txt"
PrivateKeyHexFile = "private_key_hex.txt"
)

// ReadBatchKeys reads the batch keys from the given folder
// and returns the list of BatchKey
// folder: folder where the keys are stored, relative to the current directory
func ReadBatchKeys(folder string, isECDSA bool) ([]BatchKey, error) {
var batchKey []BatchKey
absFolder, err := filepath.Abs(folder)
if err != nil {
return nil, err
}

// read the private key file
privateKeyFile, err := os.Open(filepath.Clean(absFolder + "/" + PrivateKeyHexFile))
if err != nil {
fmt.Println("Error opening the file:", err)
return nil, err
}
defer func(privateKeyFile *os.File) {
err := privateKeyFile.Close()
if err != nil {
_ = fmt.Errorf("error closing the file: %s", err)
return
}
}(privateKeyFile)

// read the password file
passwordFile, err := os.Open(filepath.Clean(absFolder + "/" + PasswordFile))
if err != nil {
fmt.Println("Error opening the file:", err)
return nil, err
}
defer func(passwordFile *os.File) {
err := passwordFile.Close()
if err != nil {
_ = fmt.Errorf("error closing the file: %s", err)
return
}
}(passwordFile)

privateKeyScanner := bufio.NewScanner(privateKeyFile)
passwordScanner := bufio.NewScanner(passwordFile)

// To create file names
fileCtr := 1
keyType := "ecdsa"
if !isECDSA {
keyType = "bls"
}
for privateKeyScanner.Scan() && passwordScanner.Scan() {
privateKey := privateKeyScanner.Text()
password := passwordScanner.Text()
fileName := fmt.Sprintf("%d.%s.key.json", fileCtr, keyType)
filePath := fmt.Sprintf("%s/%s/%s", absFolder, DefaultKeyFolder, fileName)
// Since a last line with empty string is also read
// I need to break here
// TODO(madhur): remove empty string when it gets created
if privateKey == "" {
break
}

bK := BatchKey{
FilePath: filePath,
Password: password,
PrivateKey: privateKey,
}
batchKey = append(batchKey, bK)
fileCtr++
}

if err := privateKeyScanner.Err(); err != nil {
fmt.Println("Error reading privateKey file: ", err)
return nil, err
}

if err := passwordScanner.Err(); err != nil {
fmt.Println("Error reading password file: ", err)
return nil, err
}

return batchKey, nil
}
Loading

0 comments on commit 9a026d0

Please sign in to comment.