diff --git a/cmd/melt/id_rsa b/cmd/melt/id_rsa deleted file mode 100644 index 20d26f3..0000000 --- a/cmd/melt/id_rsa +++ /dev/null @@ -1,38 +0,0 @@ ------BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn -NhAAAAAwEAAQAAAYEAwMPF0FZGHqzTGPvwEfwYJLETB35G3WKyCZPad8F7BRJMmtwTaJVA -J0H1Ie+WVq3zLS/XxPWRJM3Y+4G6e39+jeWRnubMyn5UN/1jDgFb288+ZynKTkNv/d77Fb -fP8effdcP6rDnJuYXfzdWOi6YAdiWD4d2OP2H82T7EQXYBR7uDUU0C4vrn+zrlDR2FNzH9 -hmqFs8LRPopVm+otllmr/Yb83i9HG/FYAapKucW/6BECpvMnoocN67IcuZaIk6qI9LwlBZ -AgSGI3bRFrvTzsByRkFmUG61xHX299ROw7NYfsa+w3/Wx5fJJi6JX58D+vdyuWCTqkjFXg -s5nGwAvwhUyEisukdI1IOTO7fNHkP43+S8nTgBleQDQFBgXoQYh9BhS2pvyybRchxu+TGd -/g20QH9NMr/0i52QDuf1m+SWqPdWewXRD66KZ8dRtdpqhJ71D6EfKfHCpwwBNzofdgwQEq -edC7gQmfmUP13jQLz/8BueeA8WSGUjvVghbdPiajAAAFiMfWRBDH1kQQAAAAB3NzaC1yc2 -EAAAGBAMDDxdBWRh6s0xj78BH8GCSxEwd+Rt1isgmT2nfBewUSTJrcE2iVQCdB9SHvllat -8y0v18T1kSTN2PuBunt/fo3lkZ7mzMp+VDf9Yw4BW9vPPmcpyk5Db/3e+xW3z/Hn33XD+q -w5ybmF383VjoumAHYlg+Hdjj9h/Nk+xEF2AUe7g1FNAuL65/s65Q0dhTcx/YZqhbPC0T6K -VZvqLZZZq/2G/N4vRxvxWAGqSrnFv+gRAqbzJ6KHDeuyHLmWiJOqiPS8JQWQIEhiN20Ra7 -087AckZBZlButcR19vfUTsOzWH7GvsN/1seXySYuiV+fA/r3crlgk6pIxV4LOZxsAL8IVM -hIrLpHSNSDkzu3zR5D+N/kvJ04AZXkA0BQYF6EGIfQYUtqb8sm0XIcbvkxnf4NtEB/TTK/ -9IudkA7n9Zvklqj3VnsF0Q+uimfHUbXaaoSe9Q+hHynxwqcMATc6H3YMEBKnnQu4EJn5lD -9d40C8//AbnngPFkhlI71YIW3T4mowAAAAMBAAEAAAGBALAwAFUdFfghpMy4McRuc67Nvo -Ph0mm1NN92KX+983A+OpBsyLyVdk6rhZ+f+fx9/790dDYM8b3+++1dtJH+hTCy/LeNYu9O -KjIGwovhiBFPykzGsKd6EhmrvjkqicEkW2WrXCogo5WTnGzpXeGcN93FClbeh469RDtYmE -7wydjDthIQXqwSAwjo2sRzD/jUE1LReCVG0rPkAr2AfGxox7/xqpx1//u1Sugyuxwzd+Vn -7tGje72czYd5g7HJukml2vBbKvH0Lgg44WeWBZBctjbm7dEyvKVOV7qUMRToEwHPXel+nI -vWWj+NTY02d+SWsbyHj0DkpgvIlBxrw6RWSWOPvh9ZwTlJPOioZNcQo4aM2Zkc0PTWAPuE -5/Xq0SR1JmhoYvwk3ZUot6XaJz6iK2EzzvbXtg0WLeeQzFaOvpu4LpWsduj9AqxBqhS+QA -kJUt3mq8KToevM4c5y9BrsRsx/yOfS6hPusjG7AF+bRbwumNx//RtzsYgIkIePLsgNeQAA -AMEAjCyxQl2PC6PFJxlJU1+vX/mkL/86lEif3g2zfiOqbQla5YMMcjusgk3OAYJvgHU3bG -ETE0gtI4vo3nk4tkEEpa5IRbhWU01PL2ZuRSRKEp/gShKUeM5aARVcJ5QkigNYoEtluNpb -gv2y15oe53cU5REL+Z1W2rySmecqL5NLXXFszWdVv/5WCBshsriUEUazkTmSjfP8sWaSIs -KWc/zjJ2Ox/7RAcuSY1lfiqPKr/JkH/M4+gV4Rs3vXQ9VNY6sSAAAAwQD+r+ikJooPFMqG -UBhmR00eBjFkoVpUlck+mpS7IeFC6cR7f52jY+msGeXrxGZVDITPJiA85p/lTIH3DsG0Ur -OU/CsA6pWGjh4lt2gA6dv/+vx+5dTa8HKxLkpz9diD/hH9NNzOj9OSv4a2iQpyO4Lh5gCC -+IKFXFIpEzzzoT2nrEXCtMDgbFA7iiDE1VpwnLbrUEMNo7TqYxdYyI4VYBwz0yAjDBucax -nL9sWEx+lkx2ihziGmMz6/RD5xNTLkIucAAADBAMHCJlCWFs9MKXbibvNWgOOvecmC01Ad -Hu1fRpS8GVAAT8rL0jqIRMbcaV526MFs/0gKeEmtQPCiyvBBbQ8iYrBqvK8ml/Hli/75iz -7asQD76dIYHvbpdvpbd1DYxJK0NzR0U6lFC379fazwapcUYq2InJhAKLSqK8VR2TnJ9bvm -PL/RrrIEng4GQd+iJ6Sxy/QTnV+bJJwse/NoTlh2dJJqu+ex5K73yqMM+uZLUcLZLbdv0Y -P4PhvljF9gzG3i5QAAAA9jYXJsb3NAZGFya3N0YXIBAg== ------END OPENSSH PRIVATE KEY----- diff --git a/cmd/melt/main.go b/cmd/melt/main.go index a01cd8b..b222ee8 100644 --- a/cmd/melt/main.go +++ b/cmd/melt/main.go @@ -3,6 +3,7 @@ package main import ( "crypto/ed25519" "encoding/pem" + "errors" "fmt" "io" "os" @@ -13,6 +14,7 @@ import ( "github.com/mattn/go-isatty" "github.com/muesli/coral" "golang.org/x/crypto/ssh" + "golang.org/x/term" ) var ( @@ -31,7 +33,7 @@ var ( Example: "melt backup ~/.ssh/id_ed25519", Args: coral.ExactArgs(1), RunE: func(cmd *coral.Command, args []string) error { - mnemonic, err := backup(args[0]) + mnemonic, err := backup(args[0], nil) if err != nil { return err } @@ -92,14 +94,28 @@ func maybeFile(s string) string { return string(bts) } -func backup(path string) (string, error) { +func backup(path string, pwd []byte) (string, error) { bts, err := os.ReadFile(path) if err != nil { return "", fmt.Errorf("could not read key: %w", err) } - key, err := ssh.ParseRawPrivateKey(bts) + var key interface{} + if pwd == nil { + key, err = ssh.ParseRawPrivateKey(bts) + } else { + key, err = ssh.ParseRawPrivateKeyWithPassphrase(bts, pwd) + } if err != nil { + pwderr := &ssh.PassphraseMissingError{} + if errors.As(err, &pwderr) { + fmt.Fprintf(os.Stderr, "Enter the password to decrypt %q: ", path) + pwd, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return "", fmt.Errorf("could not read password for key: %w", err) + } + return backup(path, pwd) + } return "", fmt.Errorf("could not parse key: %w", err) } diff --git a/cmd/melt/main_test.go b/cmd/melt/main_test.go index e48824a..97dbe11 100644 --- a/cmd/melt/main_test.go +++ b/cmd/melt/main_test.go @@ -20,27 +20,44 @@ func TestBackupRestoreKnownKey(t *testing.T) { const expectedSum = "ba34175ef608633b29f046b40cce596dd221347b77abba40763eef2e7ae51fe9" t.Run("backup", func(t *testing.T) { + mnemonic, err := backup("testdata/id_ed25519", nil) is := is.New(t) - mnemonic, err := backup("testdata/id_ed25519") is.NoErr(err) is.Equal(mnemonic, strings.Join(strings.Fields(expectedMnemonic), " ")) }) t.Run("backup file that does not exist", func(t *testing.T) { - _, err := backup("nope") + _, err := backup("nope", nil) is.New(t).True(err != nil) }) t.Run("backup invalid ssh key", func(t *testing.T) { - _, err := backup("testdata/not-a-key") + _, err := backup("testdata/not-a-key", nil) is.New(t).True(err != nil) }) t.Run("backup key of another type", func(t *testing.T) { - _, err := backup("testdata/id_rsa") + _, err := backup("testdata/id_rsa", nil) is.New(t).True(err != nil) }) + t.Run("backup key without password", func(t *testing.T) { + _, err := backup("testdata/pwd_id_ed25519", nil) + is := is.New(t) + is.True(err != nil) + }) + + t.Run("backup key with password", func(t *testing.T) { + const expectedMnemonic = `assume knee laundry logic soft fit quantum + puppy vault snow author alien famous comfort neglect habit + emerge fabric trophy wine hold inquiry clown govern` + + mnemonic, err := backup("testdata/pwd_id_ed25519", []byte("asd")) + is := is.New(t) + 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") diff --git a/cmd/melt/testdata/pwd_id_ed25519 b/cmd/melt/testdata/pwd_id_ed25519 new file mode 100644 index 0000000..c66e79e --- /dev/null +++ b/cmd/melt/testdata/pwd_id_ed25519 @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBnlzGNfk +mM3f0qJgdEHufbAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIC7fLAHYurOuCxI9 +f9cNQnoB9ErLerbYPebfpw+DapxBAAAAkDC4nF/SF4oHZFMOHh+Up17Y1iXcOyDyfoGD9g +0rZMTeuzm5ftpXuVmyUeDZjc17KEU5q9+zguI60XACuYO+JI10rhK/1ZaWIE9ucGhr5Gka +7dqOp7HUydHlvU2tiMqkpdlxVHA8jErHY0rWElN3awOpmkPA1AxSotZhDCmi6o9+EsPrTP +xx8dnflg5zZE2KRA== +-----END OPENSSH PRIVATE KEY----- diff --git a/go.mod b/go.mod index baa410a..5db8bfa 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/muesli/coral v1.0.0 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 ) require (