From 77143ad1e6770266b21248dd1a58296e2af3ace2 Mon Sep 17 00:00:00 2001
From: Andrey Petrov <andrey.petrov@shazow.net>
Date: Wed, 15 Apr 2020 14:19:28 -0400
Subject: [PATCH 1/4] main: Add --unsafe-passphrase

---
 cmd/ssh-chat/cmd.go | 46 +++++++++++++++++++++++++++++++++++----------
 sshd/auth.go        |  1 +
 2 files changed, 37 insertions(+), 10 deletions(-)

diff --git a/cmd/ssh-chat/cmd.go b/cmd/ssh-chat/cmd.go
index ac7639a..23db63e 100644
--- a/cmd/ssh-chat/cmd.go
+++ b/cmd/ssh-chat/cmd.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"bufio"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -9,6 +10,7 @@ import (
 	"os/signal"
 	"os/user"
 	"strings"
+	"time"
 
 	"github.com/alexcesaro/log"
 	"github.com/alexcesaro/log/golog"
@@ -19,23 +21,25 @@ import (
 	"github.com/shazow/ssh-chat/chat"
 	"github.com/shazow/ssh-chat/chat/message"
 	"github.com/shazow/ssh-chat/sshd"
+
+	_ "net/http/pprof"
 )
-import _ "net/http/pprof"
 
 // Version of the binary, assigned during build.
 var Version string = "dev"
 
 // Options contains the flag options
 type Options struct {
-	Verbose   []bool `short:"v" long:"verbose" description:"Show verbose logging."`
-	Version   bool   `long:"version" description:"Print version and exit."`
-	Identity  string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"`
-	Bind      string `long:"bind" description:"Host and port to listen on." default:"0.0.0.0:2022"`
-	Admin     string `long:"admin" description:"File of public keys who are admins."`
-	Whitelist string `long:"whitelist" description:"Optional file of public keys who are allowed to connect."`
-	Motd      string `long:"motd" description:"Optional Message of the Day file."`
-	Log       string `long:"log" description:"Write chat log to this file."`
-	Pprof     int    `long:"pprof" description:"Enable pprof http server for profiling."`
+	Verbose    []bool `short:"v" long:"verbose" description:"Show verbose logging."`
+	Version    bool   `long:"version" description:"Print version and exit."`
+	Identity   string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"`
+	Bind       string `long:"bind" description:"Host and port to listen on." default:"0.0.0.0:2022"`
+	Admin      string `long:"admin" description:"File of public keys who are admins."`
+	Whitelist  string `long:"whitelist" description:"Optional file of public keys who are allowed to connect."`
+	Passphrase string `long:"unsafe-passphrase" description:"Require an interactive passphrase to connect. Whitelist feature is more secure."`
+	Motd       string `long:"motd" description:"Optional Message of the Day file."`
+	Log        string `long:"log" description:"Write chat log to this file."`
+	Pprof      int    `long:"pprof" description:"Enable pprof http server for profiling."`
 }
 
 var logLevels = []log.Level{
@@ -110,6 +114,28 @@ func main() {
 	config.AddHostKey(signer)
 	config.ServerVersion = "SSH-2.0-Go ssh-chat"
 
+	if options.Passphrase != "" {
+		cb := config.KeyboardInteractiveCallback
+		config.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
+			perm, err := cb(conn, challenge)
+			if err != nil {
+				return perm, err
+			}
+			answers, err := challenge("", "", []string{"Passphrase required to connect: "}, []bool{true})
+			if err != nil {
+				return nil, err
+			}
+			if len(answers) == 1 && answers[0] == options.Passphrase {
+				// Success
+				return perm, nil
+			}
+			// It's not gonna do much but may as well throttle brute force attempts a little
+			time.Sleep(2 * time.Second)
+
+			return nil, errors.New("incorrect passphrase")
+		}
+	}
+
 	s, err := sshd.ListenSSH(options.Bind, config)
 	if err != nil {
 		fail(4, "Failed to listen on socket: %v\n", err)
diff --git a/sshd/auth.go b/sshd/auth.go
index afa7271..a140413 100644
--- a/sshd/auth.go
+++ b/sshd/auth.go
@@ -19,6 +19,7 @@ type Auth interface {
 }
 
 // MakeAuth makes an ssh.ServerConfig which performs authentication against an Auth implementation.
+// TODO: Switch to using ssh.AuthMethod instead?
 func MakeAuth(auth Auth) *ssh.ServerConfig {
 	config := ssh.ServerConfig{
 		NoClientAuth: false,

From b1bce027ad9948e217f85e200d9b760b0a79f294 Mon Sep 17 00:00:00 2001
From: Andrey Petrov <andrey.petrov@shazow.net>
Date: Thu, 16 Apr 2020 11:30:13 -0400
Subject: [PATCH 2/4] main: Force passphrase auth even with pubkey auth

---
 cmd/ssh-chat/cmd.go | 57 ++++++++++++++++++++++++++++++---------------
 1 file changed, 38 insertions(+), 19 deletions(-)

diff --git a/cmd/ssh-chat/cmd.go b/cmd/ssh-chat/cmd.go
index 23db63e..91813de 100644
--- a/cmd/ssh-chat/cmd.go
+++ b/cmd/ssh-chat/cmd.go
@@ -82,7 +82,8 @@ func main() {
 	}
 
 	logLevel := logLevels[numVerbose]
-	sshchat.SetLogger(golog.New(os.Stderr, logLevel))
+	logger := golog.New(os.Stderr, logLevel)
+	sshchat.SetLogger(logger)
 
 	if logLevel == log.Debug {
 		// Enable logging from submodules
@@ -113,27 +114,45 @@ func main() {
 	config := sshd.MakeAuth(auth)
 	config.AddHostKey(signer)
 	config.ServerVersion = "SSH-2.0-Go ssh-chat"
+	// FIXME: Should we be using config.NoClientAuth = true by default?
 
 	if options.Passphrase != "" {
-		cb := config.KeyboardInteractiveCallback
-		config.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
-			perm, err := cb(conn, challenge)
-			if err != nil {
-				return perm, err
-			}
-			answers, err := challenge("", "", []string{"Passphrase required to connect: "}, []bool{true})
-			if err != nil {
-				return nil, err
-			}
-			if len(answers) == 1 && answers[0] == options.Passphrase {
-				// Success
-				return perm, nil
-			}
-			// It's not gonna do much but may as well throttle brute force attempts a little
-			time.Sleep(2 * time.Second)
-
-			return nil, errors.New("incorrect passphrase")
+		if options.Whitelist != "" {
+			logger.Warning("Passphrase is disabled while whitelist is enabled.")
 		}
+		{
+			cb := config.KeyboardInteractiveCallback
+			config.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
+				perm, err := cb(conn, challenge)
+				if err != nil {
+					return perm, err
+				}
+				answers, err := challenge("", "", []string{"Passphrase required to connect: "}, []bool{true})
+				if err != nil {
+					return nil, err
+				}
+				if len(answers) == 1 && answers[0] == options.Passphrase {
+					// Success
+					return perm, nil
+				}
+				// It's not gonna do much but may as well throttle brute force attempts a little
+				time.Sleep(2 * time.Second)
+
+				return nil, errors.New("incorrect passphrase")
+			}
+		}
+		{
+			// We also need to override the PublicKeyCallback to prevent rando pubkeys from bypassing
+			cb := config.PublicKeyCallback
+			config.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
+				perms, err := cb(conn, key)
+				if err == nil {
+					err = errors.New("passphrase authentication required")
+				}
+				return perms, err
+			}
+		}
+
 	}
 
 	s, err := sshd.ListenSSH(options.Bind, config)

From 6e9705faf516f655226a90c6392e8f770feff9e5 Mon Sep 17 00:00:00 2001
From: Andrey Petrov <andrey.petrov@shazow.net>
Date: Thu, 16 Apr 2020 12:32:12 -0400
Subject: [PATCH 3/4] main: Clarify passphrase shenanigans

---
 cmd/ssh-chat/cmd.go | 57 ++++++++++++++++++++++-----------------------
 1 file changed, 28 insertions(+), 29 deletions(-)

diff --git a/cmd/ssh-chat/cmd.go b/cmd/ssh-chat/cmd.go
index 91813de..1a2134b 100644
--- a/cmd/ssh-chat/cmd.go
+++ b/cmd/ssh-chat/cmd.go
@@ -120,37 +120,36 @@ func main() {
 		if options.Whitelist != "" {
 			logger.Warning("Passphrase is disabled while whitelist is enabled.")
 		}
-		{
-			cb := config.KeyboardInteractiveCallback
-			config.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
-				perm, err := cb(conn, challenge)
-				if err != nil {
-					return perm, err
-				}
-				answers, err := challenge("", "", []string{"Passphrase required to connect: "}, []bool{true})
-				if err != nil {
-					return nil, err
-				}
-				if len(answers) == 1 && answers[0] == options.Passphrase {
-					// Success
-					return perm, nil
-				}
-				// It's not gonna do much but may as well throttle brute force attempts a little
-				time.Sleep(2 * time.Second)
-
-				return nil, errors.New("incorrect passphrase")
-			}
+		if config.KeyboardInteractiveCallback != nil {
+			fail(1, "Passphrase authentication conflicts with existing KeyboardInteractive setup.") // This should not happen
 		}
-		{
-			// We also need to override the PublicKeyCallback to prevent rando pubkeys from bypassing
-			cb := config.PublicKeyCallback
-			config.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
-				perms, err := cb(conn, key)
-				if err == nil {
-					err = errors.New("passphrase authentication required")
-				}
-				return perms, err
+
+		// We use KeyboardInteractiveCallback instead of PasswordCallback to
+		// avoid preventing the client from including a pubkey in the user
+		// identification.
+		config.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
+			answers, err := challenge("", "", []string{"Passphrase required to connect: "}, []bool{true})
+			if err != nil {
+				return nil, err
 			}
+			if len(answers) == 1 && answers[0] == options.Passphrase {
+				// Success
+				return nil, nil
+			}
+			// It's not gonna do much but may as well throttle brute force attempts a little
+			time.Sleep(2 * time.Second)
+
+			return nil, errors.New("incorrect passphrase")
+		}
+
+		// We also need to override the PublicKeyCallback to prevent rando pubkeys from bypassing
+		cb := config.PublicKeyCallback
+		config.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
+			perms, err := cb(conn, key)
+			if err == nil {
+				err = errors.New("passphrase authentication required")
+			}
+			return perms, err
 		}
 
 	}

From 99d303e19672dcbc11d61d431b15f104ea552999 Mon Sep 17 00:00:00 2001
From: Andrey Petrov <andrey.petrov@shazow.net>
Date: Thu, 16 Apr 2020 12:44:20 -0400
Subject: [PATCH 4/4] main: Add extraHelp

---
 cmd/ssh-chat/cmd.go | 36 ++++++++++++++++++++++++++----------
 1 file changed, 26 insertions(+), 10 deletions(-)

diff --git a/cmd/ssh-chat/cmd.go b/cmd/ssh-chat/cmd.go
index 1a2134b..abce387 100644
--- a/cmd/ssh-chat/cmd.go
+++ b/cmd/ssh-chat/cmd.go
@@ -30,18 +30,31 @@ var Version string = "dev"
 
 // Options contains the flag options
 type Options struct {
-	Verbose    []bool `short:"v" long:"verbose" description:"Show verbose logging."`
-	Version    bool   `long:"version" description:"Print version and exit."`
-	Identity   string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"`
-	Bind       string `long:"bind" description:"Host and port to listen on." default:"0.0.0.0:2022"`
-	Admin      string `long:"admin" description:"File of public keys who are admins."`
-	Whitelist  string `long:"whitelist" description:"Optional file of public keys who are allowed to connect."`
-	Passphrase string `long:"unsafe-passphrase" description:"Require an interactive passphrase to connect. Whitelist feature is more secure."`
-	Motd       string `long:"motd" description:"Optional Message of the Day file."`
-	Log        string `long:"log" description:"Write chat log to this file."`
-	Pprof      int    `long:"pprof" description:"Enable pprof http server for profiling."`
+	Verbose   []bool `short:"v" long:"verbose" description:"Show verbose logging."`
+	Version   bool   `long:"version" description:"Print version and exit."`
+	Identity  string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"`
+	Bind      string `long:"bind" description:"Host and port to listen on." default:"0.0.0.0:2022"`
+	Admin     string `long:"admin" description:"File of public keys who are admins."`
+	Whitelist string `long:"whitelist" description:"Optional file of public keys who are allowed to connect."`
+	Motd      string `long:"motd" description:"Optional Message of the Day file."`
+	Log       string `long:"log" description:"Write chat log to this file."`
+	Pprof     int    `long:"pprof" description:"Enable pprof http server for profiling."`
+
+	// Hidden flags, because they're discouraged from being used casually.
+	Passphrase string `long:"unsafe-passphrase" description:"Require an interactive passphrase to connect. Whitelist feature is more secure." hidden:"true"`
 }
 
+const extraHelp = `There are hidden options and easter eggs in ssh-chat. The source code is a good
+place to start looking. Some useful links:
+
+* Project Repository:
+  https://github.com/shazow/ssh-chat
+* Project Wiki FAQ:
+  https://github.com/shazow/ssh-chat/wiki/FAQ
+* Command Flags Declaration:
+  https://github.com/shazow/ssh-chat/blob/master/cmd/ssh-chat/cmd.go#L29
+`
+
 var logLevels = []log.Level{
 	log.Warning,
 	log.Info,
@@ -61,6 +74,9 @@ func main() {
 		if p == nil {
 			fmt.Print(err)
 		}
+		if flagErr, ok := err.(*flags.Error); ok && flagErr.Type == flags.ErrHelp {
+			fmt.Print(extraHelp)
+		}
 		return
 	}