git » remoteu2f » master » tree

[master] / remoteu2f-cli / main.go

// remoteu2f command-line interface
package main

import (
	"bufio"
	"fmt"
	"os"
	"os/user"
	"sort"
	"strings"

	"github.com/urfave/cli"

	"blitiri.com.ar/go/remoteu2f/internal/client"
)

var stdinScanner *bufio.Scanner

// readLine reads a line from os.Stdin and returns it.
// It exits the process on errors.
func readLine() string {
	if !stdinScanner.Scan() && stdinScanner.Err() != nil {
		fmt.Printf("Error reading from stdin: %v\n", stdinScanner.Err())
		os.Exit(1)
	}
	return strings.TrimSpace(stdinScanner.Text())
}

func main() {
	app := cli.NewApp()
	app.Name = "remoteu2f-cli"
	app.Usage = "remoteu2f command line tool"
	app.Flags = []cli.Flag{
		cli.StringFlag{
			Name:  "ca_file",
			Usage: "path to the CA file (default: use system's)",
		},
	}
	app.Commands = []cli.Command{
		{
			Name:   "init",
			Usage:  "Create initial configuration (interactive)",
			Action: Init,
			Flags: []cli.Flag{
				cli.BoolFlag{
					Name:  "override",
					Usage: "override the configuration if it exists",
				},
			},
		},
		{
			Name:   "register",
			Usage:  "Register a new security key",
			Action: Register,
		},
		{
			Name:   "auth",
			Usage:  "Perform a test authentication",
			Action: Authenticate,
		},
		{
			Name:   "new_backup_codes",
			Usage:  "Generate new backup codes, remove the old ones",
			Action: NewBackupCodes,
		},
		{
			Name:   "print_config",
			Usage:  "Print the config (useful for debugging)",
			Action: PrintConfig,
		},
		{
			Name:   "pam",
			Usage:  "Perform an authentication for PAM",
			Action: PAM,
			Flags: []cli.Flag{
				cli.BoolFlag{
					Name:  "nullok",
					Usage: "return success if there is no configuration",
				},
			},
		},
	}

	// Initialize the stdin scanner so the subcommands can use it.
	stdinScanner = bufio.NewScanner(os.Stdin)

	app.RunAndExitOnError()
}

func fatalf(format string, a ...interface{}) {
	fmt.Printf(format, a...)
	os.Exit(1)
}

func mustReadConfig() *client.Config {
	conf, err := client.ReadDefaultConfig("")
	if err != nil {
		fatalf("Error reading config: %v\n", err)
	}

	return conf
}

func mustWriteConfig(c *client.Config, homedir string) {
	err := c.WriteToDefaultPath(homedir)
	if err != nil {
		fatalf("Error writing config: %v\n", err)
	}
}

func mustGRPCClient(addr, token, caFile string) *client.RemoteU2FClient {
	c, err := client.GRPCClient(addr, token, caFile)
	if err != nil {
		fatalf("Error connecting with the server: %v\n", err)
	}

	return c
}

func mustUserInfo() (string, string) {
	user, err := user.Current()
	if err != nil {
		fatalf("error getting current user: %v", err)
	}

	hostname, err := os.Hostname()
	if err != nil {
		fatalf("error getting hostname: %v", err)
	}

	return user.Username, hostname
}

func mustLookupHomeDir(username string) string {
	info, err := user.Lookup(username)
	if err != nil {
		fatalf("Could not find $HOME for user: %v\n", err)
	}

	return info.HomeDir
}

func printBackupCodes(conf *client.Config) {
	// Sort the codes so we get stable and more friendly output.
	var codes []string
	for c := range conf.BackupCodes {
		codes = append(codes, c)
	}
	sort.Strings(codes)

	for _, c := range codes {
		fmt.Printf("  %v\n", c)
	}
}

func printRegistrations(conf *client.Config) {
	// Sort the descriptions so we get stable and more friendly output.
	var ds []string
	for d := range conf.Registrations {
		ds = append(ds, d)
	}
	sort.Strings(ds)

	for _, d := range ds {
		fmt.Printf("  %q\n", d)
	}
}

func Init(ctx *cli.Context) {
	if !ctx.Bool("override") {
		// We don't want to accidentally override the config.
		_, err := client.ReadDefaultConfig("")
		if err == nil {
			fmt.Printf("Configuration already exists at %s\n",
				client.DefaultConfigFullPath(""))
			fmt.Printf("Use --override to continue anyway.\n")
			os.Exit(1)
		}
	}

	fmt.Printf("- GRPC server address to use? (e.g. 'mydomain.com:8801')\n")
	addr := readLine()
	fmt.Printf("- Authorization token? (given to you by the server admin)\n")
	token := readLine()

	fmt.Printf("- Contacting server...\n")
	c, err := client.GRPCClient(addr, token, ctx.GlobalString("ca_file"))
	if err != nil {
		fmt.Printf("Error connecting with the server: %v\n", err)
		fmt.Printf("Check the parameters above and try again.\n")
		os.Exit(1)
	}

	appID, err := c.GetAppID()
	if err != nil {
		fmt.Printf("RPC error: %v\n", err)
		fmt.Printf("Check the parameters above and try again.\n")
		os.Exit(1)
	}

	fmt.Printf("It worked!  AppID: %s\n", appID)

	conf := &client.Config{
		Addr:          addr,
		Token:         token,
		AppID:         appID,
		Registrations: map[string][]byte{},
	}

	err = conf.NewBackupCodes()
	if err != nil {
		fatalf("Error generating backup codes: %v\n", err)
	}

	mustWriteConfig(conf, "")
	fmt.Printf("Config written to %s\n", client.DefaultConfigFullPath(""))

	fmt.Printf("\n")
	fmt.Printf("Please write down your backup codes:\n")
	printBackupCodes(conf)

	fmt.Printf("\n")
	fmt.Printf("All done!\n")
	fmt.Printf("To register a security key, run:  remoteu2f-cli register\n")
}

func PrintConfig(ctx *cli.Context) {
	conf := mustReadConfig()
	fmt.Printf("GRPC address:   %s\n", conf.Addr)
	fmt.Printf("Client token:   %s\n", conf.Token)
	fmt.Printf("Application ID: %s\n", conf.AppID)

	fmt.Printf("Registered keys:\n")
	printRegistrations(conf)

	fmt.Printf("Backup codes:\n")
	printBackupCodes(conf)
}

func Register(ctx *cli.Context) {
	conf := mustReadConfig()
	c := mustGRPCClient(conf.Addr, conf.Token, ctx.GlobalString("ca_file"))

	user, hostname := mustUserInfo()
	msg := fmt.Sprintf("%s@%s", user, hostname)

	pr, err := c.PrepareRegister(msg, conf.AppID, conf.RegistrationValues())
	if err != nil {
		fatalf("Error preparing registration: %v\n", err)
	}
	fmt.Printf("Go to:  %s\n", pr.Key.Url)

	reg, err := c.CompleteRegister(pr)
	if err != nil {
		fatalf("Error completing registration: %v\n", err)
	}

	fmt.Printf("Description for this security key:\n")
	desc := readLine()

	if conf.Registrations == nil {
		conf.Registrations = map[string][]byte{}
	}
	conf.Registrations[desc] = reg

	mustWriteConfig(conf, "")
	fmt.Println("Success, registration written to config")
}

func Authenticate(ctx *cli.Context) {
	conf := mustReadConfig()
	c := mustGRPCClient(conf.Addr, conf.Token, ctx.GlobalString("ca_file"))

	user, hostname := mustUserInfo()
	msg := fmt.Sprintf("%s@%s", user, hostname)

	if len(conf.Registrations) == 0 {
		fmt.Printf("Error: no registrations found\n")
		fatalf("To register a security key, run:  remoteu2f-cli register\n")
	}

	pa, err := c.PrepareAuthentication(
		msg, conf.AppID, conf.RegistrationValues())
	if err != nil {
		fatalf("Error preparing authentication: %v\n", err)
	}
	fmt.Printf("Go to:  %s\n", pa.Key.Url)

	err = c.CompleteAuthentication(pa)
	if err != nil {
		fatalf("Error completing authentication: %v\n", err)
	}

	fmt.Println("Authentication succeeded")
}

func NewBackupCodes(ctx *cli.Context) {
	conf := mustReadConfig()
	err := conf.NewBackupCodes()
	if err != nil {
		fatalf("Error generating new backup codes: %v\n", err)
	}

	mustWriteConfig(conf, "")

	fmt.Printf("New backup codes:\n")
	for s, _ := range conf.BackupCodes {
		fmt.Printf("  %v\n", s)
	}
}

func PAM(ctx *cli.Context) {
	// We need to find the user's home first.
	username := os.Getenv("PAM_USER")
	homedir := mustLookupHomeDir(username)

	nullok := ctx.Bool("nullok")
	conf, err := client.ReadDefaultConfig(homedir)
	if err != nil {
		if nullok {
			os.Exit(0)
		} else {
			fatalf("Error reading config: %v\n", err)
		}
	}

	if len(conf.Registrations) == 0 {
		if nullok {
			os.Exit(0)
		} else {
			fatalf("Error: no registrations found\n")
		}
	}

	c := mustGRPCClient(conf.Addr, conf.Token, ctx.GlobalString("ca_file"))

	hostname, err := os.Hostname()
	if err != nil {
		fatalf("Error getting hostname: %v", err)
	}
	msg := fmt.Sprintf("%s@%s", username, hostname)

	pa, err := c.PrepareAuthentication(
		msg, conf.AppID, conf.RegistrationValues())
	if err != nil {
		fatalf("Error preparing authentication: %v\n", err)
	}

	fmt.Printf("Authenticate here and press enter:  %s\n", pa.Key.Url)

	// Closing stdout makes pam_prompt_exec send the prompt over.
	os.Stdout.Close()

	// Read input, and check if it's a backup code.
	// Never take a backup code of less than 6 characters, just in case some
	// data handling error makes them appear in conf.BackupCodes.
	input := readLine()
	if _, ok := conf.BackupCodes[input]; len(input) >= 6 && ok {
		delete(conf.BackupCodes, input)
		mustWriteConfig(conf, homedir)
		os.Exit(0)
	}

	err = c.CompleteAuthentication(pa)
	if err != nil {
		fatalf("Error completing authentication: %v\n", err)
	}

	os.Exit(0)
}