//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 }