-
Notifications
You must be signed in to change notification settings - Fork 897
/
Copy pathkeysource.go
319 lines (283 loc) · 10.3 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
package age
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"filippo.io/age"
"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"
// 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.X25519Recipient
}
// 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 {
identities, err := parseIdentities(identity...)
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
}
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,
// SopsAgeKeyUserConfigPath). It will load all found references, and expects
// at least one configuration to be present.
func (key *MasterKey) loadIdentities() (ParsedIdentities, error) {
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 {
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 {
// 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
}
}
var identities ParsedIdentities
for n, r := range readers {
ids, err := age.ParseIdentities(r)
if err != nil {
return nil, fmt.Errorf("failed to parse '%s' age identities: %w", n, err)
}
identities = append(identities, ids...)
}
return identities, nil
}
// parseRecipient attempts to parse a string containing an encoded age public
// key.
func parseRecipient(recipient string) (*age.X25519Recipient, error) {
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
}
// parseIdentities attempts to parse the string set of encoded age identities.
// A single identity argument is allowed to be a multiline string containing
// multiple identities. Empty lines and lines starting with "#" are ignored.
func parseIdentities(identity ...string) (ParsedIdentities, error) {
var identities []age.Identity
for _, i := range identity {
parsed, err := age.ParseIdentities(strings.NewReader(i))
if err != nil {
return nil, err
}
identities = append(identities, parsed...)
}
return identities, nil
}