diff --git a/cmd/ro/README.md b/cmd/ro/README.md new file mode 100644 index 0000000..d5521e3 --- /dev/null +++ b/cmd/ro/README.md @@ -0,0 +1,18 @@ +# ro +This is a command line based Red October client. It is still under development. + +## Usage +See + +$ ro -h + +## Example +Assume username and password is stored at RO\_USER and RO\_PASS env variables. + +1. To see the current user and delegation summary: + + $ ro -server HOSTNAME:PORT summary + +2. To decrypt a RO encrypted file: + + $ ro -server HOSTNAME:PORT -in FILE -out FILE decrypt diff --git a/cmd/ro/gopass/gopass.go b/cmd/ro/gopass/gopass.go new file mode 100644 index 0000000..26e16aa --- /dev/null +++ b/cmd/ro/gopass/gopass.go @@ -0,0 +1,111 @@ +// Author: johnsiilver@gmail.com (John Doak) + +/* +gopass is a library for getting hidden input from a terminal. + +This library's main use is to allow a user to enter a password at the +command line without having it echoed to the screen. + +The libary currently supports unix systems by manipulating stty. + +This code is based upon code by RogerV in the golang-nuts thread: +https://groups.google.com/group/golang-nuts/browse_thread/thread/40cc41e9d9fc9247 +*/ +package gopass + +import ( + "bufio" + "fmt" + "os" + "os/signal" + "strings" + "syscall" +) + +const ( + sttyArg0 = "/bin/stty" + exec_cwdir = "" +) + +// Tells the terminal to turn echo off. +var sttyArgvEOff []string = []string{"stty", "-echo"} + +// Tells the terminal to turn echo on. +var sttyArgvEOn []string = []string{"stty", "echo"} + +var ws syscall.WaitStatus = 0 + +// GetPass gets input hidden from the terminal from a user. +// This is accomplished by turning off terminal echo, +// reading input from the user and finally turning on terminal echo. +// prompt is a string to display before the user's input. +func GetPass(prompt string) (passwd string, err error) { + sig := make(chan os.Signal, 10) + brk := make(chan bool) + + // Display the prompt. + fmt.Print(prompt) + + // File descriptors for stdin, stdout, and stderr. + fd := []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()} + + // Setup notifications of termination signals to channel sig, create a process to + // watch for these signals so we can turn back on echo if need be. + signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGKILL, syscall.SIGQUIT, + syscall.SIGTERM) + go catchSignal(fd, sig, brk) + + // Turn off the terminal echo. + pid, err := echoOff(fd) + if err != nil { + return "", err + } + + // Turn on the terminal echo and stop listening for signals. + defer close(brk) + defer echoOn(fd) + + rd := bufio.NewReader(os.Stdin) + syscall.Wait4(pid, &ws, 0, nil) + + line, err := rd.ReadString('\n') + if err == nil { + passwd = strings.TrimSpace(line) + } else { + err = fmt.Errorf("failed during password entry: %s", err) + } + + // Carraige return after the user input. + fmt.Println("") + + return passwd, err +} + +func echoOff(fd []uintptr) (int, error) { + pid, err := syscall.ForkExec(sttyArg0, sttyArgvEOff, &syscall.ProcAttr{Dir: exec_cwdir, Files: fd}) + if err != nil { + return 0, fmt.Errorf("failed turning off console echo for password entry:\n\t%s", err) + } + return pid, nil +} + +// echoOn turns back on the terminal echo. +func echoOn(fd []uintptr) { + // Turn on the terminal echo. + pid, e := syscall.ForkExec(sttyArg0, sttyArgvEOn, &syscall.ProcAttr{Dir: exec_cwdir, Files: fd}) + if e == nil { + syscall.Wait4(pid, &ws, 0, nil) + } +} + +// catchSignal tries to catch SIGKILL, SIGQUIT and SIGINT so that we can turn terminal +// echo back on before the program ends. Otherwise the user is left with echo off on +// their terminal. +func catchSignal(fd []uintptr, sig chan os.Signal, brk chan bool) { + select { + case <-sig: + echoOn(fd) + os.Exit(-1) + case <-brk: + } +} \ No newline at end of file diff --git a/cmd/ro/main.go b/cmd/ro/main.go new file mode 100644 index 0000000..b3a91d3 --- /dev/null +++ b/cmd/ro/main.go @@ -0,0 +1,203 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "strings" + + "github.com/cloudflare/redoctober/client" + "github.com/cloudflare/redoctober/cmd/ro/gopass" + "github.com/cloudflare/redoctober/core" +) + +var action, user, pswd, userEnv, pswdEnv, server, caPath string + +var owners, lefters, righters, inPath, labels, outPath, outEnv string + +var uses int + +var time, users string + +type command struct { + Run func() + Desc string +} + +var roServer *client.RemoteServer + +var commandSet = map[string]command{ + "create": command{Run: runCreate, Desc: "create a user account"}, + "summary": command{Run: runSummary, Desc: "list the user and delegation summary"}, + "delegate": command{Run: runDelegate, Desc: "do decryption delegation"}, + "encrypt": command{Run: runEncrypt, Desc: "encrypt a file"}, + "decrypt": command{Run: runDecrypt, Desc: "decrypt a file"}, +} + +func registerFlags() { + flag.StringVar(&server, "server", "localhost:8080", "server address") + flag.StringVar(&caPath, "ca", "", "ca file path") + flag.StringVar(&owners, "owners", "", "comma separated owner list") + flag.StringVar(&users, "users", "", "comma separated user list") + flag.IntVar(&uses, "uses", 0, "number of delegated key uses") + flag.StringVar(&time, "time", "0h", "duration of delegated key uses") + flag.StringVar(&lefters, "left", "", "comma separated left owners") + flag.StringVar(&righters, "right", "", "comma separated right owners") + flag.StringVar(&labels, "labels", "", "comma separated labels") + flag.StringVar(&inPath, "in", "", "input data file") + flag.StringVar(&outPath, "out", "", "output data file") + flag.StringVar(&outEnv, "outenv", "", "env variable for output data") + flag.StringVar(&user, "user", "", "username") + flag.StringVar(&pswd, "password", "", "password") + flag.StringVar(&userEnv, "userenv", "RO_USER", "env variable for user name") + flag.StringVar(&pswdEnv, "pswdenv", "RO_PASS", "env variable for user password") +} + +func getUserCredentials() { + user = os.Getenv(userEnv) + pswd = os.Getenv(pswdEnv) + if user == "" || pswd == "" { + fmt.Print("Username:") + fmt.Scan(&user) + var err error + pswd, err = gopass.GetPass("Password:") + processError(err) + } +} + +func processError(err error) { + if err != nil { + log.Fatal("error:", err) + } +} + +func processCSL(s string) []string { + if s == "" { + return nil + } + + return strings.Split(s, ",") +} + +func runCreate() { + req := core.CreateRequest{ + Name: user, + Password: pswd, + } + resp, err := roServer.Create(req) + processError(err) + fmt.Println(resp.Status) +} + +func runDelegate() { + req := core.DelegateRequest{ + Name: user, + Password: pswd, + Uses: uses, + Time: time, + Users: processCSL(users), + Labels: processCSL(labels), + } + resp, err := roServer.Delegate(req) + processError(err) + fmt.Println(resp.Status) +} + +func runSummary() { + req := core.SummaryRequest{ + Name: user, + Password: pswd, + } + resp, err := roServer.Summary(req) + processError(err) + fmt.Println(resp) +} + +func runEncrypt() { + inBytes, err := ioutil.ReadFile(inPath) + processError(err) + req := core.EncryptRequest{ + Name: user, + Password: pswd, + Owners: processCSL(owners), + LeftOwners: processCSL(lefters), + RightOwners: processCSL(righters), + Labels: processCSL(labels), + Data: inBytes, + } + + resp, err := roServer.Encrypt(req) + processError(err) + fmt.Println("Response Status:", resp.Status) + outBytes := []byte(base64.StdEncoding.EncodeToString(resp.Response)) + ioutil.WriteFile(outPath, outBytes, 0644) +} + +func runDecrypt() { + inBytes, err := ioutil.ReadFile(inPath) + processError(err) + + // base64 decode the input + encBytes, err := base64.StdEncoding.DecodeString(string(inBytes)) + if err != nil { + log.Println("fail to base64 decode the data, proceed with raw data") + encBytes = inBytes + } + + req := core.DecryptRequest{ + Name: user, + Password: pswd, + Data: encBytes, + } + + resp, err := roServer.Decrypt(req) + processError(err) + var msg core.DecryptWithDelegates + err = json.Unmarshal(resp.Response, &msg) + processError(err) + fmt.Println("Response Status:", resp.Status) + fmt.Println("Secure:", msg.Secure) + fmt.Println("Delegates:", msg.Delegates) + ioutil.WriteFile(outPath, msg.Data, 0644) +} + +func main() { + flag.Usage = func() { + fmt.Println("Usage: ro [options] subcommand") + fmt.Println("Currently supported subcommands are:") + for key := range commandSet { + fmt.Println("\t", key, ":", commandSet[key].Desc) + } + + fmt.Println("Options:") + flag.PrintDefaults() + } + + registerFlags() + flag.Parse() + + if flag.NArg() != 1 { + flag.Usage() + os.Exit(1) + } + + action := flag.Arg(0) + + cmd, found := commandSet[action] + if !found { + fmt.Println("Unsupported subcommand:", action) + flag.Usage() + os.Exit(1) + } else { + var err error + roServer, err = client.NewRemoteServer(server, caPath) + processError(err) + + getUserCredentials() + cmd.Run() + } +}