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
This commit is contained in:
1
go.mod
1
go.mod
@@ -7,6 +7,7 @@ toolchain go1.23.7
|
|||||||
require golang.org/x/crypto v0.36.0
|
require golang.org/x/crypto v0.36.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03
|
||||||
cloud.google.com/go v0.119.0 // indirect
|
cloud.google.com/go v0.119.0 // indirect
|
||||||
cloud.google.com/go/auth v0.15.0 // indirect
|
cloud.google.com/go/auth v0.15.0 // indirect
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
|
||||||
|
|||||||
2
go.sum
2
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.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 h1:tw7OjErMzJKbbjaEHkrt60KQrK5Wus/boCZ7tm5/RNE=
|
||||||
cloud.google.com/go v0.119.0/go.mod h1:fwB8QLzTcNevxqi8dcpR+hoMIs3jBherGS9VUBDAW08=
|
cloud.google.com/go v0.119.0/go.mod h1:fwB8QLzTcNevxqi8dcpR+hoMIs3jBherGS9VUBDAW08=
|
||||||
|
|||||||
15
pkg/plugin/definition/encrypter.go
Normal file
15
pkg/plugin/definition/encrypter.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
18
pkg/plugin/implementation/encrypter/cmd/plugin.go
Normal file
18
pkg/plugin/implementation/encrypter/cmd/plugin.go
Normal file
@@ -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{}
|
||||||
49
pkg/plugin/implementation/encrypter/cmd/plugin_test.go
Normal file
49
pkg/plugin/implementation/encrypter/cmd/plugin_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
70
pkg/plugin/implementation/encrypter/encrypter.go
Normal file
70
pkg/plugin/implementation/encrypter/encrypter.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
183
pkg/plugin/implementation/encrypter/encrypter_test.go
Normal file
183
pkg/plugin/implementation/encrypter/encrypter_test.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ type Config struct {
|
|||||||
Root string `yaml:"root"`
|
Root string `yaml:"root"`
|
||||||
Signer PluginConfig `yaml:"signer"`
|
Signer PluginConfig `yaml:"signer"`
|
||||||
Verifier PluginConfig `yaml:"verifier"`
|
Verifier PluginConfig `yaml:"verifier"`
|
||||||
|
Encrypter PluginConfig `yaml:"encrypter"`
|
||||||
Publisher PluginConfig `yaml:"publisher"`
|
Publisher PluginConfig `yaml:"publisher"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ type PluginConfig struct {
|
|||||||
type Manager struct {
|
type Manager struct {
|
||||||
sp definition.SignerProvider
|
sp definition.SignerProvider
|
||||||
vp definition.VerifierProvider
|
vp definition.VerifierProvider
|
||||||
|
ep definition.EncrypterProvider
|
||||||
pb definition.PublisherProvider
|
pb definition.PublisherProvider
|
||||||
cfg *Config
|
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 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.
|
// 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
|
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.
|
// Publisher retrieves the publisher plugin instance.
|
||||||
func (m *Manager) Publisher(ctx context.Context) (definition.Publisher, error) {
|
func (m *Manager) Publisher(ctx context.Context) (definition.Publisher, error) {
|
||||||
if m.pb == nil {
|
if m.pb == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user