Skip to content

Commit

Permalink
Merge pull request #1 from charmbracelet/style
Browse files Browse the repository at this point in the history
chore: style and copy edits
  • Loading branch information
caarlos0 authored Mar 11, 2022
2 parents 26ddbec + cf4490c commit 3f79202
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 33 deletions.
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
# Melt

Backup and restore a SSH private key to mnemonic word set you can memorize.

## Usage

You check its `backup` and `restore` subcommands:
Backup and restore SSH private keys using memorizable seed phrases.

```shell
# Generate a seed phrase from an SSH key
melt ~/.ssh/id_ed25519
melt restore --mnemonic "words from backup" ./recovered_id_ed25519

# Rebuild the key from the seed phrase
melt restore ./my-key --seed "seed phrase"
```

You can also pipe to and from a file directly, e.g.:
You can also pipe to and from a file directly:

```shell
melt ~/.ssh/id_ed25519 > words
melt restore ./recovered_id_ed25519 < words
```

## How it works
## How it Works

It all comes down to the private key __seed__:

Expand Down Expand Up @@ -59,4 +58,6 @@ The key should be basically the same though.

Part of [Charm](https://charm.sh).

<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge-unrounded.jpg" width="400"></a>
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>

Charm热爱开源 • Charm loves open source
104 changes: 83 additions & 21 deletions cmd/melt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,47 @@ import (
"fmt"
"io"
"os"
"strings"

"github.com/caarlos0/sshmarshal"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/melt"
"github.com/mattn/go-isatty"
"github.com/muesli/coral"
mcoral "github.com/muesli/mango-coral"
"github.com/muesli/reflow/wordwrap"
"github.com/muesli/roff"
"github.com/muesli/termenv"
"golang.org/x/crypto/ssh"
"golang.org/x/term"
)

const (
maxWidth = 72
)

var (
headerStyle = lipgloss.NewStyle().Italic(true)
mnemonicStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("63")).Margin(1).Width(60)
restoreStyle = lipgloss.NewStyle().Bold(true).Margin(1)
baseStyle = lipgloss.NewStyle().Margin(0, 0, 1, 2)
violet = lipgloss.Color(completeColor("#6B50FF", "63", "12"))
cmdStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#FF5E8E", Dark: "#FF5E8E"}).
Background(lipgloss.AdaptiveColor{Light: completeColor("#ECECEC", "255", "7"), Dark: "#1F1F1F"}).
Padding(0, 1)
mnemonicStyle = baseStyle.Copy().
Foreground(violet).
Background(lipgloss.AdaptiveColor{Light: completeColor("#EEEBFF", "255", "7"), Dark: completeColor("#1B1731", "235", "8")}).
Padding(1, 2)
keyPathStyle = lipgloss.NewStyle().Foreground(violet)

rootCmd = &coral.Command{
Use: "melt",
Example: ` melt ~/.ssh/id_ed25519
melt ~/.ssh/id_ed25519 > mnemonic
melt restore --mnemonic \"list of words\" ./restored_id25519
melt restore ./restored_id25519 < mnemonic`,
Short: "Backup a SSH private key to a mnemonic set of keys",
Long: `melt uses bip39 to create a mnemonic set of words that represents your SSH keys.
You can then use those words to restore your private key at any time.`,
melt ~/.ssh/id_ed25519 > seed
melt restore --seed "seed phrase" ./restored_id25519
melt restore ./restored_id25519 < seed`,
Short: "Generate a seed phrase from an SSH key",
Long: `melt generates a seed phrase from an SSH key. That phrase can
be used to rebuild your public and private keys.`,
Args: coral.ExactArgs(1),
SilenceUsage: true,
RunE: func(cmd *coral.Command, args []string) error {
Expand All @@ -42,11 +56,34 @@ You can then use those words to restore your private key at any time.`,
return err
}
if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Println(headerStyle.Render(`Success!!!
You can now use the words bellow to recreate your key using the 'melt restore' command.
Store them somewhere safe, print or memorize them.`))
fmt.Println(mnemonicStyle.Render(mnemonic))
b := strings.Builder{}
w := getWidth(maxWidth)

b.WriteRune('\n')
meltCmd := cmdStyle.Render(os.Args[0])
renderBlock(&b, baseStyle, w, fmt.Sprintf("OK! Your key has been melted down to the seed phrase below. Store it somewhere safe. You can use %s to recover your key at any time.", meltCmd))
renderBlock(&b, mnemonicStyle, w, mnemonic)
renderBlock(&b, baseStyle, w, "To recreate this key run:")

// Build formatted restore command
const cmdEOL = " \\"
cmd := wordwrap.String(
os.Args[0]+` restore ./my-key --seed "`+mnemonic+`"`,
w-lipgloss.Width(cmdEOL)-baseStyle.GetHorizontalFrameSize()*2,
)
leftPad := strings.Repeat(" ", baseStyle.GetMarginLeft())
cmdLines := strings.Split(cmd, "\n")
for i, l := range cmdLines {
b.WriteString(leftPad)
b.WriteString(l)
if i < len(cmdLines)-1 {
b.WriteString(cmdEOL)
b.WriteRune('\n')
}
}
b.WriteRune('\n')

fmt.Println(b.String())
} else {
fmt.Print(mnemonic)
}
Expand All @@ -57,17 +94,19 @@ Store them somewhere safe, print or memorize them.`))
mnemonic string
restoreCmd = &coral.Command{
Use: "restore",
Short: "Recreate a key using the given mnemonic words",
Example: ` melt restore --mnemonic \"list of words\" ./restored_id25519
melt restore ./restored_id25519 < mnemonic`,
Short: "Recreate a key using the given seed phrase",
Example: ` melt restore --seed "seed phrase" ./restored_id25519
melt restore ./restored_id25519 < seed`,
Aliases: []string{"res", "r"},
Args: coral.ExactArgs(1),
RunE: func(cmd *coral.Command, args []string) error {
if err := restore(maybeFile(mnemonic), args[0]); err != nil {
return err
}

fmt.Println(restoreStyle.Render(fmt.Sprintf(`Successfully restored keys to '%[1]s' and '%[1]s.pub'!`, args[0])))
pub := keyPathStyle.Render(args[0])
priv := keyPathStyle.Render(args[0] + ".pub")
fmt.Println(baseStyle.Render(fmt.Sprintf("\nSuccessfully restored keys to %s and %s", pub, priv)))
return nil
},
}
Expand All @@ -94,8 +133,8 @@ Store them somewhere safe, print or memorize them.`))
func init() {
rootCmd.AddCommand(restoreCmd, manCmd)

restoreCmd.PersistentFlags().StringVarP(&mnemonic, "mnemonic", "m", "-", "Mnemonic set of words given by the backup command")
_ = restoreCmd.MarkFlagRequired("mnemonic")
restoreCmd.PersistentFlags().StringVarP(&mnemonic, "seed", "s", "-", "Seed phrase")
_ = restoreCmd.MarkFlagRequired("seed")
}

func main() {
Expand Down Expand Up @@ -176,3 +215,26 @@ func restore(mnemonic, path string) error {
}
return nil
}

func getWidth(max int) int {
w, _, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil || w > max {
return maxWidth
}
return w
}

func renderBlock(w io.Writer, s lipgloss.Style, width int, str string) {
_, _ = io.WriteString(w, s.Copy().Width(width).Render(str))
_, _ = io.WriteString(w, "\n")
}

func completeColor(truecolor, ansi256, ansi string) string {
switch lipgloss.ColorProfile() {
case termenv.TrueColor:
return truecolor
case termenv.ANSI256:
return ansi256
}
return ansi
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ require (
github.com/muesli/coral v1.0.0
github.com/muesli/mango-coral v1.0.1
github.com/muesli/roff v0.1.0
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0
github.com/tyler-smith/go-bip39 v1.1.0
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
)

require (
Expand All @@ -22,7 +23,6 @@ require (
github.com/muesli/mango v0.1.0 // indirect
github.com/muesli/mango-pflag v0.1.0 // indirect
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 // indirect
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7 // indirect
Expand Down
3 changes: 2 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7 h1:8IVLkfbr2cLhv0a/vKq4UFUcJym8RmDoDboxCFWEjYE=
golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
Expand Down

0 comments on commit 3f79202

Please sign in to comment.