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 (
|
||||
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
|
||||
|
||||
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.119.0 h1:tw7OjErMzJKbbjaEHkrt60KQrK5Wus/boCZ7tm5/RNE=
|
||||
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"`
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user