// hkexsh client // // Copyright (c) 2017-2018 Russell Magee // Licensed under the terms of the MIT license (see LICENSE.mit in this // distribution) // // golang implementation by Russ Magee (rmagee_at_gmail.com) package main import ( "flag" "fmt" "io" "io/ioutil" "log" "os" "os/exec" "os/user" "runtime" "strings" "sync" hkexsh "blitter.com/go/hkexsh" "blitter.com/go/hkexsh/hkexnet" isatty "github.com/mattn/go-isatty" ) type cmdSpec struct { op []byte who []byte cmd []byte authCookie []byte status int // UNIX exit status is uint8, but os.Exit() wants int } var ( wg sync.WaitGroup defPort = "2000" ) // Get terminal size using 'stty' command func GetSize() (cols, rows int, err error) { cmd := exec.Command("stty", "size") cmd.Stdin = os.Stdin out, err := cmd.Output() if err != nil { log.Println(err) cols, rows = 80, 24 //failsafe } else { fmt.Sscanf(string(out), "%d %d\n", &rows, &cols) } return } func parseNonSwitchArgs(a []string, dp string) (user, host, port, path string, isDest bool, otherArgs []string) { // Whether fancyArg is src or dst file depends on flag.Args() index; // fancyArg as last flag.Args() element denotes dstFile // fancyArg as not-last flag.Args() element denotes srcFile var fancyUser, fancyHost, fancyPort, fancyPath string for i, arg := range a { if strings.Contains(arg, ":") || strings.Contains(arg, "@") { fancyArg := strings.Split(flag.Arg(i), "@") var fancyHostPortPath []string if len(fancyArg) < 2 { //TODO: no user specified, use current fancyUser = "[default:getUser]" fancyHostPortPath = strings.Split(fancyArg[0], ":") } else { // user@.... fancyUser = fancyArg[0] fancyHostPortPath = strings.Split(fancyArg[1], ":") } // [...@]host[:port[:path]] if len(fancyHostPortPath) > 2 { fancyPath = fancyHostPortPath[2] } else if len(fancyHostPortPath) > 1 { fancyPort = fancyHostPortPath[1] } fancyHost = fancyHostPortPath[0] if fancyPort == "" { fancyPort = dp } //if fancyPath == "" { // fancyPath = "." //} if i == len(a)-1 { isDest = true fmt.Println("remote path isDest") } fmt.Println("fancyArgs: user:", fancyUser, "host:", fancyHost, "port:", fancyPort, "path:", fancyPath) } else { otherArgs = append(otherArgs, a[i]) } } return fancyUser, fancyHost, fancyPort, fancyPath, isDest, otherArgs } // doCopyMode begins a secure hkexsh local<->remote file copy operation. func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, recurs bool, rec *cmdSpec) { // TODO: Bring in runShellAs(), stripped down, from hkexshd // and build either side of tar pipeline: names? // runTarSrc(), runTarSink() ? if remoteDest { fmt.Println("local files:", files, "remote filepath:", string(rec.cmd)) } else { fmt.Println("remote filepath:", string(rec.cmd), "local files:", files) } } // doShellMode begins an hkexsh shell session (one-shot command or interactive). func doShellMode(isInteractive bool, conn *hkexnet.Conn, oldState *hkexsh.State, rec *cmdSpec) { //client reader (from server) goroutine //Read remote end's stdout wg.Add(1) go func() { defer wg.Done() // By deferring a call to wg.Done(), // each goroutine guarantees that it marks // its direction's stream as finished. // io.Copy() expects EOF so normally this will // exit with inerr == nil _, inerr := io.Copy(os.Stdout, conn) if inerr != nil { fmt.Println(inerr) _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. os.Exit(1) } rec.status = int(conn.GetStatus()) log.Println("rec.status:", rec.status) if isInteractive { log.Println("[* Got EOF *]") _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. } }() // Only look for data from stdin to send to remote end // for interactive sessions. if isInteractive { handleTermResizes(conn) // client writer (to server) goroutine // Write local stdin to remote end wg.Add(1) go func() { defer wg.Done() //!defer wg.Done() // Copy() expects EOF so this will // exit with outerr == nil //!_, outerr := io.Copy(conn, os.Stdin) _, outerr := func(conn *hkexnet.Conn, r io.Reader) (w int64, e error) { w, e = io.Copy(conn, r) return w, e }(conn, os.Stdin) if outerr != nil { log.Println(outerr) fmt.Println(outerr) _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. os.Exit(255) } log.Println("[Sent EOF]") }() } // Wait until both stdin and stdout goroutines finish before returning // (ensure client gets all data from server before closing) wg.Wait() } // hkexsh - a client for secure shell and file copy operations. // // While conforming to the basic net.Conn interface HKex.Conn has extra // capabilities designed to allow apps to define connection options, // encryption/hmac settings and operations across the encrypted channel. // // Initial setup is the same as using plain net.Dial(), but one may // specify extra extension tags (strings) to set the cipher and hmac // setting desired; as well as the intended operation mode for the // connection (app-specific, passed through to the server to use or // ignore at its discretion). func main() { version := "0.1pre (NO WARRANTY)" var vopt bool var dbg bool var shellMode bool // if true act as shell, else file copier var cAlg string var hAlg string var server string var cmdStr string var recursiveCopy bool var copySrc []byte var copyDst string var altUser string var authCookie string var chaffEnabled bool var chaffFreqMin uint var chaffFreqMax uint var chaffBytesMax uint var op []byte isInteractive := false flag.BoolVar(&vopt, "v", false, "show version") flag.BoolVar(&dbg, "d", false, "debug logging") flag.StringVar(&cAlg, "c", "C_AES_256", "cipher [\"C_AES_256\" | \"C_TWOFISH_128\" | \"C_BLOWFISH_64\"]") flag.StringVar(&hAlg, "m", "H_SHA256", "hmac [\"H_SHA256\"]") flag.StringVar(&server, "s", "localhost:"+defPort, "server hostname/address[:port]") flag.StringVar(&altUser, "u", "", "specify alternate user") flag.StringVar(&authCookie, "a", "", "auth cookie") flag.BoolVar(&chaffEnabled, "e", true, "enabled chaff pkts (default true)") flag.UintVar(&chaffFreqMin, "f", 100, "chaff pkt freq min (msecs)") flag.UintVar(&chaffFreqMax, "F", 5000, "chaff pkt freq max (msecs)") flag.UintVar(&chaffBytesMax, "B", 64, "chaff pkt size max (bytes)") // Find out what program we are (shell or copier) myPath := strings.Split(os.Args[0], string(os.PathSeparator)) if myPath[len(myPath)-1] != "hkexcp" && myPath[len(myPath)-1] != "hkexcp.exe" { // hkexsh accepts a command (-x) but not // a srcpath (-r) or dstpath (-t) flag.StringVar(&cmdStr, "x", "", "command to run (default empty - interactive shell)") shellMode = true } else { // Note: only makes sense for client->server copies flag.BoolVar(&recursiveCopy, "r", false, "recursive copy/preserve tree copy") } flag.Parse() tmpUser, tmpHost, tmpPort, tmpPath, pathIsDest, otherArgs := parseNonSwitchArgs(flag.Args(), defPort /* defPort */) fmt.Println("otherArgs:", otherArgs) //fmt.Println("tmpHost:", tmpHost) //fmt.Println("tmpPath:", tmpPath) if tmpUser != "" { altUser = tmpUser } if tmpHost != "" { server = tmpHost + ":" + tmpPort //fmt.Println("tmpHost sets server to", server) } var fileArgs string if !shellMode && tmpPath != "" { // -if pathIsSrc && len(otherArgs) > 1 ERROR // -else flatten otherArgs into space-delim list => copySrc if pathIsDest { if len(otherArgs) == 0 { log.Fatal("ERROR: Must specify at least one src path for copy") } else { for _, v := range otherArgs { copySrc = append(copySrc, ' ') copySrc = append(copySrc, v...) } copyDst = tmpPath fileArgs = string(copySrc) } } else { if len(otherArgs) == 0 { log.Fatal("ERROR: Must specify dest path for copy") } else if len(otherArgs) == 1 { copyDst = otherArgs[0] if strings.Contains(copyDst, "*") || strings.Contains(copyDst, "?") { log.Fatal("ERROR: wildcards not allowed in dest path for copy") } } else { log.Fatal("ERROR: cannot specify more than one dest path for copy") } copySrc = []byte(tmpPath) fileArgs = copyDst } } // Do some more option consistency checks //fmt.Println("server finally is:", server) if flag.NFlag() == 0 && server == "" { flag.Usage() os.Exit(0) } if vopt { fmt.Printf("version v%s\n", version) os.Exit(0) } if len(cmdStr) != 0 && (len(copySrc) != 0 || len(copyDst) != 0) { log.Fatal("incompatible options -- either cmd (-x) or copy ops but not both") } //------------------------------------------------------------------- // Here we have parsed all options and can now carry out // either the shell session or copy operation. _ = shellMode if dbg { log.SetOutput(os.Stdout) } else { log.SetOutput(ioutil.Discard) } if shellMode { // We must make the decision about interactivity before Dial() // as it affects chaffing behaviour. 20180805 if len(cmdStr) == 0 { op = []byte{'s'} isInteractive = true } else { op = []byte{'c'} // non-interactive cmds may complete quickly, so chaff earlier/faster // to help ensure there's some cover to the brief traffic. // (ignoring cmdline values) chaffFreqMin = 2 chaffFreqMax = 10 } } else { // as copy mode is also non-interactive, set up chaffing // just like the 'c' mode above chaffFreqMin = 2 chaffFreqMax = 10 if pathIsDest { // client->server file copy // src file list is in copySrc op = []byte{'D'} fmt.Println("client->server copy:", string(copySrc), "->", copyDst) cmdStr = copyDst } else { // server->client file copy // remote src file(s) in copyDsr op = []byte{'S'} fmt.Println("server->client copy:", string(copySrc), "->", copyDst) cmdStr = string(copySrc) } } conn, err := hkexnet.Dial("tcp", server, cAlg, hAlg) if err != nil { fmt.Println("Err!") 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 var oldState *hkexsh.State if shellMode { if isatty.IsTerminal(os.Stdin.Fd()) { oldState, err = hkexsh.MakeRaw(int(os.Stdin.Fd())) if err != nil { panic(err) } defer func() { _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. } else { log.Println("NOT A TTY") } } var uname string if len(altUser) == 0 { u, _ := user.Current() uname = u.Username } else { uname = altUser } if len(authCookie) == 0 { fmt.Printf("Gimme cookie:") ab, err := hkexsh.ReadPassword(int(os.Stdin.Fd())) fmt.Printf("\r\n") if err != nil { panic(err) } authCookie = string(ab) // Security scrub ab = nil runtime.GC() } rec := &cmdSpec{ op: op, who: []byte(uname), cmd: []byte(cmdStr), authCookie: []byte(authCookie), status: 0} _, err = fmt.Fprintf(conn, "%d %d %d %d\n", len(rec.op), len(rec.who), len(rec.cmd), len(rec.authCookie)) _, err = conn.Write(rec.op) _, err = conn.Write(rec.who) _, err = conn.Write(rec.cmd) _, err = conn.Write(rec.authCookie) // Set up chaffing to server conn.SetupChaff(chaffFreqMin, chaffFreqMax, chaffBytesMax) // enable client->server chaffing if chaffEnabled { conn.EnableChaff() defer conn.DisableChaff() defer conn.ShutdownChaff() } if shellMode { doShellMode(isInteractive, conn, oldState, rec) } else { doCopyMode(conn, pathIsDest, fileArgs, recursiveCopy, rec) } if oldState != nil { _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. } os.Exit(rec.status) }