Added hkexpasswd util; moved minimal term stuff into hkexauth.go

This commit is contained in:
Russ Magee 2018-01-23 13:53:05 -08:00
parent 3ca98d364c
commit d484ec7fd1
6 changed files with 400 additions and 138 deletions

View file

@ -18,10 +18,10 @@
-- --
This is a drop-in replacement for the golang/pkg/net facilities Package herradurakex is a drop-in replacement for golang/pkg/net facilities
(net.Dial(), net.Listen(), net.Accept() and net.Conn type) using the (net.Dial(), net.Listen(), net.Accept() and the net.Conn type) using the
experimental HerraduraKEx 'secure' key exchange algorithm, first released at 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 One can simply replace calls to net.Dial() with hkex.Dial(), and likewise
net.Listen() with hkex.Listen(), to obtain connections (hkex.Conn) conforming net.Listen() with hkex.Listen(), to obtain connections (hkex.Conn) conforming

View file

@ -13,7 +13,6 @@ import (
hkex "blitter.com/herradurakex" hkex "blitter.com/herradurakex"
isatty "github.com/mattn/go-isatty" isatty "github.com/mattn/go-isatty"
"golang.org/x/sys/unix"
) )
type cmdSpec struct { type cmdSpec struct {
@ -69,16 +68,17 @@ func main() {
panic(err) panic(err)
} }
defer conn.Close() defer conn.Close()
// From this point on, conn is a secure encrypted channel
// Set stdin in raw mode if it's an interactive session // Set stdin in raw mode if it's an interactive session
// TODO: send flag to server side indicating this // TODO: send flag to server side indicating this
// affects shell command used // affects shell command used
if isatty.IsTerminal(os.Stdin.Fd()) { if isatty.IsTerminal(os.Stdin.Fd()) {
oldState, err := MakeRaw(int(os.Stdin.Fd())) oldState, err := hkex.MakeRaw(int(os.Stdin.Fd()))
if err != nil { if err != nil {
panic(err) panic(err)
} }
defer func() { _ = Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. defer func() { _ = hkex.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort.
} else { } else {
log.Println("NOT A TTY") log.Println("NOT A TTY")
} }
@ -108,7 +108,8 @@ func main() {
if len(authCookie) == 0 { if len(authCookie) == 0 {
fmt.Printf("Gimme cookie:") 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 { if err != nil {
panic(err) panic(err)
} }
@ -179,124 +180,3 @@ func main() {
// Wait until both stdin and stdout goroutines finish // Wait until both stdin and stdout goroutines finish
wg.Wait() 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
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -13,6 +13,7 @@ import (
"syscall" "syscall"
hkex "blitter.com/herradurakex" hkex "blitter.com/herradurakex"
"blitter.com/spinsult"
"github.com/kr/pty" "github.com/kr/pty"
) )
@ -109,8 +110,7 @@ func runShellAs(who string, cmd string, interactive bool, conn hkex.Conn) (err e
} }
func rejectUserMsg() string { func rejectUserMsg() string {
// TODO: Use Shakespeare insult generator. :p return "Begone, " + spinsult.GetSentence() + "\r\n"
return "Invalid user\r\n"
} }
// Demo of a simple server that listens and spawns goroutines for each // 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") valid, allowedCmds := hkex.AuthUser(string(rec.who), string(rec.authCookie), "/etc/hkexsh.passwd")
if !valid { if !valid {
log.Println("Invalid user", string(rec.who)) log.Println("Invalid user", string(rec.who))
c.Write([]byte(rejectUserMsg())) c.Write([]byte(rejectUserMsg()))
return return
} }
log.Printf("[allowedCmds:%s]\n", allowedCmds) log.Printf("[allowedCmds:%s]\n", allowedCmds)

View file

@ -10,9 +10,12 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"runtime" "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) b, _ := ioutil.ReadFile(fname)
r := csv.NewReader(bytes.NewReader(b)) r := csv.NewReader(bytes.NewReader(b))
@ -21,7 +24,7 @@ func AuthUser(username string, authcookie string, fname string) (valid bool, all
r.Comma = ':' r.Comma = ':'
r.Comment = '#' r.Comment = '#'
r.FieldsPerRecord = 3 // username:authCookie:disallowedCmdList (a,b,...) r.FieldsPerRecord = 4 // username:salt:authCookie:disallowedCmdList (a,b,...)
for { for {
record, err := r.Read() record, err := r.Read()
if err == io.EOF { if err == io.EOF {
@ -31,9 +34,11 @@ func AuthUser(username string, authcookie string, fname string) (valid bool, all
log.Fatal(err) log.Fatal(err)
} }
if username == record[0] && if username == record[0] {
authcookie == record[1] { tmp, _ := bcrypt.Hash(auth, record[1])
valid = true if tmp == record[2] {
valid = true
}
break break
} }
@ -41,3 +46,124 @@ func AuthUser(username string, authcookie string, fname string) (valid bool, all
} }
return 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
}
}
}