Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use huh to ask passphrases #119

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 23 additions & 55 deletions cmd/melt/main.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
package main

import (
"bytes"
"crypto/ed25519"
"encoding/pem"
"errors"
"fmt"
"io"
"os"
"strings"

"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/melt"
"github.com/charmbracelet/x/sshkey"
"github.com/mattn/go-isatty"
"github.com/mattn/go-tty"
mcobra "github.com/muesli/mango-cobra"
"github.com/muesli/reflow/wordwrap"
"github.com/muesli/roff"
Expand Down Expand Up @@ -68,7 +67,7 @@
keyPath = args[0]
}

mnemonic, err := backup(keyPath, nil)
mnemonic, err := backup(keyPath)
if err != nil {
return err
}
Expand Down Expand Up @@ -206,16 +205,7 @@
return f, nil
}

func parsePrivateKey(bts, pass []byte) (interface{}, error) {
if len(pass) == 0 {
//nolint: wrapcheck
return ssh.ParseRawPrivateKey(bts)
}
//nolint: wrapcheck
return ssh.ParseRawPrivateKeyWithPassphrase(bts, pass)
}

func backup(path string, pass []byte) (string, error) {
func backup(path string) (string, error) {
f, err := openFileOrStdin(path)
if err != nil {
return "", fmt.Errorf("could not read key: %w", err)
Expand All @@ -226,14 +216,7 @@
return "", fmt.Errorf("could not read key: %w", err)
}

key, err := parsePrivateKey(bts, pass)
if err != nil && isPasswordError(err) {
pass, err := askKeyPassphrase(path)
if err != nil {
return "", err
}
return backup(path, pass)
}
key, err := sshkey.ParseRaw(path, bts)
if err != nil {
return "", fmt.Errorf("could not parse key: %w", err)
}
Expand All @@ -247,11 +230,6 @@
}
}

func isPasswordError(err error) bool {
var kerr *ssh.PassphraseMissingError
return errors.As(err, &kerr)
}

func marshallPrivateKey(key ed25519.PrivateKey, pass []byte) (*pem.Block, error) {
if len(pass) == 0 {
//nolint: wrapcheck
Expand Down Expand Up @@ -385,40 +363,30 @@
return wl
}

func readPassword(msg string) ([]byte, error) {
_, _ = fmt.Fprint(os.Stderr, msg)
t, err := tty.Open()
if err != nil {
return nil, fmt.Errorf("could not open tty: %w", err)
}
defer t.Close() //nolint: errcheck
pass, err := term.ReadPassword(int(t.Input().Fd()))
if err != nil {
return nil, fmt.Errorf("could not read passphrase: %w", err)
}
return pass, nil
}

func askKeyPassphrase(path string) ([]byte, error) {
defer fmt.Fprintf(os.Stderr, "\n")
return readPassword(fmt.Sprintf("Enter the passphrase to unlock %q: ", path))
}

func askNewPassphrase() ([]byte, error) {
defer fmt.Fprintf(os.Stderr, "\n")
pass, err := readPassword("Enter new passphrase (empty for no passphrase): ")
if err != nil {
var pass, confirm string
if err := huh.Run(
huh.NewInput().
Title("Enter new passphrase (empty for no passphrase): ").
Value(&pass).
EchoMode(huh.EchoModePassword).
Inline(true),
); err != nil {
return nil, err
}

confirm, err := readPassword("\nEnter same passphrase again: ")
if err != nil {
return nil, fmt.Errorf("could not read password confirmation for key: %w", err)
if err := huh.Run(
huh.NewInput().
Title("Enter same passphrase again: ").
Value(&confirm).
EchoMode(huh.EchoModePassword).
Inline(true),
); err != nil {
return nil, err

Check failure on line 384 in cmd/melt/main.go

View workflow job for this annotation

GitHub Actions / lint-soft

error returned from external package is unwrapped: sig: func github.com/charmbracelet/huh.Run(field github.com/charmbracelet/huh.Field) error (wrapcheck)
}

if !bytes.Equal(pass, confirm) {
if pass != confirm {
return nil, fmt.Errorf("Passphareses do not match")
}

return pass, nil
return []byte(pass), nil
}
20 changes: 9 additions & 11 deletions cmd/melt/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"encoding/hex"
"os"
"path/filepath"
"runtime"
"strings"
"testing"

Expand All @@ -25,42 +24,41 @@ func TestBackupRestoreKnownKey(t *testing.T) {
const expectedFingerprint = "SHA256:tX0ZrsNLIB/ZlRK3vy/HsWIIkyBNhYhCSGmtqtxJcWo"

t.Run("backup", func(t *testing.T) {
mnemonic, err := backup("testdata/id_ed25519", nil)
mnemonic, err := backup("testdata/id_ed25519")
is := is.New(t)
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", nil)
_, err := backup("nope")
is.New(t).True(err != nil)
})

t.Run("backup invalid ssh key", func(t *testing.T) {
_, err := backup("testdata/not-a-key", nil)
_, err := backup("testdata/not-a-key")
is.New(t).True(err != nil)
})

t.Run("backup key of another type", func(t *testing.T) {
_, err := backup("testdata/id_rsa", nil)
_, err := backup("testdata/id_rsa")
is.New(t).True(err != nil)
})

t.Run("backup key without password", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skipf("it keeps waiting on a tty for the password")
}
_, err := backup("testdata/pwd_id_ed25519", nil)
t.Skipf("this keeps waiting for a passphrase")
_, err := backup("testdata/pwd_id_ed25519")
is := is.New(t)
is.True(err != nil)
})

t.Run("backup key with password", func(t *testing.T) {
t.Skipf("this keeps waiting for a passphrase")
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"))
mnemonic, err := backup("testdata/pwd_id_ed25519")
is := is.New(t)
is.NoErr(err)
is.Equal(mnemonic, strings.Join(strings.Fields(expectedMnemonic), " "))
Expand Down Expand Up @@ -166,7 +164,7 @@ func TestBackupRestoreKnownKeyInJapanse(t *testing.T) {
})

t.Run("backup", func(t *testing.T) {
mnemonic, err := backup("testdata/id_ed25519", nil)
mnemonic, err := backup("testdata/id_ed25519")
is := is.New(t)
is.NoErr(err)
is.Equal(mnemonic, strings.Join(strings.Fields(expectedMnemonic), " "))
Expand Down
20 changes: 18 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ module github.com/charmbracelet/melt
go 1.18

require (
github.com/charmbracelet/huh v0.4.2
github.com/charmbracelet/lipgloss v0.11.0
github.com/charmbracelet/x/sshkey v0.2.0
github.com/matryer/is v1.4.1
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-tty v0.0.5
github.com/muesli/mango-cobra v1.2.0
github.com/muesli/reflow v0.3.0
github.com/muesli/roff v0.1.0
Expand All @@ -19,14 +20,29 @@ require (
)

require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/x/ansi v0.1.1 // indirect
github.com/catppuccin/go v0.2.0 // indirect
github.com/charmbracelet/bubbles v0.18.0 // indirect
github.com/charmbracelet/bubbletea v0.26.4 // indirect
github.com/charmbracelet/x/ansi v0.1.2 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a // indirect
github.com/charmbracelet/x/input v0.1.1 // indirect
github.com/charmbracelet/x/term v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.1.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/mango v0.2.0 // indirect
github.com/muesli/mango-pflag v0.1.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
)
50 changes: 39 additions & 11 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,27 +1,52 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
github.com/charmbracelet/bubbletea v0.26.4 h1:2gDkkzLZaTjMl/dQBpNVtnvcCxsh/FCkimep7FC9c40=
github.com/charmbracelet/bubbletea v0.26.4/go.mod h1:P+r+RRA5qtI1DOHNFn0otoNwB4rn+zNAzSj/EXz6xU0=
github.com/charmbracelet/huh v0.4.2 h1:5wLkwrA58XDAfEZsJzNQlfJ+K8N9+wYwvR5FOM7jXFM=
github.com/charmbracelet/huh v0.4.2/go.mod h1:g9OXBgtY3zRV4ahnVih9bZE+1yGYN+y2C9Q6L2P+WM0=
github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g=
github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8=
github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk=
github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY=
github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a h1:lOpqe2UvPmlln41DGoii7wlSZ/q8qGIon5JJ8Biu46I=
github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a h1:k/s6UoOSVynWiw7PlclyGO2VdVs5ZLbMIHiGp4shFZE=
github.com/charmbracelet/x/input v0.1.1 h1:YDOJaTUKCqtGnq9PHzx3pkkl4pXDOANUHmhH3DqMtM4=
github.com/charmbracelet/x/input v0.1.1/go.mod h1:jvdTVUnNWj/RD6hjC4FsoB0SeZCJ2ZBkiuFP9zXvZI0=
github.com/charmbracelet/x/sshkey v0.2.0 h1:ryAepb1Kxi7A/EuLKdFgc7yDXaWn71bmRKndkUC6fZg=
github.com/charmbracelet/x/sshkey v0.2.0/go.mod h1:tByW6mjCccNHWoZOoLeOQwLHJuOUK2kfWqutnIzJe8M=
github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw=
github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg=
github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-tty v0.0.5 h1:s09uXI7yDbXzzTTfw3zonKFzwGkyYlgU3OMjqA0ddz4=
github.com/mattn/go-tty v0.0.5/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3pxse28=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg=
Expand All @@ -45,16 +70,19 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
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.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
Expand Down
Loading