ataraskov.dev

About everything and nothing


Pipes and passwords

How hard can it be to read a password in your next CLI project written in Go? Should be as straightforward as:

1
2
3
4
5
6
7
8
9
func getPass(prompt string) (string, error) {
	stdin := int(os.Stdin.Fd())
	fmt.Print(prompt)
	pass, err := term.ReadPassword(stdin)
	if err != nil {
		return "", err
	}
    return pass, nil
}

 

Nice, issue closed, commit is ready, we are done for today! But wait, what if we do not like to pass password as an argument (any one will see it as argument in top/ps/… output). Easy, just pass it via standard input, shouldn’t we? Let’s test quickly:

$ echo $secret|go run main.go
Password: panic: inappropriate ioctl for device

goroutine 1 [running]:
main.main()
        main.go:15 +0x154
exit status 2

 

Oops. term.ReadPassword is not able to handle pipes. Pipes are just special files, so we can check for pipes via fs.ModeNamedPipe bit mask. And just read from it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func getPass() (string, error) {
	fi, err := os.Stdin.Stat()
    ...

    // check if we are dealing with a pipe
	if fi.Mode()&fs.ModeNamedPipe != 0 {
			reader := bufio.NewReader(os.Stdin)
	        pass, err := reader.ReadString('\n')
    }
    ...
}

 

And smashing everything together. Plus a bonus of handling termination and interruption of password prompt (thanks to an old google groups thread):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// readFile reads password from a file descriptor
func readFile(fd *os.File) (string, error) {
	reader := bufio.NewReader(os.Stdin)
	pass, err := reader.ReadString('\n')
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(pass), nil
}

// readTerm reads password from a terminal
func readTerm(prompt string) (string, error) {
	stdin := int(os.Stdin.Fd())
	// Get the initial state of the terminal.
	initialTermState, err := term.GetState(stdin)
	if err != nil {
		return "", err
	}
	// Restore it in the event of an interrupt.
	c := make(chan os.Signal, 1)
	signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
	go func() {
		<-c
		_ = term.Restore(stdin, initialTermState)
		os.Exit(1)
	}()
	defer signal.Stop(c)

	fmt.Print(prompt)
	pass, err := term.ReadPassword(stdin)
	fmt.Println("")
	if err != nil {
		return "", err
	}

	return string(pass), nil
}

// getPassword reads password from terminal or pipe
func getPassword(prompt string) (string, error) {
	fi, err := os.Stdin.Stat()
	if err != nil {
		return "", err
	}
	fmt.Printf(">> Stdin mode: %v\n", fi.Mode())

	switch {
	case fi.Mode()&fs.ModeNamedPipe != 0:
		fmt.Println(">> Reading from a pipe")
		return readFile(os.Stdin)
	case fi.Mode()&fs.ModeDevice != 0:
		fmt.Println(">> Reading from a term")
		return readTerm(prompt)
	default:
        // this will break *nix Heredoc, but this is intentional
		return "", fmt.Errorf("unsupported input")
	}
}

func main() {
	pass, err := getPassword("Password: ")
	if err != nil {
		panic(err)
	}
	fmt.Printf("Password is %s\n", pass)
}

 

This is not a production ready code, and it has some debug/demonstration prints in it, but something consice and functional enough to start with ;)

References:

Wed Aug 16, 2023 / 534 words / Golang Programming Cli