Issues (3)

decrypt_pem.go (2 issues)

Severity
1
package decryptpem
2
3
import (
4
	"crypto/x509"
5
	"encoding/pem"
6
	"fmt"
7
	"io/ioutil"
8
	"strings"
9
	"syscall"
10
	"time"
11
12
	"github.com/phayes/errors"
13
14
	"golang.org/x/crypto/ssh/terminal"
15
)
16
17
// Configuration
18
var (
19
20
	// PasswordDelay sets the delay for any password tries and retries as a defence against brute force password guessing
21
	// By default there is no delay
22
	PasswordDelay time.Duration
23
24
	// MaxTries sets the maximum number of times a password may be tried before erroring out.
25
	// A MaxTries of 1 means that there is only one try allowed (no retries)
26
	// A MaxTries of 0 means infinite retries are allowed.
27
	// When tries run out, an error of x509.IncorrectPasswordError will be returned.
28
	MaxTries int
29
)
30
31
// Errors
32
var (
33
	ErrReadFile     = errors.New("decryptpem: Cannot read and decrypt file")
34
	ErrDecryptBlock = errors.New("decryptpem: Cannot decrypt pem block")
35
	ErrNoBlockFound = errors.New("decryptpem: No PEM block found")
36
)
37
38
// DecryptFileWithPassword retrieives the pem file and decrypts it with the provided password
39
func DecryptFileWithPassword(filename string, password string) (*pem.Block, error) {
40
	content, err := ioutil.ReadFile(filename)
41
	if err != nil {
42
		return nil, errors.Wrap(err, ErrReadFile)
43
	}
44
45
	block, _, err := DecryptBytesWithPassword(content, password)
46
47
	if err != nil {
48
		return nil, errors.Wrap(err, ErrReadFile)
49
	}
50
	if block == nil {
51
		return nil, errors.Wrap(ErrNoBlockFound, ErrReadFile)
52
	}
53
54
	return block, nil
55
}
56
57
// DecryptFileWithPrompt retrieives the pem file and decrypts it using a prompt from the user
58
// When password retries run out, a x509.IncorrectPasswordError error will be returned.
59
func DecryptFileWithPrompt(filename string) (*pem.Block, error) {
60
	content, err := ioutil.ReadFile(filename)
61
	if err != nil {
62
		return nil, errors.Wrap(err, ErrReadFile)
63
	}
64
65
	prompt := "Enter password for " + filename + ": "
66
	incorrectMessage := "Incorrect password, please try again"
67
68
	block, _, err := DecryptBytesWithPrompt(content, prompt, incorrectMessage)
69
70
	if err != nil {
71
		return nil, errors.Wrap(err, ErrReadFile)
72
	}
73
	if block == nil {
74
		return nil, errors.Wrap(ErrNoBlockFound, ErrReadFile)
75
	}
76
77
	return block, nil
78
}
79
80
// DecryptBytesWithPassword will find the next PEM formatted block (certificate, private key etc) in the input.
81
// It returns that block decrypted and the remainder of the input.
82
// If no PEM data is found, block is nil and the whole of the input is returned in rest.
83
func DecryptBytesWithPassword(pembytes []byte, password string) (block *pem.Block, rest []byte, err error) {
84
	block, rest = pem.Decode(pembytes)
85
	if block == nil {
86
		return nil, rest, errors.Wrap(ErrNoBlockFound, ErrDecryptBlock)
87
	}
88
89
	// If it's not encrypted, just return it
90
	if !x509.IsEncryptedPEMBlock(block) {
91
		return block, rest, nil
92
	}
93
94
	// It's encrypted, decrypt it
95
	der, err := x509.DecryptPEMBlock(block, []byte(password))
96
	if err != nil {
97
		if errors.IsA(err, x509.IncorrectPasswordError) {
98
			return nil, rest, err
99
		}
100
		return nil, rest, errors.Wrap(err, ErrDecryptBlock)
101
	}
102
103
	// Decryption OK, strip encryption headers and return the decrypted PEMBlock
104
	delete(block.Headers, "Proc-Type")
105
	delete(block.Headers, "DEK-Info")
106
	block.Bytes = der
107
108
	return block, rest, nil
109
}
110
111
// DecryptBytesWithPrompt is the same as DecryptBytesWithPassword, but if the
112
// pem block is password protected, it will prompt stdout / stdin for a password
113
// When password retries run out, a x509.IncorrectPasswordError error will be returned.
114
func DecryptBytesWithPrompt(pembytes []byte, prompt string, incorrectMessage string) (block *pem.Block, rest []byte, err error) {
115
	block, rest = pem.Decode(pembytes)
116
	if block == nil {
117
		return nil, rest, errors.Wrap(ErrNoBlockFound, ErrDecryptBlock)
118
	}
119
120
	// If it's not encrypted, just return it
121
	if !x509.IsEncryptedPEMBlock(block) {
122
		return block, rest, nil
123
	}
124
125
	// It's encrypted, prompt for password, retrying as needed
126
	tries := 1
127
	for {
128
129
		// Password prompt delay if configured
130
		if PasswordDelay != 0 {
131
			time.Sleep(PasswordDelay)
132
		}
133
134
		// Get the password
135
		fmt.Print(prompt)
136
		password, err := getPassword()
137
		if err != nil {
138
			return nil, rest, err
139
		}
140
		// Print a linebreak to make the password return feel natural
141
		fmt.Println("")
142
143
		block, rest, err = DecryptBytesWithPassword(pembytes, password)
144
		if err != nil {
145
			if err == x509.IncorrectPasswordError {
146
				// If the password is incorrect, either error out or try again depending on configuration
147
				if MaxTries != 0 && tries >= MaxTries {
148
					return nil, rest, err
149
				} else {
0 ignored issues
show
if block ends with a return statement, so drop this else and outdent its block
Loading history...
150
					fmt.Println(incorrectMessage)
151
					tries++
152
					continue
153
				}
154
			}
155
			return nil, rest, errors.Wrap(ErrNoBlockFound, ErrDecryptBlock)
156
		}
157
158
		// Decryption OK, return block
159
		return block, rest, nil
160
	}
161
162
}
163
164
func getPassword() (string, error) {
165
	bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
0 ignored issues
show
unnecessary conversion
Loading history...
166
	if err != nil {
167
		return "", err
168
	}
169
	password := string(bytePassword)
170
171
	return strings.TrimSpace(password), nil
172
}
173