feat: initial goget release — modern IPv6-first download utility
This commit is contained in:
@@ -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, ":")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user