1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
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)
}
|