feat: initial goget release — modern IPv6-first download utility

This commit is contained in:
2026-06-26 21:21:58 +02:00
commit 53db81e1f7
147 changed files with 45931 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
//go:build linux || freebsd
// +build linux freebsd
package crypto
import (
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"strings"
)
// CertLoader loads and parses certificates
type CertLoader struct {
pool *x509.CertPool
}
// NewCertLoader creates new cert loader
func NewCertLoader() *CertLoader {
return &CertLoader{
pool: x509.NewCertPool(),
}
}
// LoadCAFile loads CA certificates from a file
func (cl *CertLoader) LoadCAFile(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read ca file: %w", err)
}
if !cl.pool.AppendCertsFromPEM(data) {
return fmt.Errorf("failed to parse ca certificates")
}
return nil
}
// LoadSystemCerts loads system certificates
func (cl *CertLoader) LoadSystemCerts() error {
pool, err := x509.SystemCertPool()
if err != nil {
// Fallback for systems without system certificates
cl.pool = x509.NewCertPool()
return nil
}
cl.pool = pool
return nil
}
// GetPool returns the certificate pool
func (cl *CertLoader) GetPool() *x509.CertPool {
return cl.pool
}
// ParseCertificate parses a PEM certificate
func ParseCertificate(data []byte) (*x509.Certificate, error) {
block, _ := pem.Decode(data)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
return x509.ParseCertificate(block.Bytes)
}
// LoadCertificateFile loads a certificate from a file
func LoadCertificateFile(path string) (*x509.Certificate, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return ParseCertificate(data)
}
// FingerprintSHA256 calculates the SHA-256 fingerprint of a certificate
func FingerprintSHA256(cert *x509.Certificate) string {
if cert == nil {
return ""
}
hash := sha256.Sum256(cert.Raw)
// Format as colon-separated hex pairs (like OpenSSL format)
parts := make([]string, len(hash))
for i, b := range hash {
parts[i] = fmt.Sprintf("%02X", b)
}
return strings.Join(parts, ":")
}
+150
View File
@@ -0,0 +1,150 @@
//go:build linux || freebsd
// +build linux freebsd
package crypto
import (
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"fmt"
"hash"
"io"
"os"
"golang.org/x/crypto/blake2b"
"golang.org/x/crypto/sha3"
)
// ChecksumType defines the checksum type
type ChecksumType string
const (
SHA256 ChecksumType = "sha256"
SHA512 ChecksumType = "sha512"
BLAKE2b ChecksumType = "blake2b"
SHA3_256 ChecksumType = "sha3-256"
SHA3_512 ChecksumType = "sha3-512"
MD5 ChecksumType = "md5"
)
// ChecksumVerifier verifies a file checksum
type ChecksumVerifier struct {
hashType ChecksumType
hasher hash.Hash
expected string
}
// NewChecksumVerifier creates new verifier
func NewChecksumVerifier(hashType ChecksumType, expected string) (*ChecksumVerifier, error) {
var hasher hash.Hash
switch hashType {
case SHA256:
hasher = sha256.New()
case SHA512:
hasher = sha512.New()
case BLAKE2b:
h, err := blake2b.New256(nil)
if err != nil {
return nil, fmt.Errorf("failed to create blake2b hasher: %w", err)
}
hasher = h
case SHA3_256:
hasher = sha3.New256()
case SHA3_512:
hasher = sha3.New512()
case MD5:
return nil, fmt.Errorf("md5 is deprecated and not supported")
default:
return nil, fmt.Errorf("unsupported hash type: %s", hashType)
}
return &ChecksumVerifier{
hashType: hashType,
hasher: hasher,
expected: expected,
}, nil
}
// VerifyFile verifies the checksum of a file
func (cv *ChecksumVerifier) VerifyFile(path string) (bool, string, error) {
file, err := os.Open(path)
if err != nil {
return false, "", fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
cv.hasher.Reset()
_, err = io.Copy(cv.hasher, file)
if err != nil {
return false, "", fmt.Errorf("failed to read file: %w", err)
}
actual := hex.EncodeToString(cv.hasher.Sum(nil))
match := actual == cv.expected
return match, actual, nil
}
// VerifyReader verifies checksum from a reader (streaming)
func (cv *ChecksumVerifier) VerifyReader(r io.Reader) (string, error) {
cv.hasher.Reset()
_, err := io.Copy(cv.hasher, r)
if err != nil {
return "", err
}
return hex.EncodeToString(cv.hasher.Sum(nil)), nil
}
// Hasher returns the underlying hasher for streaming verification
func (cv *ChecksumVerifier) Hasher() hash.Hash {
return cv.hasher
}
// Expected returns the expected checksum
func (cv *ChecksumVerifier) Expected() string {
return cv.expected
}
// Type returns the hash function type
func (cv *ChecksumVerifier) Type() ChecksumType {
return cv.hashType
}
// ComputeFileChecksum calculates the checksum of a file
func ComputeFileChecksum(path string, hashType ChecksumType) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()
var hasher hash.Hash
switch hashType {
case SHA256:
hasher = sha256.New()
case SHA512:
hasher = sha512.New()
case BLAKE2b:
h, err := blake2b.New256(nil)
if err != nil {
return "", fmt.Errorf("failed to create blake2b hasher: %w", err)
}
hasher = h
case SHA3_256:
hasher = sha3.New256()
case SHA3_512:
hasher = sha3.New512()
default:
return "", fmt.Errorf("unsupported hash type: %s", hashType)
}
_, err = io.Copy(hasher, file)
if err != nil {
return "", err
}
return hex.EncodeToString(hasher.Sum(nil)), nil
}
+190
View File
@@ -0,0 +1,190 @@
//go:build linux || freebsd
// +build linux freebsd
package crypto
import (
"os"
"strings"
"testing"
)
func TestNewChecksumVerifier(t *testing.T) {
tests := []struct {
hashType ChecksumType
valid bool
}{
{SHA256, true},
{SHA512, true},
{BLAKE2b, true},
{SHA3_256, true},
{SHA3_512, true},
{MD5, false}, // MD5 is deprecated
{"invalid", false},
}
for _, test := range tests {
_, err := NewChecksumVerifier(test.hashType, "abc123")
if test.valid && err != nil {
t.Errorf("NewChecksumVerifier(%s) failed: %v", test.hashType, err)
}
if !test.valid && err == nil {
t.Errorf("NewChecksumVerifier(%s) should have failed", test.hashType)
}
}
}
func TestChecksumVerifier(t *testing.T) {
// Test SHA-256
verifier, err := NewChecksumVerifier(SHA256, "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae")
if err != nil {
t.Fatalf("Failed to create verifier: %v", err)
}
// Test with string "foo"
actual, err := verifier.VerifyReader(strings.NewReader("foo"))
if err != nil {
t.Fatalf("VerifyReader failed: %v", err)
}
if actual != "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" {
t.Errorf("Unexpected hash: %s", actual)
}
}
func TestComputeFileChecksum(t *testing.T) {
// Create a temp file for testing
tmpFile := t.TempDir() + "/test.txt"
writeTestFile(tmpFile, "test content")
hash, err := ComputeFileChecksum(tmpFile, SHA256)
if err != nil {
t.Fatalf("ComputeFileChecksum failed: %v", err)
}
if hash == "" {
t.Error("Expected non-empty hash")
}
// Verify it's a valid hex string
if len(hash) != 64 {
t.Errorf("Expected SHA-256 hash length 64, got %d", len(hash))
}
}
func TestComputeFileChecksumTypes(t *testing.T) {
tmpFile := t.TempDir() + "/test.txt"
writeTestFile(tmpFile, "test")
tests := []struct {
hashType ChecksumType
length int
}{
{SHA256, 64},
{SHA512, 128},
{BLAKE2b, 64},
{SHA3_256, 64},
{SHA3_512, 128},
}
for _, test := range tests {
hash, err := ComputeFileChecksum(tmpFile, test.hashType)
if err != nil {
t.Errorf("ComputeFileChecksum(%s) failed: %v", test.hashType, err)
continue
}
if len(hash) != test.length {
t.Errorf("Expected %s hash length %d, got %d", test.hashType, test.length, len(hash))
}
}
}
func TestChecksumVerifierType(t *testing.T) {
verifier, _ := NewChecksumVerifier(SHA256, "abc")
if verifier.Type() != SHA256 {
t.Errorf("Expected type SHA256, got %s", verifier.Type())
}
}
func TestChecksumVerifierExpected(t *testing.T) {
expected := "abc123def456"
verifier, _ := NewChecksumVerifier(SHA256, expected)
if verifier.Expected() != expected {
t.Errorf("Expected %s, got %s", expected, verifier.Expected())
}
}
func TestChecksumVerifierHasher(t *testing.T) {
verifier, _ := NewChecksumVerifier(SHA256, "abc")
hasher := verifier.Hasher()
if hasher == nil {
t.Error("Expected non-nil hasher")
}
}
func writeTestFile(path, content string) {
f, _ := os.Create(path)
f.WriteString(content)
f.Close()
}
func TestVerifyFileSuccess(t *testing.T) {
path := "/tmp/goget_test_verify_ok.tmp"
writeTestFile(path, "test data for verification")
defer os.Remove(path)
hash, err := ComputeFileChecksum(path, SHA256)
if err != nil {
t.Fatalf("ComputeFileChecksum failed: %v", err)
}
verifier, err := NewChecksumVerifier(SHA256, hash)
if err != nil {
t.Fatalf("NewChecksumVerifier failed: %v", err)
}
match, actual, err := verifier.VerifyFile(path)
if err != nil {
t.Fatalf("VerifyFile failed: %v", err)
}
if !match {
t.Errorf("unexpected mismatch: expected %s, got %s", hash, actual)
}
}
func TestVerifyFileMismatch(t *testing.T) {
path := "/tmp/goget_test_verify_bad.tmp"
writeTestFile(path, "correct data")
defer os.Remove(path)
verifier, err := NewChecksumVerifier(SHA256, strings.Repeat("0", 64))
if err != nil {
t.Fatalf("NewChecksumVerifier failed: %v", err)
}
match, _, err := verifier.VerifyFile(path)
if err != nil {
t.Fatalf("VerifyFile failed: %v", err)
}
if match {
t.Error("expected checksum mismatch for wrong hash")
}
}
func TestVerifyFileNonexistent(t *testing.T) {
verifier, _ := NewChecksumVerifier(SHA256, strings.Repeat("a", 64))
_, _, err := verifier.VerifyFile("/nonexistent/path")
if err == nil {
t.Error("expected error for nonexistent file")
}
}
func TestAllSupportedChecksumTypes(t *testing.T) {
types := []ChecksumType{SHA256, SHA512, BLAKE2b, SHA3_256, SHA3_512}
for _, ct := range types {
t.Run(string(ct), func(t *testing.T) {
_, err := NewChecksumVerifier(ct, strings.Repeat("a", 128))
if err != nil {
t.Errorf("NewChecksumVerifier(%s) failed: %v", ct, err)
}
})
}
}
+107
View File
@@ -0,0 +1,107 @@
//go:build linux || freebsd
// +build linux freebsd
package crypto
import (
"io"
)
// Hasher is the interface for hashing functions
type Hasher interface {
// Name returns the name of the hash function
Name() string
// Hash calculates a hash from a reader
Hash(r io.Reader) (string, error)
// HashFile calculates a hash from a file
HashFile(path string) (string, error)
// Verify verifies a hash against the expected value
Verify(r io.Reader, expected string) (bool, error)
// VerifyFile verifies a file hash against the expected value
VerifyFile(path string, expected string) (bool, error)
}
// Encryptor is the interface for encryption
type Encryptor interface {
// Name returns the name of the cipher
Name() string
// Encrypt encrypts data
Encrypt(r io.Reader, w io.Writer, key []byte) error
// Decrypt decrypts data
Decrypt(r io.Reader, w io.Writer, key []byte) error
}
// Signer is the interface for digital signatures
type Signer interface {
// Name returns the name of the signature scheme
Name() string
// Sign creates a signature
Sign(data []byte, key []byte) ([]byte, error)
// Verify verifies a signature
Verify(data []byte, signature []byte, key []byte) (bool, error)
}
// KeyLoader is the interface for loading keys
type KeyLoader interface {
// LoadPrivateKey loads a private key
LoadPrivateKey(path string, passphrase []byte) ([]byte, error)
// LoadPublicKey loads a public key
LoadPublicKey(path string) ([]byte, error)
// GenerateKeyPair generates a key pair
GenerateKeyPair() (privateKey, publicKey []byte, err error)
}
// BaseHasher is a basic hasher implementation
type BaseHasher struct {
name string
}
// NewBaseHasher creates new base hasher
func NewBaseHasher(name string) *BaseHasher {
return &BaseHasher{name: name}
}
// Name returns the name of the hash function
func (bh *BaseHasher) Name() string {
return bh.name
}
// Hash calculates a hash from a reader
func (bh *BaseHasher) Hash(r io.Reader) (string, error) {
// Implementation in concrete hashers
return "", nil
}
// HashFile calculates a hash from a file
func (bh *BaseHasher) HashFile(path string) (string, error) {
// Implementation in concrete hashers
return "", nil
}
// Verify verifies a hash against the expected value
func (bh *BaseHasher) Verify(r io.Reader, expected string) (bool, error) {
actual, err := bh.Hash(r)
if err != nil {
return false, err
}
return actual == expected, nil
}
// VerifyFile verifies a file hash against the expected value
func (bh *BaseHasher) VerifyFile(path string, expected string) (bool, error) {
actual, err := bh.HashFile(path)
if err != nil {
return false, err
}
return actual == expected, nil
}
+500
View File
@@ -0,0 +1,500 @@
//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
}