Skip to content

Commit

Permalink
Resolves #14, Unable to save a password on headless Linux (#15)
Browse files Browse the repository at this point in the history
* πŸ› Fix issue #14 headless linux
* πŸ› Fix: check if GUI available on Linux

On Linux, gut uses gnome-keyring to save passwords. To unlock the store, gnome-keyring opens a pop-up. However, this pop-up cannot be shown when using a headless Linux server.

To solve this issue, I had to rewrite the logic to save, delete, and retrieve passwords.

If a GUI is available on Linux, gut will continue to work with gnome-keyring. However, if a GUI is not available, it will work with pass through 99designs/keyring. It is important to note that using pass requires some setup, and therefore gut will guide the user through this process.

On other platforms, gut will continue to use the zalando/go-keyring library.
  • Loading branch information
julien040 authored Feb 21, 2023
1 parent 88717d3 commit bdcd66e
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 6 deletions.
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/julien040/gut
go 1.19

require (
github.com/99designs/keyring v1.2.2
github.com/AlecAivazis/survey/v2 v2.3.6
github.com/BurntSushi/toml v1.2.1
github.com/briandowns/spinner v1.20.0
Expand All @@ -15,10 +16,15 @@ require (
)

require (
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/alessio/shellescape v1.4.1 // indirect
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/stretchr/testify v1.8.0 // indirect
)

Expand Down
13 changes: 13 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk=
github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw=
github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
Expand Down Expand Up @@ -45,6 +49,8 @@ github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnG
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM=
github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
Expand All @@ -65,11 +71,15 @@ github.com/go-git/go-git-fixtures/v4 v4.3.1 h1:y5z6dd3qi8Hl+stezc8p3JxDkoTRqMAlK
github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
github.com/go-git/go-git/v5 v5.5.1 h1:5vtv2TB5PM/gPM+EvsHJ16hJh4uAkdGcKilcwY7FYwo=
github.com/go-git/go-git/v5 v5.5.1/go.mod h1:uz5PQ3d0gz7mSgzZhSJToM6ALPaKCdSnl58/Xb5hzr8=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
Expand Down Expand Up @@ -111,6 +121,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/pjbgf/sha1cd v0.2.3 h1:uKQP/7QOzNtKYH7UTohZLcjF5/55EnTw0jO/Ru4jZwI=
Expand Down Expand Up @@ -228,6 +240,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
Expand Down
219 changes: 213 additions & 6 deletions src/profile/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"

"github.com/julien040/gut/src/print"
nanoid "github.com/matoous/go-nanoid/v2"

keyringLinux "github.com/99designs/keyring"
"github.com/BurntSushi/toml"
keyring "github.com/zalando/go-keyring"
)
Expand All @@ -34,13 +38,15 @@ var configPath string
var profiles []Profile

func exit(err error, message string) {
print.Message(message, "error")
print.Message(message, print.Error)
fmt.Println(err)
os.Exit(1)
}

const serviceName = "gut"

var ring keyringLinux.Keyring

// Init a config file for the profiles and load it into the config package
func init() {
// Get user home directory
Expand All @@ -51,6 +57,12 @@ func init() {
// Path to the config file
configPath = filepath.Join(home, "/.gut/", "profiles.toml")

// Init keyring
ring, _ = keyringLinux.Open(keyringLinux.Config{
ServiceName: serviceName,
AllowedBackends: []keyringLinux.BackendType{keyringLinux.PassBackend},
})

// Check if .gut directory exists
if _, err := os.Stat(filepath.Join(home, "/.gut/")); os.IsNotExist(err) {
// Create .gut directory
Expand Down Expand Up @@ -95,9 +107,13 @@ func init() {

for key, val := range data {
// Get password from keyring
password, err := keyring.Get(serviceName, key)
var password string
var err error

password, err = retrievePassword(key)
if err != nil {
print.Message("The profile "+key+" doesn't have a password, I'll skip it", print.Warning)
print.Message("I can't retrieve the password for the profile %s, I'll skip it", print.Warning, key)
print.Message("Error: %s", print.Error, err.Error())
continue
}
val := val.(map[string]interface{})
Expand Down Expand Up @@ -127,7 +143,7 @@ func init() {
Id: key,
Alias: alias,
Username: username,
Password: string(password),
Password: password,
Website: website,
Email: email,
})
Expand Down Expand Up @@ -168,14 +184,204 @@ func saveFile() {

}

// Check if an executable is in the path
//
// Intended to check if a command is available
func isExecInPath(executable string) bool {
_, err := exec.LookPath(executable)
return err == nil
}

// Check if the user is using a GUI on Linux. Returns true if yes.
//
// We have to check because gnome-keyring doesn't work on a server (requires to fill in a popup)
func checkGUIOnLinux() bool {
// Try to find the Xorg executable
// https://unix.stackexchange.com/a/237750 CC BY-SA 3.0

// We use lookPath rather than "type Xorg" because I think type is a shell builtin
// Go'll try to find the executable in the path to run it

_, err := exec.LookPath("Xorg")
fmt.Println(err)
return err == nil

}

// Check if a folder exists
func checkFolderExists(path string) bool {
// https://gist.github.com/mattes/d13e273314c3b3ade33f
if _, err := os.Stat(path); os.IsNotExist(err) {
return false
}
return true

}

// Save the password in the keyring
//
// If the user is using a GUI on Linux, we use the default gnome-keyring.
// If not, we use pass.
// On other OS, we use the default keyring defined by zalando/go-keyring
func savePassword(id string, password string) {
// https://github.com/julien040/gut/issues/14
if runtime.GOOS == "linux" {
// Check if the user is using a GUI
// If yes, we use the default gnome-keyring
guiAvailable := checkGUIOnLinux()
if guiAvailable {
// Check if gnome-keyring is installed
if !isExecInPath("gnome-keyring-daemon") {
print.Message("To install gnome-keyring-daemon, run: sudo apt install gnome-keyring", print.None)
print.Message("If you use YUM, run: sudo yum install gnome-keyring", print.None)
exit(nil, "I can't find gnome-keyring-daemon in your path. Please install it πŸ˜“")
}

// We use the default gnome-keyring
err := keyring.Set(serviceName, id, password)
if err != nil {
exit(err, "I can't save the password in the keyring πŸ˜“")
}

// If not, we use pass.
} else {
// Check if pass is installed
if isExecInPath("pass") {
// We check if the password store exists
homeDir, err := os.UserHomeDir()
if err != nil {
exit(err, "I can't get your home directory πŸ˜“")
}
dirPassFolder := path.Join(homeDir, ".password-store")
if !checkFolderExists(dirPassFolder) {
// We prompt the user to create the password store
print.Message("Please set up a password store with pass. To do so, follow this guide: https://gut-cli.dev/error/setup-pass-store", print.None)

os.Exit(1)
} else {
// We save the password in the password store
err := ring.Set(keyringLinux.Item{
Key: id,
Data: []byte(password),
Label: "Password for " + id,
})
if err != nil {
exit(err, "I can't save the password in the keyring πŸ˜“")
}

}

// If not, we explain to the user how to install it on his distro
} else {
exit(errors.New("pass not installed"), "Please install pass with your package manager (https://www.passwordstore.org/#download)")
}
}

} else {
err := keyring.Set(serviceName, id, password)
if err != nil {
exit(err, "I can't save the password in the keyring πŸ˜“")
}
}
}

func retrievePassword(id string) (string, error) {
// https://github.com/julien040/gut/issues/14
if runtime.GOOS == "linux" {
// Check if the user is using a GUI
// If yes, we use the default gnome-keyring
guiAvailable := checkGUIOnLinux()
if guiAvailable {
// We use the default gnome-keyring
password, err := keyring.Get(serviceName, id)
return password, err
} else {
// If not, we use pass.
// Check if pass is installed
if isExecInPath("pass") {
// We check if the password store exists
homeDir, err := os.UserHomeDir()
if err != nil {
return "", errors.New("unable to get your home directory")
}
dirPassFolder := path.Join(homeDir, ".password-store")
if !checkFolderExists(dirPassFolder) {
// We prompt the user to create the password store
print.Message("Please set up a password store with pass. To do so, follow this guide: https://gut-cli.dev/error/setup-pass-store", print.None)
return "", errors.New("pass is not set up")
} else {
// We retrieve the password from the password store
password, err := ring.Get(id)
if err != nil {
print.Message("Unlock your password store with pass first", print.Info)
print.Message("You can do it with the following command:", print.Info)
print.Message(" pass show "+id, print.None)
print.Message("To learn more about this, follow this guide: https://gut-cli.dev/error/unlock-pass-store", print.None)
return "", errors.New("unable to retrieve the password from the keyring")
}
return string(password.Data), nil
}
} else {
print.Message("To install pass, follow this guide: https://www.passwordstore.org/#download", print.None)
return "", errors.New("pass not installed")
}
}

} else {
password, err := keyring.Get(serviceName, id)
if err != nil {
return "", err
}
return password, nil
}
}

func deletePassword(id string) error {
if runtime.GOOS == "linux" {
// Check if the user is using a GUI
// If yes, we use the default gnome-keyring
guiAvailable := checkGUIOnLinux()
if guiAvailable {
// We use the default gnome-keyring
return keyring.Delete(serviceName, id)
} else {
// If not, we use pass.
// Check if pass is installed
if isExecInPath("pass") {
// We check if the password store exists
homeDir, err := os.UserHomeDir()
if err != nil {
return errors.New("unable to get your home directory")
}
dirPassFolder := path.Join(homeDir, ".password-store")
if !checkFolderExists(dirPassFolder) {
// We prompt the user to create the password store
return errors.New("pass is not set up")
} else {
// We delete the password from the password store
return ring.Remove(id)
}
} else {
fmt.Println("To install pass, follow this guide: https://www.passwordstore.org/#download")
return errors.New("pass not installed")
}
}

} else {
return keyring.Delete(serviceName, id)
}
}

// Add a profile to the config file and return the id
func AddProfile(profile Profile) string {
id, err := nanoid.New()
if err != nil {
exit(err, "Sorry, I can't generate an id πŸ˜“")
}

err = keyring.Set(serviceName, id, profile.Password)
// Save password in the keyring
savePassword(id, profile.Password)

if err != nil {
exit(err, "Sorry, I can't save the password in the keyring πŸ˜“")
}
Expand Down Expand Up @@ -203,7 +409,8 @@ func RemoveProfile(id string) {
}
}
// Remove password from the keyring
err := keyring.Delete(serviceName, id)
err := deletePassword(id)

if err != nil {
exit(err, "Sorry, I can't remove the password from the keyring πŸ˜“")
}
Expand Down

0 comments on commit bdcd66e

Please sign in to comment.