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.
This commit is contained in:
parent
248de1e9df
commit
a46477c8fd
5 changed files with 193 additions and 1 deletions
2
go.mod
2
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
|
||||
|
|
|
|||
4
go.sum
4
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=
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
109
pkg/crypt/std/pgp/pgp.go
Normal file
109
pkg/crypt/std/pgp/pgp.go
Normal file
|
|
@ -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
|
||||
}
|
||||
47
pkg/crypt/std/pgp/pgp_test.go
Normal file
47
pkg/crypt/std/pgp/pgp_test.go
Normal file
|
|
@ -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")
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue