-
Notifications
You must be signed in to change notification settings - Fork 5.6k
/
Copy pathsecret.go
315 lines (263 loc) · 9.29 KB
/
secret.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
package config
import (
"fmt"
"log"
"regexp"
"strings"
"sync/atomic"
"github.com/influxdata/telegraf"
)
// unlinkedSecrets contains the list of secrets that contain
// references not yet linked to their corresponding secret-store.
// Those secrets must later (after reading the config) be linked
// by the config to their respective secret-stores.
// Secrets containing constant strings will not be found in this
// list.
var unlinkedSecrets = make([]*Secret, 0)
// secretStorePattern is a regex to validate secret-store IDs
var secretStorePattern = regexp.MustCompile(`^\w+$`)
// secretPattern is a regex to extract references to secrets store in a secret-store
var secretPattern = regexp.MustCompile(`@\{(\w+:\w+)\}`)
// secretCandidatePattern is a regex to find secret candidates to warn users on invalid characters in references
var secretCandidatePattern = regexp.MustCompile(`@\{.+?:.+?}`)
// secretCount is the number of secrets use in Telegraf
var secretCount atomic.Int64
// selectedImpl is the configured implementation for secrets
var selectedImpl secretImpl = &protectedSecretImpl{}
// secretImpl represents an abstraction for different implementations of secrets
type secretImpl interface {
Container(secret []byte) secretContainer
EmptyBuffer() SecretBuffer
Wipe(secret []byte)
}
func EnableSecretProtection() {
selectedImpl = &protectedSecretImpl{}
}
func DisableSecretProtection() {
selectedImpl = &unprotectedSecretImpl{}
}
// secretContainer represents an abstraction of the container holding the
// actual secret value
type secretContainer interface {
Destroy()
Equals(ref []byte) (bool, error)
Buffer() (SecretBuffer, error)
AsBuffer(secret []byte) SecretBuffer
Replace(secret []byte)
}
// SecretBuffer allows to access the content of the secret
type SecretBuffer interface {
// Size returns the length of the buffer content
Size() int
// Grow will grow the capacity of the underlying buffer to the given size
Grow(capacity int)
// Bytes returns the content of the buffer as bytes.
// NOTE: The returned bytes shall NOT be accessed after destroying the
// buffer using 'Destroy()' as the underlying the memory area might be
// wiped and invalid.
Bytes() []byte
// TemporaryString returns the content of the buffer as a string.
// NOTE: The returned String shall NOT be accessed after destroying the
// buffer using 'Destroy()' as the underlying the memory area might be
// wiped and invalid.
TemporaryString() string
// String returns a copy of the underlying buffer's content as string.
// It is safe to use the returned value after destroying the buffer.
String() string
// Destroy will wipe the buffer's content and destroy the underlying
// buffer. Do not access the buffer after destroying it.
Destroy()
}
// Secret safely stores sensitive data such as a password or token
type Secret struct {
// container is the implementation for holding the secret. It can be
// protected or not depending on the concrete implementation.
container secretContainer
// resolvers are the functions for resolving a given secret-id (key)
resolvers map[string]telegraf.ResolveFunc
// unlinked contains all references in the secret that are not yet
// linked to the corresponding secret store.
unlinked []string
// notempty denotes if the secret is completely empty
notempty bool
}
// NewSecret creates a new secret from the given bytes
func NewSecret(b []byte) Secret {
s := Secret{}
s.init(b)
return s
}
// UnmarshalText creates a secret from a toml value following the "string" rule.
func (s *Secret) UnmarshalText(b []byte) error {
// Unmarshal secret from TOML and put it into protected memory
s.init(b)
// Keep track of secrets that contain references to secret-stores
// for later resolving by the config.
if len(s.unlinked) > 0 && s.notempty {
unlinkedSecrets = append(unlinkedSecrets, s)
}
return nil
}
// Initialize the secret content
func (s *Secret) init(secret []byte) {
// Keep track of the number of secrets...
secretCount.Add(1)
// Remember if the secret is completely empty
s.notempty = len(secret) != 0
// Find all secret candidates and check if they are really a valid
// reference. Otherwise issue a warning to let the user know that there is
// a potential issue with their secret instead of silently ignoring it.
candidates := secretCandidatePattern.FindAllString(string(secret), -1)
s.unlinked = make([]string, 0, len(candidates))
for _, c := range candidates {
if secretPattern.MatchString(c) {
s.unlinked = append(s.unlinked, c)
} else {
log.Printf("W! Secret %q contains invalid character(s), only letters, digits and underscores are allowed.", c)
}
}
s.resolvers = nil
// Setup the container implementation
s.container = selectedImpl.Container(secret)
}
// Destroy the secret content
func (s *Secret) Destroy() {
s.resolvers = nil
s.unlinked = nil
s.notempty = false
if s.container != nil {
s.container.Destroy()
s.container = nil
// Keep track of the number of used secrets...
secretCount.Add(-1)
}
}
// Empty return if the secret is completely empty
func (s *Secret) Empty() bool {
return !s.notempty
}
// EqualTo performs a constant-time comparison of the secret to the given reference
func (s *Secret) EqualTo(ref []byte) (bool, error) {
if s.container == nil {
return false, nil
}
if len(s.unlinked) > 0 {
return false, fmt.Errorf("unlinked parts in secret: %v", strings.Join(s.unlinked, ";"))
}
return s.container.Equals(ref)
}
// Get return the string representation of the secret
func (s *Secret) Get() (SecretBuffer, error) {
if s.container == nil {
return selectedImpl.EmptyBuffer(), nil
}
if len(s.unlinked) > 0 {
return nil, fmt.Errorf("unlinked parts in secret: %v", strings.Join(s.unlinked, ";"))
}
// Decrypt the secret so we can return it
buffer, err := s.container.Buffer()
if err != nil {
return nil, err
}
// We've got a static secret so simply return the buffer
if len(s.resolvers) == 0 {
return buffer, nil
}
defer buffer.Destroy()
replaceErrs := make([]string, 0)
newsecret := secretPattern.ReplaceAllFunc(buffer.Bytes(), func(match []byte) []byte {
resolver, found := s.resolvers[string(match)]
if !found {
replaceErrs = append(replaceErrs, fmt.Sprintf("no resolver for %q", match))
return match
}
replacement, _, err := resolver()
if err != nil {
replaceErrs = append(replaceErrs, fmt.Sprintf("resolving %q failed: %v", match, err))
return match
}
return replacement
})
if len(replaceErrs) > 0 {
selectedImpl.Wipe(newsecret)
return nil, fmt.Errorf("replacing secrets failed: %s", strings.Join(replaceErrs, ";"))
}
return s.container.AsBuffer(newsecret), nil
}
// Set overwrites the secret's value with a new one. Please note, the secret
// is not linked again, so only references to secret-stores can be used, e.g. by
// adding more clear-text or reordering secrets.
func (s *Secret) Set(value []byte) error {
// Link the new value can be resolved
secret, res, replaceErrs := resolve(value, s.resolvers)
if len(replaceErrs) > 0 {
return fmt.Errorf("linking new secrets failed: %s", strings.Join(replaceErrs, ";"))
}
// Set the new secret
s.container.Replace(secret)
s.resolvers = res
s.notempty = len(value) > 0
return nil
}
// GetUnlinked return the parts of the secret that is not yet linked to a resolver
func (s *Secret) GetUnlinked() []string {
return s.unlinked
}
// Link used the given resolver map to link the secret parts to their
// secret-store resolvers.
func (s *Secret) Link(resolvers map[string]telegraf.ResolveFunc) error {
// Decrypt the secret so we can return it
if s.container == nil {
return nil
}
buffer, err := s.container.Buffer()
if err != nil {
return err
}
defer buffer.Destroy()
// Iterate through the parts and try to resolve them. For static parts
// we directly replace them, while for dynamic ones we store the resolver.
newsecret, res, replaceErrs := resolve(buffer.Bytes(), resolvers)
if len(replaceErrs) > 0 {
return fmt.Errorf("linking secrets failed: %s", strings.Join(replaceErrs, ";"))
}
s.resolvers = res
// Store the secret if it has changed
if buffer.TemporaryString() != string(newsecret) {
s.container.Replace(newsecret)
}
// All linked now
s.unlinked = nil
return nil
}
func resolve(secret []byte, resolvers map[string]telegraf.ResolveFunc) ([]byte, map[string]telegraf.ResolveFunc, []string) {
// Iterate through the parts and try to resolve them. For static parts
// we directly replace them, while for dynamic ones we store the resolver.
replaceErrs := make([]string, 0)
remaining := make(map[string]telegraf.ResolveFunc)
newsecret := secretPattern.ReplaceAllFunc(secret, func(match []byte) []byte {
resolver, found := resolvers[string(match)]
if !found {
replaceErrs = append(replaceErrs, fmt.Sprintf("unlinked part %q", match))
return match
}
replacement, dynamic, err := resolver()
if err != nil {
replaceErrs = append(replaceErrs, fmt.Sprintf("resolving %q failed: %v", match, err))
return match
}
// Replace static parts right away
if !dynamic {
return replacement
}
// Keep the resolver for dynamic secrets
remaining[string(match)] = resolver
return match
})
return newsecret, remaining, replaceErrs
}
func splitLink(s string) (storeID, key string) {
// There should _ALWAYS_ be two parts due to the regular expression match
parts := strings.SplitN(s[2:len(s)-1], ":", 2)
return parts[0], parts[1]
}