Skip to content

Commit

Permalink
wip: reorganizing code
Browse files Browse the repository at this point in the history
Signed-off-by: Carlos A Becker <caarlos0@gmail.com>
  • Loading branch information
caarlos0 committed Mar 8, 2022
1 parent 007b026 commit e3df8ed
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 140 deletions.
73 changes: 53 additions & 20 deletions cmd/melt/main.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package main

import (
"crypto/ed25519"
"encoding/pem"
"fmt"
"io"
"os"

"github.com/caarlos0/sshmarshal"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/melt"
"github.com/mattn/go-isatty"
"github.com/muesli/coral"
"golang.org/x/crypto/ssh"
)

var (
Expand All @@ -27,23 +31,15 @@ var (
Example: "melt backup ~/.ssh/id_ed25519",
Args: coral.ExactArgs(1),
RunE: func(cmd *coral.Command, args []string) error {
mnemonic, sum, err := melt.Backup(args[0])
mnemonic, err := backup(args[0])
if err != nil {
return err
}
if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Println(headerStyle.Render(fmt.Sprintf(`
Success!!!
1. Key's sha256 checksum:
%s %s
2. mnemonic set of words
fmt.Println(headerStyle.Render(`Success!!!
You can now use the words bellow to recreate your key using the 'keys restore' command.
Store them somewhere safe, print or memorize them.
`, sum, args[0])))
Store them somewhere safe, print or memorize them.`))
fmt.Println(mnemonicStyle.Render(mnemonic))
} else {
fmt.Print(mnemonic)
Expand All @@ -60,18 +56,11 @@ Store them somewhere safe, print or memorize them.
Example: "melt restore --mnemonic \"list of words\" ./id_ed25519_restored",
Args: coral.ExactArgs(1),
RunE: func(cmd *coral.Command, args []string) error {
sum, err := melt.Restore(args[0], maybeFile(mnemonic), algo)
if err != nil {
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'.
The private key's sha256sum is:
%s %[1]s
`, args[0], sum)),
)
fmt.Println(restoreStyle.Render(fmt.Sprintf(`Successfully restored keys to '%[1]s' and '%[1]s.pub'!`, args[0])))
return nil
},
}
Expand Down Expand Up @@ -104,3 +93,47 @@ func maybeFile(s string) string {
}
return string(bts)
}

func backup(path string) (string, error) {
bts, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("could not read key: %w", err)
}

key, err := ssh.ParseRawPrivateKey(bts)
if err != nil {
return "", fmt.Errorf("could not parse key: %w", err)
}

switch key := key.(type) {
case *ed25519.PrivateKey:
return melt.ToMnemonic(key)
default:
return "", fmt.Errorf("unknown key type: %v", key)
}
}

func restore(mnemonic, path string) error {
pvtKey, err := melt.FromMnemonic(mnemonic)
if err != nil {
return err
}
block, err := sshmarshal.MarshalPrivateKey(pvtKey, "")
if err != nil {
return fmt.Errorf("could not marshal private key: %w", err)
}
bts := pem.EncodeToMemory(block)
pubkey, err := ssh.NewPublicKey(pvtKey.Public())
if err != nil {
return fmt.Errorf("could not prepare public key: %w", err)
}

if err := os.WriteFile(path, bts, 0o600); err != nil {
return fmt.Errorf("failed to write private key: %w", err)
}

if err := os.WriteFile(path+".pub", ssh.MarshalAuthorizedKey(pubkey), 0o600); err != nil {
return fmt.Errorf("failed to write public key: %w", err)
}
return nil
}
36 changes: 36 additions & 0 deletions cmd/melt/main_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,49 @@
package main

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strings"
"testing"

"github.com/matryer/is"
)

func TestBackupRestoreKnownKey(t *testing.T) {
const expectedMnemonic = `
model tone century code pilot
ball polar sauce machine crisp
plate soccer salon awake monkey
own install all broccoli marine
print smart square impact
`
const expectedSum = "4ec2b1e65bb86ef635991c3e31341c3bdaf6862e9b1efcde0a9c0307081ffc4c"

t.Run("backup", func(t *testing.T) {
is := is.New(t)
mnemonic, err := backup("testdata/test_ed25519")
is.NoErr(err)
is.Equal(mnemonic, strings.Join(strings.Fields(expectedMnemonic), " "))
})

t.Run("restore", func(t *testing.T) {
is := is.New(t)
path := filepath.Join(t.TempDir(), "key")
is.NoErr(restore(path, expectedMnemonic))
})
}

func sha256sum(bts []byte) (string, error) {
digest := sha256.New()
if _, err := digest.Write(bts); err != nil {
return "", fmt.Errorf("failed to sha256sum key: %w", err)
}
return hex.EncodeToString(digest.Sum(nil)), nil
}

func TestMaybeFile(t *testing.T) {
t.Run("is a file", func(t *testing.T) {
is := is.New(t)
Expand Down
2 changes: 1 addition & 1 deletion testdata/test_ed25519 → cmd/melt/testdata/test_ed25519
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz
c2gtZWQyNTUxOQAAACAV84f5iFg4YcIW0NpDTxEFhU98BsACUQgKJia4yhGU+AAA
AIiaywRCmssEQgAAAAtzc2gtZWQyNTUxOQAAACAV84f5iFg4YcIW0NpDTxEFhU98
AIgb2DVoG9g1aAAAAAtzc2gtZWQyNTUxOQAAACAV84f5iFg4YcIW0NpDTxEFhU98
BsACUQgKJia4yhGU+AAAAECOnIiVlopOI+nd/IWGdpjm6+ggY8zwdWDMccQKrZk0
2xXzh/mIWDhhwhbQ2kNPEQWFT3wGwAJRCAomJrjKEZT4AAAAAAECAwQF
-----END OPENSSH PRIVATE KEY-----
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ module github.com/charmbracelet/melt
go 1.17

require (
github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3
github.com/charmbracelet/lipgloss v0.5.0
github.com/matryer/is v1.4.0
github.com/mattn/go-isatty v0.0.14
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a
github.com/muesli/coral v1.0.0
github.com/tyler-smith/go-bip39 v1.1.0
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70
)

require (
Expand All @@ -20,5 +20,5 @@ require (
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-20210630005230-0f9fa26af87c // indirect
golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7 // indirect
)
16 changes: 9 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3 h1:w2ANoiT4ubmh4Nssa3/QW1M7lj3FZkma8f8V5aBDxXM=
github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA=
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
Expand All @@ -12,8 +14,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE=
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ=
github.com/muesli/coral v1.0.0 h1:odyqkoEg4aJAINOzvnjN4tUsdp+Zleccs7tRIAkkYzU=
github.com/muesli/coral v1.0.0/go.mod h1:bf91M/dkp7iHQw73HOoR9PekdTJMTD6ihJgWoDitde8=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9FnmSjdbWUlLGvcxrY0Rw3ATltrxOhk=
Expand All @@ -30,20 +30,22 @@ github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2n
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.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 h1:syTAU9FwmvzEoIYMqcPHOcVm4H3U5u90WsvuYgwpETU=
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
74 changes: 7 additions & 67 deletions melt.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,84 +2,24 @@ package melt

import (
"crypto/ed25519"
"crypto/sha256"
"encoding/hex"
"encoding/pem"
"fmt"
"os"

"github.com/mikesmitty/edkey"
"github.com/tyler-smith/go-bip39"
"golang.org/x/crypto/ssh"
)

func Backup(path string) (string, string, error) {
bts, err := os.ReadFile(path)
func ToMnemonic(key *ed25519.PrivateKey) (string, error) {
words, err := bip39.NewMnemonic(key.Seed())
if err != nil {
return "", "", fmt.Errorf("could not read key: %w", err)
return "", fmt.Errorf("could not create a mnemonics: %w", err)
}

key, err := ssh.ParseRawPrivateKey(bts)
if err != nil {
return "", "", fmt.Errorf("could not parse key: %w", err)
}

var seed []byte
switch key := key.(type) {
case *ed25519.PrivateKey:
seed = key.Seed()
default:
return "", "", fmt.Errorf("unknown key type: %v", key)
}

words, err := bip39.NewMnemonic(seed)
if err != nil {
return "", "", fmt.Errorf("could not create a mnemonic for %s: %w", path, err)
}

sum, err := sha256sum(bts)
return words, sum, err
return words, nil
}

func Restore(path, mnemonic, keyType string) (string, error) {
func FromMnemonic(mnemonic string) (ed25519.PrivateKey, error) {
seed, err := bip39.EntropyFromMnemonic(mnemonic)
if err != nil {
return "", err
}

var bts []byte
var pubkey ssh.PublicKey

switch keyType {
case "ed25519":
pvtKey := ed25519.NewKeyFromSeed(seed)
bts = pem.EncodeToMemory(&pem.Block{
Type: "OPENSSH PRIVATE KEY",
Bytes: edkey.MarshalED25519PrivateKey(pvtKey),
})
pubkey, err = ssh.NewPublicKey(pvtKey.Public())
if err != nil {
return "", fmt.Errorf("could not prepare public key: %w", err)
}
default:
return "", fmt.Errorf("unsupported key type: %q", keyType)
}

if err := os.WriteFile(path, bts, 0o600); err != nil {
return "", fmt.Errorf("failed to write private key: %w", err)
}

if err := os.WriteFile(path+".pub", ssh.MarshalAuthorizedKey(pubkey), 0o655); err != nil {
return "", fmt.Errorf("failed to write public key: %w", err)
}

return sha256sum(bts)
}

func sha256sum(bts []byte) (string, error) {
digest := sha256.New()
if _, err := digest.Write(bts); err != nil {
return "", fmt.Errorf("failed to sha256sum key: %w", err)
return nil, fmt.Errorf("failed to get seed from mnemonic: %w", err)
}
return hex.EncodeToString(digest.Sum(nil)), nil
return ed25519.NewKeyFromSeed(seed), nil
}
42 changes: 0 additions & 42 deletions melt_test.go
Original file line number Diff line number Diff line change
@@ -1,43 +1 @@
package melt

import (
"path/filepath"
"strings"
"testing"

"github.com/matryer/is"
)

func TestBackupRestoreKnownKey(t *testing.T) {
const expectedMnemonic = `
model tone century code pilot
ball polar sauce machine crisp
plate soccer salon awake monkey
own install all broccoli marine
print smart square impact
`
const expectedSum = "4ec2b1e65bb86ef635991c3e31341c3bdaf6862e9b1efcde0a9c0307081ffc4c"

t.Run("backup", func(t *testing.T) {
is := is.New(t)
mnemonic, sum, err := Backup("testdata/test_ed25519")
is.NoErr(err)
is.Equal(mnemonic, strings.Join(strings.Fields(expectedMnemonic), " "))
is.Equal(sum, expectedSum)
})

t.Run("restore", func(t *testing.T) {
is := is.New(t)
path := filepath.Join(t.TempDir(), "key")
sum, err := Restore(path, expectedMnemonic, "ed25519")
is.NoErr(err)
is.Equal(sum, expectedSum)
})
}

func TestRestore(t *testing.T) {
t.Run("invalid arg", func(t *testing.T) {
_, err := Restore(t.TempDir(), "does not matter", "rsa")
is.New(t).True(err != nil)
})
}

0 comments on commit e3df8ed

Please sign in to comment.