commit dd0939ffaa6930317004111d9592dc0982a72a5b
parent 7f61cf23bf90509d3cabab43f7bc25619b89552d
Author: Matt Layher <mdlayher@gmail.com>
Date: Mon, 7 Oct 2019 18:09:22 -0400
cmd/age: initial support for SSH identities and recipients
Signed-off-by: Matt Layher <mdlayher@gmail.com>
Diffstat:
3 files changed, 81 insertions(+), 3 deletions(-)
diff --git a/cmd/age/age.go b/cmd/age/age.go
@@ -13,6 +13,7 @@ import (
"io"
"log"
"os"
+ "path/filepath"
"time"
"github.com/FiloSottile/age/internal/age"
@@ -89,7 +90,17 @@ func decrypt() {
var identities []age.Identity
// TODO: use the default location if no arguments are provided.
for _, name := range flag.Args() {
- ids, err := parseIdentitiesFile(name)
+ var (
+ ids []age.Identity
+ err error
+ )
+
+ // TODO: smarter detection logic than looking for .ssh/* in the path.
+ if filepath.Base(filepath.Dir(name)) == ".ssh" {
+ ids, err = parseSSHIdentity(name)
+ } else {
+ ids, err = parseIdentitiesFile(name)
+ }
if err != nil {
log.Fatalf("Error: %v", err)
}
diff --git a/cmd/age/parse.go b/cmd/age/parse.go
@@ -9,6 +9,8 @@ package main
import (
"bufio"
"fmt"
+ "io"
+ "io/ioutil"
"os"
"strings"
@@ -16,10 +18,14 @@ import (
)
func parseRecipient(arg string) (age.Recipient, error) {
- if strings.HasPrefix(arg, "pubkey:") {
+ switch {
+ case strings.HasPrefix(arg, "pubkey:"):
return age.ParseX25519Recipient(arg)
+ case strings.HasPrefix(arg, "ssh-"):
+ return age.ParseSSHRecipient(arg)
}
- return nil, fmt.Errorf("unknown recipient type: %s", arg)
+
+ return nil, fmt.Errorf("unknown recipient type: %q", arg)
}
func parseIdentitiesFile(name string) ([]age.Identity, error) {
@@ -50,3 +56,26 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
}
return ids, nil
}
+
+func parseSSHIdentity(name string) ([]age.Identity, error) {
+ f, err := os.Open(name)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open file: %v", err)
+ }
+ defer f.Close()
+
+ // Don't allow unbounded reads.
+ // TODO: support for multiple keys in the same stream, such as user.keys
+ // on GitHub.
+ pemBytes, err := ioutil.ReadAll(io.LimitReader(f, 1<<20))
+ if err != nil {
+ return nil, fmt.Errorf("failed to read %q: %v", name, err)
+ }
+
+ id, err := age.ParseSSHIdentity(pemBytes)
+ if err != nil {
+ return nil, fmt.Errorf("malformed SSH identity in %q: %v", name, err)
+ }
+
+ return []age.Identity{id}, nil
+}
diff --git a/internal/age/ssh.go b/internal/age/ssh.go
@@ -160,6 +160,28 @@ func NewSSHEd25519Recipient(pk ssh.PublicKey) (*SSHEd25519Recipient, error) {
return r, nil
}
+func ParseSSHRecipient(s string) (Recipient, error) {
+ pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(s))
+ if err != nil {
+ return nil, fmt.Errorf("malformed SSH recipient: %q: %v", s, err)
+ }
+
+ var r Recipient
+ switch t := pubKey.Type(); t {
+ case "ssh-rsa":
+ r, err = NewSSHRSARecipient(pubKey)
+ case "ssh-ed25519":
+ r, err = NewSSHEd25519Recipient(pubKey)
+ default:
+ return nil, fmt.Errorf("unknown SSH recipient type: %q", t)
+ }
+ if err != nil {
+ return nil, fmt.Errorf("malformed SSH recipient: %q: %v", s, err)
+ }
+
+ return r, nil
+}
+
var curve25519P, _ = new(big.Int).SetString("57896044618658097711785492504343953926634992332820282019728792003956564819949", 10)
func ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) []byte {
@@ -260,6 +282,22 @@ func NewSSHEd25519Identity(key ed25519.PrivateKey) (*SSHEd25519Identity, error)
return i, nil
}
+func ParseSSHIdentity(pemBytes []byte) (Identity, error) {
+ k, err := ssh.ParseRawPrivateKey(pemBytes)
+ if err != nil {
+ return nil, err
+ }
+
+ switch k := k.(type) {
+ case *ed25519.PrivateKey:
+ return NewSSHEd25519Identity(*k)
+ case *rsa.PrivateKey:
+ return NewSSHRSAIdentity(k)
+ }
+
+ return nil, fmt.Errorf("unsupported SSH identity type: %T", k)
+}
+
func ed25519PrivateKeyToCurve25519(pk ed25519.PrivateKey) []byte {
h := sha512.New()
h.Write(pk[:32])