-
Notifications
You must be signed in to change notification settings - Fork 910
/
Copy pathkeysource.go
406 lines (353 loc) · 12.9 KB
/
keysource.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
400
401
402
403
404
405
406
package age
import (
"bufio"
"bytes"
"errors"
"filippo.io/age/plugin"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"filippo.io/age"
"filippo.io/age/agessh"
"filippo.io/age/armor"
"github.com/sirupsen/logrus"
"github.com/getsops/sops/v3/logging"
)
const (
// SopsAgeKeyEnv can be set as an environment variable with a string list
// of age keys as value.
SopsAgeKeyEnv = "SOPS_AGE_KEY"
// SopsAgeKeyFileEnv can be set as an environment variable pointing to an
// age keys file.
SopsAgeKeyFileEnv = "SOPS_AGE_KEY_FILE"
// SopsAgeSshPrivateKeyFileEnv can be set as an environment variable pointing to
// a private SSH key file.
SopsAgeSshPrivateKeyFileEnv = "SOPS_AGE_SSH_PRIVATE_KEY_FILE"
// SopsAgeKeyUserConfigPath is the default age keys file path in
// getUserConfigDir().
SopsAgeKeyUserConfigPath = "sops/age/keys.txt"
// On macOS, os.UserConfigDir() ignores XDG_CONFIG_HOME. So we handle that manually.
xdgConfigHome = "XDG_CONFIG_HOME"
// KeyTypeIdentifier is the string used to identify an age MasterKey.
KeyTypeIdentifier = "age"
)
// log is the global logger for any age MasterKey.
var log *logrus.Logger
func init() {
log = logging.NewLogger("AGE")
}
// MasterKey is an age key used to Encrypt and Decrypt SOPS' data key.
type MasterKey struct {
// Identity used to contain a Bench32-encoded private key.
// Deprecated: private keys are no longer publicly exposed.
// Instead, they are either injected by a (local) key service server
// using ParsedIdentities.ApplyToMasterKey, or loaded from the runtime
// environment (variables) as defined by the `SopsAgeKey*` constants.
Identity string
// Recipient contains the Bench32-encoded age public key used to Encrypt.
Recipient string
// EncryptedKey contains the SOPS data key encrypted with age.
EncryptedKey string
// parsedIdentities contains a slice of parsed age identities.
// It is used to lazy-load the Identities at-most once.
// It can also be injected by a (local) keyservice.KeyServiceServer using
// ParsedIdentities.ApplyToMasterKey().
parsedIdentities []age.Identity
// parsedRecipient contains a parsed age public key.
// It is used to lazy-load the Recipient at-most once.
parsedRecipient age.Recipient
}
// MasterKeysFromRecipients takes a comma-separated list of Bech32-encoded
// public keys, parses them, and returns a slice of new MasterKeys.
func MasterKeysFromRecipients(commaSeparatedRecipients string) ([]*MasterKey, error) {
if commaSeparatedRecipients == "" {
// otherwise Split returns [""] and MasterKeyFromRecipient is unhappy
return make([]*MasterKey, 0), nil
}
recipients := strings.Split(commaSeparatedRecipients, ",")
var keys []*MasterKey
for _, recipient := range recipients {
key, err := MasterKeyFromRecipient(recipient)
if err != nil {
return nil, err
}
keys = append(keys, key)
}
return keys, nil
}
// MasterKeyFromRecipient takes a Bech32-encoded age public key, parses it, and
// returns a new MasterKey.
func MasterKeyFromRecipient(recipient string) (*MasterKey, error) {
recipient = strings.TrimSpace(recipient)
parsedRecipient, err := parseRecipient(recipient)
if err != nil {
return nil, err
}
return &MasterKey{
Recipient: recipient,
parsedRecipient: parsedRecipient,
}, nil
}
// ParsedIdentities contains a set of parsed age identities.
// It allows for creating a (local) keyservice.KeyServiceServer which parses
// identities only once, to then inject them using ApplyToMasterKey() for all
// requests.
type ParsedIdentities []age.Identity
// Import attempts to parse the given identities, to then add them to itself.
// It returns any parsing error.
// A single identity argument is allowed to be a multiline string containing
// multiple identities. Empty lines and lines starting with "#" are ignored.
// It is not thread safe, and parallel importing would better be done by
// parsing (using age.ParseIdentities) and appending to the slice yourself, in
// combination with e.g. a sync.Mutex.
func (i *ParsedIdentities) Import(identity ...string) error {
// one identity per line
r := strings.NewReader(strings.Join(identity, "\n"))
identities, err := parseIdentities(r)
if err != nil {
return fmt.Errorf("failed to parse and add to age identities: %w", err)
}
*i = append(*i, identities...)
return nil
}
// ApplyToMasterKey configures the ParsedIdentities on the provided key.
func (i ParsedIdentities) ApplyToMasterKey(key *MasterKey) {
key.parsedIdentities = i
}
// Encrypt takes a SOPS data key, encrypts it with the Recipient, and stores
// the result in the EncryptedKey field.
func (key *MasterKey) Encrypt(dataKey []byte) error {
if key.parsedRecipient == nil {
parsedRecipient, err := parseRecipient(key.Recipient)
if err != nil {
log.WithField("recipient", key.parsedRecipient).Info("Encryption failed")
return err
}
key.parsedRecipient = parsedRecipient
}
var buffer bytes.Buffer
aw := armor.NewWriter(&buffer)
w, err := age.Encrypt(aw, key.parsedRecipient)
if err != nil {
log.WithField("recipient", key.parsedRecipient).Info("Encryption failed")
return fmt.Errorf("failed to create writer for encrypting sops data key with age: %w", err)
}
if _, err := w.Write(dataKey); err != nil {
log.WithField("recipient", key.parsedRecipient).Info("Encryption failed")
return fmt.Errorf("failed to encrypt sops data key with age: %w", err)
}
if err := w.Close(); err != nil {
log.WithField("recipient", key.parsedRecipient).Info("Encryption failed")
return fmt.Errorf("failed to close writer for encrypting sops data key with age: %w", err)
}
if err := aw.Close(); err != nil {
log.WithField("recipient", key.parsedRecipient).Info("Encryption failed")
return fmt.Errorf("failed to close armored writer: %w", err)
}
key.SetEncryptedDataKey(buffer.Bytes())
log.WithField("recipient", key.parsedRecipient).Info("Encryption succeeded")
return nil
}
// EncryptIfNeeded encrypts the provided SOPS data key, if it has not been
// encrypted yet.
func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error {
if key.EncryptedKey == "" {
return key.Encrypt(dataKey)
}
return nil
}
// EncryptedDataKey returns the encrypted SOPS data key this master key holds.
func (key *MasterKey) EncryptedDataKey() []byte {
return []byte(key.EncryptedKey)
}
// SetEncryptedDataKey sets the encrypted SOPS data key for this master key.
func (key *MasterKey) SetEncryptedDataKey(enc []byte) {
key.EncryptedKey = string(enc)
}
// Decrypt decrypts the EncryptedKey with the parsed or loaded identities, and
// returns the result.
func (key *MasterKey) Decrypt() ([]byte, error) {
if len(key.parsedIdentities) == 0 {
ids, err := key.loadIdentities()
if err != nil {
log.Info("Decryption failed")
return nil, fmt.Errorf("failed to load age identities: %w", err)
}
ids.ApplyToMasterKey(key)
}
src := bytes.NewReader([]byte(key.EncryptedKey))
ar := armor.NewReader(src)
r, err := age.Decrypt(ar, key.parsedIdentities...)
if err != nil {
log.Info("Decryption failed")
return nil, fmt.Errorf("failed to create reader for decrypting sops data key with age: %w", err)
}
var b bytes.Buffer
if _, err := io.Copy(&b, r); err != nil {
log.Info("Decryption failed")
return nil, fmt.Errorf("failed to copy age decrypted data into bytes.Buffer: %w", err)
}
log.Info("Decryption succeeded")
return b.Bytes(), nil
}
// NeedsRotation returns whether the data key needs to be rotated or not.
func (key *MasterKey) NeedsRotation() bool {
return false
}
// ToString converts the key to a string representation.
func (key *MasterKey) ToString() string {
return key.Recipient
}
// ToMap converts the MasterKey to a map for serialization purposes.
func (key *MasterKey) ToMap() map[string]interface{} {
out := make(map[string]interface{})
out["recipient"] = key.Recipient
out["enc"] = key.EncryptedKey
return out
}
// TypeToIdentifier returns the string identifier for the MasterKey type.
func (key *MasterKey) TypeToIdentifier() string {
return KeyTypeIdentifier
}
// loadAgeSSHIdentity attempts to load the age SSH identity based on an SSH
// private key from the SopsAgeSshPrivateKeyFileEnv environment variable. If the
// environment variable is not present, it will fall back to `~/.ssh/id_ed25519`
// or `~/.ssh/id_rsa`. If no age SSH identity is found, it will return nil.
func loadAgeSSHIdentity() (age.Identity, error) {
sshKeyFilePath, ok := os.LookupEnv(SopsAgeSshPrivateKeyFileEnv)
if ok {
return parseSSHIdentityFromPrivateKeyFile(sshKeyFilePath)
}
userHomeDir, err := os.UserHomeDir()
if err != nil || userHomeDir == "" {
log.Warnf("could not determine the user home directory: %v", err)
return nil, nil
}
sshEd25519PrivateKeyPath := filepath.Join(userHomeDir, ".ssh", "id_ed25519")
if _, err := os.Stat(sshEd25519PrivateKeyPath); err == nil {
return parseSSHIdentityFromPrivateKeyFile(sshEd25519PrivateKeyPath)
}
sshRsaPrivateKeyPath := filepath.Join(userHomeDir, ".ssh", "id_rsa")
if _, err := os.Stat(sshRsaPrivateKeyPath); err == nil {
return parseSSHIdentityFromPrivateKeyFile(sshRsaPrivateKeyPath)
}
return nil, nil
}
func getUserConfigDir() (string, error) {
if runtime.GOOS == "darwin" {
if userConfigDir, ok := os.LookupEnv(xdgConfigHome); ok && userConfigDir != "" {
return userConfigDir, nil
}
}
return os.UserConfigDir()
}
// loadIdentities attempts to load the age identities based on runtime
// environment configurations (e.g. SopsAgeKeyEnv, SopsAgeKeyFileEnv,
// SopsAgeSshPrivateKeyFileEnv, SopsAgeKeyUserConfigPath). It will load all
// found references, and expects at least one configuration to be present.
func (key *MasterKey) loadIdentities() (ParsedIdentities, error) {
var identities ParsedIdentities
sshIdentity, err := loadAgeSSHIdentity()
if err != nil {
return nil, fmt.Errorf("failed to get SSH identity: %w", err)
}
if sshIdentity != nil {
identities = append(identities, sshIdentity)
}
var readers = make(map[string]io.Reader, 0)
if ageKey, ok := os.LookupEnv(SopsAgeKeyEnv); ok {
readers[SopsAgeKeyEnv] = strings.NewReader(ageKey)
}
if ageKeyFile, ok := os.LookupEnv(SopsAgeKeyFileEnv); ok {
f, err := os.Open(ageKeyFile)
if err != nil {
return nil, fmt.Errorf("failed to open %s file: %w", SopsAgeKeyFileEnv, err)
}
defer f.Close()
readers[SopsAgeKeyFileEnv] = f
}
userConfigDir, err := getUserConfigDir()
if err != nil && len(readers) == 0 && len(identities) == 0 {
return nil, fmt.Errorf("user config directory could not be determined: %w", err)
}
if userConfigDir != "" {
ageKeyFilePath := filepath.Join(userConfigDir, filepath.FromSlash(SopsAgeKeyUserConfigPath))
f, err := os.Open(ageKeyFilePath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("failed to open file: %w", err)
}
if errors.Is(err, os.ErrNotExist) && len(readers) == 0 && len(identities) == 0 {
// If we have no other readers, presence of the file is required.
return nil, fmt.Errorf("failed to open file: %w", err)
}
if err == nil {
defer f.Close()
readers[ageKeyFilePath] = f
}
}
for n, r := range readers {
ids, err := unwrapIdentities(n, r)
if err != nil {
return nil, err
}
identities = append(identities, ids...)
}
return identities, nil
}
// parseRecipient attempts to parse a string containing an encoded age public
// key or a public ssh key.
func parseRecipient(recipient string) (age.Recipient, error) {
switch {
case strings.HasPrefix(recipient, "age1") && strings.Count(recipient, "1") > 1:
parsedRecipient, err := plugin.NewRecipient(recipient, pluginTerminalUI)
if err != nil {
return nil, fmt.Errorf("failed to parse input as age key from age plugin: %w", err)
}
return parsedRecipient, nil
case strings.HasPrefix(recipient, "age1"):
parsedRecipient, err := age.ParseX25519Recipient(recipient)
if err != nil {
return nil, fmt.Errorf("failed to parse input as Bech32-encoded age public key: %w", err)
}
return parsedRecipient, nil
case strings.HasPrefix(recipient, "ssh-"):
parsedRecipient, err := agessh.ParseRecipient(recipient)
if err != nil {
return nil, fmt.Errorf("failed to parse input as age-ssh public key: %w", err)
}
return parsedRecipient, nil
}
return nil, fmt.Errorf("failed to parse input, unknown recipient type: %q", recipient)
}
// parseIdentities attempts to parse one or more age identities from the provided reader.
// One identity per line.
// Empty lines and lines starting with "#" are ignored.
func parseIdentities(r io.Reader) (ParsedIdentities, error) {
var identities ParsedIdentities
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parsed, err := parseIdentity(line)
if err != nil {
return nil, err
}
identities = append(identities, parsed)
}
return identities, nil
}
func parseIdentity(s string) (age.Identity, error) {
switch {
case strings.HasPrefix(s, "AGE-PLUGIN-"):
return plugin.NewIdentity(s, pluginTerminalUI)
case strings.HasPrefix(s, "AGE-SECRET-KEY-1"):
return age.ParseX25519Identity(s)
default:
return nil, fmt.Errorf("unknown identity type")
}
}