mirror of
https://github.com/FiloSottile/age.git
synced 2025-12-23 05:25:14 +00:00
This is a breaking change, but like the other changes to these interfaces it should not matter to consumers of the API that don't implement custom Recipients or Identities, which is all of them so far, as far as I can tell. It became clear working on plugins that we might want Recipient to return multiple recipient stanzas, for example if the plugin recipient is an alias or a group. The Identity side is less important, but it might help avoid round-trips and it makes sense to keep things symmetric.
115 lines
3.5 KiB
Go
115 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.Identity implementation based on a passphrase
|
|
// encrypted SSH private key.
|
|
//
|
|
// It requests the passphrase only if the public key matches a recipient stanza.
|
|
// If the application knows it will always 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 it 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.Identity = &EncryptedSSHIdentity{}
|
|
|
|
// Unwrap implements age.Identity. If the private key is still encrypted, and
|
|
// any of the stanzas match the public key, it will request the passphrase. The
|
|
// decrypted private key will be cached after the first successful invocation.
|
|
func (i *EncryptedSSHIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {
|
|
if i.decrypted != nil {
|
|
return i.decrypted.Unwrap(stanzas)
|
|
}
|
|
|
|
var match bool
|
|
for _, s := range stanzas {
|
|
if s.Type != i.pubKey.Type() {
|
|
continue
|
|
}
|
|
if len(s.Args) < 1 {
|
|
return nil, fmt.Errorf("invalid %v recipient block", i.pubKey.Type())
|
|
}
|
|
if s.Args[0] != sshFingerprint(i.pubKey) {
|
|
continue
|
|
}
|
|
match = true
|
|
break
|
|
}
|
|
if !match {
|
|
return nil, age.ErrIncorrectIdentity
|
|
}
|
|
|
|
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)
|
|
// TODO: here and below, better check that the two public keys match,
|
|
// rather than just the type.
|
|
if i.pubKey.Type() != ssh.KeyAlgoED25519 {
|
|
return nil, fmt.Errorf("mismatched private (%s) and public (%s) SSH key types", ssh.KeyAlgoED25519, i.pubKey.Type())
|
|
}
|
|
case *rsa.PrivateKey:
|
|
i.decrypted, err = NewRSAIdentity(k)
|
|
if i.pubKey.Type() != ssh.KeyAlgoRSA {
|
|
return nil, fmt.Errorf("mismatched private (%s) and public (%s) SSH key types", 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(stanzas)
|
|
}
|