diff options
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | main.go | 227 |
2 files changed, 112 insertions, 119 deletions
@@ -1,6 +1,6 @@ PROJECT_NAME := ssh-bip39gen BUILD_DIR := build -GOFLAGS := -ldflags "-s -w" -trimpath +GOFLAGS := -ldflags "-s -w" -trimpath -buildvcs=false GO_BUILD := go build $(GOFLAGS) .PHONY: all clean linux windows darwin tidy @@ -49,4 +49,4 @@ $(BUILD_DIR)/$(PROJECT_NAME)-darwin-arm64: tidy | $(BUILD_DIR) GOOS=darwin GOARCH=arm64 $(GO_BUILD) -o $(BUILD_DIR)/$(PROJECT_NAME)-darwin-arm64 clean: - rm -rf $(BUILD_DIR)
\ No newline at end of file + rm -rf $(BUILD_DIR) @@ -1,141 +1,134 @@ package main import ( - "crypto/sha256" - "encoding/pem" - "flag" - "fmt" - "os" - "strings" - - "github.com/tyler-smith/go-bip39" - "golang.org/x/crypto/ed25519" - "golang.org/x/crypto/ssh" + "crypto/sha256" + "encoding/pem" + "flag" + "fmt" + "os" + "strings" + + "github.com/tyler-smith/go-bip39" + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/ssh" ) -const helpMessage = `ssh-bip39gen: generate dd25519 ssh keys from a bip-39 mnemonic - -this tool creates deterministic ed25519 ssh key pairs using a 24-word bip-39 mnemonic phrase. -if no mnemonic is provided, it generates a new one with 256-bit entropy. the mnemonic is your -key to regenerate the same SSH key pair later - keep it safe! - -usage: - ssh-bip39gen [-f output_file] [-mnemonic "24-word phrase"] - -examples: - generate a new key pair (default: id_ed25519): - ssh-bip39gen - - generate with custom output file: - ssh-bip39gen -f test - - regenerate from a mnemonic: - ssh-bip39gen -f test -mnemonic "abandon ability able about ... actress" - -flags: -` - type KeyType string const ( - ED25519 KeyType = "ed25519" + ED25519 KeyType = "ed25519" ) type seededRand struct { - seed []byte - pos int + seed []byte + pos int } func (r *seededRand) Read(p []byte) (n int, err error) { - for n < len(p) { - counter := uint32(r.pos / len(r.seed)) - hash := sha256.Sum256(append(r.seed, byte(counter), byte(counter>>8), byte(counter>>16), byte(counter>>24))) - n += copy(p[n:], hash[:]) - r.pos += len(hash) - } - return len(p), nil + for n < len(p) { + counter := uint32(r.pos / len(r.seed)) + hash := sha256.Sum256(append(r.seed, byte(counter), byte(counter>>8), byte(counter>>16), byte(counter>>24))) + n += copy(p[n:], hash[:]) + r.pos += len(hash) + } + return len(p), nil } func GenerateEd25519Key(seed []byte) (ed25519.PublicKey, ed25519.PrivateKey, error) { - if len(seed) < 32 { - return nil, nil, fmt.Errorf("seed too short for ed25519; need 32 bytes, got %d", len(seed)) - } - publicKey, privateKey, err := ed25519.GenerateKey(&seededRand{seed: seed[:32]}) - return publicKey, privateKey, err + if len(seed) < 32 { + return nil, nil, fmt.Errorf("[err] seed too short for ed25519; need 32 bytes, got %d", len(seed)) + } + publicKey, privateKey, err := ed25519.GenerateKey(&seededRand{seed: seed[:32]}) + return publicKey, privateKey, err } func SaveKeys(privateKey ed25519.PrivateKey, privFile, pubFile string) error { - block, err := ssh.MarshalPrivateKey(privateKey, "") - if err != nil { - return fmt.Errorf("failed to marshal ed25519 private key: %v", err) - } - privBytes := pem.EncodeToMemory(block) - - pub, err := ssh.NewPublicKey(privateKey.Public()) - if err != nil { - return fmt.Errorf("failed to generate ed25519 public key: %v", err) - } - pubBytes := ssh.MarshalAuthorizedKey(pub) - - if err := os.WriteFile(privFile, privBytes, 0600); err != nil { - return fmt.Errorf("failed to write private key to %s: %v", privFile, err) - } - if err := os.WriteFile(pubFile, pubBytes, 0644); err != nil { - return fmt.Errorf("failed to write public key to %s: %v", pubFile, err) - } - return nil + block, err := ssh.MarshalPrivateKey(privateKey, "") + if err != nil { + return fmt.Errorf("[err] failed to marshal ed25519 private key: %v", err) + } + privBytes := pem.EncodeToMemory(block) + + pub, err := ssh.NewPublicKey(privateKey.Public()) + if err != nil { + return fmt.Errorf("[err] failed to generate ed25519 public key: %v", err) + } + pubBytes := ssh.MarshalAuthorizedKey(pub) + + if err := os.WriteFile(privFile, privBytes, 0600); err != nil { + return fmt.Errorf("[err] failed to write private key to %s: %v", privFile, err) + } + if err := os.WriteFile(pubFile, pubBytes, 0644); err != nil { + return fmt.Errorf("[err] failed to write public key to %s: %v", pubFile, err) + } + return nil +} + +func init() { + const usageHeader = ` +command-line tool that generates deterministic ed25519 ssh key pairs from a 24-word bip-39 mnemonic phrase + +author: heqnx - https://heqnx.com + +` + flag.Usage = func() { + fmt.Fprint(os.Stderr, usageHeader) + fmt.Fprintf(os.Stderr, "usage of %s:\n", os.Args[0]) + flag.PrintDefaults() + } + flag.CommandLine.SetOutput(os.Stderr) } func main() { - flag.Usage = func() { - fmt.Fprint(os.Stderr, helpMessage) - flag.PrintDefaults() - } - - mnemonic := flag.String("mnemonic", "", "bip-39 mnemonic phrase (leave empty to generate a new 24-word mnemonic)") - outputFile := flag.String("f", "bip39-id_ed25519", "output file for private key (public key will be <file>.pub)") - flag.Parse() - - privFile := *outputFile - pubFile := *outputFile + ".pub" - - var seed []byte - if *mnemonic == "" { - entropy, err := bip39.NewEntropy(256) - if err != nil { - fmt.Printf("error generating entropy: %v\n", err) - os.Exit(1) - } - *mnemonic, err = bip39.NewMnemonic(entropy) - if err != nil { - fmt.Printf("error generating mnemonic: %v\n", err) - os.Exit(1) - } - fmt.Printf("new mnemonic generated - keep it secure:\n%s\n\n", *mnemonic) - seed = bip39.NewSeed(*mnemonic, "") - } else { - wordCount := len(strings.Fields(*mnemonic)) - if wordCount != 24 { - fmt.Printf("error: mnemonic must contain exactly 24 words, got %d\n", wordCount) - os.Exit(1) - } - if !bip39.IsMnemonicValid(*mnemonic) { - fmt.Println("error: invalid mnemonic phrase") - os.Exit(1) - } - seed = bip39.NewSeed(*mnemonic, "") - } - - _, priv, err := GenerateEd25519Key(seed) - if err != nil { - fmt.Printf("error generating ed25519 key: %v\n", err) - os.Exit(1) - } - - err = SaveKeys(priv, privFile, pubFile) - if err != nil { - fmt.Printf("error saving keys: %v\n", err) - os.Exit(1) - } - fmt.Printf("ed25519 generated keys:\n - %s (private)\n - %s (public)\n", privFile, pubFile) + mnemonic := flag.String("mnemonic", "", "bip-39 mnemonic phrase (leave empty to generate a new 24-word mnemonic)") + outputFile := flag.String("f", "", "output file for private key (public key will be <file>.pub)") + flag.Parse() + + if flag.NFlag() == 0 && flag.NArg() == 0 { + flag.Usage() + os.Exit(1) + } + + privFile := *outputFile + pubFile := *outputFile + ".pub" + + var seed []byte + if *mnemonic == "" { + entropy, err := bip39.NewEntropy(256) + if err != nil { + fmt.Errorf("[err] error generating entropy: %v\n", err) + os.Exit(1) + } + *mnemonic, err = bip39.NewMnemonic(entropy) + if err != nil { + fmt.Errorf("[err] error generating mnemonic: %v\n", err) + os.Exit(1) + } + fmt.Printf("[inf] new mnemonic generated - keep it secure:\n%s\n\n", *mnemonic) + seed = bip39.NewSeed(*mnemonic, "") + } else { + wordCount := len(strings.Fields(*mnemonic)) + if wordCount != 24 { + fmt.Errorf("[err] mnemonic must contain exactly 24 words, got %d\n", wordCount) + os.Exit(1) + } + if !bip39.IsMnemonicValid(*mnemonic) { + fmt.Errorf("[err] invalid mnemonic phrase\n") + os.Exit(1) + } + seed = bip39.NewSeed(*mnemonic, "") + } + + _, priv, err := GenerateEd25519Key(seed) + if err != nil { + fmt.Errorf("[err] error generating ed25519 key: %v\n", err) + os.Exit(1) + } + + err = SaveKeys(priv, privFile, pubFile) + if err != nil { + fmt.Errorf("[err] error saving keys: %v\n", err) + os.Exit(1) + } + fmt.Printf("[inf] ed25519 generated keys:\n - %s (private)\n - %s (public)\n", privFile, pubFile) } |