/
gosecret.go
399 lines (340 loc) · 12.2 KB
/
gosecret.go
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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
// This repository provides the gosecret package for encrypting and decrypting all or part of a []byte using AES-256-GCM.
// gosecret was written to work with tools such as https://github.com/ryanbreen/git2consul,
// https://github.com/ryanbreen/fsconsul, and https://github.com/hashicorp/envconsul, providing a mechanism for storing
// and moving secure secrets around the network and decrypting them on target systems via a previously installed key.
//
// gosecret is built on the assumption that only part of any given file should be encrypted: in most configuration files,
// there are few fields that need to be encrypted and the rest can safely be left as plaintext. gosecret can be used in a
// mode where the entire file is a single encrypted tag, but you should examine whether there's a good reason to do so.
//
// To signify that you wish a portion of a file to be encrypted, you need to denote that portion of the file with a tag.
// Imagine that your file contains this bit of JSON:
//
// { 'dbpassword': 'kadjf454nkklz' }
//
// To have gosecret encrypt just the password, you might create a tag like this:
//
// { 'dbpassword': '[gosecret|my mongo db password|kadjf454nkklz]' }
//
// The components of the tag are, in order:
//
// 1. The gosecret header
// 2. An auth data string.
// 3. The plaintext we wish to encrypt.
//
// Note that auth data can be any string (as long as it doesn't contain the pipe character, '|'). This tag is hashed and
// included as part of the ciphertext. It's helpful if this tag has some semantic meaning describing the encrypted data.
// Auth data string is not private data. It is hashed and used as part of the ciphertext such that decryption will fail if
// any of auth data, initialization vector, and key are incorrect for a specific piece of ciphertext. This increases the
// security of the encryption algorithm by obviating attacks that seek to learn about the key and initialization vector through
// repeated decryption attempts.
//
// With this tag in place, you can encrypt the file via 'gosecret-cli'. The result will yield something that looks like this,
// assuming you encrypted it with a keyfile named 'myteamkey-2014-09-19':
//
// { 'dbpassword': '[gosecret|my mongo db password|TtRotEctptR1LfA5tSn3kAtzjyWjAp+dMOHe6lc=|FJA7qz+dUdubwv9G|myteamkey-2014-09-19]' }
//
// The components of the tag are, in order:
//
// 1. The gosecret header
// 2. The auth data string
// 3. The ciphertext, in Base64
// 4. The initialization vector, in Base64
// 5. The key name
//
// A key may be used any number of times, but a new initialization vector should be created each time the key is used. This is
// handled for you automatically by gosecret.
//
// When this is decrypted by a system that contains key 'myteamkey-2014-09-19', the key and initialization vector are used to both
// authenticate the auth data string and (if authentic) decrypt the ciphertext back to plaintext. This will result in the
// encrypted tag being replaced by the plaintext, returning us to our original form:
//
// { 'dbpassword': 'kadjf454nkklz' }
//
// A file can contain any number of goscecret tags, or the entire file can be a gosecret tag. It's up to you as the application
// developer or system maintainer to decide what balance of security vs readability you desire.
package api
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"path/filepath"
"regexp"
"strings"
"unicode/utf8"
)
type EncryptionTag struct {
AuthData []byte
Plaintext []byte
KeyName string
}
type DecryptionTag struct {
AuthData []byte
CipherText []byte
InitVector []byte
KeyName string
}
//Encrypt the tag, returns the cypher text
func (et *EncryptionTag) EncryptTag(keystore string, iv []byte) ([]byte, error) {
keypath := filepath.Join(keystore, et.KeyName)
key, err := getBytesFromBase64File(keypath)
aes, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(aes)
if err != nil {
return nil, err
}
return aesgcm.Seal(nil, iv, et.Plaintext, et.AuthData), nil
}
func ParseEncrytionTag(keystore string, s ...string) (DecryptionTag, error) {
// If the function does not contain correct number of arguments
if len(s) != 3 {
return DecryptionTag{}, fmt.Errorf("expected 3 arguments, got %d", len(s))
}
//Create EncryptionTag object
et := EncryptionTag{
[]byte(s[0]),
[]byte(s[1]),
s[2],
}
iv := createIV()
cipherText, err := et.EncryptTag(keystore, iv)
if err != nil {
return DecryptionTag{}, err
}
dt := DecryptionTag {
[]byte(s[0]),
cipherText,
iv,
s[2],
}
return dt, nil
}
func (dt *DecryptionTag) DecryptTag(keystore string) ([]byte, error) {
keypath, err := getBytesFromBase64File(filepath.Join(keystore, dt.KeyName))
if err != nil {
fmt.Println("Unable to read file for decryption", err)
return nil, err
}
aesgcm, err := createCipher(keypath)
if err != nil {
return nil, err
}
return aesgcm.Open(nil, dt.InitVector, dt.CipherText, dt.AuthData)
}
func ParseDecryptionTag(keystore string, s ...string) (string, error) {
if len(s) != 4 {
return "", fmt.Errorf("expected 4 arguments, go %d", len(s))
}
ct, err := base64.StdEncoding.DecodeString(s[1])
if err != nil {
fmt.Println("Unable to decode ciphertext", err)
return "", err
}
iv, err := base64.StdEncoding.DecodeString(s[2])
if err != nil {
fmt.Println("Unable to decode IV", err)
return "", err
}
dt := DecryptionTag{
[]byte(s[0]),
ct,
iv,
s[3],
}
plaintext, err := dt.DecryptTag(keystore)
if err != nil {
return "", err
}
return string(plaintext), nil
}
//////////////////////////////////////////////
// Old functions and methods for handling tags
//////////////////////////////////////////////
var gosecretRegex, _ = regexp.Compile("\\[(gosecret\\|[^\\]]*)\\]")
// Create a random array of bytes. This is used to create keys and IVs.
func createRandomBytes(length int) []byte {
random_bytes := make([]byte, length)
rand.Read(random_bytes)
return random_bytes
}
// Create a random 256-bit array suitable for use as an AES-256 cipher key.
func CreateKey() []byte {
return createRandomBytes(32)
}
// Create a random initialization vector to use for encryption. Each gosecret tag should have a different
// initialization vector.
func createIV() []byte {
return createRandomBytes(12)
}
// Create an AES-256 GCM cipher for use by gosecret. This is the only form of encryption supported by gosecret,
// and barring any major flaws being discovered 256-bit keys should be adequate for quite some time.
func createCipher(key []byte) (cipher.AEAD, error) {
aes, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(aes)
if err != nil {
return nil, err
}
return aesgcm, nil
}
// Given an input plaintext []byte and key, initialization vector, and auth data []bytes, encrypt the plaintext
// using an AES-GCM cipher and return a []byte containing the result.
func encrypt(plaintext, key, iv, ad []byte) ([]byte, error) {
aesgcm, err := createCipher(key)
if err != nil {
return nil, err
}
return aesgcm.Seal(nil, iv, plaintext, ad), nil
}
// Given an input ciphertext []byte and the key, initialization vector, and auth data []bytes used to encrypt it,
// decrypt using an AES-GCM cipher and return a []byte containing the result.
func decrypt(ciphertext, key, iv, ad []byte) ([]byte, error) {
aesgcm, err := createCipher(key)
if err != nil {
return nil, err
}
return aesgcm.Open(nil, iv, ciphertext, ad)
}
// Given an input []byte of Base64 encoded data, return a slice containing the decoded data.
func decodeBase64(input []byte) ([]byte, error) {
output := make([]byte, base64.StdEncoding.DecodedLen(len(input)))
l, err := base64.StdEncoding.Decode(output, input)
if err != nil {
return nil, err
}
return output[:l], nil
}
// Given a file path known to contain Base64 encoded data, return a slice containing the decoded data.
func getBytesFromBase64File(filepath string) ([]byte, error) {
file, err := ioutil.ReadFile(filepath)
if err != nil {
fmt.Println("Unable to read file", err)
return nil, err
}
return decodeBase64(file)
}
// Given an array of encrypted tag parts and a directory of keys, convert the encrypted gosecret tag into
// a plaintext []byte.
func decryptTag(tagParts []string, keyroot string) ([]byte, error) {
ct, err := base64.StdEncoding.DecodeString(tagParts[2])
if err != nil {
fmt.Println("Unable to decode ciphertext", tagParts[2], err)
return nil, err
}
iv, err := base64.StdEncoding.DecodeString(tagParts[3])
if err != nil {
fmt.Println("Unable to decode IV", err)
return nil, err
}
key, err := getBytesFromBase64File(filepath.Join(keyroot, tagParts[4]))
if err != nil {
fmt.Println("Unable to read file for decryption", err)
return nil, err
}
plaintext, err := decrypt(ct, key, iv, []byte(tagParts[1]))
if err != nil {
return nil, err
}
return plaintext, nil
}
// Given an array of unencrypted tag parts, a []byte containing the key, and a name for the key, generate
// an encrypted gosecret tag.
func encryptTag(tagParts []string, key []byte, keyname string) ([]byte, error) {
iv := createIV()
cipherText, err := encrypt([]byte(tagParts[2]), key, iv, []byte(tagParts[1]))
if err != nil {
return []byte(""), err
}
return []byte(fmt.Sprintf("[gosecret|%s|%s|%s|%s]",
tagParts[1],
base64.StdEncoding.EncodeToString(cipherText),
base64.StdEncoding.EncodeToString(iv),
keyname)), nil
}
// EncryptTags looks for any tagged data of the form [gosecret|authtext|plaintext] in the input content byte
// array and replaces each with an encrypted gosecret tag. Note that the input content must be valid UTF-8.
// The second parameter is the name of the keyfile to use for encrypting all tags in the content, and the
// third parameter is the 256-bit key itself.
// EncryptTags returns a []byte with all unencrypted [gosecret] blocks replaced by encrypted gosecret tags.
func EncryptTags(content []byte, keyname, keyroot string, rotate bool) ([]byte, error) {
if !utf8.Valid(content) {
return nil, errors.New("File is not valid UTF-8")
}
match := gosecretRegex.Match(content)
if match {
keypath := filepath.Join(keyroot, keyname)
key, err := getBytesFromBase64File(keypath)
if err != nil {
fmt.Println("Unable to read encryption key")
return nil, err
}
content = gosecretRegex.ReplaceAllFunc(content, func(match []byte) []byte {
matchString := string(match)
matchString = matchString[:len(matchString)-1]
parts := strings.Split(string(matchString), "|")
if len(parts) > 3 {
if rotate {
plaintext, err := decryptTag(parts, keyroot)
if err != nil {
fmt.Println("Unable to decrypt ciphertext", parts[2], err)
return nil
}
parts[2] = string(plaintext)
replacement, err := encryptTag(parts, key, keyname)
if err != nil {
fmt.Println("Failed to encrypt tag", err)
return nil
}
return replacement
} else {
return match
}
} else {
replacement, err := encryptTag(parts, key, keyname)
if err != nil {
fmt.Println("Failed to encrypt tag", err)
return nil
}
return replacement
}
})
}
return content, nil
}
// DecryptTags looks for any tagged data of the form [gosecret|authtext|ciphertext|initvector|keyname] in the
// input content byte array and replaces each with a decrypted version of the ciphertext. Note that the
// input content must be valid UTF-8. The second parameter is the path to the directory in which keyfiles
// live. For each |keyname| in a gosecret block, there must be a corresponding file of the same name in the
// keystore directory.
// DecryptTags returns a []byte with all [gosecret] blocks replaced by plaintext.
func DecryptTags(content []byte, keyroot string) ([]byte, error) {
if !utf8.Valid(content) {
return nil, errors.New("File is not valid UTF-8")
}
content = gosecretRegex.ReplaceAllFunc(content, func(match []byte) []byte {
matchString := string(match)
matchString = matchString[:len(matchString)-1]
parts := strings.Split(matchString, "|")
if len(parts) < 5 {
// Block is not encrypted. Noop.
return match
} else {
plaintext, err := decryptTag(parts, keyroot)
if err != nil {
fmt.Println("Unable to decrypt tag", err)
return nil
}
return plaintext
}
})
return content, nil
}