Files
age/agessh/encrypted_keys.go
Filippo Valsorda 6546df3bac age: remove Type method from Recipient and Identity interfaces
The Type() method was a mistake, as proven by the fact that I can remove
it without losing any functionality. It gives special meaning to the
"0th argument" of recipient stanzas, when actually it should be left up
to Recipient implementations to make their own stanzas recognizable to
their Identity counterparts.

More importantly, there are totally reasonable Identity (and probably
Recipient) implementations that don't know their own stanza type in
advance. For example, a proxy plugin.

Concretely, it was only used to special-case "scrypt" recipients, and to
skip invoking Unwrap. The former can be done based on the returned
recipient stanza, and the latter is best avoided entirely: the Identity
should start by looking at the stanza and returning ErrIncorrectIdentity
if it's of the wrong type.

This is a breaking API change. However, we are still in beta, and none
of the public downstreams look like they would be affected, as they only
use Recipient and Identity implementations from this package, they only
use them with the interfaces defined in this package, and they don't
directly use the Type() method.
2021-02-08 19:55:28 +01:00

112 lines
3.5 KiB
Go

// Copyright 2019 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 agessh
import (
"crypto/ed25519"
"crypto/rsa"
"fmt"
"filippo.io/age"
"golang.org/x/crypto/ssh"
)
// EncryptedSSHIdentity is an age.IdentityMatcher implementation based on a
// passphrase encrypted SSH private key.
//
// It provides public key based matching and deferred decryption so the
// passphrase is only requested if necessary. If the application knows it will
// unconditionally have to decrypt the private key, it would be simpler to use
// ssh.ParseRawPrivateKeyWithPassphrase directly and pass the result to
// NewEd25519Identity or NewRSAIdentity.
type EncryptedSSHIdentity struct {
pubKey ssh.PublicKey
pemBytes []byte
passphrase func() ([]byte, error)
decrypted age.Identity
}
// NewEncryptedSSHIdentity returns a new EncryptedSSHIdentity.
//
// pubKey must be the public key associated with the encrypted private key, and
// it must have type "ssh-ed25519" or "ssh-rsa". For OpenSSH encrypted files it
// can be extracted from an ssh.PassphraseMissingError, otherwise in can often
// be found in ".pub" files.
//
// pemBytes must be a valid input to ssh.ParseRawPrivateKeyWithPassphrase.
// passphrase is a callback that will be invoked by Unwrap when the passphrase
// is necessary.
func NewEncryptedSSHIdentity(pubKey ssh.PublicKey, pemBytes []byte, passphrase func() ([]byte, error)) (*EncryptedSSHIdentity, error) {
switch t := pubKey.Type(); t {
case "ssh-ed25519", "ssh-rsa":
default:
return nil, fmt.Errorf("unsupported SSH key type: %v", t)
}
return &EncryptedSSHIdentity{
pubKey: pubKey,
pemBytes: pemBytes,
passphrase: passphrase,
}, nil
}
var _ age.IdentityMatcher = &EncryptedSSHIdentity{}
// Unwrap implements age.Identity. If the private key is still encrypted, it
// will request the passphrase. The decrypted private key will be cached after
// the first successful invocation.
func (i *EncryptedSSHIdentity) Unwrap(block *age.Stanza) (fileKey []byte, err error) {
if i.decrypted != nil {
return i.decrypted.Unwrap(block)
}
passphrase, err := i.passphrase()
if err != nil {
return nil, fmt.Errorf("failed to obtain passphrase: %v", err)
}
k, err := ssh.ParseRawPrivateKeyWithPassphrase(i.pemBytes, passphrase)
if err != nil {
return nil, fmt.Errorf("failed to decrypt SSH key file: %v", err)
}
switch k := k.(type) {
case *ed25519.PrivateKey:
i.decrypted, err = NewEd25519Identity(*k)
if i.pubKey.Type() != ssh.KeyAlgoED25519 {
return nil, fmt.Errorf("mismatched SSH key type: got %q, expected %q", ssh.KeyAlgoED25519, i.pubKey.Type())
}
case *rsa.PrivateKey:
i.decrypted, err = NewRSAIdentity(k)
if i.pubKey.Type() != ssh.KeyAlgoRSA {
return nil, fmt.Errorf("mismatched SSH key type: got %q, expected %q", ssh.KeyAlgoRSA, i.pubKey.Type())
}
default:
return nil, fmt.Errorf("unexpected SSH key type: %T", k)
}
if err != nil {
return nil, fmt.Errorf("invalid SSH key: %v", err)
}
return i.decrypted.Unwrap(block)
}
// Match implements age.IdentityMatcher without decrypting the private key, to
// ensure the passphrase is only obtained if necessary.
func (i *EncryptedSSHIdentity) Match(block *age.Stanza) error {
if block.Type != i.pubKey.Type() {
return age.ErrIncorrectIdentity
}
if len(block.Args) < 1 {
return fmt.Errorf("invalid %v recipient block", i.pubKey.Type())
}
if block.Args[0] != sshFingerprint(i.pubKey) {
return age.ErrIncorrectIdentity
}
return nil
}