diff --git a/cmd/melt/main.go b/cmd/melt/main.go index e4d2714..c4a834c 100644 --- a/cmd/melt/main.go +++ b/cmd/melt/main.go @@ -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 ( @@ -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) @@ -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 }, } @@ -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 +} diff --git a/cmd/melt/main_test.go b/cmd/melt/main_test.go index 2aa6660..1290b85 100644 --- a/cmd/melt/main_test.go +++ b/cmd/melt/main_test.go @@ -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) diff --git a/testdata/test_ed25519 b/cmd/melt/testdata/test_ed25519 similarity index 83% rename from testdata/test_ed25519 rename to cmd/melt/testdata/test_ed25519 index 9672af1..b0cdce9 100644 --- a/testdata/test_ed25519 +++ b/cmd/melt/testdata/test_ed25519 @@ -1,7 +1,7 @@ -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz c2gtZWQyNTUxOQAAACAV84f5iFg4YcIW0NpDTxEFhU98BsACUQgKJia4yhGU+AAA -AIiaywRCmssEQgAAAAtzc2gtZWQyNTUxOQAAACAV84f5iFg4YcIW0NpDTxEFhU98 +AIgb2DVoG9g1aAAAAAtzc2gtZWQyNTUxOQAAACAV84f5iFg4YcIW0NpDTxEFhU98 BsACUQgKJia4yhGU+AAAAECOnIiVlopOI+nd/IWGdpjm6+ggY8zwdWDMccQKrZk0 2xXzh/mIWDhhwhbQ2kNPEQWFT3wGwAJRCAomJrjKEZT4AAAAAAECAwQF -----END OPENSSH PRIVATE KEY----- diff --git a/go.mod b/go.mod index 4e4b81d..baa410a 100644 --- a/go.mod +++ b/go.mod @@ -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 ( @@ -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 ) diff --git a/go.sum b/go.sum index 1a1fbec..d047608 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= diff --git a/melt.go b/melt.go index 1e3eb46..d41aa99 100644 --- a/melt.go +++ b/melt.go @@ -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 } diff --git a/melt_test.go b/melt_test.go index 75bf3fb..10a2e91 100644 --- a/melt_test.go +++ b/melt_test.go @@ -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) - }) -}