Skip to content

Commit 19939ea

Browse files
authored
Merge pull request #82 from slok/slok/age-public-keys-new-lines
Improve age public keys loading by sanitizing the input data
2 parents 24c5bd6 + 796941d commit 19939ea

File tree

4 files changed

+152
-88
lines changed

4 files changed

+152
-88
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22

33
## [Unreleased]
44

5+
### Changed
6+
7+
- Fixed bug that wouldn't allow loading `X25519` (Age) public keys with comments or newlines.
8+
- Allow loading `X25519` (Age) public keys in the form of `Public key: {PUBLIC_KEY}` (e.g: Using `age-keygen -o ./priv.key 2> ./pub.key`).
9+
510
## [v0.5.1] - 2021-05-15
611

712
### Changed
813

9-
- Fixed bug that wouldn't allow loading `X25519` (Age) keys with comments or newlines.
14+
- Fixed bug that wouldn't allow loading `X25519` (Age) private keys with comments or newlines.
1015

1116
## [v0.5.0] - 2021-05-03
1217

internal/key/age/age.go

Lines changed: 13 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,8 @@ import (
44
"context"
55
"fmt"
66
"io"
7-
"os"
8-
"regexp"
9-
"strings"
107

118
"filippo.io/age"
12-
"filippo.io/age/agessh"
13-
"golang.org/x/crypto/ssh"
14-
"golang.org/x/term"
159

1610
"github.com/slok/agebox/internal/key"
1711
"github.com/slok/agebox/internal/log"
@@ -52,15 +46,18 @@ func (p PrivateKey) AgeIdentity() age.Identity { return p.identity }
5246

5347
var _ model.PrivateKey = &PrivateKey{}
5448

49+
type publicKeyParser func(ctx context.Context, key string) (age.Recipient, error)
50+
type privateKeyParser func(ctx context.Context, key string) (age.Identity, error)
51+
5552
type factory struct {
5653
// These are the key parsers used to load keys, they will work in
5754
// brute force mode being used as a chain, if one fails we continue
5855
// until one is correct.
5956
//
6057
// TODO(slok): We could optimize this as age does, checking
6158
// the keys headers and selecting the correct one.
62-
publicKeyParsers []func(string) (age.Recipient, error)
63-
privateKeyParsers []func(string) (age.Identity, error)
59+
publicKeyParsers []publicKeyParser
60+
privateKeyParsers []privateKeyParser
6461
}
6562

6663
// Factory is the key.Factory implementation for age supported keys.
@@ -72,13 +69,13 @@ func NewFactory(passphraseReader io.Reader, logger log.Logger) key.Factory {
7269
logger = logger.WithValues(log.Kv{"svc": "key.age.Factory"})
7370

7471
return factory{
75-
publicKeyParsers: []func(string) (age.Recipient, error){
76-
agessh.ParseRecipient,
77-
func(d string) (age.Recipient, error) { return age.ParseX25519Recipient(d) },
72+
publicKeyParsers: []publicKeyParser{
73+
parseSSHPublic(),
74+
parseAgePublic(),
7875
},
79-
privateKeyParsers: []func(string) (age.Identity, error){
80-
parseSSHIdentityFunc(passphraseReader, logger),
81-
parseAgeIdentityFunc(),
76+
privateKeyParsers: []privateKeyParser{
77+
parseSSHPrivateFunc(passphraseReader, logger),
78+
parseAgePrivateFunc(),
8279
},
8380
}
8481
}
@@ -88,7 +85,7 @@ var _ key.Factory = factory{}
8885
func (f factory) GetPublicKey(ctx context.Context, data []byte) (model.PublicKey, error) {
8986
sdata := string(data)
9087
for _, f := range f.publicKeyParsers {
91-
recipient, err := f(sdata)
88+
recipient, err := f(ctx, sdata)
9289
// If no error, we have our public key.
9390
if err == nil {
9491
return PublicKey{
@@ -104,7 +101,7 @@ func (f factory) GetPublicKey(ctx context.Context, data []byte) (model.PublicKey
104101
func (f factory) GetPrivateKey(ctx context.Context, data []byte) (model.PrivateKey, error) {
105102
sdata := string(data)
106103
for _, f := range f.privateKeyParsers {
107-
identity, err := f(sdata)
104+
identity, err := f(ctx, sdata)
108105
// If no error, we have our private key.
109106
if err == nil {
110107
return PrivateKey{
@@ -116,74 +113,3 @@ func (f factory) GetPrivateKey(ctx context.Context, data []byte) (model.PrivateK
116113

117114
return nil, fmt.Errorf("invalid private key")
118115
}
119-
120-
func parseSSHIdentityFunc(passphraseR io.Reader, logger log.Logger) func(string) (age.Identity, error) {
121-
return func(d string) (age.Identity, error) {
122-
// Get the SSH private key.
123-
secretData := []byte(d)
124-
id, err := agessh.ParseIdentity(secretData)
125-
if err == nil {
126-
return id, nil
127-
}
128-
129-
// If passphrase required, ask for it.
130-
sshErr, ok := err.(*ssh.PassphraseMissingError)
131-
if !ok {
132-
return nil, err
133-
}
134-
135-
if sshErr.PublicKey == nil {
136-
return nil, fmt.Errorf("passphrase required and public key can't be obtained from private key")
137-
}
138-
139-
// Ask for passphrase and get identity.
140-
i, err := agessh.NewEncryptedSSHIdentity(sshErr.PublicKey, secretData, askPasswordStdin(passphraseR, logger))
141-
if err != nil {
142-
return nil, err
143-
}
144-
145-
return i, nil
146-
}
147-
}
148-
149-
func askPasswordStdin(r io.Reader, logger log.Logger) func() ([]byte, error) {
150-
return func() ([]byte, error) {
151-
// If not stdin just return the passphrase.
152-
if r != os.Stdin {
153-
return io.ReadAll(r)
154-
}
155-
156-
// Check if is a valid terminal and try getting it.
157-
fd := int(os.Stdin.Fd())
158-
if !term.IsTerminal(fd) {
159-
tty, err := os.Open("/dev/tty")
160-
if err != nil {
161-
return nil, fmt.Errorf("standard input is not available or not a terminal, and opening /dev/tty failed: %v", err)
162-
}
163-
defer tty.Close()
164-
fd = int(tty.Fd())
165-
}
166-
167-
// Ask for password.
168-
logger.Warningf("SSH key passphrase required")
169-
logger.Infof("Enter passphrase for ssh key: ")
170-
171-
p, err := term.ReadPassword(fd)
172-
if err != nil {
173-
return nil, err
174-
}
175-
176-
return p, nil
177-
}
178-
}
179-
180-
var removeCommentRegexp = regexp.MustCompile("(?m)(^#.*$)")
181-
182-
func parseAgeIdentityFunc() func(s string) (age.Identity, error) {
183-
return func(d string) (age.Identity, error) {
184-
d = removeCommentRegexp.ReplaceAllString(d, "")
185-
d = strings.TrimSpace(d)
186-
187-
return age.ParseX25519Identity(d)
188-
}
189-
}

internal/key/age/age_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,28 @@ func TestKeyFactoryPublicKey(t *testing.T) {
3939
key: `age1dsnalzl92c076vh54s3xwqet87de2qde60gcfrpwnm9t3ghc6s7qadhjay`,
4040
expErr: false,
4141
},
42+
43+
"X25519 keys with newlines should be valid.": {
44+
key: ` age1dsnalzl92c076vh54s3xwqet87de2qde60gcfrpwnm9t3ghc6s7qadhjay
45+
46+
`,
47+
expErr: false,
48+
},
49+
50+
"X25519 keys with default age-keygen comment should be valid.": {
51+
key: `
52+
Public key: age1dsnalzl92c076vh54s3xwqet87de2qde60gcfrpwnm9t3ghc6s7qadhjay
53+
`,
54+
expErr: false,
55+
},
56+
"X25519 keys with comments should be valid.": {
57+
key: `
58+
# Public key
59+
age1dsnalzl92c076vh54s3xwqet87de2qde60gcfrpwnm9t3ghc6s7qadhjay
60+
61+
`,
62+
expErr: false,
63+
},
4264
}
4365

4466
for name, test := range tests {

internal/key/age/parsers.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package age
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"os"
8+
"regexp"
9+
"strings"
10+
11+
"filippo.io/age"
12+
"filippo.io/age/agessh"
13+
"golang.org/x/crypto/ssh"
14+
"golang.org/x/term"
15+
16+
"github.com/slok/agebox/internal/log"
17+
)
18+
19+
func parseSSHPublic() publicKeyParser {
20+
return func(ctx context.Context, key string) (age.Recipient, error) {
21+
return agessh.ParseRecipient(key)
22+
}
23+
}
24+
25+
var (
26+
// Some users could make this directly `age-keygen -o ./priv.key 2> ./pub.key`
27+
// This will create a public key in the form of: `Public key: {KEY}`, so we help the user
28+
// by removing this so we can load keys of this kind directly, anyway, if the key is invalid
29+
// for any other reason than this, age library will not load an return an error.
30+
removeAgeDefPhraseRegexp = regexp.MustCompile("(?m)(^Public key:)")
31+
removeCommentRegexp = regexp.MustCompile("(?m)(^#.*$)")
32+
)
33+
34+
func parseAgePublic() publicKeyParser {
35+
return func(ctx context.Context, key string) (age.Recipient, error) {
36+
key = removeCommentRegexp.ReplaceAllString(key, "")
37+
key = removeAgeDefPhraseRegexp.ReplaceAllString(key, "")
38+
key = strings.TrimSpace(key)
39+
40+
return age.ParseX25519Recipient(key)
41+
}
42+
}
43+
44+
func parseAgePrivateFunc() privateKeyParser {
45+
return func(ctx context.Context, key string) (age.Identity, error) {
46+
key = removeCommentRegexp.ReplaceAllString(key, "")
47+
key = strings.TrimSpace(key)
48+
49+
return age.ParseX25519Identity(key)
50+
}
51+
}
52+
53+
func parseSSHPrivateFunc(passphraseR io.Reader, logger log.Logger) privateKeyParser {
54+
return func(ctx context.Context, key string) (age.Identity, error) {
55+
// Get the SSH private key.
56+
secretData := []byte(key)
57+
id, err := agessh.ParseIdentity(secretData)
58+
if err == nil {
59+
return id, nil
60+
}
61+
62+
// If passphrase required, ask for it.
63+
sshErr, ok := err.(*ssh.PassphraseMissingError)
64+
if !ok {
65+
return nil, err
66+
}
67+
68+
if sshErr.PublicKey == nil {
69+
return nil, fmt.Errorf("passphrase required and public key can't be obtained from private key")
70+
}
71+
72+
// Ask for passphrase and get identity.
73+
i, err := agessh.NewEncryptedSSHIdentity(sshErr.PublicKey, secretData, askPasswordStdin(passphraseR, logger))
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
return i, nil
79+
}
80+
}
81+
82+
func askPasswordStdin(r io.Reader, logger log.Logger) func() ([]byte, error) {
83+
return func() ([]byte, error) {
84+
// If not stdin just return the passphrase.
85+
if r != os.Stdin {
86+
return io.ReadAll(r)
87+
}
88+
89+
// Check if is a valid terminal and try getting it.
90+
fd := int(os.Stdin.Fd())
91+
if !term.IsTerminal(fd) {
92+
tty, err := os.Open("/dev/tty")
93+
if err != nil {
94+
return nil, fmt.Errorf("standard input is not available or not a terminal, and opening /dev/tty failed: %v", err)
95+
}
96+
defer tty.Close()
97+
fd = int(tty.Fd())
98+
}
99+
100+
// Ask for password.
101+
logger.Warningf("SSH key passphrase required")
102+
logger.Infof("Enter passphrase for ssh key: ")
103+
104+
p, err := term.ReadPassword(fd)
105+
if err != nil {
106+
return nil, err
107+
}
108+
109+
return p, nil
110+
}
111+
}

0 commit comments

Comments
 (0)