231 lines
7.1 KiB
Go
231 lines
7.1 KiB
Go
|
|
// Package pgp provides OpenPGP key generation, encryption, decryption,
|
||
|
|
// signing, and verification using the ProtonMail go-crypto library.
|
||
|
|
//
|
||
|
|
// Ported from Enchantrix (github.com/Snider/Enchantrix/pkg/crypt/std/pgp).
|
||
|
|
package pgp
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
|
||
|
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||
|
|
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||
|
|
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||
|
|
)
|
||
|
|
|
||
|
|
// KeyPair holds armored PGP public and private keys.
|
||
|
|
type KeyPair struct {
|
||
|
|
PublicKey string
|
||
|
|
PrivateKey string
|
||
|
|
}
|
||
|
|
|
||
|
|
// CreateKeyPair generates a new PGP key pair for the given identity.
|
||
|
|
// If password is non-empty, the private key is encrypted with it.
|
||
|
|
// Returns a KeyPair with armored public and private keys.
|
||
|
|
func CreateKeyPair(name, email, password string) (*KeyPair, error) {
|
||
|
|
entity, err := openpgp.NewEntity(name, "", email, nil)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("pgp: failed to create entity: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sign all the identities
|
||
|
|
for _, id := range entity.Identities {
|
||
|
|
_ = id.SelfSignature.SignUserId(id.UserId.Id, entity.PrimaryKey, entity.PrivateKey, nil)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Encrypt private key with password if provided
|
||
|
|
if password != "" {
|
||
|
|
err = entity.PrivateKey.Encrypt([]byte(password))
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("pgp: failed to encrypt private key: %w", err)
|
||
|
|
}
|
||
|
|
for _, subkey := range entity.Subkeys {
|
||
|
|
err = subkey.PrivateKey.Encrypt([]byte(password))
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("pgp: failed to encrypt subkey: %w", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Serialize public key
|
||
|
|
pubKeyBuf := new(bytes.Buffer)
|
||
|
|
pubKeyWriter, err := armor.Encode(pubKeyBuf, openpgp.PublicKeyType, nil)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("pgp: failed to create armored public key writer: %w", err)
|
||
|
|
}
|
||
|
|
if err := entity.Serialize(pubKeyWriter); err != nil {
|
||
|
|
pubKeyWriter.Close()
|
||
|
|
return nil, fmt.Errorf("pgp: failed to serialize public key: %w", err)
|
||
|
|
}
|
||
|
|
pubKeyWriter.Close()
|
||
|
|
|
||
|
|
// Serialize private key
|
||
|
|
privKeyBuf := new(bytes.Buffer)
|
||
|
|
privKeyWriter, err := armor.Encode(privKeyBuf, openpgp.PrivateKeyType, nil)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("pgp: failed to create armored private key writer: %w", err)
|
||
|
|
}
|
||
|
|
if password != "" {
|
||
|
|
// Manual serialization to avoid re-signing encrypted keys
|
||
|
|
if err := serializeEncryptedEntity(privKeyWriter, entity); err != nil {
|
||
|
|
privKeyWriter.Close()
|
||
|
|
return nil, fmt.Errorf("pgp: failed to serialize private key: %w", err)
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
if err := entity.SerializePrivate(privKeyWriter, nil); err != nil {
|
||
|
|
privKeyWriter.Close()
|
||
|
|
return nil, fmt.Errorf("pgp: failed to serialize private key: %w", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
privKeyWriter.Close()
|
||
|
|
|
||
|
|
return &KeyPair{
|
||
|
|
PublicKey: pubKeyBuf.String(),
|
||
|
|
PrivateKey: privKeyBuf.String(),
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// serializeEncryptedEntity manually serializes an entity with encrypted private keys
|
||
|
|
// to avoid the panic from re-signing encrypted keys.
|
||
|
|
func serializeEncryptedEntity(w io.Writer, e *openpgp.Entity) error {
|
||
|
|
if err := e.PrivateKey.Serialize(w); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
for _, ident := range e.Identities {
|
||
|
|
if err := ident.UserId.Serialize(w); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err := ident.SelfSignature.Serialize(w); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for _, subkey := range e.Subkeys {
|
||
|
|
if err := subkey.PrivateKey.Serialize(w); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err := subkey.Sig.Serialize(w); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Encrypt encrypts data for the recipient identified by their armored public key.
|
||
|
|
// Returns the encrypted data as armored PGP output.
|
||
|
|
func Encrypt(data []byte, publicKeyArmor string) ([]byte, error) {
|
||
|
|
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(publicKeyArmor)))
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("pgp: failed to read public key ring: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
buf := new(bytes.Buffer)
|
||
|
|
armoredWriter, err := armor.Encode(buf, "PGP MESSAGE", nil)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("pgp: failed to create armor encoder: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
w, err := openpgp.Encrypt(armoredWriter, keyring, nil, nil, nil)
|
||
|
|
if err != nil {
|
||
|
|
armoredWriter.Close()
|
||
|
|
return nil, fmt.Errorf("pgp: failed to create encryption writer: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
if _, err := w.Write(data); err != nil {
|
||
|
|
w.Close()
|
||
|
|
armoredWriter.Close()
|
||
|
|
return nil, fmt.Errorf("pgp: failed to write data: %w", err)
|
||
|
|
}
|
||
|
|
w.Close()
|
||
|
|
armoredWriter.Close()
|
||
|
|
|
||
|
|
return buf.Bytes(), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Decrypt decrypts armored PGP data using the given armored private key.
|
||
|
|
// If the private key is encrypted, the password is used to decrypt it first.
|
||
|
|
func Decrypt(data []byte, privateKeyArmor, password string) ([]byte, error) {
|
||
|
|
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(privateKeyArmor)))
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("pgp: failed to read private key ring: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Decrypt the private key if it is encrypted
|
||
|
|
for _, entity := range keyring {
|
||
|
|
if entity.PrivateKey != nil && entity.PrivateKey.Encrypted {
|
||
|
|
if err := entity.PrivateKey.Decrypt([]byte(password)); err != nil {
|
||
|
|
return nil, fmt.Errorf("pgp: failed to decrypt private key: %w", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for _, subkey := range entity.Subkeys {
|
||
|
|
if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted {
|
||
|
|
_ = subkey.PrivateKey.Decrypt([]byte(password))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Decode armored message
|
||
|
|
block, err := armor.Decode(bytes.NewReader(data))
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("pgp: failed to decode armored message: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
md, err := openpgp.ReadMessage(block.Body, keyring, nil, nil)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("pgp: failed to read message: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
plaintext, err := io.ReadAll(md.UnverifiedBody)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("pgp: failed to read plaintext: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return plaintext, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sign creates an armored detached signature for the given data using
|
||
|
|
// the armored private key. If the key is encrypted, the password is used
|
||
|
|
// to decrypt it first.
|
||
|
|
func Sign(data []byte, privateKeyArmor, password string) ([]byte, error) {
|
||
|
|
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(privateKeyArmor)))
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("pgp: failed to read private key ring: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
signer := keyring[0]
|
||
|
|
if signer.PrivateKey == nil {
|
||
|
|
return nil, fmt.Errorf("pgp: private key not found in keyring")
|
||
|
|
}
|
||
|
|
|
||
|
|
if signer.PrivateKey.Encrypted {
|
||
|
|
if err := signer.PrivateKey.Decrypt([]byte(password)); err != nil {
|
||
|
|
return nil, fmt.Errorf("pgp: failed to decrypt private key: %w", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
buf := new(bytes.Buffer)
|
||
|
|
config := &packet.Config{}
|
||
|
|
err = openpgp.ArmoredDetachSign(buf, signer, bytes.NewReader(data), config)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("pgp: failed to sign message: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return buf.Bytes(), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verify verifies an armored detached signature against the given data
|
||
|
|
// and armored public key. Returns nil if the signature is valid.
|
||
|
|
func Verify(data, signature []byte, publicKeyArmor string) error {
|
||
|
|
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(publicKeyArmor)))
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("pgp: failed to read public key ring: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
_, err = openpgp.CheckArmoredDetachedSignature(keyring, bytes.NewReader(data), bytes.NewReader(signature), nil)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("pgp: signature verification failed: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|