diff options
author | heqnx <root@heqnx.com> | 2025-03-12 18:38:05 +0200 |
---|---|---|
committer | heqnx <root@heqnx.com> | 2025-03-12 18:38:05 +0200 |
commit | aeecd7cd0872296e8b2a385097fc6639b5c1efac (patch) | |
tree | 876934f122c38d31c0a7cab057895a2877ce222a | |
parent | c9138cf6956b19a63a620ae525835a13b36678a0 (diff) | |
download | ssh-bip39gen-aeecd7cd0872296e8b2a385097fc6639b5c1efac.tar.gz ssh-bip39gen-aeecd7cd0872296e8b2a385097fc6639b5c1efac.zip |
initial commit on ssh-bip39gen
-rw-r--r-- | Makefile | 52 | ||||
-rw-r--r-- | README.md | 112 | ||||
-rw-r--r-- | go.mod | 10 | ||||
-rw-r--r-- | go.sum | 14 | ||||
-rw-r--r-- | main.go | 141 |
5 files changed, 329 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6fd82ca --- /dev/null +++ b/Makefile @@ -0,0 +1,52 @@ +PROJECT_NAME := ssh-bip39gen +BUILD_DIR := build +GOFLAGS := -ldflags "-s -w" -trimpath +GO_BUILD := go build $(GOFLAGS) +.PHONY: all clean linux windows darwin tidy + +all: tidy linux windows darwin + +$(BUILD_DIR): + mkdir -p $(BUILD_DIR) + +tidy: + go mod tidy + +linux: linux-amd64 linux-386 + +linux-amd64: $(BUILD_DIR)/$(PROJECT_NAME)-linux-amd64 + +$(BUILD_DIR)/$(PROJECT_NAME)-linux-amd64: tidy | $(BUILD_DIR) + GOOS=linux GOARCH=amd64 $(GO_BUILD) -o $(BUILD_DIR)/$(PROJECT_NAME)-linux-amd64 + +linux-386: $(BUILD_DIR)/$(PROJECT_NAME)-linux-386 + +$(BUILD_DIR)/$(PROJECT_NAME)-linux-386: tidy | $(BUILD_DIR) + GOOS=linux GOARCH=386 $(GO_BUILD) -o $(BUILD_DIR)/$(PROJECT_NAME)-linux-386 + +windows: windows-amd64 windows-386 + +windows-amd64: $(BUILD_DIR)/$(PROJECT_NAME)-windows-amd64.exe + +$(BUILD_DIR)/$(PROJECT_NAME)-windows-amd64.exe: tidy | $(BUILD_DIR) + GOOS=windows GOARCH=amd64 $(GO_BUILD) -o $(BUILD_DIR)/$(PROJECT_NAME)-windows-amd64.exe + +windows-386: $(BUILD_DIR)/$(PROJECT_NAME)-windows-386.exe + +$(BUILD_DIR)/$(PROJECT_NAME)-windows-386.exe: tidy | $(BUILD_DIR) + GOOS=windows GOARCH=386 $(GO_BUILD) -o $(BUILD_DIR)/$(PROJECT_NAME)-windows-386.exe + +darwin: darwin-amd64 darwin-arm64 + +darwin-amd64: $(BUILD_DIR)/$(PROJECT_NAME)-darwin-amd64 + +$(BUILD_DIR)/$(PROJECT_NAME)-darwin-amd64: tidy | $(BUILD_DIR) + GOOS=darwin GOARCH=amd64 $(GO_BUILD) -o $(BUILD_DIR)/$(PROJECT_NAME)-darwin-amd64 + +darwin-arm64: $(BUILD_DIR)/$(PROJECT_NAME)-darwin-arm64 + +$(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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc7a134 --- /dev/null +++ b/README.md @@ -0,0 +1,112 @@ +# ssh-bip39gen + +`ssh-bip39gen` is a command-line tool that generates deterministic Ed25519 SSH key pairs from a 24-word BIP-39 mnemonic phrase. Unlike traditional SSH key generation, this tool allows you to regenerate the same key pair using only the mnemonic—no need to back up key files. It’s ideal for scenarios where you want to recover your SSH keys without storing them, provided you keep the mnemonic secure. + +## Features + +- Generates Ed25519 SSH keys with 256-bit entropy (24-word mnemonic). +- Deterministic: Same mnemonic produces the same key pair. +- Cross-platform: Binaries for Linux, Windows, and macOS (amd64 and 386/arm64). +- Simple usage with a familiar `ssh-keygen`-like interface. + +## Installation + +### Pre-built Binaries + +Download the latest release from the [Releases page](https://github.com/heqnx/ssh-bip39gen/releases) for your platform: +- `ssh-bip39gen-linux-amd64` +- `ssh-bip39gen-linux-386` +- `ssh-bip39gen-windows-amd64.exe` +- `ssh-bip39gen-windows-386.exe` +- `ssh-bip39gen-darwin-amd64` +- `ssh-bip39gen-darwin-arm64` + +Move the binary to a directory in your PATH (e.g., `/usr/local/bin` on Unix-like systems). + +### Build from Source + +Requires Go 1.21+ and `make`. + +1. Clone the repository: + +``` +$ git clone https://github.com/heqnx/ssh-bip39gen.git +$ cd ssh-bip39gen +``` + +2. Build all binaries: + +``` +$ make +``` + +- Output is in the build/ directory. + +3. (Optional) Build for a specific platform: +``` +$ make linux +$ make windows +$ make darwin +``` + +4. Clean up: + +``` +$ make clean +``` + +## Usage + +### Generate a New Key Pair + +``` +$ ssh-bip39gen +``` + +- Creates id_ed25519 (private) and id_ed25519.pub (public). +- Outputs a 24-word mnemonic (e.g., "abandon ability able about ... actress"). +- Save the mnemonic securely - it’s your only way to regenerate the keys! + +### Generate with Custom Output File + +``` +$ ssh-bip39gen -f testkey +``` + +- Creates testkey (private) and testkey.pub (public). + +### Regenerate from a Mnemonic + +``` +$ ssh-bip39gen -f testkey -mnemonic "abandon ability able about above absent absorb abstract absurd abuse access accident account accuse achieve acid acoustic acquire across act action actor actress" +``` + +- Regenerates the same key pair using the provided 24-word mnemonic. + +### Help + +``` +$ ssh-bip39gen -h +``` + +- Displays usage instructions and flags. + +## License + +- GNU GENERAL PUBLIC LICENSE Version 3 + +## Security Notes + +- Mnemonic Security: The mnemonic is your private key. Treat it like a secret - write it down on paper, store it in a safe, or use a hardware wallet. Do not store it digitally unless encrypted. +- Entropy: 256-bit entropy. +- Determinism: If the mnemonic is compromised, an attacker can regenerate your keys. Use a unique, randomly generated mnemonic for each key pair. + +Contributing +Feel free to submit issues or pull requests on GitHub. Ensure any changes maintain the 24-word mnemonic requirement and Ed25519 key type. +License +MIT License (LICENSE) - Free to use, modify, and distribute. + +## Acknowledgments + +- Built with Go, go-bip39, and x/crypto. +- Inspired by SSH key management needs and BIP-39's mnemonic standard. @@ -0,0 +1,10 @@ +module ssh-bip39gen + +go 1.23.4 + +require ( + github.com/tyler-smith/go-bip39 v1.1.0 + golang.org/x/crypto v0.36.0 +) + +require golang.org/x/sys v0.31.0 // indirect @@ -0,0 +1,14 @@ +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -0,0 +1,141 @@ +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" +) + +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" +) + +type seededRand struct { + 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 +} + +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 +} + +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 +} + +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) +} |