MSYS+mintty support; pkg renaming to hkexsh

This commit is contained in:
Russ Magee 2018-04-04 15:43:27 -07:00
parent dd746cf343
commit 5da70447b0
15 changed files with 281 additions and 377 deletions

View File

@ -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')
// ...
}

View File

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

View File

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

View File

@ -1,11 +1,16 @@
// Package herradurakex - socket lib conforming to // Package hkexsh - socket lib conforming to
// golang.org/pkg/net Conn interface, with // golang.org/pkg/net Conn interface, with
// experimental key exchange algorithm by // experimental key exchange algorithm by
// Omar Alejandro Herrera Reyna // Omar Alejandro Herrera Reyna.
//
// (https://github.com/Caume/HerraduraKEx) // (https://github.com/Caume/HerraduraKEx)
//
// Demonstration server (hkexshd) and
// client (hkexsh)
// //
// See README.md for full license info. // See README.md for full license info.
package herradurakex package hkexsh
/* Herradura - a Key exchange scheme in the style of Diffie-Hellman Key Exchange. /* Herradura - a Key exchange scheme in the style of Diffie-Hellman Key Exchange.
Copyright (C) 2017 Omar Alejandro Herrera Reyna Copyright (C) 2017 Omar Alejandro Herrera Reyna

View File

@ -1,6 +1,6 @@
// Authentication routines for the HKExSh // Authentication routines for the HKExSh
package herradurakex package hkexsh
import ( import (
"bytes" "bytes"
@ -11,7 +11,6 @@ import (
"runtime" "runtime"
"github.com/jameskeane/bcrypt" "github.com/jameskeane/bcrypt"
"golang.org/x/sys/unix"
) )
func AuthUser(username string, auth string, fname string) (valid bool, allowedCmds string) { 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 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
}
}
}

View File

@ -1,4 +1,4 @@
package herradurakex package hkexsh
/* Support functions to set up encryption once an HKEx Conn has been /* Support functions to set up encryption once an HKEx Conn has been
established with FA exchange and support channel operations established with FA exchange and support channel operations
@ -37,7 +37,6 @@ const (
HmacNoneDisallowed HmacNoneDisallowed
) )
/*TODO: HMAC derived from HKEx FA.*/
/* Support functionality to set up encryption after a channel has /* Support functionality to set up encryption after a channel has
been negotiated via hkexnet.go been negotiated via hkexnet.go
*/ */

View File

@ -16,7 +16,7 @@
golang implementation by Russ Magee (rmagee_at_gmail.com) */ golang implementation by Russ Magee (rmagee_at_gmail.com) */
package herradurakex package hkexsh
// Implementation of HKEx-wrapped versions of the golang standard // Implementation of HKEx-wrapped versions of the golang standard
// net package interfaces, allowing clients and servers to simply replace // net package interfaces, allowing clients and servers to simply replace

View File

@ -12,8 +12,8 @@ import (
"os" "os"
"os/user" "os/user"
hkex "github.com/Russtopia/hkexsh"
"github.com/jameskeane/bcrypt" "github.com/jameskeane/bcrypt"
hkexsh "blitter.com/hkexsh"
) )
func main() { func main() {
@ -40,7 +40,7 @@ func main() {
uname = u.Username uname = u.Username
fmt.Printf("New Password:") fmt.Printf("New Password:")
ab, err := hkex.ReadPassword(int(os.Stdin.Fd())) ab, err := hkexsh.ReadPassword(int(os.Stdin.Fd()))
fmt.Printf("\r\n") fmt.Printf("\r\n")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -49,7 +49,7 @@ func main() {
newpw = string(ab) newpw = string(ab)
fmt.Printf("Confirm:") fmt.Printf("Confirm:")
ab, err = hkex.ReadPassword(int(os.Stdin.Fd())) ab, err = hkexsh.ReadPassword(int(os.Stdin.Fd()))
fmt.Printf("\r\n") fmt.Printf("\r\n")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -11,7 +11,7 @@ import (
"strings" "strings"
"sync" "sync"
hkex "blitter.com/hkexsh" hkexsh "blitter.com/hkexsh"
isatty "github.com/mattn/go-isatty" isatty "github.com/mattn/go-isatty"
) )
@ -62,7 +62,7 @@ func main() {
log.SetOutput(ioutil.Discard) log.SetOutput(ioutil.Discard)
} }
conn, err := hkex.Dial("tcp", server, cAlg, hAlg) conn, err := hkexsh.Dial("tcp", server, cAlg, hAlg)
if err != nil { if err != nil {
fmt.Println("Err!") fmt.Println("Err!")
panic(err) panic(err)
@ -74,11 +74,11 @@ func main() {
// 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 := hkex.MakeRaw(int(os.Stdin.Fd())) oldState, err := hkexsh.MakeRaw(int(os.Stdin.Fd()))
if err != nil { if err != nil {
panic(err) 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 { } else {
log.Println("NOT A TTY") log.Println("NOT A TTY")
} }
@ -108,7 +108,7 @@ func main() {
if len(authCookie) == 0 { if len(authCookie) == 0 {
fmt.Printf("Gimme cookie:") fmt.Printf("Gimme cookie:")
ab, err := hkex.ReadPassword(int(os.Stdin.Fd())) ab, err := hkexsh.ReadPassword(int(os.Stdin.Fd()))
fmt.Printf("\r\n") fmt.Printf("\r\n")
if err != nil { if err != nil {
panic(err) panic(err)

32
hkexsh/mintty_wrapper.sh Normal file
View File

@ -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 $@

View File

@ -11,8 +11,8 @@ import (
"os/user" "os/user"
"syscall" "syscall"
hkex "blitter.com/hkexsh" hkexsh "blitter.com/hkexsh"
"blitter.com/hkexsh/demo/spinsult" "blitter.com/hkexsh/spinsult"
"github.com/kr/pty" "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 // Run a command (via default shell) as a specific user
// //
// Uses ptys to support commands which expect a terminal. // 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) u, _ := user.Lookup(who)
var uid, gid uint32 var uid, gid uint32
fmt.Sscanf(u.Uid, "%d", &uid) fmt.Sscanf(u.Uid, "%d", &uid)
@ -153,7 +153,7 @@ func main() {
// Listen on TCP port 2000 on all available unicast and // Listen on TCP port 2000 on all available unicast and
// anycast IP addresses of the local system. // anycast IP addresses of the local system.
l, err := hkex.Listen("tcp", laddr) l, err := hkexsh.Listen("tcp", laddr)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -171,7 +171,7 @@ func main() {
// Handle the connection in a new goroutine. // Handle the connection in a new goroutine.
// The loop then returns to accepting, so that // The loop then returns to accepting, so that
// multiple connections may be served concurrently. // multiple connections may be served concurrently.
go func(c hkex.Conn) (e error) { go func(c hkexsh.Conn) (e error) {
defer c.Close() defer c.Close()
//We use io.ReadFull() here to guarantee we consume //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", log.Printf("[cmdSpec: op:%c who:%s cmd:%s auth:****]\n",
rec.op[0], string(rec.who), string(rec.cmd)) 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 { if !valid {
log.Println("Invalid user", string(rec.who)) log.Println("Invalid user", string(rec.who))
c.Write([]byte(rejectUserMsg())) c.Write([]byte(rejectUserMsg()))

130
termmode_unix.go Normal file
View File

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

93
termmode_windows.go Normal file
View File

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