2025-10-24 04:55:10 +01:00
|
|
|
package openpgp
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2025-10-30 14:18:37 +00:00
|
|
|
"os"
|
2025-10-24 04:55:10 +01:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/ProtonMail/go-crypto/openpgp"
|
|
|
|
|
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
2025-10-30 14:18:37 +00:00
|
|
|
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
2025-10-24 04:55:10 +01:00
|
|
|
)
|
|
|
|
|
|
2025-10-30 14:18:37 +00:00
|
|
|
// readRecipientEntity reads an armored PGP public key from the given path.
|
|
|
|
|
func readRecipientEntity(path string) (entity *openpgp.Entity, err error) {
|
|
|
|
|
recipientFile, err := os.Open(path)
|
2025-10-24 04:55:10 +01:00
|
|
|
if err != nil {
|
2025-10-30 14:18:37 +00:00
|
|
|
return nil, fmt.Errorf("openpgp: failed to open recipient public key file at %s: %w", path, err)
|
|
|
|
|
}
|
|
|
|
|
defer func() {
|
|
|
|
|
if closeErr := recipientFile.Close(); closeErr != nil && err == nil {
|
|
|
|
|
err = fmt.Errorf("openpgp: failed to close recipient key file: %w", closeErr)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
block, err := armor.Decode(recipientFile)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("openpgp: failed to decode armored key from %s: %w", path, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if block.Type != openpgp.PublicKeyType {
|
|
|
|
|
return nil, fmt.Errorf("openpgp: invalid key type in %s: expected public key, got %s", path, block.Type)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entity, err = openpgp.ReadEntity(packet.NewReader(block.Body))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("openpgp: failed to read entity from public key: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return entity, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// readSignerEntity reads and decrypts an armored PGP private key.
|
|
|
|
|
func readSignerEntity(path, passphrase string) (entity *openpgp.Entity, err error) {
|
|
|
|
|
signerFile, err := os.Open(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("openpgp: failed to open signer private key file at %s: %w", path, err)
|
|
|
|
|
}
|
|
|
|
|
defer func() {
|
|
|
|
|
if closeErr := signerFile.Close(); closeErr != nil && err == nil {
|
|
|
|
|
err = fmt.Errorf("openpgp: failed to close signer key file: %w", closeErr)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
block, err := armor.Decode(signerFile)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("openpgp: failed to decode armored key from %s: %w", path, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if block.Type != openpgp.PrivateKeyType {
|
|
|
|
|
return nil, fmt.Errorf("openpgp: invalid key type in %s: expected private key, got %s", path, block.Type)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entity, err = openpgp.ReadEntity(packet.NewReader(block.Body))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("openpgp: failed to read entity from private key: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Decrypt the primary private key.
|
|
|
|
|
if entity.PrivateKey != nil && entity.PrivateKey.Encrypted {
|
|
|
|
|
if err := entity.PrivateKey.Decrypt([]byte(passphrase)); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("openpgp: failed to decrypt private key: %w", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Decrypt all subkeys.
|
|
|
|
|
for _, subkey := range entity.Subkeys {
|
|
|
|
|
if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted {
|
|
|
|
|
if err := subkey.PrivateKey.Decrypt([]byte(passphrase)); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("openpgp: failed to decrypt subkey: %w", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-30 14:18:37 +00:00
|
|
|
return entity, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// readRecipientKeyRing reads an armored PGP key ring from the given path.
|
|
|
|
|
func readRecipientKeyRing(path string) (entityList openpgp.EntityList, err error) {
|
|
|
|
|
recipientFile, err := os.Open(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("openpgp: failed to open recipient key file at %s: %w", path, err)
|
|
|
|
|
}
|
|
|
|
|
defer func() {
|
|
|
|
|
if closeErr := recipientFile.Close(); closeErr != nil && err == nil {
|
|
|
|
|
err = fmt.Errorf("openpgp: failed to close recipient key file: %w", closeErr)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
entityList, err = openpgp.ReadArmoredKeyRing(recipientFile)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("openpgp: failed to read armored key ring from %s: %w", path, err)
|
|
|
|
|
}
|
|
|
|
|
if len(entityList) == 0 {
|
|
|
|
|
return nil, fmt.Errorf("openpgp: no keys found in recipient key file %s", path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return entityList, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// EncryptPGP encrypts a string using PGP, writing the armored, encrypted
|
|
|
|
|
// result to the provided io.Writer.
|
|
|
|
|
func EncryptPGP(writer io.Writer, recipientPath, data string, signerPath, signerPassphrase *string) error {
|
|
|
|
|
// 1. Read the recipient's public key
|
|
|
|
|
recipientEntity, err := readRecipientEntity(recipientPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Set up the list of recipients
|
|
|
|
|
to := openpgp.EntityList{recipientEntity}
|
|
|
|
|
|
|
|
|
|
// 3. Handle optional signing
|
2025-10-24 04:55:10 +01:00
|
|
|
var signer *openpgp.Entity
|
2025-10-30 14:18:37 +00:00
|
|
|
if signerPath != nil {
|
|
|
|
|
var passphrase string
|
|
|
|
|
if signerPassphrase != nil {
|
|
|
|
|
passphrase = *signerPassphrase
|
|
|
|
|
}
|
|
|
|
|
signer, err = readSignerEntity(*signerPath, passphrase)
|
2025-10-24 04:55:10 +01:00
|
|
|
if err != nil {
|
2025-10-30 14:18:37 +00:00
|
|
|
return fmt.Errorf("openpgp: failed to prepare signer: %w", err)
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 14:18:37 +00:00
|
|
|
// 4. Create an armored writer and encrypt the message
|
|
|
|
|
armoredWriter, err := armor.Encode(writer, "PGP MESSAGE", nil)
|
2025-10-24 04:55:10 +01:00
|
|
|
if err != nil {
|
2025-10-30 14:18:37 +00:00
|
|
|
return fmt.Errorf("openpgp: failed to create armored writer: %w", err)
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-30 14:18:37 +00:00
|
|
|
plaintext, err := openpgp.Encrypt(armoredWriter, to, signer, nil, nil)
|
2025-10-24 04:55:10 +01:00
|
|
|
if err != nil {
|
2025-10-30 14:18:37 +00:00
|
|
|
_ = armoredWriter.Close() // Attempt to close, but prioritize the encryption error.
|
|
|
|
|
return fmt.Errorf("openpgp: failed to begin encryption: %w", err)
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-30 14:18:37 +00:00
|
|
|
_, err = plaintext.Write([]byte(data))
|
|
|
|
|
if err != nil {
|
|
|
|
|
_ = plaintext.Close()
|
|
|
|
|
_ = armoredWriter.Close()
|
|
|
|
|
return fmt.Errorf("openpgp: failed to write data to encryption stream: %w", err)
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-30 14:18:37 +00:00
|
|
|
// 5. Explicitly close the writers to finalize the message.
|
|
|
|
|
if err := plaintext.Close(); err != nil {
|
|
|
|
|
return fmt.Errorf("openpgp: failed to finalize plaintext writer: %w", err)
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|
|
|
|
|
if err := armoredWriter.Close(); err != nil {
|
2025-10-30 14:18:37 +00:00
|
|
|
return fmt.Errorf("openpgp: failed to finalize armored writer: %w", err)
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-30 14:18:37 +00:00
|
|
|
return nil
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-30 14:18:37 +00:00
|
|
|
// DecryptPGP decrypts an armored PGP message.
|
2025-10-26 00:02:40 +01:00
|
|
|
func DecryptPGP(recipientPath, message, passphrase string, signerPath *string) (string, error) {
|
2025-10-30 14:18:37 +00:00
|
|
|
// 1. Read the recipient's private key
|
|
|
|
|
entityList, err := readRecipientKeyRing(recipientPath)
|
2025-10-24 04:55:10 +01:00
|
|
|
if err != nil {
|
2025-10-30 14:18:37 +00:00
|
|
|
return "", err
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-30 14:18:37 +00:00
|
|
|
// 2. Decode the armored message
|
|
|
|
|
block, err := armor.Decode(strings.NewReader(message))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("openpgp: failed to decode armored message: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if block.Type != "PGP MESSAGE" {
|
|
|
|
|
return "", fmt.Errorf("openpgp: invalid message type: got %s, want PGP MESSAGE", block.Type)
|
|
|
|
|
}
|
2025-10-24 04:55:10 +01:00
|
|
|
|
2025-10-30 14:18:37 +00:00
|
|
|
// 3. If signature verification is required, add signer's public key to keyring
|
|
|
|
|
var signerEntity *openpgp.Entity
|
|
|
|
|
keyring := entityList
|
2025-10-24 04:55:10 +01:00
|
|
|
if signerPath != nil {
|
2025-10-30 14:18:37 +00:00
|
|
|
signerEntity, err = readRecipientEntity(*signerPath)
|
2025-10-24 04:55:10 +01:00
|
|
|
if err != nil {
|
2025-10-30 14:18:37 +00:00
|
|
|
return "", fmt.Errorf("openpgp: failed to read signer public key: %w", err)
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|
2025-10-30 14:18:37 +00:00
|
|
|
keyring = append(keyring, signerEntity)
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-30 14:18:37 +00:00
|
|
|
// 4. Decrypt the message body
|
|
|
|
|
md, err := openpgp.ReadMessage(block.Body, keyring, func(keys []openpgp.Key, symmetric bool) ([]byte, error) {
|
|
|
|
|
return []byte(passphrase), nil
|
|
|
|
|
}, nil)
|
2025-10-24 04:55:10 +01:00
|
|
|
if err != nil {
|
2025-10-30 14:18:37 +00:00
|
|
|
return "", fmt.Errorf("openpgp: failed to read PGP message: %w", err)
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-30 14:18:37 +00:00
|
|
|
// Buffer the unverified body. Do not return or act on it until signature checks pass.
|
|
|
|
|
plaintextBuffer := new(bytes.Buffer)
|
|
|
|
|
if _, err := io.Copy(plaintextBuffer, md.UnverifiedBody); err != nil {
|
|
|
|
|
return "", fmt.Errorf("openpgp: failed to buffer plaintext message body: %w", err)
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-30 14:18:37 +00:00
|
|
|
// 5. Handle optional signature verification
|
2025-10-24 04:55:10 +01:00
|
|
|
if signerPath != nil {
|
2025-10-30 14:18:37 +00:00
|
|
|
// First, ensure a signature actually exists when one is expected.
|
|
|
|
|
if md.SignedByKeyId == 0 {
|
|
|
|
|
return "", fmt.Errorf("openpgp: signature verification failed: message is not signed")
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|
2025-10-30 14:18:37 +00:00
|
|
|
|
|
|
|
|
if md.SignatureError != nil {
|
|
|
|
|
return "", fmt.Errorf("openpgp: signature verification failed: %w", md.SignatureError)
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|
2025-10-30 14:18:37 +00:00
|
|
|
if signerEntity != nil && md.SignedByKeyId != signerEntity.PrimaryKey.KeyId {
|
|
|
|
|
match := false
|
|
|
|
|
for _, subkey := range signerEntity.Subkeys {
|
|
|
|
|
if subkey.PublicKey != nil && subkey.PublicKey.KeyId == md.SignedByKeyId {
|
|
|
|
|
match = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !match {
|
|
|
|
|
return "", fmt.Errorf("openpgp: signature from unexpected key id: got %d, want one of signer key IDs", md.SignedByKeyId)
|
|
|
|
|
}
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 14:18:37 +00:00
|
|
|
return plaintextBuffer.String(), nil
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|