Skip to content

Commit b8ea2f4

Browse files
committed
Always load age identities from all locations, and report unused locations in error messages.
Signed-off-by: Felix Fontein <felix@fontein.de>
1 parent 1c1b3c8 commit b8ea2f4

File tree

2 files changed

+92
-46
lines changed

2 files changed

+92
-46
lines changed

age/encrypted_keys.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err
104104
return fileKey, err
105105
}
106106

107-
func unwrapIdentities(key string, reader io.Reader) (ParsedIdentities, error) {
107+
func unwrapIdentities(location string, reader io.Reader) (ParsedIdentities, error) {
108108
b := bufio.NewReader(reader)
109109
p, _ := b.Peek(14) // length of "age-encryption" and "-----BEGIN AGE"
110110
peeked := string(p)
@@ -119,10 +119,10 @@ func unwrapIdentities(key string, reader io.Reader) (ParsedIdentities, error) {
119119
const privateKeySizeLimit = 1 << 24 // 16 MiB
120120
contents, err := io.ReadAll(io.LimitReader(r, privateKeySizeLimit))
121121
if err != nil {
122-
return nil, fmt.Errorf("failed to read '%s': %w", key, err)
122+
return nil, fmt.Errorf("failed to read '%s': %w", location, err)
123123
}
124124
if len(contents) == privateKeySizeLimit {
125-
return nil, fmt.Errorf("failed to read '%s': file too long", key)
125+
return nil, fmt.Errorf("failed to read '%s': file too long", location)
126126
}
127127
IncorrectPassphrase := func() {
128128
conn, err := gpgagent.NewConn()
@@ -134,7 +134,7 @@ func unwrapIdentities(key string, reader io.Reader) (ParsedIdentities, error) {
134134
log.Errorf("failed to close connection with gpg-agent: %s", err)
135135
}
136136
}(conn)
137-
err = conn.RemoveFromCache(key)
137+
err = conn.RemoveFromCache(location)
138138
if err != nil {
139139
log.Warnf("gpg-agent remove cache request errored: %s", err)
140140
return
@@ -145,7 +145,7 @@ func unwrapIdentities(key string, reader io.Reader) (ParsedIdentities, error) {
145145
Passphrase: func() (string, error) {
146146
conn, err := gpgagent.NewConn()
147147
if err != nil {
148-
passphrase, err := readSecret("Enter passphrase for identity " + key + ":")
148+
passphrase, err := readSecret(fmt.Sprintf("Enter passphrase for identity '%s':", location))
149149
if err != nil {
150150
return "", err
151151
}
@@ -159,9 +159,9 @@ func unwrapIdentities(key string, reader io.Reader) (ParsedIdentities, error) {
159159

160160
req := gpgagent.PassphraseRequest{
161161
// TODO is the cachekey good enough?
162-
CacheKey: key,
162+
CacheKey: location,
163163
Prompt: "Passphrase",
164-
Desc: fmt.Sprintf("Enter passphrase for identity '%s':", key),
164+
Desc: fmt.Sprintf("Enter passphrase for identity '%s':", location),
165165
}
166166
pass, err := conn.GetPassphrase(&req)
167167
if err != nil {
@@ -175,15 +175,15 @@ func unwrapIdentities(key string, reader io.Reader) (ParsedIdentities, error) {
175175
},
176176
IncorrectPassphrase: IncorrectPassphrase,
177177
NoMatchWarning: func() {
178-
log.Warnf("encrypted identity '%s' didn't match file's recipients", key)
178+
log.Warnf("encrypted identity '%s' didn't match file's recipients", location)
179179
},
180180
}}
181181
return ids, nil
182182
// An unencrypted age identity file.
183183
default:
184184
ids, err := parseIdentities(b)
185185
if err != nil {
186-
return nil, fmt.Errorf("failed to parse '%s' age identities: %w", key, err)
186+
return nil, fmt.Errorf("failed to parse '%s' age identities: %w", location, err)
187187
}
188188
return ids, nil
189189
}

age/keysource.go

Lines changed: 83 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -206,16 +206,41 @@ func (key *MasterKey) SetEncryptedDataKey(enc []byte) {
206206
key.EncryptedKey = string(enc)
207207
}
208208

209+
func formatError(msg string, err error, errs errSet, unusedLocations []string) error {
210+
var loadSuffix string
211+
if len(errs) > 0 {
212+
loadSuffix = fmt.Sprintf(". Errors while loading age identities: %s", errs.Error())
213+
}
214+
var unusedSuffix string
215+
if len(unusedLocations) > 0 {
216+
count := len(unusedLocations)
217+
if count == 1 {
218+
unusedSuffix = fmt.Sprintf(" '%s'", unusedLocations[0])
219+
} else if count == 2 {
220+
unusedSuffix = fmt.Sprintf("s '%s' and '%s'", unusedLocations[0], unusedLocations[1])
221+
} else {
222+
unusedSuffix = fmt.Sprintf("s '%s', and '%s'", strings.Join(unusedLocations[:count - 1], "', '"), unusedLocations[count - 1])
223+
}
224+
unusedSuffix = fmt.Sprintf(". Did not found keys in location%s.", unusedSuffix)
225+
}
226+
if err != nil {
227+
return fmt.Errorf("%s: %w%s%s", msg, err, loadSuffix, unusedSuffix)
228+
} else {
229+
return fmt.Errorf("%s%s%s", msg, loadSuffix, unusedSuffix)
230+
}
231+
}
232+
209233
// Decrypt decrypts the EncryptedKey with the parsed or loaded identities, and
210234
// returns the result.
211235
func (key *MasterKey) Decrypt() ([]byte, error) {
212236
var errs errSet
237+
var unusedLocations []string
213238
if len(key.parsedIdentities) == 0 {
214239
var ids ParsedIdentities
215-
ids, errs = key.loadIdentities()
240+
ids, unusedLocations, errs = key.loadIdentities()
216241
if len(ids) == 0 {
217242
log.Info("Decryption failed")
218-
return nil, fmt.Errorf("failed to load age identities: %w", errs)
243+
return nil, formatError("failed to load age identities", nil, errs, unusedLocations)
219244
}
220245
ids.ApplyToMasterKey(key)
221246
}
@@ -225,11 +250,7 @@ func (key *MasterKey) Decrypt() ([]byte, error) {
225250
r, err := age.Decrypt(ar, key.parsedIdentities...)
226251
if err != nil {
227252
log.Info("Decryption failed")
228-
var loadErrors string
229-
if len(errs) > 0 {
230-
loadErrors = fmt.Sprintf(". Errors while loading age identities: %s", errs.Error())
231-
}
232-
return nil, fmt.Errorf("failed to create reader for decrypting sops data key with age: %w%s", err, loadErrors)
253+
return nil, formatError("failed to create reader for decrypting sops data key with age", err, errs, unusedLocations)
233254
}
234255

235256
var b bytes.Buffer
@@ -269,29 +290,55 @@ func (key *MasterKey) TypeToIdentifier() string {
269290
// private key from the SopsAgeSshPrivateKeyFileEnv environment variable. If the
270291
// environment variable is not present, it will fall back to `~/.ssh/id_ed25519`
271292
// or `~/.ssh/id_rsa`. If no age SSH identity is found, it will return nil.
272-
func loadAgeSSHIdentity() (age.Identity, error) {
293+
func loadAgeSSHIdentities() ([]age.Identity, []string, errSet) {
294+
var identities []age.Identity
295+
var unusedLocations []string
296+
var errs errSet
297+
273298
sshKeyFilePath, ok := os.LookupEnv(SopsAgeSshPrivateKeyFileEnv)
274299
if ok {
275-
return parseSSHIdentityFromPrivateKeyFile(sshKeyFilePath)
300+
identity, err := parseSSHIdentityFromPrivateKeyFile(sshKeyFilePath)
301+
if err != nil {
302+
errs = append(errs, err)
303+
} else {
304+
identities = append(identities, identity)
305+
}
306+
} else {
307+
unusedLocations = append(unusedLocations, SopsAgeSshPrivateKeyFileEnv)
276308
}
277309

278310
userHomeDir, err := os.UserHomeDir()
279-
if err != nil || userHomeDir == "" {
311+
if err != nil {
312+
errs = append(errs, err)
313+
} else if userHomeDir == "" {
280314
log.Warnf("could not determine the user home directory: %v", err)
281-
return nil, nil
282-
}
283-
284-
sshEd25519PrivateKeyPath := filepath.Join(userHomeDir, ".ssh", "id_ed25519")
285-
if _, err := os.Stat(sshEd25519PrivateKeyPath); err == nil {
286-
return parseSSHIdentityFromPrivateKeyFile(sshEd25519PrivateKeyPath)
287-
}
315+
} else {
316+
sshEd25519PrivateKeyPath := filepath.Join(userHomeDir, ".ssh", "id_ed25519")
317+
if _, err := os.Stat(sshEd25519PrivateKeyPath); err == nil {
318+
identity, err := parseSSHIdentityFromPrivateKeyFile(sshEd25519PrivateKeyPath)
319+
if err != nil {
320+
errs = append(errs, err)
321+
} else {
322+
identities = append(identities, identity)
323+
}
324+
} else {
325+
unusedLocations = append(unusedLocations, sshEd25519PrivateKeyPath)
326+
}
288327

289-
sshRsaPrivateKeyPath := filepath.Join(userHomeDir, ".ssh", "id_rsa")
290-
if _, err := os.Stat(sshRsaPrivateKeyPath); err == nil {
291-
return parseSSHIdentityFromPrivateKeyFile(sshRsaPrivateKeyPath)
328+
sshRsaPrivateKeyPath := filepath.Join(userHomeDir, ".ssh", "id_rsa")
329+
if _, err := os.Stat(sshRsaPrivateKeyPath); err == nil {
330+
identity, err := parseSSHIdentityFromPrivateKeyFile(sshRsaPrivateKeyPath)
331+
if err != nil {
332+
errs = append(errs, err)
333+
} else {
334+
identities = append(identities, identity)
335+
}
336+
} else {
337+
unusedLocations = append(unusedLocations, sshRsaPrivateKeyPath)
338+
}
292339
}
293340

294-
return nil, nil
341+
return identities, unusedLocations, errs
295342
}
296343

297344
func getUserConfigDir() (string, error) {
@@ -307,22 +354,15 @@ func getUserConfigDir() (string, error) {
307354
// environment configurations (e.g. SopsAgeKeyEnv, SopsAgeKeyFileEnv,
308355
// SopsAgeSshPrivateKeyFileEnv, SopsAgeKeyUserConfigPath). It will load all
309356
// found references, and expects at least one configuration to be present.
310-
func (key *MasterKey) loadIdentities() (ParsedIdentities, errSet) {
311-
var identities ParsedIdentities
312-
313-
var errs errSet
314-
315-
sshIdentity, err := loadAgeSSHIdentity()
316-
if err != nil {
317-
errs = append(errs, fmt.Errorf("failed to get SSH identity: %w", err))
318-
} else if sshIdentity != nil {
319-
identities = append(identities, sshIdentity)
320-
}
357+
func (key *MasterKey) loadIdentities() (ParsedIdentities, []string, errSet) {
358+
identities, unusedLocations, errs := loadAgeSSHIdentities()
321359

322360
var readers = make(map[string]io.Reader, 0)
323361

324362
if ageKey, ok := os.LookupEnv(SopsAgeKeyEnv); ok {
325363
readers[SopsAgeKeyEnv] = strings.NewReader(ageKey)
364+
} else {
365+
unusedLocations = append(unusedLocations, SopsAgeKeyEnv)
326366
}
327367

328368
if ageKeyFile, ok := os.LookupEnv(SopsAgeKeyFileEnv); ok {
@@ -333,6 +373,8 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, errSet) {
333373
defer f.Close()
334374
readers[SopsAgeKeyFileEnv] = f
335375
}
376+
} else {
377+
unusedLocations = append(unusedLocations, SopsAgeKeyFileEnv)
336378
}
337379

338380
if ageKeyCmd, ok := os.LookupEnv(SopsAgeKeyCmdEnv); ok {
@@ -347,6 +389,8 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, errSet) {
347389
readers[SopsAgeKeyCmdEnv] = bytes.NewReader(out)
348390
}
349391
}
392+
} else {
393+
unusedLocations = append(unusedLocations, SopsAgeKeyCmdEnv)
350394
}
351395

352396
userConfigDir, err := getUserConfigDir()
@@ -358,23 +402,25 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, errSet) {
358402
if err != nil && !errors.Is(err, os.ErrNotExist) {
359403
errs = append(errs, fmt.Errorf("failed to open file: %w", err))
360404
} else if errors.Is(err, os.ErrNotExist) && len(readers) == 0 && len(identities) == 0 {
361-
// If we have no other readers, presence of the file is required.
362-
errs = append(errs, fmt.Errorf("failed to open file: %w", err))
405+
unusedLocations = append(unusedLocations, ageKeyFilePath)
363406
} else if err == nil {
364407
defer f.Close()
365408
readers[ageKeyFilePath] = f
366409
}
367410
}
368411

369-
for n, r := range readers {
370-
ids, err := unwrapIdentities(n, r)
412+
for location, r := range readers {
413+
ids, err := unwrapIdentities(location, r)
371414
if err != nil {
372415
errs = append(errs, err)
373416
} else {
374417
identities = append(identities, ids...)
418+
if len(ids) == 0 {
419+
unusedLocations = append(unusedLocations, location)
420+
}
375421
}
376422
}
377-
return identities, errs
423+
return identities, unusedLocations, errs
378424
}
379425

380426
// parseRecipient attempts to parse a string containing an encoded age public

0 commit comments

Comments
 (0)