From c7635a1ef05aa8c3e26a939e95da225156a5ef74 Mon Sep 17 00:00:00 2001 From: Randall Winkhart Date: Fri, 17 May 2024 11:42:44 -0400 Subject: [PATCH] Implement initial support for passphrase-protected SSH keyfiles --- src/backend/configParser.go | 3 ++- src/cli/init.go | 7 +++++-- src/cli/utilitiesMisc.go | 4 ++-- src/sync/client.go | 36 +++++++++++++++++++++++------------- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/backend/configParser.go b/src/backend/configParser.go index 7655e72..bc933df 100644 --- a/src/backend/configParser.go +++ b/src/backend/configParser.go @@ -48,5 +48,6 @@ func ReadConfig(readKeys []string, missingValueError string) []string { // sshUser = // sshIP = // sshPort = -// sshIdentity = +// sshKey = +// sshKeyProtected = // netPinEnabled = diff --git a/src/cli/init.go b/src/cli/init.go index b1508c9..c3a2b42 100644 --- a/src/cli/init.go +++ b/src/cli/init.go @@ -5,6 +5,7 @@ import ( "github.com/rwinkhart/MUTN/src/backend" "github.com/rwinkhart/MUTN/src/sync" "os" + "strconv" ) // TempInitCli initializes the MUTN environment based on user input @@ -31,13 +32,15 @@ func TempInitCli() { configSSH := inputBinary("Configure SSH settings (for synchronization)?") if configSSH { // necessary SSH info + fmt.Print(AnsiBold+"Note:"+backend.AnsiReset, "Only key-based authentication is supported (keys may optionally be passphrase-protected).\nThe remote server must already be in your ~/.ssh/known_hosts file.\n\n") sshUser := input("Remote SSH username:") sshIP := input("Remote SSH IP address:") sshPort := input("Remote SSH port:") - sshIdentity := input("SSH private identity file path:") // TODO implement generator and selector + sshKey := input("SSH private identity file path:") // TODO implement generator and selector + sshKeyProtected := inputBinary("Is the identity file password-protected?") // write config file - backend.TempInit(map[string]string{"textEditor": textEditor, "gpgID": gpgID, "sshUser": sshUser, "sshIP": sshIP, "sshPort": sshPort, "sshIdentity": sshIdentity}) + backend.TempInit(map[string]string{"textEditor": textEditor, "gpgID": gpgID, "sshUser": sshUser, "sshIP": sshIP, "sshPort": sshPort, "sshIdentity": sshKey, "sshIDProtected": strconv.FormatBool(sshKeyProtected)}) // generate device ID sync.DeviceIDGen() diff --git a/src/cli/utilitiesMisc.go b/src/cli/utilitiesMisc.go index 2ebad10..8c68c88 100644 --- a/src/cli/utilitiesMisc.go +++ b/src/cli/utilitiesMisc.go @@ -25,8 +25,8 @@ func input(prompt string) string { return strings.TrimRight(userInput, "\n\r ") // remove trailing newlines, carriage returns, and spaces } -// inputHidden prompts the user for input and returns the input as a string, hiding the input from the terminal -func inputHidden(prompt string) string { +// InputHidden prompts the user for input and returns the input as a string, hiding the input from the terminal +func InputHidden(prompt string) string { fmt.Print("\n" + prompt + " ") byteInput, _ := terminal.ReadPassword(int(os.Stdin.Fd())) password := string(byteInput) diff --git a/src/sync/client.go b/src/sync/client.go index 5d0f906..5eb7015 100644 --- a/src/sync/client.go +++ b/src/sync/client.go @@ -3,6 +3,7 @@ package sync import ( "fmt" "github.com/rwinkhart/MUTN/src/backend" + "github.com/rwinkhart/MUTN/src/cli" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/knownhosts" "os" @@ -18,18 +19,18 @@ const ( ) // GetSSHOutput runs a command over SSH and returns the output -// currently only supports password-less key-based authentication TODO add password support, still require key +// only supports key-based authentication (passphrase-protected keys are supported in a CLI environment) TODO create a more generic interface for passphrase input func GetSSHOutput(cmd string, manualSync bool) string { // get SSH config info, exit if not configured (displaying an error if the sync job was called manually) - var sshUserIPPortIdentity []string + var sshUserConfig []string if manualSync { - sshUserIPPortIdentity = backend.ReadConfig([]string{"sshUser", "sshIP", "sshPort", "sshIdentity"}, "SSH settings not configured - run \"mutn init\" to configure") + sshUserConfig = backend.ReadConfig([]string{"sshUser", "sshIP", "sshPort", "sshKey", "sshKeyProtected"}, "SSH settings not configured - run \"mutn init\" to configure") } else { - sshUserIPPortIdentity = backend.ReadConfig([]string{"sshUser", "sshIP", "sshPort", "sshIdentity"}, "0") + sshUserConfig = backend.ReadConfig([]string{"sshUser", "sshIP", "sshPort", "sshKey", "sshKeyProtected"}, "0") } - var user, ip, port, identity string - for i, key := range sshUserIPPortIdentity { + var user, ip, port, keyFile, keyFileProtected string + for i, key := range sshUserConfig { switch i { case 0: user = key @@ -38,19 +39,28 @@ func GetSSHOutput(cmd string, manualSync bool) string { case 2: port = key case 3: - identity = key + keyFile = key + case 4: + keyFileProtected = key } } - // read and parse private key - key, err := os.ReadFile(identity) + // read private key + key, err := os.ReadFile(keyFile) if err != nil { - fmt.Println(backend.AnsiError+"Sync failed - unable to read private key file:", identity+backend.AnsiReset) + fmt.Println(backend.AnsiError+"Sync failed - unable to read private key file:", keyFile+backend.AnsiReset) os.Exit(1) } - parsedKey, err := ssh.ParsePrivateKey(key) + + // parse private key + var parsedKey ssh.Signer + if keyFileProtected != "true" { + parsedKey, err = ssh.ParsePrivateKey(key) + } else { + parsedKey, err = ssh.ParsePrivateKeyWithPassphrase(key, []byte(cli.InputHidden("Enter passphrase for \""+keyFile+"\":"))) // TODO test passphrase-protected keys + } if err != nil { - fmt.Println(backend.AnsiError+"Sync failed - Unable to parse private key:", identity+backend.AnsiReset) + fmt.Println(backend.AnsiError+"Sync failed - Unable to parse private key:", keyFile+backend.AnsiReset) os.Exit(1) } @@ -67,7 +77,7 @@ func GetSSHOutput(cmd string, manualSync bool) string { Auth: []ssh.AuthMethod{ ssh.PublicKeys(parsedKey), }, - HostKeyCallback: hostKeyCallback, // TODO notify user that the server must already be in known_hosts + HostKeyCallback: hostKeyCallback, } // connect to SSH server