From 5da70447b080257d8005e92dea80cf9428d8c54f Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Wed, 4 Apr 2018 15:43:27 -0700 Subject: [PATCH] MSYS+mintty support; pkg renaming to hkexsh --- demo/clientp.go | 20 --- demo/hkexpasswd/hkexpasswd.go~ | 128 ----------------- demo/serverp.go | 85 ------------ herradurakex.go | 13 +- hkexauth.go | 124 +---------------- hkexchan.go | 3 +- hkexnet.go | 2 +- {demo/hkexpasswd => hkexpasswd}/hkexpasswd.go | 6 +- demo/client/client.go => hkexsh/hkexsh.go | 10 +- hkexsh/mintty_wrapper.sh | 32 +++++ demo/server/server.go => hkexshd/hkexshd.go | 12 +- {demo/spinsult => spinsult}/spinsult.go | 0 {demo/spinsult => spinsult}/spinsult_test.go | 0 termmode_unix.go | 130 ++++++++++++++++++ termmode_windows.go | 93 +++++++++++++ 15 files changed, 281 insertions(+), 377 deletions(-) delete mode 100644 demo/clientp.go delete mode 100644 demo/hkexpasswd/hkexpasswd.go~ delete mode 100644 demo/serverp.go rename {demo/hkexpasswd => hkexpasswd}/hkexpasswd.go (95%) rename demo/client/client.go => hkexsh/hkexsh.go (93%) create mode 100644 hkexsh/mintty_wrapper.sh rename demo/server/server.go => hkexshd/hkexshd.go (96%) rename {demo/spinsult => spinsult}/spinsult.go (100%) rename {demo/spinsult => spinsult}/spinsult_test.go (100%) create mode 100644 termmode_unix.go create mode 100644 termmode_windows.go diff --git a/demo/clientp.go b/demo/clientp.go deleted file mode 100644 index 4805bed..0000000 --- a/demo/clientp.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import ( - "fmt" - - "net" -) - -func main() { - conn, err := net.Dial("tcp", "localhost:2000") - if err != nil { - // handle error - fmt.Println("Err!") - } - fmt.Fprintf(conn, "\x01\x02\x03\x04") - //fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n") - //status, err := bufio.NewReader(conn).ReadString('\n') - //_, err = bufio.NewReader(conn).ReadString('\n') - // ... -} diff --git a/demo/hkexpasswd/hkexpasswd.go~ b/demo/hkexpasswd/hkexpasswd.go~ deleted file mode 100644 index 2e3269e..0000000 --- a/demo/hkexpasswd/hkexpasswd.go~ +++ /dev/null @@ -1,128 +0,0 @@ -// Util to generate/store passwords for users in a file akin to /etc/passwd -// suitable for the demo hkexsh server, using bcrypt. -package main - -import ( - "bytes" - "encoding/csv" - "flag" - "fmt" - "io/ioutil" - "log" - "os" - "os/user" - - hkex "github.com/Russtopia/herradurakex" - "github.com/jameskeane/bcrypt" -) - -func main() { - var pfName string - var newpw string - var confirmpw string - var userName string - - flag.StringVar(&userName, "u", "", "username") - flag.StringVar(&pfName, "f", "/etc/hkexsh.passwd", "passwd file") - flag.Parse() - - var uname string - if len(userName) == 0 { - log.Println("specify username with -u") - os.Exit(1) - } - - u, err := user.Lookup(userName) - if err != nil { - log.Printf("Invalid user %s\n", userName) - log.Fatal(err) - } - uname = u.Username - - fmt.Printf("New Password:") - ab, err := hkex.ReadPassword(int(os.Stdin.Fd())) - fmt.Printf("\r\n") - if err != nil { - log.Fatal(err) - os.Exit(1) - } - newpw = string(ab) - - fmt.Printf("Confirm:") - ab, err = hkex.ReadPassword(int(os.Stdin.Fd())) - fmt.Printf("\r\n") - if err != nil { - log.Fatal(err) - os.Exit(1) - } - confirmpw = string(ab) - - if confirmpw != newpw { - log.Println("New passwords do not match.") - os.Exit(1) - } - - // generate a random salt with specific rounds of complexity - // (default in jameskeane/bcrypt is 12 but we'll be explicit here) - salt, err := bcrypt.Salt(12) - if err != nil { - fmt.Println("ERROR: bcrypt.Salt() failed.") - os.Exit(2) - } - - // hash and verify a password with explicit (random) salt - hash, err := bcrypt.Hash(newpw, salt) - if err != nil || !bcrypt.Match(newpw, hash) { - fmt.Println("ERROR: bcrypt.Match() failed.") - log.Fatal(err) - } - //fmt.Println("Salt:", salt, "Hash:", hash) - - b, err := ioutil.ReadFile(pfName) - if err != nil { - log.Fatal(err) - } - r := csv.NewReader(bytes.NewReader(b)) - - r.Comma = ':' - r.Comment = '#' - r.FieldsPerRecord = 4 // username:salt:authCookie:disallowedCmdList (a,b,...) - - records, err := r.ReadAll() - if err != nil { - log.Fatal(err) - } - for i, _ := range records { - //fmt.Println(records[i]) - if records[i][0] == uname { - records[i][1] = salt - records[i][2] = hash - } - //// csv lib doesn't preserve comment in record, so put it back - //if records[i][0][0] == '!' { - // records[i][0] = "#" + records[i][0] - //} - } - - outFile, err := ioutil.TempFile("", "hkexsh-passwd") - if err != nil { - log.Fatal(err) - } - w := csv.NewWriter(outFile) - w.Comma = ':' - //w.FieldsPerRecord = 4 // username:salt:authCookie:disallowedCmdList (a,b,...) - w.Write([]string{"#username", "salt", "authCookie", "disallowedCmdList"}) - w.WriteAll(records) - if err = w.Error(); err != nil { - log.Fatal(err) - } - - err = os.Remove(pfName) - if err != nil { - log.Fatal(err) - } - err = os.Rename(outFile.Name(), pfName) - if err != nil { - log.Fatal(err) - } -} diff --git a/demo/serverp.go b/demo/serverp.go deleted file mode 100644 index 58781f0..0000000 --- a/demo/serverp.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -import ( - "fmt" - "log" - "time" - - "net" -) - -func main() { - // Listen on TCP port 2000 on all available unicast and - // anycast IP addresses of the local system. - l, err := net.Listen("tcp", ":2000") - if err != nil { - log.Fatal(err) - } - defer l.Close() - - fmt.Println("Serving on port 2000") - for { - // Wait for a connection. - conn, err := l.Accept() - if err != nil { - log.Fatal(err) - } - - fmt.Println("Accepted client") - - // Handle the connection in a new goroutine. - // The loop then returns to accepting, so that - // multiple connections may be served concurrently. - go func(c net.Conn) (e error) { - ch := make(chan []byte) - chN := 0 - eCh := make(chan error) - - // Start a goroutine to read from our net connection - go func(ch chan []byte, eCh chan error) { - for { - // try to read the data - data := make([]byte, 512) - chN, err = c.Read(data) - if err != nil { - // send an error if it's encountered - eCh <- err - return - } - // send data if we read some. - ch <- data[0:chN] - } - }(ch, eCh) - - ticker := time.Tick(time.Second) - Term: - // continuously read from the connection - for { - select { - // This case means we recieved data on the connection - case data := <-ch: - // Do something with the data - fmt.Printf("Client sent %+v\n", data[0:chN]) - //fmt.Printf("Client sent %s\n", string(data)) - // This case means we got an error and the goroutine has finished - case err := <-eCh: - // handle our error then exit for loop - if err.Error() == "EOF" { - fmt.Printf("[Client disconnected]\n") - } else { - fmt.Printf("Error reading client data! (%+v)\n", err) - } - break Term - // This will timeout on the read. - case <-ticker: - // do nothing? this is just so we can time out if we need to. - // you probably don't even need to have this here unless you want - // do something specifically on the timeout. - } - } - // Shut down the connection. - c.Close() - return - }(conn) - } -} diff --git a/herradurakex.go b/herradurakex.go index 9e38b8d..1f57da5 100644 --- a/herradurakex.go +++ b/herradurakex.go @@ -1,11 +1,16 @@ -// Package herradurakex - socket lib conforming to +// Package hkexsh - socket lib conforming to // golang.org/pkg/net Conn interface, with // experimental key exchange algorithm by -// Omar Alejandro Herrera Reyna +// Omar Alejandro Herrera Reyna. +// // (https://github.com/Caume/HerraduraKEx) +// +// Demonstration server (hkexshd) and +// client (hkexsh) + // // See README.md for full license info. -package herradurakex +package hkexsh /* Herradura - a Key exchange scheme in the style of Diffie-Hellman Key Exchange. Copyright (C) 2017 Omar Alejandro Herrera Reyna @@ -22,7 +27,7 @@ package herradurakex You should have received a copy of the GNU General Public License along with this program. If not, see . - + golang implementation by Russ Magee (rmagee_at_gmail.com) */ /* This is the core KEx algorithm. For client/server net support code, diff --git a/hkexauth.go b/hkexauth.go index 4c861e6..f34db67 100644 --- a/hkexauth.go +++ b/hkexauth.go @@ -1,6 +1,6 @@ // Authentication routines for the HKExSh -package herradurakex +package hkexsh import ( "bytes" @@ -11,7 +11,6 @@ import ( "runtime" "github.com/jameskeane/bcrypt" - "golang.org/x/sys/unix" ) func AuthUser(username string, auth string, fname string) (valid bool, allowedCmds string) { @@ -48,124 +47,3 @@ func AuthUser(username string, auth string, fname string) (valid bool, allowedCm } return } - -/* ------------- minimal terminal APIs brought in from ssh/terminal - * (they have no real business being there as they aren't specific to - * ssh, but as of v1.10, early 2018, core go stdlib hasn't yet done - * the planned terminal lib reorgs.) - * ------------- - */ - -// From github.com/golang/crypto/blob/master/ssh/terminal/util_linux.go -const ioctlReadTermios = unix.TCGETS -const ioctlWriteTermios = unix.TCSETS - -// From github.com/golang/crypto/blob/master/ssh/terminal/util.go -// State contains the state of a terminal. -type State struct { - termios unix.Termios -} - -// MakeRaw put the terminal connected to the given file descriptor into raw -// mode and returns the previous state of the terminal so that it can be -// restored. -func MakeRaw(fd int) (*State, error) { - termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) - if err != nil { - return nil, err - } - - oldState := State{termios: *termios} - - // This attempts to replicate the behaviour documented for cfmakeraw in - // the termios(3) manpage. - termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON - termios.Oflag &^= unix.OPOST - termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN - termios.Cflag &^= unix.CSIZE | unix.PARENB - termios.Cflag |= unix.CS8 - termios.Cc[unix.VMIN] = 1 - termios.Cc[unix.VTIME] = 0 - if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil { - return nil, err - } - - return &oldState, nil -} - -// GetState returns the current state of a terminal which may be useful to -// restore the terminal after a signal. -func GetState(fd int) (*State, error) { - termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) - if err != nil { - return nil, err - } - - return &State{termios: *termios}, nil -} - -// Restore restores the terminal connected to the given file descriptor to a -// previous state. -func Restore(fd int, state *State) error { - return unix.IoctlSetTermios(fd, ioctlWriteTermios, &state.termios) -} - -// ReadPassword reads a line of input from a terminal without local echo. This -// is commonly used for inputting passwords and other sensitive data. The slice -// returned does not include the \n. -func ReadPassword(fd int) ([]byte, error) { - termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) - if err != nil { - return nil, err - } - - newState := *termios - newState.Lflag &^= unix.ECHO - newState.Lflag |= unix.ICANON | unix.ISIG - newState.Iflag |= unix.ICRNL - if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newState); err != nil { - return nil, err - } - - defer func() { - unix.IoctlSetTermios(fd, ioctlWriteTermios, termios) - }() - - return readPasswordLine(passwordReader(fd)) -} - -// passwordReader is an io.Reader that reads from a specific file descriptor. -type passwordReader int - -func (r passwordReader) Read(buf []byte) (int, error) { - return unix.Read(int(r), buf) -} - -// readPasswordLine reads from reader until it finds \n or io.EOF. -// The slice returned does not include the \n. -// readPasswordLine also ignores any \r it finds. -func readPasswordLine(reader io.Reader) ([]byte, error) { - var buf [1]byte - var ret []byte - - for { - n, err := reader.Read(buf[:]) - if n > 0 { - switch buf[0] { - case '\n': - return ret, nil - case '\r': - // remove \r from passwords on Windows - default: - ret = append(ret, buf[0]) - } - continue - } - if err != nil { - if err == io.EOF && len(ret) > 0 { - return ret, nil - } - return ret, err - } - } -} diff --git a/hkexchan.go b/hkexchan.go index 828d4fd..d3fdf46 100644 --- a/hkexchan.go +++ b/hkexchan.go @@ -1,4 +1,4 @@ -package herradurakex +package hkexsh /* Support functions to set up encryption once an HKEx Conn has been established with FA exchange and support channel operations @@ -37,7 +37,6 @@ const ( HmacNoneDisallowed ) -/*TODO: HMAC derived from HKEx FA.*/ /* Support functionality to set up encryption after a channel has been negotiated via hkexnet.go */ diff --git a/hkexnet.go b/hkexnet.go index 6858902..18e5b18 100644 --- a/hkexnet.go +++ b/hkexnet.go @@ -16,7 +16,7 @@ golang implementation by Russ Magee (rmagee_at_gmail.com) */ -package herradurakex +package hkexsh // Implementation of HKEx-wrapped versions of the golang standard // net package interfaces, allowing clients and servers to simply replace diff --git a/demo/hkexpasswd/hkexpasswd.go b/hkexpasswd/hkexpasswd.go similarity index 95% rename from demo/hkexpasswd/hkexpasswd.go rename to hkexpasswd/hkexpasswd.go index 9991c50..0e5f99b 100644 --- a/demo/hkexpasswd/hkexpasswd.go +++ b/hkexpasswd/hkexpasswd.go @@ -12,8 +12,8 @@ import ( "os" "os/user" - hkex "github.com/Russtopia/hkexsh" "github.com/jameskeane/bcrypt" + hkexsh "blitter.com/hkexsh" ) func main() { @@ -40,7 +40,7 @@ func main() { uname = u.Username fmt.Printf("New Password:") - ab, err := hkex.ReadPassword(int(os.Stdin.Fd())) + ab, err := hkexsh.ReadPassword(int(os.Stdin.Fd())) fmt.Printf("\r\n") if err != nil { log.Fatal(err) @@ -49,7 +49,7 @@ func main() { newpw = string(ab) fmt.Printf("Confirm:") - ab, err = hkex.ReadPassword(int(os.Stdin.Fd())) + ab, err = hkexsh.ReadPassword(int(os.Stdin.Fd())) fmt.Printf("\r\n") if err != nil { log.Fatal(err) diff --git a/demo/client/client.go b/hkexsh/hkexsh.go similarity index 93% rename from demo/client/client.go rename to hkexsh/hkexsh.go index 9621fca..befe409 100644 --- a/demo/client/client.go +++ b/hkexsh/hkexsh.go @@ -11,7 +11,7 @@ import ( "strings" "sync" - hkex "blitter.com/hkexsh" + hkexsh "blitter.com/hkexsh" isatty "github.com/mattn/go-isatty" ) @@ -62,7 +62,7 @@ func main() { log.SetOutput(ioutil.Discard) } - conn, err := hkex.Dial("tcp", server, cAlg, hAlg) + conn, err := hkexsh.Dial("tcp", server, cAlg, hAlg) if err != nil { fmt.Println("Err!") panic(err) @@ -74,11 +74,11 @@ func main() { // TODO: send flag to server side indicating this // affects shell command used if isatty.IsTerminal(os.Stdin.Fd()) { - oldState, err := hkex.MakeRaw(int(os.Stdin.Fd())) + oldState, err := hkexsh.MakeRaw(int(os.Stdin.Fd())) if err != nil { panic(err) } - defer func() { _ = hkex.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. + defer func() { _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. } else { log.Println("NOT A TTY") } @@ -108,7 +108,7 @@ func main() { if len(authCookie) == 0 { fmt.Printf("Gimme cookie:") - ab, err := hkex.ReadPassword(int(os.Stdin.Fd())) + ab, err := hkexsh.ReadPassword(int(os.Stdin.Fd())) fmt.Printf("\r\n") if err != nil { panic(err) diff --git a/hkexsh/mintty_wrapper.sh b/hkexsh/mintty_wrapper.sh new file mode 100644 index 0000000..48ced78 --- /dev/null +++ b/hkexsh/mintty_wrapper.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# +## This wrapper may be used within the MSYS/mintty Windows +## shell environment to have a functioning hkexsh client with +## working 'raw' mode and hidden password entry. +## +## mintty uses named pipes and ptys to get a more POSIX-like +## terminal (incl. VT/ANSI codes) rather than the dumb Windows +## console interface; however Go on Windows does not have functioning +## MSYS/mintty code to set raw, echo etc. modes. +## +## Someday it would be preferable to put native Windows term mode +## code into the client build, but this is 'good enough' for now +## (with the exception of tty rows/cols not being set based on +## info from the server). +## +## INSTALLATION +## -- +## Build the client, put it somewhere in your $PATH with this +## wrapper and edit the name of the client binary +## eg., +## $ cp hkexsh.exe /usr/bin/.hkexsh.exe +## $ cp mintty_wrapper.sh /usr/bin/hkexsh +#### +trap cleanup EXIT ERR + +cleanup() { + stty sane +} + +stty -echo raw icrnl +./hkexsh $@ diff --git a/demo/server/server.go b/hkexshd/hkexshd.go similarity index 96% rename from demo/server/server.go rename to hkexshd/hkexshd.go index ac8a362..7237003 100644 --- a/demo/server/server.go +++ b/hkexshd/hkexshd.go @@ -11,8 +11,8 @@ import ( "os/user" "syscall" - hkex "blitter.com/hkexsh" - "blitter.com/hkexsh/demo/spinsult" + hkexsh "blitter.com/hkexsh" + "blitter.com/hkexsh/spinsult" "github.com/kr/pty" ) @@ -71,7 +71,7 @@ func runCmdAs(who string, cmd string, conn hkex.Conn) (err error) { // Run a command (via default shell) as a specific user // // Uses ptys to support commands which expect a terminal. -func runShellAs(who string, cmd string, interactive bool, conn hkex.Conn) (err error) { +func runShellAs(who string, cmd string, interactive bool, conn hkexsh.Conn) (err error) { u, _ := user.Lookup(who) var uid, gid uint32 fmt.Sscanf(u.Uid, "%d", &uid) @@ -153,7 +153,7 @@ func main() { // Listen on TCP port 2000 on all available unicast and // anycast IP addresses of the local system. - l, err := hkex.Listen("tcp", laddr) + l, err := hkexsh.Listen("tcp", laddr) if err != nil { log.Fatal(err) } @@ -171,7 +171,7 @@ func main() { // Handle the connection in a new goroutine. // The loop then returns to accepting, so that // multiple connections may be served concurrently. - go func(c hkex.Conn) (e error) { + go func(c hkexsh.Conn) (e error) { defer c.Close() //We use io.ReadFull() here to guarantee we consume @@ -220,7 +220,7 @@ func main() { log.Printf("[cmdSpec: op:%c who:%s cmd:%s auth:****]\n", rec.op[0], string(rec.who), string(rec.cmd)) - valid, allowedCmds := hkex.AuthUser(string(rec.who), string(rec.authCookie), "/etc/hkexsh.passwd") + valid, allowedCmds := hkexsh.AuthUser(string(rec.who), string(rec.authCookie), "/etc/hkexsh.passwd") if !valid { log.Println("Invalid user", string(rec.who)) c.Write([]byte(rejectUserMsg())) diff --git a/demo/spinsult/spinsult.go b/spinsult/spinsult.go similarity index 100% rename from demo/spinsult/spinsult.go rename to spinsult/spinsult.go diff --git a/demo/spinsult/spinsult_test.go b/spinsult/spinsult_test.go similarity index 100% rename from demo/spinsult/spinsult_test.go rename to spinsult/spinsult_test.go diff --git a/termmode_unix.go b/termmode_unix.go new file mode 100644 index 0000000..1d8578d --- /dev/null +++ b/termmode_unix.go @@ -0,0 +1,130 @@ +// +build linux + +package hkexsh + +import ( + "io" + + unix "golang.org/x/sys/unix" +) + +/* ------------- + * minimal terminal APIs brought in from ssh/terminal + * (they have no real business being there as they aren't specific to + * ssh, but as of Go v1.10, early 2018, core go stdlib hasn't yet done + * the planned terminal lib reorgs.) + * ------------- */ + +// From github.com/golang/crypto/blob/master/ssh/terminal/util_linux.go +const ioctlReadTermios = unix.TCGETS +const ioctlWriteTermios = unix.TCSETS + +// From github.com/golang/crypto/blob/master/ssh/terminal/util.go +// State contains the state of a terminal. +type State struct { + termios unix.Termios +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd int) (*State, error) { + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { + return nil, err + } + + oldState := State{termios: *termios} + + // This attempts to replicate the behaviour documented for cfmakeraw in + // the termios(3) manpage. + termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON + termios.Oflag &^= unix.OPOST + termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN + termios.Cflag &^= unix.CSIZE | unix.PARENB + termios.Cflag |= unix.CS8 + termios.Cc[unix.VMIN] = 1 + termios.Cc[unix.VTIME] = 0 + if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil { + return nil, err + } + + return &oldState, nil +} + +// GetState returns the current state of a terminal which may be useful to +// restore the terminal after a signal. +func GetState(fd int) (*State, error) { + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { + return nil, err + } + + return &State{termios: *termios}, nil +} + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func Restore(fd int, state *State) error { + return unix.IoctlSetTermios(fd, ioctlWriteTermios, &state.termios) +} + +// ReadPassword reads a line of input from a terminal without local echo. This +// is commonly used for inputting passwords and other sensitive data. The slice +// returned does not include the \n. +func ReadPassword(fd int) ([]byte, error) { + termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + if err != nil { + return nil, err + } + + newState := *termios + newState.Lflag &^= unix.ECHO + newState.Lflag |= unix.ICANON | unix.ISIG + newState.Iflag |= unix.ICRNL + if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newState); err != nil { + return nil, err + } + + defer func() { + unix.IoctlSetTermios(fd, ioctlWriteTermios, termios) + }() + + return readPasswordLine(passwordReader(fd)) +} + +// passwordReader is an io.Reader that reads from a specific file descriptor. +type passwordReader int + +func (r passwordReader) Read(buf []byte) (int, error) { + return unix.Read(int(r), buf) +} + +// readPasswordLine reads from reader until it finds \n or io.EOF. +// The slice returned does not include the \n. +// readPasswordLine also ignores any \r it finds. +func readPasswordLine(reader io.Reader) ([]byte, error) { + var buf [1]byte + var ret []byte + + for { + n, err := reader.Read(buf[:]) + if n > 0 { + switch buf[0] { + case '\n': + return ret, nil + case '\r': + // remove \r from passwords on Windows + default: + ret = append(ret, buf[0]) + } + continue + } + if err != nil { + if err == io.EOF && len(ret) > 0 { + return ret, nil + } + return ret, err + } + } +} diff --git a/termmode_windows.go b/termmode_windows.go new file mode 100644 index 0000000..6254ece --- /dev/null +++ b/termmode_windows.go @@ -0,0 +1,93 @@ +// +build windows +// +// Note the terminal manipulation functions herein are mostly stubs. They +// don't really do anything and the hkexsh demo client depends on a wrapper +// script using the 'stty' tool to actually set the proper mode for +// password login and raw mode required, then restoring it upon logout/exit. +// +// mintty uses named pipes and ptys rather than Windows 'console' +// mode, and Go's x/crypto/ssh/terminal libs only work for the latter, so +// until some truly cross-platform terminal mode handling makes it into the +// go std lib I'm not going to jump through hoops trying to be cross-platform +// here; the wrapper does the bare minimum to make the client workable +// under MSYS+mintty which is what I use. + +package hkexsh + +import ( + "io" + "os/exec" + + "golang.org/x/sys/windows" +) + +type State struct { +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd int) (*State, error) { + // This doesn't really work. The exec.Command() runs a sub-shell + // so the stty mods don't affect the client process. + cmd := exec.Command("stty", "-echo raw") + cmd.Run() + return &State{}, nil +} + +// GetState returns the current state of a terminal which may be useful to +// restore the terminal after a signal. +func GetState(fd int) (*State, error) { + return &State{}, nil +} + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func Restore(fd int, state *State) error { + cmd := exec.Command("stty", "echo cooked") + cmd.Run() + return nil +} + +// ReadPassword reads a line of input from a terminal without local echo. This +// is commonly used for inputting passwords and other sensitive data. The slice +// returned does not include the \n. +func ReadPassword(fd int) ([]byte, error) { + return readPasswordLine(passwordReader(fd)) +} + +// passwordReader is an io.Reader that reads from a specific file descriptor. +type passwordReader windows.Handle + +func (r passwordReader) Read(buf []byte) (int, error) { + return windows.Read(windows.Handle(r), buf) +} + +// readPasswordLine reads from reader until it finds \n or io.EOF. +// The slice returned does not include the \n. +// readPasswordLine also ignores any \r it finds. +func readPasswordLine(reader io.Reader) ([]byte, error) { + var buf [1]byte + var ret []byte + + for { + n, err := reader.Read(buf[:]) + if n > 0 { + switch buf[0] { + case '\n': + return ret, nil + case '\r': + // remove \r from passwords on Windows + default: + ret = append(ret, buf[0]) + } + continue + } + if err != nil { + if err == io.EOF && len(ret) > 0 { + return ret, nil + } + return ret, err + } + } +}