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
introduced
by
![]() |
|||
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
|
|||
166 | if err != nil { |
||
167 | return "", err |
||
168 | } |
||
169 | password := string(bytePassword) |
||
170 | |||
171 | return strings.TrimSpace(password), nil |
||
172 | } |
||
173 |