mirror of
https://github.com/FiloSottile/age.git
synced 2026-01-03 19:03:57 +00:00
internal/plugin,cmd/age: implement preliminary plugin client support
This commit is contained in:
@@ -249,7 +249,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
encryptPass(pass, in, out, armorFlag)
|
encryptPass(pass, in, out, armorFlag)
|
||||||
default:
|
default:
|
||||||
encryptKeys(recipientFlags, recipientsFileFlags, identityFlags, in, out, armorFlag)
|
encryptNotPass(recipientFlags, recipientsFileFlags, identityFlags, in, out, armorFlag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +279,7 @@ func passphrasePromptForEncryption() (string, error) {
|
|||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func encryptKeys(keys, files, identities []string, in io.Reader, out io.Writer, armor bool) {
|
func encryptNotPass(keys, files, identities []string, in io.Reader, out io.Writer, armor bool) {
|
||||||
var recipients []age.Recipient
|
var recipients []age.Recipient
|
||||||
for _, arg := range keys {
|
for _, arg := range keys {
|
||||||
r, err := parseRecipient(arg)
|
r, err := parseRecipient(arg)
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ func (i *EncryptedIdentity) decrypt() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to decrypt identity file: %v", err)
|
return fmt.Errorf("failed to decrypt identity file: %v", err)
|
||||||
}
|
}
|
||||||
i.identities, err = age.ParseIdentities(d)
|
i.identities, err = parseIdentities(d)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"filippo.io/age"
|
"filippo.io/age"
|
||||||
"filippo.io/age/agessh"
|
"filippo.io/age/agessh"
|
||||||
"filippo.io/age/armor"
|
"filippo.io/age/armor"
|
||||||
|
"filippo.io/age/internal/plugin"
|
||||||
"golang.org/x/crypto/cryptobyte"
|
"golang.org/x/crypto/cryptobyte"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
@@ -32,6 +33,8 @@ func (gitHubRecipientError) Error() string {
|
|||||||
|
|
||||||
func parseRecipient(arg string) (age.Recipient, error) {
|
func parseRecipient(arg string) (age.Recipient, error) {
|
||||||
switch {
|
switch {
|
||||||
|
case strings.HasPrefix(arg, "age1") && strings.Count(arg, "1") > 1:
|
||||||
|
return plugin.NewRecipient(arg)
|
||||||
case strings.HasPrefix(arg, "age1"):
|
case strings.HasPrefix(arg, "age1"):
|
||||||
return age.ParseX25519Recipient(arg)
|
return age.ParseX25519Recipient(arg)
|
||||||
case strings.HasPrefix(arg, "ssh-"):
|
case strings.HasPrefix(arg, "ssh-"):
|
||||||
@@ -187,7 +190,7 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
|
|||||||
|
|
||||||
// An unencrypted age identity file.
|
// An unencrypted age identity file.
|
||||||
default:
|
default:
|
||||||
ids, err := age.ParseIdentities(b)
|
ids, err := parseIdentities(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read %q: %v", name, err)
|
return nil, fmt.Errorf("failed to read %q: %v", name, err)
|
||||||
}
|
}
|
||||||
@@ -195,6 +198,45 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseIdentity(s string) (age.Identity, error) {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(s, "AGE-PLUGIN-"):
|
||||||
|
return plugin.NewIdentity(s)
|
||||||
|
case strings.HasPrefix(s, "AGE-SECRET-KEY-1"):
|
||||||
|
return age.ParseX25519Identity(s)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unknown identity type")
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseIdentities is like age.ParseIdentities, but supports plugin identities.
|
||||||
|
func parseIdentities(f io.Reader) ([]age.Identity, error) {
|
||||||
|
const privateKeySizeLimit = 1 << 24 // 16 MiB
|
||||||
|
var ids []age.Identity
|
||||||
|
scanner := bufio.NewScanner(io.LimitReader(f, privateKeySizeLimit))
|
||||||
|
var n int
|
||||||
|
for scanner.Scan() {
|
||||||
|
n++
|
||||||
|
line := scanner.Text()
|
||||||
|
if strings.HasPrefix(line, "#") || line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
i, err := parseIdentity(line)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error at line %d: %v", n, err)
|
||||||
|
}
|
||||||
|
ids = append(ids, i)
|
||||||
|
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read secret keys file: %v", err)
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil, fmt.Errorf("no secret keys found")
|
||||||
|
}
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
|
func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
|
||||||
id, err := agessh.ParseIdentity(pemBytes)
|
id, err := agessh.ParseIdentity(pemBytes)
|
||||||
if sshErr, ok := err.(*ssh.PassphraseMissingError); ok {
|
if sshErr, ok := err.(*ssh.PassphraseMissingError); ok {
|
||||||
|
|||||||
280
internal/plugin/client.go
Normal file
280
internal/plugin/client.go
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
// Copyright 2021 Google LLC
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file or at
|
||||||
|
// https://developers.google.com/open-source/licenses/bsd
|
||||||
|
|
||||||
|
// Package plugin implements the age plugin protocol.
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"filippo.io/age"
|
||||||
|
"filippo.io/age/internal/bech32"
|
||||||
|
"filippo.io/age/internal/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Recipient struct {
|
||||||
|
name string
|
||||||
|
encoding string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ age.Recipient = &Recipient{}
|
||||||
|
|
||||||
|
func NewRecipient(s string) (*Recipient, error) {
|
||||||
|
hrp, _, err := bech32.Decode(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid recipient encoding %q: %v", s, err)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(hrp, "age1") {
|
||||||
|
return nil, fmt.Errorf("not a plugin recipient %q: %v", s, err)
|
||||||
|
}
|
||||||
|
name := strings.TrimPrefix(hrp, "age1")
|
||||||
|
return &Recipient{
|
||||||
|
name: name, encoding: s,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the plugin name, which is used in the recipient ("age1name1...")
|
||||||
|
// and identity ("AGE-PLUGIN-NAME-1...") encodings, as well as in the plugin
|
||||||
|
// binary name ("age-plugin-name").
|
||||||
|
func (r *Recipient) Name() string {
|
||||||
|
return r.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
|
||||||
|
cmd := exec.Command("age-plugin-"+r.name, "--age-plugin=recipient-v1")
|
||||||
|
stderr := &bytes.Buffer{}
|
||||||
|
cmd.Stderr = stderr
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cmd.Dir = filepath.Clean("/") // TODO: does this work on Windows
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 1: client sends recipient and file key
|
||||||
|
s := &format.Stanza{
|
||||||
|
Type: "add-recipient",
|
||||||
|
Args: []string{r.encoding},
|
||||||
|
}
|
||||||
|
if err := s.Marshal(stdin); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s = &format.Stanza{
|
||||||
|
Type: "wrap-file-key",
|
||||||
|
Body: fileKey,
|
||||||
|
}
|
||||||
|
if err := s.Marshal(stdin); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s = &format.Stanza{
|
||||||
|
Type: "done",
|
||||||
|
}
|
||||||
|
if err := s.Marshal(stdin); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: plugin responds with stanzas
|
||||||
|
var out []*age.Stanza
|
||||||
|
sr := format.NewStanzaReader(bufio.NewReader(stdout))
|
||||||
|
ReadLoop:
|
||||||
|
for {
|
||||||
|
s, err := sr.ReadStanza()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s.Type {
|
||||||
|
case "recipient-stanza":
|
||||||
|
if len(s.Args) < 2 {
|
||||||
|
return nil, fmt.Errorf("plugin error: received malformed recipient stanza")
|
||||||
|
}
|
||||||
|
n, err := strconv.Atoi(s.Args[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("plugin error: received malformed recipient stanza")
|
||||||
|
}
|
||||||
|
// Currently, we only send a single file key, so the index must be 0.
|
||||||
|
if n != 0 {
|
||||||
|
return nil, fmt.Errorf("plugin error: received malformed recipient stanza")
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, &age.Stanza{
|
||||||
|
Type: s.Args[1],
|
||||||
|
Args: s.Args[2:],
|
||||||
|
Body: s.Body,
|
||||||
|
})
|
||||||
|
case "error":
|
||||||
|
return nil, fmt.Errorf("plugin error: %q", s.Body)
|
||||||
|
case "done":
|
||||||
|
break ReadLoop
|
||||||
|
default:
|
||||||
|
// Unknown commands are ignored.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil, fmt.Errorf("plugin error: received zero recipient stanzas")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stdin.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Identity struct {
|
||||||
|
name string
|
||||||
|
encoding string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ age.Identity = &Identity{}
|
||||||
|
|
||||||
|
func NewIdentity(s string) (*Identity, error) {
|
||||||
|
hrp, _, err := bech32.Decode(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid identity encoding: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(hrp, "AGE-PLUGIN-") || !strings.HasSuffix(hrp, "-") {
|
||||||
|
return nil, fmt.Errorf("not a plugin identity: %v", err)
|
||||||
|
}
|
||||||
|
name := strings.TrimSuffix(strings.TrimPrefix(hrp, "AGE-PLUGIN-"), "-")
|
||||||
|
name = strings.ToLower(name)
|
||||||
|
return &Identity{
|
||||||
|
name: name, encoding: s,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the plugin name, which is used in the recipient ("age1name1...")
|
||||||
|
// and identity ("AGE-PLUGIN-NAME-1...") encodings, as well as in the plugin
|
||||||
|
// binary name ("age-plugin-name").
|
||||||
|
func (i *Identity) Name() string {
|
||||||
|
return i.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Identity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {
|
||||||
|
// TODO: DRY up connection management into a connection type, and defer
|
||||||
|
// closing the connection.
|
||||||
|
cmd := exec.Command("age-plugin-"+i.name, "--age-plugin=identity-v1")
|
||||||
|
stderr := &bytes.Buffer{}
|
||||||
|
cmd.Stderr = stderr
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cmd.Dir = filepath.Clean("/") // TODO: does this work on Windows
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 1: client sends the plugin the identity string and the stanzas
|
||||||
|
s := &format.Stanza{
|
||||||
|
Type: "add-identity",
|
||||||
|
Args: []string{i.encoding},
|
||||||
|
}
|
||||||
|
if err := s.Marshal(stdin); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rs := range stanzas {
|
||||||
|
s = &format.Stanza{
|
||||||
|
Type: "recipient-stanza",
|
||||||
|
Args: append([]string{"0", rs.Type}, rs.Args...),
|
||||||
|
Body: rs.Body,
|
||||||
|
}
|
||||||
|
if err := s.Marshal(stdin); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s = &format.Stanza{
|
||||||
|
Type: "done",
|
||||||
|
}
|
||||||
|
if err := s.Marshal(stdin); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: plugin responds with various commands and a file key
|
||||||
|
var out []byte
|
||||||
|
sr := format.NewStanzaReader(bufio.NewReader(stdout))
|
||||||
|
ReadLoop:
|
||||||
|
for {
|
||||||
|
s, err := sr.ReadStanza()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s.Type {
|
||||||
|
case "msg":
|
||||||
|
// TODO: unimplemented.
|
||||||
|
ss := &format.Stanza{Type: "ok"}
|
||||||
|
if err := ss.Marshal(stdin); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "request-secret":
|
||||||
|
// TODO: unimplemented.
|
||||||
|
ss := &format.Stanza{Type: "fail"}
|
||||||
|
if err := ss.Marshal(stdin); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "file-key":
|
||||||
|
if len(s.Args) != 1 {
|
||||||
|
return nil, fmt.Errorf("plugin error: received malformed file-key stanza")
|
||||||
|
}
|
||||||
|
n, err := strconv.Atoi(s.Args[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("plugin error: received malformed file-key stanza")
|
||||||
|
}
|
||||||
|
// Currently, we only send a single file key, so the index must be 0.
|
||||||
|
if n != 0 {
|
||||||
|
return nil, fmt.Errorf("plugin error: received malformed file-key stanza")
|
||||||
|
}
|
||||||
|
|
||||||
|
out = s.Body
|
||||||
|
|
||||||
|
ss := &format.Stanza{Type: "ok"}
|
||||||
|
if err := ss.Marshal(stdin); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "error":
|
||||||
|
ss := &format.Stanza{Type: "ok"}
|
||||||
|
if err := ss.Marshal(stdin); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("plugin error: %q", s.Body)
|
||||||
|
case "done":
|
||||||
|
break ReadLoop
|
||||||
|
default:
|
||||||
|
// Unknown commands are ignored.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if out == nil {
|
||||||
|
return nil, age.ErrIncorrectIdentity
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user