mirror of
https://github.com/FiloSottile/age.git
synced 2026-01-05 03:43:57 +00:00
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.
112 lines
3.5 KiB
Go
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
|
|
}
|