aboutsummaryrefslogtreecommitdiff
path: root/cmd/plutia/keygen.go
blob: 2828f60ceb85432ae87e734e64e204ff0865c348 (plain) (blame)
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
package main

import (
	"crypto/ed25519"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"flag"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
)

func runKeygen(args []string) error {
	fs := flag.NewFlagSet("keygen", flag.ExitOnError)
	out := fs.String("out", "", "output private key path")
	force := fs.Bool("force", false, "overwrite output file if it exists")

	if err := fs.Parse(args); err != nil {
		return err
	}

	if strings.TrimSpace(*out) == "" {
		return fmt.Errorf("--out is required")
	}

	fingerprint, err := writeMirrorPrivateKey(*out, *force, rand.Reader)

	if err != nil {
		return err
	}

	fmt.Printf("generated key fingerprint=%s path=%s\n", fingerprint, *out)

	return nil
}

func writeMirrorPrivateKey(out string, force bool, random io.Reader) (string, error) {
	seed := make([]byte, ed25519.SeedSize)

	if _, err := io.ReadFull(random, seed); err != nil {
		return "", fmt.Errorf("generate seed: %w", err)
	}

	fingerprint := keyFingerprint(ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey))

	if err := writeAtomicFile(out, append([]byte(hex.EncodeToString(seed)), '\n'), force, 0o600); err != nil {
		return "", err
	}

	return fingerprint, nil
}

func writeAtomicFile(path string, data []byte, force bool, mode os.FileMode) error {
	if path == "" {
		return fmt.Errorf("output path is required")
	}

	if _, err := os.Stat(path); err == nil && !force {
		return fmt.Errorf("output path already exists: %s (use --force to overwrite)", path)
	} else if err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("stat output path: %w", err)
	}

	dir := filepath.Dir(path)

	if err := os.MkdirAll(dir, 0o755); err != nil {
		return fmt.Errorf("mkdir output dir: %w", err)
	}

	tmp, err := os.CreateTemp(dir, ".plutia-keygen-*")

	if err != nil {
		return fmt.Errorf("create temp key file: %w", err)
	}

	tmpPath := tmp.Name()
	closed := false

	defer func() {
		if !closed {
			_ = tmp.Close()
		}

		_ = os.Remove(tmpPath)
	}()

	if err := tmp.Chmod(mode); err != nil {
		return fmt.Errorf("chmod temp key file: %w", err)
	}

	if _, err := tmp.Write(data); err != nil {
		return fmt.Errorf("write temp key file: %w", err)
	}

	if err := tmp.Sync(); err != nil {
		return fmt.Errorf("sync temp key file: %w", err)
	}

	if err := tmp.Close(); err != nil {
		return fmt.Errorf("close temp key file: %w", err)
	}

	closed = true

	if force {
		if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
			return fmt.Errorf("remove existing output file: %w", err)
		}
	}

	if err := os.Rename(tmpPath, path); err != nil {
		return fmt.Errorf("rename temp key file: %w", err)
	}

	if err := os.Chmod(path, mode); err != nil {
		return fmt.Errorf("chmod output key file: %w", err)
	}

	return nil
}

func keyFingerprint(pub ed25519.PublicKey) string {
	sum := sha256.Sum256(pub)

	return "ed25519:" + hex.EncodeToString(sum[:8])
}