Files

501 lines
12 KiB
Go

//go:build linux || freebsd
// +build linux freebsd
package crypto
import (
"bytes"
"fmt"
"io"
"os"
"strings"
// Deprecated: golang.org/x/crypto/openpgp is frozen; no more security fixes.
// Will be replaced with an alternative in a future release.
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/armor"
"golang.org/x/crypto/openpgp/clearsign"
)
// PGPConfig configures PGP operations
type PGPConfig struct {
// Path to private key
PrivateKeyPath string
// Path to public key
PublicKeyPath string
// Passphrase for private key
Passphrase string
// Armor output (ASCII vs binary)
Armor bool
// KeyRing for verification
KeyRing openpgp.EntityList
}
// DefaultPGPConfig returns the default configuration
func DefaultPGPConfig() *PGPConfig {
return &PGPConfig{
Armor: true,
}
}
// PGPDecryptor decrypts PGP encrypted data
type PGPDecryptor struct {
config *PGPConfig
}
// NewPGPDecryptor creates a new PGP decryptor
func NewPGPDecryptor(cfg *PGPConfig) *PGPDecryptor {
if cfg == nil {
cfg = DefaultPGPConfig()
}
return &PGPDecryptor{config: cfg}
}
// Decrypt decrypts PGP encrypted data
func (pd *PGPDecryptor) Decrypt(r io.Reader, w io.Writer) error {
var keyring openpgp.EntityList
var err error
// Load private key if configured
if pd.config.PrivateKeyPath != "" {
keyring, err = loadPrivateKeyRing(pd.config.PrivateKeyPath, []byte(pd.config.Passphrase))
if err != nil {
return fmt.Errorf("failed to load private key: %w", err)
}
}
// Check if input is armored
buf := make([]byte, 64)
n, err := io.ReadFull(r, buf)
if err != nil && err != io.ErrUnexpectedEOF {
return fmt.Errorf("failed to read input: %w", err)
}
input := io.MultiReader(bytes.NewReader(buf[:n]), r)
// Try to decode armor
if bytes.HasPrefix(bytes.TrimSpace(buf[:n]), []byte("-----BEGIN PGP")) {
armoredBlock, err := armor.Decode(input)
if err != nil {
return fmt.Errorf("failed to decode pgp armor: %w", err)
}
input = armoredBlock.Body
}
// Decrypt the data
md, err := openpgp.ReadMessage(input, keyring, pd.config.passphrasePrompt(), nil)
if err != nil {
return fmt.Errorf("failed to decrypt message: %w", err)
}
// Read all decrypted data
_, err = io.Copy(w, md.UnverifiedBody)
readErr := err
// Close the body (this also verifies the signature if present)
// UnverifiedBody is actually a ReadCloser, but declared as io.Reader
if closer, ok := md.UnverifiedBody.(io.Closer); ok {
closeErr := closer.Close()
if closeErr != nil {
if readErr == nil {
return fmt.Errorf("signature verification failed: %w", closeErr)
}
}
}
if readErr != nil {
return fmt.Errorf("failed to read decrypted data: %w", readErr)
}
// Verify signature if present
if md.IsSigned && md.SignedBy != nil {
// Message was signed and verified
}
return nil
}
// DecryptFile decrypts a PGP encrypted file
func (pd *PGPDecryptor) DecryptFile(inputPath, outputPath string) error {
inputFile, err := os.Open(inputPath)
if err != nil {
return fmt.Errorf("failed to open input file: %w", err)
}
defer inputFile.Close()
outputFile, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer outputFile.Close()
return pd.Decrypt(inputFile, outputFile)
}
// PGPEncryptor encrypts data using PGP
type PGPEncryptor struct {
config *PGPConfig
}
// NewPGPEncryptor creates a new PGP encryptor
func NewPGPEncryptor(cfg *PGPConfig) *PGPEncryptor {
if cfg == nil {
cfg = DefaultPGPConfig()
}
return &PGPEncryptor{config: cfg}
}
// Encrypt encrypts data using PGP
func (pe *PGPEncryptor) Encrypt(r io.Reader, w io.Writer) error {
// Load public key
if pe.config.PublicKeyPath == "" {
return fmt.Errorf("public key path is required for encryption")
}
entity, err := loadPublicKey(pe.config.PublicKeyPath)
if err != nil {
return fmt.Errorf("failed to load public key: %w", err)
}
// Prepare hints
hints := &openpgp.FileHints{
IsBinary: true,
FileName: "", // Don't store filename
}
var out io.WriteCloser
if pe.config.Armor {
out, err = armor.Encode(w, "PGP MESSAGE", nil)
if err != nil {
return fmt.Errorf("failed to create armor encoder: %w", err)
}
} else {
out = nopWriteCloser{w}
}
// Encrypt the data
plaintext, err := openpgp.Encrypt(out, []*openpgp.Entity{entity}, nil, hints, nil)
if err != nil {
if out != nil {
out.Close()
}
return fmt.Errorf("failed to encrypt: %w", err)
}
_, err = io.Copy(plaintext, r)
if err != nil {
plaintext.Close()
if out != nil {
out.Close()
}
return fmt.Errorf("failed to write encrypted data: %w", err)
}
plaintext.Close()
if pe.config.Armor && out != nil {
out.Close()
}
return nil
}
// EncryptFile encrypts a file using PGP
func (pe *PGPEncryptor) EncryptFile(inputPath, outputPath string) error {
inputFile, err := os.Open(inputPath)
if err != nil {
return fmt.Errorf("failed to open input file: %w", err)
}
defer inputFile.Close()
outputFile, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer outputFile.Close()
return pe.Encrypt(inputFile, outputFile)
}
// PGPVerifier verifies PGP signatures
type PGPVerifier struct {
config *PGPConfig
}
// NewPGPVerifier creates a new PGP verifier
func NewPGPVerifier(cfg *PGPConfig) *PGPVerifier {
if cfg == nil {
cfg = DefaultPGPConfig()
}
return &PGPVerifier{config: cfg}
}
// Verify verifies a PGP signature (detached signature)
func (pv *PGPVerifier) Verify(data io.Reader, signature []byte) (bool, string, error) {
// Load keyring
var keyring openpgp.EntityList
var err error
if pv.config.PublicKeyPath != "" {
keyring, err = loadPublicKeyRing(pv.config.PublicKeyPath)
if err != nil {
return false, "", fmt.Errorf("failed to load public key: %w", err)
}
} else if pv.config.KeyRing != nil {
keyring = pv.config.KeyRing
} else {
// Use empty keyring - will try to verify with any available key
keyring = openpgp.EntityList{}
}
// Check if signature is armored
sigReader := bytes.NewReader(signature)
var sigInput io.Reader = sigReader
if bytes.HasPrefix(bytes.TrimSpace(signature), []byte("-----BEGIN PGP")) {
armoredBlock, err := armor.Decode(sigReader)
if err != nil {
return false, "", fmt.Errorf("failed to decode signature armor: %w", err)
}
sigInput = armoredBlock.Body
}
// Verify the signature
signer, err := openpgp.CheckDetachedSignature(keyring, data, sigInput)
if err != nil {
return false, "", fmt.Errorf("signature verification failed: %w", err)
}
if signer == nil {
return false, "", fmt.Errorf("no valid signature found")
}
// Get signer identity
identity := ""
for _, id := range signer.Identities {
if identity == "" {
identity = id.Name
}
if id.SelfSignature != nil {
identity = id.Name
break
}
}
if identity == "" {
identity = "Unknown"
}
return true, identity, nil
}
// VerifyFile verifies PGP signature of a file (detached signature)
func (pv *PGPVerifier) VerifyFile(filePath string, signaturePath string) (bool, string, error) {
// Read signature file
sigData, err := os.ReadFile(signaturePath)
if err != nil {
return false, "", fmt.Errorf("failed to read signature file: %w", err)
}
// Open data file
dataFile, err := os.Open(filePath)
if err != nil {
return false, "", fmt.Errorf("failed to open data file: %w", err)
}
defer dataFile.Close()
return pv.Verify(dataFile, sigData)
}
// VerifyClearsign verifies a clearsigned message
func (pv *PGPVerifier) VerifyClearsign(data []byte) (bool, string, []byte, error) {
// Check if it's a clearsigned message
if !bytes.Contains(data, []byte("-----BEGIN PGP SIGNED MESSAGE-----")) {
return false, "", nil, fmt.Errorf("not a clearsigned message")
}
// Load keyring
var keyring openpgp.EntityList
var err error
if pv.config.PublicKeyPath != "" {
keyring, err = loadPublicKeyRing(pv.config.PublicKeyPath)
if err != nil {
return false, "", nil, fmt.Errorf("failed to load public key: %w", err)
}
} else if pv.config.KeyRing != nil {
keyring = pv.config.KeyRing
}
block, rest := clearsign.Decode(data)
if len(rest) > 0 {
// There's additional data after the clearsigned block
}
signer, err := openpgp.CheckDetachedSignature(keyring, bytes.NewReader(block.Bytes), block.ArmoredSignature.Body)
if err != nil {
return false, "", nil, fmt.Errorf("signature verification failed: %w", err)
}
if signer == nil {
return false, "", nil, fmt.Errorf("no valid signature found")
}
// Get signer identity
identity := "Unknown"
for _, id := range signer.Identities {
if id.SelfSignature != nil {
identity = id.Name
break
}
}
return true, identity, block.Bytes, nil
}
// IsPGPEncrypted determines whether data is PGP encrypted
func IsPGPEncrypted(data []byte) bool {
if len(data) < 10 {
return false
}
// Check for ASCII armor header
header := string(data[:min(len(data), 50)])
if strings.Contains(header, "-----BEGIN PGP MESSAGE-----") ||
strings.Contains(header, "-----BEGIN PGP PUBLIC KEY BLOCK-----") ||
strings.Contains(header, "-----BEGIN PGP PRIVATE KEY BLOCK-----") ||
strings.Contains(header, "-----BEGIN PGP SIGNATURE-----") {
return true
}
// Check for binary PGP packet tags
// PGP binary packets start with specific byte patterns
if len(data) >= 2 {
firstByte := data[0]
// Check for old format packet tag (bits 7-6 = 1, bit 5 = 0)
if (firstByte&0x80) != 0 && (firstByte&0x40) == 0 {
return true
}
// Check for new format packet tag (bits 7-6 = 1, bit 5 = 1)
if (firstByte & 0xC0) == 0xC0 {
return true
}
}
return false
}
// IsPGPSignature determines whether data is a PGP signature
func IsPGPSignature(data []byte) bool {
if len(data) < 10 {
return false
}
header := string(data[:min(len(data), 50)])
return strings.Contains(header, "-----BEGIN PGP SIGNATURE-----")
}
// IsClearsignedMessage determines whether a message is clearsigned
func IsClearsignedMessage(data []byte) bool {
if len(data) < 10 {
return false
}
header := string(data[:min(len(data), 50)])
return strings.Contains(header, "-----BEGIN PGP SIGNED MESSAGE-----")
}
// passphrasePrompt returns a function to prompt for passphrase
func (c *PGPConfig) passphrasePrompt() func(keys []openpgp.Key, symmetric bool) ([]byte, error) {
return func(keys []openpgp.Key, symmetric bool) ([]byte, error) {
if c.Passphrase != "" {
return []byte(c.Passphrase), nil
}
// If no passphrase configured, try empty passphrase
return nil, nil
}
}
// loadPrivateKeyRing loads a keyring from a private key
func loadPrivateKeyRing(path string, passphrase []byte) (openpgp.EntityList, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
keyring, err := openpgp.ReadKeyRing(file)
if err != nil {
return nil, fmt.Errorf("failed to parse keyring: %w", err)
}
// Unlock private keys
for _, entity := range keyring {
if entity.PrivateKey != nil && entity.PrivateKey.Encrypted {
err := entity.PrivateKey.Decrypt(passphrase)
if err != nil {
return nil, fmt.Errorf("failed to decrypt private key: %w", err)
}
}
for _, subkey := range entity.Subkeys {
if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted {
err := subkey.PrivateKey.Decrypt(passphrase)
if err != nil {
return nil, fmt.Errorf("failed to decrypt subkey: %w", err)
}
}
}
}
return keyring, nil
}
// loadPublicKeyRing loads a keyring from a public key
func loadPublicKeyRing(path string) (openpgp.EntityList, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
keyring, err := openpgp.ReadKeyRing(file)
if err != nil {
return nil, fmt.Errorf("failed to parse keyring: %w", err)
}
return keyring, nil
}
// loadPublicKey loads a public key
func loadPublicKey(path string) (*openpgp.Entity, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
keyring, err := openpgp.ReadKeyRing(file)
if err != nil {
return nil, fmt.Errorf("failed to parse public key: %w", err)
}
if len(keyring) == 0 {
return nil, fmt.Errorf("no keys found in key file")
}
// Return first key
return keyring[0], nil
}
// nopWriteCloser helper for non-closing writer
type nopWriteCloser struct {
io.Writer
}
func (nwc nopWriteCloser) Close() error {
return nil
}