1/*
2 * Copyright (c) 2012 Chris Howey
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17package gopass
18
19import (
20	"errors"
21	"fmt"
22	"io"
23	"os"
24)
25
26type FdReader interface {
27	io.Reader
28	Fd() uintptr
29}
30
31var defaultGetCh = func(r io.Reader) (byte, error) {
32	buf := make([]byte, 1)
33	if n, err := r.Read(buf); n == 0 || err != nil {
34		if err != nil {
35			return 0, err
36		}
37		return 0, io.EOF
38	}
39	return buf[0], nil
40}
41
42var (
43	maxLength            = 512
44	ErrInterrupted       = errors.New("interrupted")
45	ErrMaxLengthExceeded = fmt.Errorf("maximum byte limit (%v) exceeded", maxLength)
46
47	// Provide variable so that tests can provide a mock implementation.
48	getch = defaultGetCh
49)
50
51// getPasswd returns the input read from terminal.
52// If prompt is not empty, it will be output as a prompt to the user
53// If masked is true, typing will be matched by asterisks on the screen.
54// Otherwise, typing will echo nothing.
55func getPasswd(prompt string, masked bool, r FdReader, w io.Writer) ([]byte, error) {
56	var err error
57	var pass, bs, mask []byte
58	if masked {
59		bs = []byte("\b \b")
60		mask = []byte("*")
61	}
62
63	if isTerminal(r.Fd()) {
64		if oldState, err := makeRaw(r.Fd()); err != nil {
65			return pass, err
66		} else {
67			defer func() {
68				restore(r.Fd(), oldState)
69				fmt.Fprintln(w)
70			}()
71		}
72	}
73
74	if prompt != "" {
75		fmt.Fprint(w, prompt)
76	}
77
78	// Track total bytes read, not just bytes in the password.  This ensures any
79	// errors that might flood the console with nil or -1 bytes infinitely are
80	// capped.
81	var counter int
82	for counter = 0; counter <= maxLength; counter++ {
83		if v, e := getch(r); e != nil {
84			err = e
85			break
86		} else if v == 127 || v == 8 {
87			if l := len(pass); l > 0 {
88				pass = pass[:l-1]
89				fmt.Fprint(w, string(bs))
90			}
91		} else if v == 13 || v == 10 {
92			break
93		} else if v == 3 {
94			err = ErrInterrupted
95			break
96		} else if v != 0 {
97			pass = append(pass, v)
98			fmt.Fprint(w, string(mask))
99		}
100	}
101
102	if counter > maxLength {
103		err = ErrMaxLengthExceeded
104	}
105
106	return pass, err
107}
108
109// GetPasswd returns the password read from the terminal without echoing input.
110// The returned byte array does not include end-of-line characters.
111func GetPasswd() ([]byte, error) {
112	return getPasswd("", false, os.Stdin, os.Stdout)
113}
114
115// GetPasswdMasked returns the password read from the terminal, echoing asterisks.
116// The returned byte array does not include end-of-line characters.
117func GetPasswdMasked() ([]byte, error) {
118	return getPasswd("", true, os.Stdin, os.Stdout)
119}
120
121// GetPasswdPrompt prompts the user and returns the password read from the terminal.
122// If mask is true, then asterisks are echoed.
123// The returned byte array does not include end-of-line characters.
124func GetPasswdPrompt(prompt string, mask bool, r FdReader, w io.Writer) ([]byte, error) {
125	return getPasswd(prompt, mask, r, w)
126}
127