1+ // These functions have been copied from the age project
2+ // https://github.com/FiloSottile/age/blob/101cc8676386b0503571a929a88618cae2f0b1cd/cmd/age/encrypted_keys.go
3+ // https://github.com/FiloSottile/age/blob/101cc8676386b0503571a929a88618cae2f0b1cd/cmd/age/parse.go
4+ //
5+ // Copyright 2021 The age Authors. All rights reserved.
6+ // Use of this source code is governed by a BSD-style
7+ // license that can be found in age's LICENSE file at
8+ // https://github.com/FiloSottile/age/blob/v1.0.0/LICENSE
9+ //
10+ // SPDX-License-Identifier: BSD-3-Clause
11+
12+ package age
13+
14+ import (
15+ "bufio"
16+ "bytes"
17+ "errors"
18+ "fmt"
19+ "io"
20+
21+ "filippo.io/age"
22+ "filippo.io/age/armor"
23+
24+ gpgagent "github.com/getsops/gopgagent"
25+ )
26+
27+ type EncryptedIdentity struct {
28+ Contents []byte
29+ Passphrase func () (string , error )
30+ NoMatchWarning func ()
31+ IncorrectPassphrase func ()
32+
33+ identities []age.Identity
34+ }
35+
36+ var _ age.Identity = & EncryptedIdentity {}
37+
38+ func (i * EncryptedIdentity ) Unwrap (stanzas []* age.Stanza ) (fileKey []byte , err error ) {
39+ if i .identities == nil {
40+ if err := i .decrypt (); err != nil {
41+ return nil , err
42+ }
43+ }
44+
45+ for _ , id := range i .identities {
46+ fileKey , err = id .Unwrap (stanzas )
47+ if errors .Is (err , age .ErrIncorrectIdentity ) {
48+ continue
49+ }
50+ if err != nil {
51+ return nil , err
52+ }
53+ return fileKey , nil
54+ }
55+ i .NoMatchWarning ()
56+ return nil , age .ErrIncorrectIdentity
57+ }
58+
59+ func (i * EncryptedIdentity ) decrypt () error {
60+ d , err := age .Decrypt (bytes .NewReader (i .Contents ), & LazyScryptIdentity {i .Passphrase })
61+ if e := new (age.NoIdentityMatchError ); errors .As (err , & e ) {
62+ // ScryptIdentity returns ErrIncorrectIdentity for an incorrect
63+ // passphrase, which would lead Decrypt to returning "no identity
64+ // matched any recipient". That makes sense in the API, where there
65+ // might be multiple configured ScryptIdentity. Since in cmd/age there
66+ // can be only one, return a better error message.
67+ i .IncorrectPassphrase ()
68+ return fmt .Errorf ("incorrect passphrase" )
69+ }
70+ if err != nil {
71+ return fmt .Errorf ("failed to decrypt identity file: %v" , err )
72+ }
73+ i .identities , err = age .ParseIdentities (d )
74+ return err
75+ }
76+
77+ // LazyScryptIdentity is an age.Identity that requests a passphrase only if it
78+ // encounters an scrypt stanza. After obtaining a passphrase, it delegates to
79+ // ScryptIdentity.
80+ type LazyScryptIdentity struct {
81+ Passphrase func () (string , error )
82+ }
83+
84+ var _ age.Identity = & LazyScryptIdentity {}
85+
86+ func (i * LazyScryptIdentity ) Unwrap (stanzas []* age.Stanza ) (fileKey []byte , err error ) {
87+ for _ , s := range stanzas {
88+ if s .Type == "scrypt" && len (stanzas ) != 1 {
89+ return nil , errors .New ("an scrypt recipient must be the only one" )
90+ }
91+ }
92+ if len (stanzas ) != 1 || stanzas [0 ].Type != "scrypt" {
93+ return nil , age .ErrIncorrectIdentity
94+ }
95+ pass , err := i .Passphrase ()
96+ if err != nil {
97+ return nil , fmt .Errorf ("could not read passphrase: %v" , err )
98+ }
99+ ii , err := age .NewScryptIdentity (pass )
100+ if err != nil {
101+ return nil , err
102+ }
103+ fileKey , err = ii .Unwrap (stanzas )
104+ return fileKey , err
105+ }
106+
107+ func unwrapIdentities (key string , reader io.Reader ) (ParsedIdentities , error ){
108+ b := bufio .NewReader (reader )
109+ p , _ := b .Peek (14 ) // length of "age-encryption" and "-----BEGIN AGE"
110+ peeked := string (p )
111+
112+ switch {
113+ // An age encrypted file, plain or armored.
114+ case peeked == "age-encryption" || peeked == "-----BEGIN AGE" :
115+ var r io.Reader = b
116+ if peeked == "-----BEGIN AGE" {
117+ r = armor .NewReader (r )
118+ }
119+ const privateKeySizeLimit = 1 << 24 // 16 MiB
120+ contents , err := io .ReadAll (io .LimitReader (r , privateKeySizeLimit ))
121+ if err != nil {
122+ return nil , fmt .Errorf ("failed to read '%s': %w" , key , err )
123+ }
124+ if len (contents ) == privateKeySizeLimit {
125+ return nil , fmt .Errorf ("failed to read '%s': file too long" , key )
126+ }
127+ IncorrectPassphrase := func () {
128+ conn , err := gpgagent .NewConn ()
129+ if err != nil {
130+ return
131+ }
132+ defer func (conn * gpgagent.Conn ) {
133+ if err := conn .Close (); err != nil {
134+ log .Errorf ("failed to close connection with gpg-agent: %s" , err )
135+ }
136+ }(conn )
137+ err = conn .RemoveFromCache (key )
138+ if err != nil {
139+ log .Warnf ("gpg-agent remove cache request errored: %s" , err )
140+ return
141+ }
142+ }
143+ ids := []age.Identity {& EncryptedIdentity {
144+ Contents : contents ,
145+ Passphrase : func () (string , error ) {
146+ conn , err := gpgagent .NewConn ()
147+ if err != nil {
148+ passphrase , err := readPassphrase ("Enter passphrase for identity " + key + ":" )
149+ if err != nil {
150+ return "" , err
151+ }
152+ return string (passphrase ), nil
153+ }
154+ defer func (conn * gpgagent.Conn ) {
155+ if err := conn .Close (); err != nil {
156+ log .Errorf ("failed to close connection with gpg-agent: %s" , err )
157+ }
158+ }(conn )
159+
160+ req := gpgagent.PassphraseRequest {
161+ // TODO is the cachekey good enough?
162+ CacheKey : key ,
163+ Prompt : "Passphrase" ,
164+ Desc : fmt .Sprintf ("Enter passphrase for identity '%s':" , key ),
165+ }
166+ pass , err := conn .GetPassphrase (& req )
167+ if err != nil {
168+ return "" , fmt .Errorf ("gpg-agent passphrase request errored: %s" , err )
169+ }
170+ //make sure that we won't store empty pass
171+ if len (pass ) == 0 {
172+ IncorrectPassphrase ()
173+ }
174+ return pass , nil
175+ },
176+ IncorrectPassphrase : IncorrectPassphrase ,
177+ NoMatchWarning : func () {
178+ log .Warnf ("encrypted identity '%s' didn't match file's recipients" , key )
179+ },
180+ }}
181+ return ids , nil
182+ // An unencrypted age identity file.
183+ default :
184+ ids , err := age .ParseIdentities (b )
185+ if err != nil {
186+ return nil , fmt .Errorf ("failed to parse '%s' age identities: %w" , key , err )
187+ }
188+ return ids , nil
189+ }
190+ }
0 commit comments