From d6ba0edb273204541e6f987f51247c505be98f67 Mon Sep 17 00:00:00 2001 From: rupinder-syngh <118511490+rupinder-syngh@users.noreply.github.com> Date: Fri, 21 Mar 2025 17:18:51 +0530 Subject: [PATCH] feat: encryption plugin (#429) * feat: Implement encryption plugin. * fix: Removed interface test file * fix: Test case changes * fix: Test case for encryption * fix: formatting changes * fix: lint change * fix: shared renamed to pkg * fix: shared rename to pkg * fix: resolved pr comments * fix: indentation, formatting * fix: removed config, ctx check, test enhancements * fix: removed close function * fix: remove config and enhance test cases * fix: formatting changes, test case enhancement --- go.mod | 1 + go.sum | 2 + pkg/plugin/definition/encrypter.go | 15 ++ .../implementation/encrypter/cmd/plugin.go | 18 ++ .../encrypter/cmd/plugin_test.go | 49 +++++ .../implementation/encrypter/encrypter.go | 70 +++++++ .../encrypter/encrypter_test.go | 183 ++++++++++++++++++ pkg/plugin/manager.go | 23 ++- 8 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 pkg/plugin/definition/encrypter.go create mode 100644 pkg/plugin/implementation/encrypter/cmd/plugin.go create mode 100644 pkg/plugin/implementation/encrypter/cmd/plugin_test.go create mode 100644 pkg/plugin/implementation/encrypter/encrypter.go create mode 100644 pkg/plugin/implementation/encrypter/encrypter_test.go diff --git a/go.mod b/go.mod index f54a3a5..c4129fc 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ toolchain go1.23.7 require golang.org/x/crypto v0.36.0 require ( + github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03 cloud.google.com/go v0.119.0 // indirect cloud.google.com/go/auth v0.15.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect diff --git a/go.sum b/go.sum index 3ea738c..2bc3958 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03 h1:m1h+vudopHsI67FPT9MOncyndWhTcdUoBtI1R1uajGY= +github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03/go.mod h1:8sheVFH84v3PCyFY/O02mIgSQY9I6wMYPWsq7mDnEZY= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.119.0 h1:tw7OjErMzJKbbjaEHkrt60KQrK5Wus/boCZ7tm5/RNE= cloud.google.com/go v0.119.0/go.mod h1:fwB8QLzTcNevxqi8dcpR+hoMIs3jBherGS9VUBDAW08= diff --git a/pkg/plugin/definition/encrypter.go b/pkg/plugin/definition/encrypter.go new file mode 100644 index 0000000..08db8a6 --- /dev/null +++ b/pkg/plugin/definition/encrypter.go @@ -0,0 +1,15 @@ +package definition + +import "context" + +// Encrypter defines the methods for encryption. +type Encrypter interface { + // Encrypt encrypts the given body using the provided privateKeyBase64 and publicKeyBase64. + Encrypt(ctx context.Context, data string, privateKeyBase64, publicKeyBase64 string) (string, error) +} + +// EncrypterProvider initializes a new encrypter instance with the given config. +type EncrypterProvider interface { + // New creates a new encrypter instance based on the provided config. + New(ctx context.Context, config map[string]string) (Encrypter, func() error, error) +} diff --git a/pkg/plugin/implementation/encrypter/cmd/plugin.go b/pkg/plugin/implementation/encrypter/cmd/plugin.go new file mode 100644 index 0000000..aad52ef --- /dev/null +++ b/pkg/plugin/implementation/encrypter/cmd/plugin.go @@ -0,0 +1,18 @@ +package main + +import ( + "context" + + "github.com/beckn/beckn-onix/pkg/plugin/definition" + "github.com/beckn/beckn-onix/pkg/plugin/implementation/encrypter" +) + +// EncrypterProvider implements the definition.EncrypterProvider interface. +type EncrypterProvider struct{} + +func (ep EncrypterProvider) New(ctx context.Context, config map[string]string) (definition.Encrypter, func() error, error) { + return encrypter.New(ctx) +} + +// Provider is the exported symbol that the plugin manager will look for. +var Provider definition.EncrypterProvider = EncrypterProvider{} diff --git a/pkg/plugin/implementation/encrypter/cmd/plugin_test.go b/pkg/plugin/implementation/encrypter/cmd/plugin_test.go new file mode 100644 index 0000000..cbb469e --- /dev/null +++ b/pkg/plugin/implementation/encrypter/cmd/plugin_test.go @@ -0,0 +1,49 @@ +package main + +import ( + "context" + "testing" +) + +func TestEncrypterProviderSuccess(t *testing.T) { + tests := []struct { + name string + ctx context.Context + config map[string]string + }{ + { + name: "Valid empty config", + ctx: context.Background(), + config: map[string]string{}, + }, + { + name: "Valid config with algorithm", + ctx: context.Background(), + config: map[string]string{ + "algorithm": "AES", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create provider and encrypter. + provider := EncrypterProvider{} + encrypter, cleanup, err := provider.New(tt.ctx, tt.config) + if err != nil { + t.Fatalf("EncrypterProvider.New() error = %v", err) + } + if encrypter == nil { + t.Fatal("EncrypterProvider.New() returned nil encrypter") + } + defer func() { + if cleanup != nil { + if err := cleanup(); err != nil { + t.Errorf("Cleanup() error = %v", err) + } + } + }() + + }) + } +} diff --git a/pkg/plugin/implementation/encrypter/encrypter.go b/pkg/plugin/implementation/encrypter/encrypter.go new file mode 100644 index 0000000..f0a8663 --- /dev/null +++ b/pkg/plugin/implementation/encrypter/encrypter.go @@ -0,0 +1,70 @@ +package encrypter + +import ( + "context" + "crypto/aes" + "crypto/cipher" + "crypto/ecdh" + "encoding/base64" + "fmt" + + "github.com/zenazn/pkcs7pad" +) + +// encrypter implements the Encrypter interface and handles the encryption process. +type encrypter struct { +} + +// New creates a new encrypter instance with the given configuration. +func New(ctx context.Context) (*encrypter, func() error, error) { + return &encrypter{}, nil, nil +} + +func (e *encrypter) Encrypt(ctx context.Context, data string, privateKeyBase64, publicKeyBase64 string) (string, error) { + privateKeyBytes, err := base64.StdEncoding.DecodeString(privateKeyBase64) + if err != nil { + return "", fmt.Errorf("invalid private key: %w", err) + } + + publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKeyBase64) + if err != nil { + return "", fmt.Errorf("invalid public key: %w", err) + } + + // Convert the input string to a byte slice. + dataByte := []byte(data) + aesCipher, err := createAESCipher(privateKeyBytes, publicKeyBytes) + if err != nil { + return "", fmt.Errorf("failed to create AES cipher: %w", err) + } + + dataByte = pkcs7pad.Pad(dataByte, aesCipher.BlockSize()) + for i := 0; i < len(dataByte); i += aesCipher.BlockSize() { + aesCipher.Encrypt(dataByte[i:i+aesCipher.BlockSize()], dataByte[i:i+aesCipher.BlockSize()]) + } + + return base64.StdEncoding.EncodeToString(dataByte), nil +} + +func createAESCipher(privateKey, publicKey []byte) (cipher.Block, error) { + x25519Curve := ecdh.X25519() + x25519PrivateKey, err := x25519Curve.NewPrivateKey(privateKey) + if err != nil { + return nil, fmt.Errorf("failed to create private key: %w", err) + } + x25519PublicKey, err := x25519Curve.NewPublicKey(publicKey) + if err != nil { + return nil, fmt.Errorf("failed to create public key: %w", err) + } + sharedSecret, err := x25519PrivateKey.ECDH(x25519PublicKey) + if err != nil { + return nil, fmt.Errorf("failed to derive shared secret: %w", err) + } + + aesCipher, err := aes.NewCipher(sharedSecret) + if err != nil { + return nil, fmt.Errorf("failed to create AES cipher: %w", err) + } + + return aesCipher, nil +} diff --git a/pkg/plugin/implementation/encrypter/encrypter_test.go b/pkg/plugin/implementation/encrypter/encrypter_test.go new file mode 100644 index 0000000..917bdc4 --- /dev/null +++ b/pkg/plugin/implementation/encrypter/encrypter_test.go @@ -0,0 +1,183 @@ +package encrypter + +import ( + "context" + "crypto/ecdh" + "crypto/rand" + "encoding/base64" + "strings" + "testing" +) + +// Helper function to generate a test X25519 key pair. +func generateTestKeyPair(t *testing.T) (string, string) { + curve := ecdh.X25519() + privateKey, err := curve.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("Failed to generate private key: %v", err) + } + + publicKeyBytes := privateKey.PublicKey().Bytes() + // Encode public and private key to base64. + publicKeyBase64 := base64.StdEncoding.EncodeToString(publicKeyBytes) + privateKeyBase64 := base64.StdEncoding.EncodeToString(privateKey.Bytes()) + + return publicKeyBase64, privateKeyBase64 +} + +// TestEncryptSuccess tests successful encryption scenarios. +func TestEncryptSuccess(t *testing.T) { + _, privateKey := generateTestKeyPair(t) + peerpublicKey, _ := generateTestKeyPair(t) + + tests := []struct { + name string + data string + pubKey string + privKey string + }{ + { + name: "Valid short message", + data: "Hello, World!", + pubKey: peerpublicKey, + privKey: privateKey, + }, + { + name: "Valid JSON message", + data: `{"key":"value"}`, + pubKey: peerpublicKey, + privKey: privateKey, + }, + { + name: "Valid empty message", + data: "", + pubKey: peerpublicKey, + privKey: privateKey, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + encrypter := &encrypter{} + encrypted, err := encrypter.Encrypt(context.Background(), tt.data, tt.privKey, tt.pubKey) + if err != nil { + t.Errorf("Encrypt() expected no error, but got: %v", err) + } + + // Verify the encrypted data is valid base64. + _, err = base64.StdEncoding.DecodeString(encrypted) + if err != nil { + t.Errorf("Encrypt() output is not valid base64: %v", err) + } + + // Since we can't decrypt without the ephemeral private key, + // we can only verify that encryption doesn't return empty data. + if encrypted == "" { + t.Error("Encrypt() returned empty string") + } + + // Verify the output is different from input (basic encryption check). + if encrypted == tt.data { + t.Error("Encrypt() output matches input, suggesting no encryption occurred") + } + + }) + } +} + +// TestEncryptFailure tests encryption failure scenarios. +func TestEncryptFailure(t *testing.T) { + // Generate a valid key pair for testing. + _, privateKey := generateTestKeyPair(t) + peerpublicKey, _ := generateTestKeyPair(t) + + tests := []struct { + name string + data string + publicKey string + privKey string + errorContains string + }{ + { + name: "Invalid public key format", + data: "test data", + publicKey: "invalid-base64!@#$", + privKey: privateKey, + errorContains: "invalid public key", + }, + { + name: "Invalid key bytes(public key)", + data: "test data", + publicKey: base64.StdEncoding.EncodeToString([]byte("invalid-key-bytes")), + privKey: privateKey, + errorContains: "failed to create public key", + }, + { + name: "Invalid key bytes(private key)", + data: "test data", + publicKey: peerpublicKey, + privKey: base64.StdEncoding.EncodeToString([]byte("invalid-key-bytes")), + errorContains: "failed to create private key", + }, + { + name: "Empty public key", + data: "test data", + publicKey: "", + privKey: privateKey, + errorContains: "invalid public key", + }, + { + name: "Too short key", + data: "test data", + publicKey: base64.StdEncoding.EncodeToString([]byte{1, 2, 3, 4}), + privKey: privateKey, + errorContains: "failed to create public key", + }, + { + name: "Invalid private key", + data: "test data", + publicKey: peerpublicKey, + privKey: "invalid-base64!@#$", + errorContains: "invalid private key", + }, + { + name: "Empty private key", + data: "test data", + publicKey: peerpublicKey, + privKey: "", + errorContains: "invalid private key", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + encrypter := &encrypter{} + _, err := encrypter.Encrypt(context.Background(), tt.data, tt.privKey, tt.publicKey) + if err != nil && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("Encrypt() error = %v, want error containing %q", err, tt.errorContains) + } + }) + } +} + +// TestNew tests the creation of new encrypter instances. +func TestNew(t *testing.T) { + tests := []struct { + name string + ctx context.Context + }{ + { + name: "Success", + ctx: context.Background(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + encrypter, _, err := New(tt.ctx) + if err == nil && encrypter == nil { + t.Error("New() returned nil encrypter") + } + }) + } +} diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index 9303962..6fff4bf 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -15,6 +15,7 @@ type Config struct { Root string `yaml:"root"` Signer PluginConfig `yaml:"signer"` Verifier PluginConfig `yaml:"verifier"` + Encrypter PluginConfig `yaml:"encrypter"` Publisher PluginConfig `yaml:"publisher"` } @@ -28,6 +29,7 @@ type PluginConfig struct { type Manager struct { sp definition.SignerProvider vp definition.VerifierProvider + ep definition.EncrypterProvider pb definition.PublisherProvider cfg *Config } @@ -56,7 +58,13 @@ func NewManager(ctx context.Context, cfg *Config) (*Manager, error) { return nil, fmt.Errorf("failed to load Verifier plugin: %w", err) } - return &Manager{sp: sp, vp: vp, pb: pb, cfg: cfg}, nil + // Load encryption plugin. + ep, err := provider[definition.EncrypterProvider](cfg.Root, cfg.Encrypter.ID) + if err != nil { + return nil, fmt.Errorf("failed to load encryption plugin: %w", err) + } + + return &Manager{sp: sp, vp: vp, pb: pb, ep: ep, cfg: cfg}, nil } // provider loads a plugin dynamically and retrieves its provider instance. @@ -115,6 +123,19 @@ func (m *Manager) Verifier(ctx context.Context) (definition.Verifier, func() err return Verifier, close, nil } +// Encrypter retrieves the encryption plugin instance. +func (m *Manager) Encrypter(ctx context.Context) (definition.Encrypter, func() error, error) { + if m.ep == nil { + return nil, nil, fmt.Errorf("encryption plugin provider not loaded") + } + + encrypter, close, err := m.ep.New(ctx, m.cfg.Encrypter.Config) + if err != nil { + return nil, nil, fmt.Errorf("failed to initialize encrypter: %w", err) + } + return encrypter, close, nil +} + // Publisher retrieves the publisher plugin instance. func (m *Manager) Publisher(ctx context.Context) (definition.Publisher, error) { if m.pb == nil {