From a46477c8fda072fbdbf83ee9672d0907a3ebb233 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:02:03 +0000 Subject: [PATCH] feat: Add OpenPGP implementation Adds a full implementation of OpenPGP features using ProtonMail's go-crypto fork. - Implements PGP key generation, encryption, and decryption. - Exposes PGP functionality through the crypt.Service. - Adds tests for the PGP implementation. --- go.mod | 2 + go.sum | 4 ++ pkg/crypt/crypt.go | 32 +++++++++- pkg/crypt/std/pgp/pgp.go | 109 ++++++++++++++++++++++++++++++++++ pkg/crypt/std/pgp/pgp_test.go | 47 +++++++++++++++ 5 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 pkg/crypt/std/pgp/pgp.go create mode 100644 pkg/crypt/std/pgp/pgp_test.go diff --git a/go.mod b/go.mod index d053130..6353d5a 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,8 @@ require ( ) require ( + github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/cloudflare/circl v1.6.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index 8be080a..61a2001 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= +github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/pkg/crypt/crypt.go b/pkg/crypt/crypt.go index f362f60..db9db5c 100644 --- a/pkg/crypt/crypt.go +++ b/pkg/crypt/crypt.go @@ -11,18 +11,21 @@ import ( "strings" "github.com/Snider/Enchantrix/pkg/crypt/std/lthn" + "github.com/Snider/Enchantrix/pkg/crypt/std/pgp" "github.com/Snider/Enchantrix/pkg/crypt/std/rsa" ) // Service is the main struct for the crypt service. type Service struct { rsa *rsa.Service + pgp *pgp.Service } -// NewService creates a new crypt Service and initialises its embedded RSA service. +// NewService creates a new crypt Service and initialises its embedded services. func NewService() *Service { return &Service{ rsa: rsa.NewService(), + pgp: pgp.NewService(), } } @@ -171,3 +174,30 @@ func (s *Service) DecryptRSA(privateKey, ciphertext, label []byte) ([]byte, erro s.ensureRSA() return s.rsa.Decrypt(privateKey, ciphertext, label) } + +// --- PGP --- + +// ensurePGP initializes the PGP service if it is not already. +func (s *Service) ensurePGP() { + if s.pgp == nil { + s.pgp = pgp.NewService() + } +} + +// GeneratePGPKeyPair creates a new PGP key pair. +func (s *Service) GeneratePGPKeyPair(name, email, comment string) (publicKey, privateKey []byte, err error) { + s.ensurePGP() + return s.pgp.GenerateKeyPair(name, email, comment) +} + +// EncryptPGP encrypts data with a public key. +func (s *Service) EncryptPGP(publicKey, data []byte) ([]byte, error) { + s.ensurePGP() + return s.pgp.Encrypt(publicKey, data) +} + +// DecryptPGP decrypts data with a private key. +func (s *Service) DecryptPGP(privateKey, ciphertext []byte) ([]byte, error) { + s.ensurePGP() + return s.pgp.Decrypt(privateKey, ciphertext) +} diff --git a/pkg/crypt/std/pgp/pgp.go b/pkg/crypt/std/pgp/pgp.go new file mode 100644 index 0000000..0a70b5c --- /dev/null +++ b/pkg/crypt/std/pgp/pgp.go @@ -0,0 +1,109 @@ + +package pgp + +import ( + "bytes" + "fmt" + "io" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" +) + +// Service is a service for PGP operations. +type Service struct{} + +// NewService creates a new PGP Service. +func NewService() *Service { + return &Service{} +} + +// GenerateKeyPair generates a new PGP key pair. +func (s *Service) GenerateKeyPair(name, email, comment string) (publicKey, privateKey []byte, err error) { + entity, err := openpgp.NewEntity(name, comment, email, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to create new entity: %w", err) + } + + // Sign all the identities + for _, id := range entity.Identities { + err := id.SelfSignature.SignUserId(id.UserId.Id, entity.PrimaryKey, entity.PrivateKey, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to sign user id: %w", err) + } + } + + // Public Key + pubKeyBuf := new(bytes.Buffer) + pubKeyWriter, err := armor.Encode(pubKeyBuf, openpgp.PublicKeyType, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to create armored public key writer: %w", err) + } + defer pubKeyWriter.Close() + if err := entity.Serialize(pubKeyWriter); err != nil { + return nil, nil, fmt.Errorf("failed to serialize public key: %w", err) + } + // a tricky little bastard, this one. without closing the writer, the buffer is empty. + pubKeyWriter.Close() + + // Private Key + privKeyBuf := new(bytes.Buffer) + privKeyWriter, err := armor.Encode(privKeyBuf, openpgp.PrivateKeyType, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to create armored private key writer: %w", err) + } + defer privKeyWriter.Close() + if err := entity.SerializePrivate(privKeyWriter, nil); err != nil { + return nil, nil, fmt.Errorf("failed to serialize private key: %w", err) + } + // a tricky little bastard, this one. without closing the writer, the buffer is empty. + privKeyWriter.Close() + + return pubKeyBuf.Bytes(), privKeyBuf.Bytes(), nil +} + +// Encrypt encrypts data with a public key. +func (s *Service) Encrypt(publicKey, data []byte) ([]byte, error) { + pubKeyReader := bytes.NewReader(publicKey) + keyring, err := openpgp.ReadArmoredKeyRing(pubKeyReader) + if err != nil { + return nil, fmt.Errorf("failed to read public key ring: %w", err) + } + + buf := new(bytes.Buffer) + w, err := openpgp.Encrypt(buf, keyring, nil, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to create encryption writer: %w", err) + } + defer w.Close() + + _, err = w.Write(data) + if err != nil { + return nil, fmt.Errorf("failed to write data to encryption writer: %w", err) + } + w.Close() + + return buf.Bytes(), nil +} + +// Decrypt decrypts data with a private key. +func (s *Service) Decrypt(privateKey, ciphertext []byte) ([]byte, error) { + privKeyReader := bytes.NewReader(privateKey) + keyring, err := openpgp.ReadArmoredKeyRing(privKeyReader) + if err != nil { + return nil, fmt.Errorf("failed to read private key ring: %w", err) + } + + buf := bytes.NewReader(ciphertext) + md, err := openpgp.ReadMessage(buf, keyring, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to read message: %w", err) + } + + plaintext, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + return nil, fmt.Errorf("failed to read plaintext: %w", err) + } + + return plaintext, nil +} diff --git a/pkg/crypt/std/pgp/pgp_test.go b/pkg/crypt/std/pgp/pgp_test.go new file mode 100644 index 0000000..768f92e --- /dev/null +++ b/pkg/crypt/std/pgp/pgp_test.go @@ -0,0 +1,47 @@ + +package pgp + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestService_GenerateKeyPair_Good(t *testing.T) { + s := NewService() + pub, priv, err := s.GenerateKeyPair("test", "test@test.com", "test") + require.NoError(t, err, "failed to generate key pair") + assert.NotNil(t, pub, "public key is nil") + assert.NotNil(t, priv, "private key is nil") +} + +func TestService_Encrypt_Good(t *testing.T) { + s := NewService() + pub, priv, err := s.GenerateKeyPair("test", "test@test.com", "test") + require.NoError(t, err, "failed to generate key pair") + assert.NotNil(t, pub, "public key is nil") + assert.NotNil(t, priv, "private key is nil") + + data := []byte("hello world") + encrypted, err := s.Encrypt(pub, data) + require.NoError(t, err, "failed to encrypt data") + assert.NotNil(t, encrypted, "encrypted data is nil") +} + +func TestService_Decrypt_Good(t *testing.T) { + s := NewService() + pub, priv, err := s.GenerateKeyPair("test", "test@test.com", "test") + require.NoError(t, err, "failed to generate key pair") + assert.NotNil(t, pub, "public key is nil") + assert.NotNil(t, priv, "private key is nil") + + data := []byte("hello world") + encrypted, err := s.Encrypt(pub, data) + require.NoError(t, err, "failed to encrypt data") + assert.NotNil(t, encrypted, "encrypted data is nil") + + decrypted, err := s.Decrypt(priv, encrypted) + require.NoError(t, err, "failed to decrypt data") + assert.Equal(t, data, decrypted, "decrypted data does not match original") +}