github.com/kubri/kubri@v0.5.1-0.20240317001612-bda2aaef967e/pkg/crypto/pgp/pgp.go (about)

     1  package pgp
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  
     8  	pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors"
     9  	"github.com/ProtonMail/gopenpgp/v2/armor"
    10  	"github.com/ProtonMail/gopenpgp/v2/constants"
    11  	pgpcrypto "github.com/ProtonMail/gopenpgp/v2/crypto"
    12  
    13  	"github.com/kubri/kubri/pkg/crypto"
    14  )
    15  
    16  type (
    17  	PrivateKey = pgpcrypto.Key
    18  	PublicKey  = pgpcrypto.Key
    19  )
    20  
    21  // NewPrivateKey returns a new private key.
    22  func NewPrivateKey(name, email string) (*PrivateKey, error) {
    23  	return pgpcrypto.GenerateKey(name, email, "x25519", 0)
    24  }
    25  
    26  // MarshalPrivateKey returns the armored private key.
    27  func MarshalPrivateKey(key *PrivateKey) ([]byte, error) {
    28  	if key == nil {
    29  		return nil, crypto.ErrInvalidKey
    30  	}
    31  	if !key.IsPrivate() {
    32  		return nil, crypto.ErrWrongKeyType
    33  	}
    34  	s, err := key.ArmorWithCustomHeaders("", "")
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  	return []byte(s), nil
    39  }
    40  
    41  // UnmarshalPrivateKey returns a private key from an armored key.
    42  func UnmarshalPrivateKey(b []byte) (*PrivateKey, error) {
    43  	key, err := pgpcrypto.NewKeyFromArmoredReader(bytes.NewReader(b))
    44  	if err != nil {
    45  		return nil, wrapError(crypto.ErrInvalidKey, err)
    46  	}
    47  	if !key.IsPrivate() {
    48  		return nil, fmt.Errorf("%w: public key supplied instead of private key", crypto.ErrInvalidKey)
    49  	}
    50  	return key, nil
    51  }
    52  
    53  // Public extracts the public key from a private key.
    54  func Public(key *PrivateKey) *PublicKey {
    55  	pub, err := key.ToPublic()
    56  	if err != nil {
    57  		return key
    58  	}
    59  	return pub
    60  }
    61  
    62  // MarshalPublicKey returns the armored public key.
    63  func MarshalPublicKey(key *PublicKey) ([]byte, error) {
    64  	if key == nil {
    65  		return nil, crypto.ErrInvalidKey
    66  	}
    67  	if key.IsPrivate() {
    68  		return nil, crypto.ErrWrongKeyType
    69  	}
    70  	s, err := key.GetArmoredPublicKeyWithCustomHeaders("", "")
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	return []byte(s), nil
    75  }
    76  
    77  // UnmarshalPublicKey returns a public key from an armored key.
    78  func UnmarshalPublicKey(b []byte) (*PublicKey, error) {
    79  	key, err := pgpcrypto.NewKeyFromArmoredReader(bytes.NewReader(b))
    80  	if err != nil {
    81  		return nil, wrapError(crypto.ErrInvalidKey, err)
    82  	}
    83  	if key.IsPrivate() {
    84  		return nil, fmt.Errorf("%w: private key supplied instead of public key", crypto.ErrInvalidKey)
    85  	}
    86  	return key, nil
    87  }
    88  
    89  // Sign signs the data with the private key.
    90  func Sign(key *PrivateKey, data []byte) ([]byte, error) {
    91  	return sign(key, data, false)
    92  }
    93  
    94  // SignText signs the data with the private key and wraps it in a signed message.
    95  // Data is considered text and canonicalised with CRLF line endings.
    96  func SignText(key *PrivateKey, data []byte) ([]byte, error) {
    97  	data = bytes.ReplaceAll(data, lf, crlf)
    98  	sig, err := sign(key, data, true)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	b := make([]byte, 0, len(startText)+len(data)+len("\r\n")+len(sig))
   103  	b = append(b, startText...)
   104  	b = append(b, data...)
   105  	b = append(b, "\r\n"...)
   106  	b = append(b, sig...)
   107  	return b, nil
   108  }
   109  
   110  func sign(key *PrivateKey, data []byte, text bool) ([]byte, error) {
   111  	if key == nil {
   112  		return nil, crypto.ErrInvalidKey
   113  	}
   114  	if !key.IsPrivate() {
   115  		return nil, crypto.ErrWrongKeyType
   116  	}
   117  
   118  	// TODO: Unlock locked key using env var passphrase.
   119  
   120  	keyring, err := pgpcrypto.NewKeyRing(key)
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  
   125  	msg := pgpcrypto.NewPlainMessage(data)
   126  	msg.TextType = text
   127  
   128  	signature, err := keyring.SignDetached(msg)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  
   133  	sig, err := armor.ArmorWithTypeAndCustomHeaders(signature.Data, constants.PGPSignatureHeader, "", "")
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	return []byte(sig), nil
   139  }
   140  
   141  // Verify verifies the signature of the data with the public key.
   142  func Verify(key *PublicKey, data, sig []byte) bool {
   143  	signature, err := pgpcrypto.NewPGPSignatureFromArmored(string(sig))
   144  	if err != nil {
   145  		return false
   146  	}
   147  
   148  	// TODO: Unlock locked key using env var passphrase.
   149  
   150  	keyring, err := pgpcrypto.NewKeyRing(key)
   151  	if err != nil {
   152  		return false
   153  	}
   154  
   155  	msg := pgpcrypto.NewPlainMessage(data)
   156  
   157  	err = keyring.VerifyDetached(msg, signature, pgpcrypto.GetUnixTime())
   158  	return err == nil
   159  }
   160  
   161  // Split splits a signed message into data and signature.
   162  func Split(msg []byte) (data, sig []byte, _ error) {
   163  	start := bytes.Index(msg, startText)
   164  	end := bytes.Index(msg, endText)
   165  
   166  	if start == -1 || end == -1 {
   167  		return nil, nil, ErrInvalidMessage
   168  	}
   169  
   170  	return bytes.ReplaceAll(msg[start+len(startText):end], crlf, lf), msg[end+2:], nil
   171  }
   172  
   173  //nolint:gochecknoglobals
   174  var (
   175  	startText = []byte("-----BEGIN PGP SIGNED MESSAGE-----\r\nHash: SHA512\r\n\r\n")
   176  	endText   = []byte("\r\n-----BEGIN PGP SIGNATURE-----")
   177  	lf        = []byte("\n")
   178  	crlf      = []byte("\r\n")
   179  )
   180  
   181  var ErrInvalidMessage = errors.New("pgp: invalid message")
   182  
   183  func wrapError(wrapErr, err error) error {
   184  	var e pgperrors.InvalidArgumentError
   185  	if errors.As(err, &e) {
   186  		return fmt.Errorf("%w: %s", wrapErr, string(e))
   187  	}
   188  	return err
   189  }