File: pass_test.go

package info (click to toggle)
golang-github-howeyc-gopass 0.0~git20190910.7cb4b85%2Bdfsg.1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, experimental, sid, trixie
  • size: 80 kB
  • sloc: makefile: 2
file content (241 lines) | stat: -rw-r--r-- 7,454 bytes parent folder | download
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
/*
 * Copyright (c) 2012 Chris Howey
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package gopass

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"testing"
	"time"
)

// TestGetPasswd tests the password creation and output based on a byte buffer
// as input to mock the underlying getch() methods.
func TestGetPasswd(t *testing.T) {
	type testData struct {
		input []byte

		// Due to how backspaces are written, it is easier to manually write
		// each expected output for the masked cases.
		masked   string
		password string
		byesLeft int
		reason   string
	}

	ds := []testData{
		testData{[]byte("abc\n"), "***", "abc", 0, "Password parsing should stop at \\n"},
		testData{[]byte("abc\r"), "***", "abc", 0, "Password parsing should stop at \\r"},
		testData{[]byte("a\nbc\n"), "*", "a", 3, "Password parsing should stop at \\n"},
		testData{[]byte("*!]|\n"), "****", "*!]|", 0, "Special characters shouldn't affect the password."},

		testData{[]byte("abc\r\n"), "***", "abc", 1,
			"Password parsing should stop at \\r; Windows LINE_MODE should be unset so \\r is not converted to \\r\\n."},

		testData{[]byte{'a', 'b', 'c', 8, '\n'}, "***\b \b", "ab", 0, "Backspace byte should remove the last read byte."},
		testData{[]byte{'a', 'b', 127, 'c', '\n'}, "**\b \b*", "ac", 0, "Delete byte should remove the last read byte."},
		testData{[]byte{'a', 'b', 127, 'c', 8, 127, '\n'}, "**\b \b*\b \b\b \b", "", 0, "Successive deletes continue to delete."},
		testData{[]byte{8, 8, 8, '\n'}, "", "", 0, "Deletes before characters are noops."},
		testData{[]byte{8, 8, 8, 'a', 'b', 'c', '\n'}, "***", "abc", 0, "Deletes before characters are noops."},

		testData{[]byte{'a', 'b', 0, 'c', '\n'}, "***", "abc", 0,
			"Nil byte should be ignored due; may get unintended nil bytes from syscalls on Windows."},
	}

	// Redirecting output for tests as they print to os.Stdout but we want to
	// capture and test the output.
	for _, masked := range []bool{true, false} {
		for _, d := range ds {
			pipeBytesToStdin(d.input)

			r, w, err := os.Pipe()
			if err != nil {
				t.Fatal(err.Error())
			}

			result, err := getPasswd("", masked, os.Stdin, w)
			if err != nil {
				t.Errorf("Error getting password: %s", err.Error())
			}
			leftOnBuffer := flushStdin()

			// Test output (masked and unmasked).  Delete/backspace actually
			// deletes, overwrites and deletes again.  As a result, we need to
			// remove those from the pipe afterwards to mimic the console's
			// interpretation of those bytes.
			w.Close()
			output, err := ioutil.ReadAll(r)
			if err != nil {
				t.Fatal(err.Error())
			}
			var expectedOutput []byte
			if masked {
				expectedOutput = []byte(d.masked)
			} else {
				expectedOutput = []byte("")
			}
			if bytes.Compare(expectedOutput, output) != 0 {
				t.Errorf("Expected output to equal %v (%q) but got %v (%q) instead when masked=%v. %s", expectedOutput, string(expectedOutput), output, string(output), masked, d.reason)
			}

			if string(result) != d.password {
				t.Errorf("Expected %q but got %q instead when masked=%v. %s", d.password, result, masked, d.reason)
			}

			if leftOnBuffer != d.byesLeft {
				t.Errorf("Expected %v bytes left on buffer but instead got %v when masked=%v. %s", d.byesLeft, leftOnBuffer, masked, d.reason)
			}
		}
	}
}

// TestPipe ensures we get our expected pipe behavior.
func TestPipe(t *testing.T) {
	type testData struct {
		input    string
		password string
		expError error
	}
	ds := []testData{
		testData{"abc", "abc", io.EOF},
		testData{"abc\n", "abc", nil},
		testData{"abc\r", "abc", nil},
		testData{"abc\r\n", "abc", nil},
	}

	for _, d := range ds {
		_, err := pipeToStdin(d.input)
		if err != nil {
			t.Log("Error writing input to stdin:", err)
			t.FailNow()
		}
		pass, err := GetPasswd()
		if string(pass) != d.password {
			t.Errorf("Expected %q but got %q instead.", d.password, string(pass))
		}
		if err != d.expError {
			t.Errorf("Expected %v but got %q instead.", d.expError, err)
		}
	}
}

// flushStdin reads from stdin for .5 seconds to ensure no bytes are left on
// the buffer.  Returns the number of bytes read.
func flushStdin() int {
	ch := make(chan byte)
	go func(ch chan byte) {
		reader := bufio.NewReader(os.Stdin)
		for {
			b, err := reader.ReadByte()
			if err != nil { // Maybe log non io.EOF errors, if you want
				close(ch)
				return
			}
			ch <- b
		}
		close(ch)
	}(ch)

	numBytes := 0
	for {
		select {
		case _, ok := <-ch:
			if !ok {
				return numBytes
			}
			numBytes++
		case <-time.After(500 * time.Millisecond):
			return numBytes
		}
	}
	return numBytes
}

// pipeToStdin pipes the given string onto os.Stdin by replacing it with an
// os.Pipe.  The write end of the pipe is closed so that EOF is read after the
// final byte.
func pipeToStdin(s string) (int, error) {
	pipeReader, pipeWriter, err := os.Pipe()
	if err != nil {
		fmt.Println("Error getting os pipes:", err)
		os.Exit(1)
	}
	os.Stdin = pipeReader
	w, err := pipeWriter.WriteString(s)
	pipeWriter.Close()
	return w, err
}

func pipeBytesToStdin(b []byte) (int, error) {
	return pipeToStdin(string(b))
}

// TestGetPasswd_Err tests errors are properly handled from getch()
func TestGetPasswd_Err(t *testing.T) {
	var inBuffer *bytes.Buffer
	getch = func(io.Reader) (byte, error) {
		b, err := inBuffer.ReadByte()
		if err != nil {
			return 13, err
		}
		if b == 'z' {
			return 'z', fmt.Errorf("Forced error; byte returned should not be considered accurate.")
		}
		return b, nil
	}
	defer func() { getch = defaultGetCh }()

	for input, expectedPassword := range map[string]string{"abc": "abc", "abzc": "ab"} {
		inBuffer = bytes.NewBufferString(input)
		p, err := GetPasswdMasked()
		if string(p) != expectedPassword {
			t.Errorf("Expected %q but got %q instead.", expectedPassword, p)
		}
		if err == nil {
			t.Errorf("Expected error to be returned.")
		}
	}
}

func TestMaxPasswordLength(t *testing.T) {
	type testData struct {
		input       []byte
		expectedErr error

		// Helper field to output in case of failure; rather than hundreds of
		// bytes.
		inputDesc string
	}

	ds := []testData{
		testData{append(bytes.Repeat([]byte{'a'}, maxLength), '\n'), nil, fmt.Sprintf("%v 'a' bytes followed by a newline", maxLength)},
		testData{append(bytes.Repeat([]byte{'a'}, maxLength+1), '\n'), ErrMaxLengthExceeded, fmt.Sprintf("%v 'a' bytes followed by a newline", maxLength+1)},
		testData{append(bytes.Repeat([]byte{0x00}, maxLength+1), '\n'), ErrMaxLengthExceeded, fmt.Sprintf("%v 0x00 bytes followed by a newline", maxLength+1)},
	}

	for _, d := range ds {
		pipeBytesToStdin(d.input)
		_, err := GetPasswd()
		if err != d.expectedErr {
			t.Errorf("Expected error to be %v; isntead got %v from %v", d.expectedErr, err, d.inputDesc)
		}
	}
}