From d484ec7fd15c80397d004985b5b48fc450089212 Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Tue, 23 Jan 2018 13:53:05 -0800 Subject: [PATCH] Added hkexpasswd util; moved minimal term stuff into hkexauth.go --- README.md | 6 +- demo/client/client.go | 130 ++----------------------------- demo/hkexpasswd/hkexpasswd.go | 128 +++++++++++++++++++++++++++++++ demo/hkexpasswd/hkexpasswd.go~ | 128 +++++++++++++++++++++++++++++++ demo/server/server.go | 10 +-- hkexauth.go | 136 +++++++++++++++++++++++++++++++-- 6 files changed, 400 insertions(+), 138 deletions(-) create mode 100644 demo/hkexpasswd/hkexpasswd.go create mode 100644 demo/hkexpasswd/hkexpasswd.go~ diff --git a/README.md b/README.md index dcaeda5..29d6a45 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,10 @@ -- -This is a drop-in replacement for the golang/pkg/net facilities -(net.Dial(), net.Listen(), net.Accept() and net.Conn type) using the +Package herradurakex is a drop-in replacement for golang/pkg/net facilities +(net.Dial(), net.Listen(), net.Accept() and the net.Conn type) using the experimental HerraduraKEx 'secure' key exchange algorithm, first released at -github.com/Caume/HerraduraKEx +(Omar Elejandro Herrera Reyna's github page)[github.com/Caume/HerraduraKEx]. One can simply replace calls to net.Dial() with hkex.Dial(), and likewise net.Listen() with hkex.Listen(), to obtain connections (hkex.Conn) conforming diff --git a/demo/client/client.go b/demo/client/client.go index bbddd1b..3756524 100644 --- a/demo/client/client.go +++ b/demo/client/client.go @@ -13,7 +13,6 @@ import ( hkex "blitter.com/herradurakex" isatty "github.com/mattn/go-isatty" - "golang.org/x/sys/unix" ) type cmdSpec struct { @@ -69,16 +68,17 @@ func main() { panic(err) } defer conn.Close() + // From this point on, conn is a secure encrypted channel // Set stdin in raw mode if it's an interactive session // TODO: send flag to server side indicating this // affects shell command used if isatty.IsTerminal(os.Stdin.Fd()) { - oldState, err := MakeRaw(int(os.Stdin.Fd())) + oldState, err := hkex.MakeRaw(int(os.Stdin.Fd())) if err != nil { panic(err) } - defer func() { _ = Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. + defer func() { _ = hkex.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. } else { log.Println("NOT A TTY") } @@ -108,7 +108,8 @@ func main() { if len(authCookie) == 0 { fmt.Printf("Gimme cookie:") - ab, err := ReadPassword(int(os.Stdin.Fd())) + ab, err := hkex.ReadPassword(int(os.Stdin.Fd())) + fmt.Printf("\r\n") if err != nil { panic(err) } @@ -179,124 +180,3 @@ func main() { // Wait until both stdin and stdout goroutines finish wg.Wait() } - -/* ------------- 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/demo/hkexpasswd/hkexpasswd.go b/demo/hkexpasswd/hkexpasswd.go new file mode 100644 index 0000000..60adb97 --- /dev/null +++ b/demo/hkexpasswd/hkexpasswd.go @@ -0,0 +1,128 @@ +// 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 "blitter.com/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/hkexpasswd/hkexpasswd.go~ b/demo/hkexpasswd/hkexpasswd.go~ new file mode 100644 index 0000000..236ae64 --- /dev/null +++ b/demo/hkexpasswd/hkexpasswd.go~ @@ -0,0 +1,128 @@ +// 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 "blitter.com/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/server/server.go b/demo/server/server.go index 6a9c978..3795a90 100644 --- a/demo/server/server.go +++ b/demo/server/server.go @@ -13,6 +13,7 @@ import ( "syscall" hkex "blitter.com/herradurakex" + "blitter.com/spinsult" "github.com/kr/pty" ) @@ -109,8 +110,7 @@ func runShellAs(who string, cmd string, interactive bool, conn hkex.Conn) (err e } func rejectUserMsg() string { - // TODO: Use Shakespeare insult generator. :p - return "Invalid user\r\n" + return "Begone, " + spinsult.GetSentence() + "\r\n" } // Demo of a simple server that listens and spawns goroutines for each @@ -201,9 +201,9 @@ func main() { valid, allowedCmds := hkex.AuthUser(string(rec.who), string(rec.authCookie), "/etc/hkexsh.passwd") if !valid { - log.Println("Invalid user", string(rec.who)) - c.Write([]byte(rejectUserMsg())) - return + log.Println("Invalid user", string(rec.who)) + c.Write([]byte(rejectUserMsg())) + return } log.Printf("[allowedCmds:%s]\n", allowedCmds) diff --git a/hkexauth.go b/hkexauth.go index 3e1cd16..be431aa 100644 --- a/hkexauth.go +++ b/hkexauth.go @@ -10,9 +10,12 @@ import ( "io/ioutil" "log" "runtime" + + "github.com/jameskeane/bcrypt" + "golang.org/x/sys/unix" ) -func AuthUser(username string, authcookie string, fname string) (valid bool, allowedCmds string) { +func AuthUser(username string, auth string, fname string) (valid bool, allowedCmds string) { b, _ := ioutil.ReadFile(fname) r := csv.NewReader(bytes.NewReader(b)) @@ -21,7 +24,7 @@ func AuthUser(username string, authcookie string, fname string) (valid bool, all r.Comma = ':' r.Comment = '#' - r.FieldsPerRecord = 3 // username:authCookie:disallowedCmdList (a,b,...) + r.FieldsPerRecord = 4 // username:salt:authCookie:disallowedCmdList (a,b,...) for { record, err := r.Read() if err == io.EOF { @@ -31,9 +34,11 @@ func AuthUser(username string, authcookie string, fname string) (valid bool, all log.Fatal(err) } - if username == record[0] && - authcookie == record[1] { - valid = true + if username == record[0] { + tmp, _ := bcrypt.Hash(auth, record[1]) + if tmp == record[2] { + valid = true + } break } @@ -41,3 +46,124 @@ func AuthUser(username string, authcookie string, fname string) (valid bool, all } 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 + } + } +}