fix: reqpreprocessor
This commit is contained in:
19
pkg/plugin/implementation/decrypter/cmd/plugin.go
Normal file
19
pkg/plugin/implementation/decrypter/cmd/plugin.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||
decrypter "github.com/beckn/beckn-onix/pkg/plugin/implementation/decrypter"
|
||||
)
|
||||
|
||||
// DecrypterProvider implements the definition.DecrypterProvider interface.
|
||||
type DecrypterProvider struct{}
|
||||
|
||||
// New creates a new Decrypter instance using the provided configuration.
|
||||
func (dp DecrypterProvider) New(ctx context.Context, config map[string]string) (definition.Decrypter, func() error, error) {
|
||||
return decrypter.New(ctx)
|
||||
}
|
||||
|
||||
// Provider is the exported symbol that the plugin manager will look for.
|
||||
var Provider definition.DecrypterProvider = DecrypterProvider{}
|
||||
49
pkg/plugin/implementation/decrypter/cmd/plugin_test.go
Normal file
49
pkg/plugin/implementation/decrypter/cmd/plugin_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDecrypterProviderSuccess(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
config map[string]string
|
||||
}{
|
||||
{
|
||||
name: "Valid context with empty config",
|
||||
ctx: context.Background(),
|
||||
config: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "Valid context with non-empty config",
|
||||
ctx: context.Background(),
|
||||
config: map[string]string{"key": "value"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
provider := DecrypterProvider{}
|
||||
decrypter, cleanup, err := provider.New(tt.ctx, tt.config)
|
||||
|
||||
// Check error.
|
||||
if err != nil {
|
||||
t.Errorf("New() error = %v, want no error", err)
|
||||
}
|
||||
|
||||
// Check decrypter.
|
||||
if decrypter == nil {
|
||||
t.Error("New() decrypter is nil, want non-nil")
|
||||
}
|
||||
|
||||
// Test cleanup function if it exists.
|
||||
if cleanup != nil {
|
||||
if err := cleanup(); err != nil {
|
||||
t.Errorf("cleanup() error = %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
85
pkg/plugin/implementation/decrypter/decrypter.go
Normal file
85
pkg/plugin/implementation/decrypter/decrypter.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package decryption
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/ecdh"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/zenazn/pkcs7pad"
|
||||
)
|
||||
|
||||
// decrypter implements the Decrypter interface and handles the decryption process.
|
||||
type decrypter struct {
|
||||
}
|
||||
|
||||
// New creates a new decrypter instance with the given configuration.
|
||||
func New(ctx context.Context) (*decrypter, func() error, error) {
|
||||
return &decrypter{}, nil, nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts the given encryptedData using the provided privateKeyBase64 and publicKeyBase64.
|
||||
func (d *decrypter) Decrypt(ctx context.Context, encryptedData, 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)
|
||||
}
|
||||
|
||||
// Decode the Base64 encoded encrypted data.
|
||||
messageByte, err := base64.StdEncoding.DecodeString(encryptedData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode encrypted data: %w", err)
|
||||
}
|
||||
|
||||
aesCipher, err := createAESCipher(privateKeyBytes, publicKeyBytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create AES cipher: %w", err)
|
||||
}
|
||||
|
||||
blocksize := aesCipher.BlockSize()
|
||||
if len(messageByte)%blocksize != 0 {
|
||||
return "", fmt.Errorf("ciphertext is not a multiple of the blocksize")
|
||||
}
|
||||
|
||||
for i := 0; i < len(messageByte); i += aesCipher.BlockSize() {
|
||||
executionSlice := messageByte[i : i+aesCipher.BlockSize()]
|
||||
aesCipher.Decrypt(executionSlice, executionSlice)
|
||||
}
|
||||
|
||||
messageByte, err = pkcs7pad.Unpad(messageByte)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to unpad data: %w", err)
|
||||
}
|
||||
|
||||
return string(messageByte), 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
|
||||
}
|
||||
251
pkg/plugin/implementation/decrypter/decrypter_test.go
Normal file
251
pkg/plugin/implementation/decrypter/decrypter_test.go
Normal file
@@ -0,0 +1,251 @@
|
||||
package decryption
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/ecdh"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zenazn/pkcs7pad"
|
||||
)
|
||||
|
||||
// Helper function to generate valid test keys.
|
||||
func generateTestKeys(t *testing.T) (privateKeyB64, publicKeyB64 string) {
|
||||
curve := ecdh.X25519()
|
||||
privateKey, err := curve.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate private key: %v", err)
|
||||
}
|
||||
|
||||
publicKey := privateKey.PublicKey()
|
||||
privateKeyB64 = base64.StdEncoding.EncodeToString(privateKey.Bytes())
|
||||
publicKeyB64 = base64.StdEncoding.EncodeToString(publicKey.Bytes())
|
||||
|
||||
return privateKeyB64, publicKeyB64
|
||||
}
|
||||
|
||||
// Helper function to encrypt test data.
|
||||
func encryptTestData(t *testing.T, data []byte, privateKeyBase64, publicKeyBase64 string) string {
|
||||
privateKeyBytes, err := base64.StdEncoding.DecodeString(privateKeyBase64)
|
||||
if err != nil {
|
||||
t.Fatalf("Invalid private key: %v", err)
|
||||
}
|
||||
|
||||
publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKeyBase64)
|
||||
if err != nil {
|
||||
t.Fatalf("Invalid public key: %v", err)
|
||||
}
|
||||
|
||||
x25519Curve := ecdh.X25519()
|
||||
x25519PrivateKey, err := x25519Curve.NewPrivateKey(privateKeyBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create private key: %v", err)
|
||||
}
|
||||
x25519PublicKey, err := x25519Curve.NewPublicKey(publicKeyBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create public key: %v", err)
|
||||
}
|
||||
|
||||
// Generate shared secret for encryption.
|
||||
sharedSecret, err := x25519PrivateKey.ECDH(x25519PublicKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create shared secret: %v", err)
|
||||
}
|
||||
|
||||
// Create AES cipher.
|
||||
block, err := aes.NewCipher(sharedSecret)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create AES cipher: %v", err)
|
||||
}
|
||||
|
||||
// Pad the data.
|
||||
paddedData := pkcs7pad.Pad(data, block.BlockSize())
|
||||
|
||||
// Encrypt the data.
|
||||
ciphertext := make([]byte, len(paddedData))
|
||||
for i := 0; i < len(paddedData); i += block.BlockSize() {
|
||||
block.Encrypt(ciphertext[i:i+block.BlockSize()], paddedData[i:i+block.BlockSize()])
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(ciphertext)
|
||||
}
|
||||
|
||||
// TestDecrypterSuccess tests successful decryption scenarios.
|
||||
func TestDecrypterSuccess(t *testing.T) {
|
||||
senderPrivateKeyB64, senderPublicKeyB64 := generateTestKeys(t)
|
||||
receiverPrivateKeyB64, receiverPublicKeyB64 := generateTestKeys(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
}{
|
||||
{
|
||||
name: "Valid decryption with small data",
|
||||
data: []byte("test"),
|
||||
},
|
||||
{
|
||||
name: "Valid decryption with medium data",
|
||||
data: []byte("medium length test data that spans multiple blocks"),
|
||||
},
|
||||
{
|
||||
name: "Valid decryption with empty data",
|
||||
data: []byte{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Encrypt the test data.
|
||||
encryptedData := encryptTestData(t, tt.data, senderPrivateKeyB64, receiverPublicKeyB64)
|
||||
|
||||
decrypter, _, err := New(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create decrypter: %v", err)
|
||||
}
|
||||
|
||||
result, err := decrypter.Decrypt(context.Background(), encryptedData, receiverPrivateKeyB64, senderPublicKeyB64)
|
||||
if err != nil {
|
||||
t.Errorf("Decrypt() error = %v", err)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if result != string(tt.data) {
|
||||
t.Errorf("Decrypt() = %v, want %v", result, string(tt.data))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDecrypterFailure tests various failure scenarios.
|
||||
func TestDecrypterFailure(t *testing.T) {
|
||||
_, senderPublicKeyB64 := generateTestKeys(t)
|
||||
receiverPrivateKeyB64, _ := generateTestKeys(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
encryptedData string
|
||||
privateKey string
|
||||
publicKey string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "Invalid private key format",
|
||||
encryptedData: base64.StdEncoding.EncodeToString(make([]byte, 32)),
|
||||
privateKey: "invalid-base64!@#$",
|
||||
publicKey: senderPublicKeyB64,
|
||||
expectedErr: "invalid private key",
|
||||
},
|
||||
{
|
||||
name: "Invalid public key format",
|
||||
encryptedData: base64.StdEncoding.EncodeToString(make([]byte, 32)),
|
||||
privateKey: receiverPrivateKeyB64,
|
||||
publicKey: "invalid-base64!@#$",
|
||||
expectedErr: "invalid public key",
|
||||
},
|
||||
{
|
||||
name: "Invalid encrypted data format",
|
||||
encryptedData: "invalid-base64!@#$",
|
||||
privateKey: receiverPrivateKeyB64,
|
||||
publicKey: senderPublicKeyB64,
|
||||
expectedErr: "failed to decode encrypted data",
|
||||
},
|
||||
{
|
||||
name: "Empty private key",
|
||||
encryptedData: base64.StdEncoding.EncodeToString(make([]byte, 32)),
|
||||
privateKey: "",
|
||||
publicKey: senderPublicKeyB64,
|
||||
expectedErr: "invalid private key",
|
||||
},
|
||||
{
|
||||
name: "Empty public key",
|
||||
encryptedData: base64.StdEncoding.EncodeToString(make([]byte, 32)),
|
||||
privateKey: receiverPrivateKeyB64,
|
||||
publicKey: "",
|
||||
expectedErr: "invalid public key",
|
||||
},
|
||||
{
|
||||
name: "Invalid base64 data",
|
||||
encryptedData: "=invalid-base64", // Invalid encrypted data.
|
||||
privateKey: receiverPrivateKeyB64,
|
||||
publicKey: senderPublicKeyB64,
|
||||
expectedErr: "failed to decode encrypted data",
|
||||
},
|
||||
{
|
||||
name: "Invalid private key size",
|
||||
encryptedData: base64.StdEncoding.EncodeToString(make([]byte, 32)),
|
||||
privateKey: base64.StdEncoding.EncodeToString([]byte("short")),
|
||||
publicKey: senderPublicKeyB64,
|
||||
expectedErr: "failed to create private key",
|
||||
},
|
||||
{
|
||||
name: "Invalid public key size",
|
||||
encryptedData: base64.StdEncoding.EncodeToString(make([]byte, 32)),
|
||||
privateKey: receiverPrivateKeyB64,
|
||||
publicKey: base64.StdEncoding.EncodeToString([]byte("short")),
|
||||
expectedErr: "failed to create public key",
|
||||
},
|
||||
{
|
||||
name: "Invalid block size",
|
||||
encryptedData: base64.StdEncoding.EncodeToString([]byte("not-block-size")),
|
||||
privateKey: receiverPrivateKeyB64,
|
||||
publicKey: senderPublicKeyB64,
|
||||
expectedErr: "ciphertext is not a multiple of the blocksize",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
decrypter, _, err := New(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create decrypter: %v", err)
|
||||
}
|
||||
|
||||
_, err = decrypter.Decrypt(context.Background(), tt.encryptedData, tt.privateKey, tt.publicKey)
|
||||
if err == nil {
|
||||
t.Error("Expected error but got none")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), tt.expectedErr) {
|
||||
t.Errorf("Expected error containing %q, got %q", tt.expectedErr, err.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewDecrypter tests the creation of new Decrypter instances.
|
||||
func TestNewDecrypter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
}{
|
||||
{
|
||||
name: "Valid context",
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
name: "Nil context",
|
||||
ctx: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
decrypter, _, err := New(tt.ctx)
|
||||
if err != nil {
|
||||
t.Errorf("New() error = %v", err)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if decrypter == nil {
|
||||
t.Error("Expected non-nil decrypter")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
21
pkg/plugin/implementation/requestPreProcessor/cmd/plugin.go
Normal file
21
pkg/plugin/implementation/requestPreProcessor/cmd/plugin.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
requestpreprocessor "github.com/beckn/beckn-onix/pkg/plugin/implementation/requestPreProcessor"
|
||||
)
|
||||
|
||||
type provider struct{}
|
||||
|
||||
func (p provider) New(ctx context.Context, c map[string]string) (func(http.Handler) http.Handler, error) {
|
||||
config := &requestpreprocessor.Config{}
|
||||
if checkKeysStr, ok := c["CheckKeys"]; ok {
|
||||
config.CheckKeys = strings.Split(checkKeysStr, ",")
|
||||
}
|
||||
return requestpreprocessor.NewUUIDSetter(config)
|
||||
}
|
||||
|
||||
var Provider = provider{}
|
||||
108
pkg/plugin/implementation/requestPreProcessor/reqpreprocessor.go
Normal file
108
pkg/plugin/implementation/requestPreProcessor/reqpreprocessor.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package requestpreprocessor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
CheckKeys []string
|
||||
Role string
|
||||
}
|
||||
|
||||
type contextKeyType string
|
||||
|
||||
const contextKey = "context"
|
||||
const subscriberIDKey contextKeyType = "subscriber_id"
|
||||
|
||||
func NewUUIDSetter(cfg *Config) (func(http.Handler) http.Handler, error) {
|
||||
if err := validateConfig(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var data map[string]any
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
http.Error(w, "Failed to decode request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
contextRaw := data[contextKey]
|
||||
if contextRaw == nil {
|
||||
http.Error(w, fmt.Sprintf("%s field not found.", contextKey), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
contextData, ok := contextRaw.(map[string]any)
|
||||
if !ok {
|
||||
http.Error(w, fmt.Sprintf("%s field is not a map.", contextKey), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var subID any
|
||||
switch cfg.Role {
|
||||
case "bap":
|
||||
subID = contextData["bap_id"]
|
||||
case "bpp":
|
||||
subID = contextData["bpp_id"]
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), subscriberIDKey, subID)
|
||||
for _, key := range cfg.CheckKeys {
|
||||
value := uuid.NewString()
|
||||
updatedValue := update(contextData, key, value)
|
||||
ctx = context.WithValue(ctx, contextKeyType(key), updatedValue)
|
||||
}
|
||||
data[contextKey] = contextData
|
||||
updatedBody, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to marshal updated JSON", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(updatedBody))
|
||||
r.ContentLength = int64(len(updatedBody))
|
||||
r = r.WithContext(ctx)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}, nil
|
||||
}
|
||||
|
||||
func update(wrapper map[string]any, key string, value any) any {
|
||||
field, exists := wrapper[key]
|
||||
if !exists || isEmpty(field) {
|
||||
wrapper[key] = value
|
||||
return value
|
||||
}
|
||||
return field
|
||||
}
|
||||
func isEmpty(v any) bool {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return v == ""
|
||||
case nil:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validateConfig(cfg *Config) error {
|
||||
if cfg == nil {
|
||||
return errors.New("config cannot be nil")
|
||||
}
|
||||
for _, key := range cfg.CheckKeys {
|
||||
if key == "" {
|
||||
return errors.New("checkKeys cannot contain empty strings")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package requestpreprocessor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewUUIDSetter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *Config
|
||||
requestBody map[string]any
|
||||
expectedCode int
|
||||
expectedKeys []string
|
||||
role string
|
||||
}{
|
||||
{
|
||||
name: "Valid keys, update missing keys with bap role",
|
||||
config: &Config{
|
||||
CheckKeys: []string{"transaction_id", "message_id"},
|
||||
Role: "bap",
|
||||
},
|
||||
requestBody: map[string]any{
|
||||
"context": map[string]any{
|
||||
"transaction_id": "",
|
||||
"message_id": nil,
|
||||
"bap_id": "bap-123",
|
||||
},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
expectedKeys: []string{"transaction_id", "message_id", "bap_id"},
|
||||
role: "bap",
|
||||
},
|
||||
{
|
||||
name: "Valid keys, do not update existing keys with bpp role",
|
||||
config: &Config{
|
||||
CheckKeys: []string{"transaction_id", "message_id"},
|
||||
Role: "bpp",
|
||||
},
|
||||
requestBody: map[string]any{
|
||||
"context": map[string]any{
|
||||
"transaction_id": "existing-transaction",
|
||||
"message_id": "existing-message",
|
||||
"bpp_id": "bpp-456",
|
||||
},
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
expectedKeys: []string{"transaction_id", "message_id", "bpp_id"},
|
||||
role: "bpp",
|
||||
},
|
||||
{
|
||||
name: "Missing context key",
|
||||
config: &Config{
|
||||
CheckKeys: []string{"transaction_id"},
|
||||
},
|
||||
requestBody: map[string]any{
|
||||
"otherKey": "value",
|
||||
},
|
||||
expectedCode: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Invalid context type",
|
||||
config: &Config{
|
||||
CheckKeys: []string{"transaction_id"},
|
||||
},
|
||||
requestBody: map[string]any{
|
||||
"context": "not-a-map",
|
||||
},
|
||||
expectedCode: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Nil config",
|
||||
config: nil,
|
||||
requestBody: map[string]any{},
|
||||
expectedCode: http.StatusInternalServerError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
middleware, err := NewUUIDSetter(tt.config)
|
||||
if tt.config == nil || len(tt.config.CheckKeys) == 0 {
|
||||
if err == nil {
|
||||
t.Fatal("Expected an error, but got none")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error while creating middleware: %v", err)
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(tt.requestBody)
|
||||
req := httptest.NewRequest(http.MethodPost, "/test", bytes.NewReader(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if subID, ok := ctx.Value(subscriberIDKey).(string); ok {
|
||||
response := map[string]any{
|
||||
"subscriber_id": subID,
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "Subscriber ID not found", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
middleware(dummyHandler).ServeHTTP(rec, req)
|
||||
if rec.Code != tt.expectedCode {
|
||||
t.Errorf("Expected status code %d, but got %d", tt.expectedCode, rec.Code)
|
||||
}
|
||||
if rec.Code == http.StatusOK {
|
||||
var responseBody map[string]any
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &responseBody); err != nil {
|
||||
t.Fatal("Failed to unmarshal response body:", err)
|
||||
}
|
||||
expectedSubIDKey := "bap_id"
|
||||
if tt.role == "bpp" {
|
||||
expectedSubIDKey = "bpp_id"
|
||||
}
|
||||
|
||||
if subID, ok := responseBody["subscriber_id"].(string); ok {
|
||||
expectedSubID := tt.requestBody["context"].(map[string]any)[expectedSubIDKey]
|
||||
if subID != expectedSubID {
|
||||
t.Errorf("Expected subscriber_id %v, but got %v", expectedSubID, subID)
|
||||
}
|
||||
} else {
|
||||
t.Error("subscriber_id not found in response")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
25
pkg/plugin/implementation/signVerifier/cmd/plugin.go
Normal file
25
pkg/plugin/implementation/signVerifier/cmd/plugin.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||
|
||||
verifier "github.com/beckn/beckn-onix/pkg/plugin/implementation/signVerifier"
|
||||
)
|
||||
|
||||
// VerifierProvider provides instances of Verifier.
|
||||
type VerifierProvider struct{}
|
||||
|
||||
// New initializes a new Verifier instance.
|
||||
func (vp VerifierProvider) New(ctx context.Context, config map[string]string) (definition.Verifier, func() error, error) {
|
||||
if ctx == nil {
|
||||
return nil, nil, errors.New("context cannot be nil")
|
||||
}
|
||||
|
||||
return verifier.New(ctx, &verifier.Config{})
|
||||
}
|
||||
|
||||
// Provider is the exported symbol that the plugin manager will look for.
|
||||
var Provider definition.VerifierProvider = VerifierProvider{}
|
||||
89
pkg/plugin/implementation/signVerifier/cmd/plugin_test.go
Normal file
89
pkg/plugin/implementation/signVerifier/cmd/plugin_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestVerifierProviderSuccess tests successful creation of a verifier.
|
||||
func TestVerifierProviderSuccess(t *testing.T) {
|
||||
provider := VerifierProvider{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
config map[string]string
|
||||
}{
|
||||
{
|
||||
name: "Successful creation",
|
||||
ctx: context.Background(),
|
||||
config: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "Nil context",
|
||||
ctx: context.TODO(),
|
||||
config: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "Empty config",
|
||||
ctx: context.Background(),
|
||||
config: map[string]string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
verifier, close, err := provider.New(tt.ctx, tt.config)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, but got: %v", err)
|
||||
}
|
||||
if verifier == nil {
|
||||
t.Fatal("Expected verifier instance to be non-nil")
|
||||
}
|
||||
if close != nil {
|
||||
if err := close(); err != nil {
|
||||
t.Fatalf("Test %q failed: cleanup function returned an error: %v", tt.name, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifierProviderFailure tests cases where verifier creation should fail.
|
||||
func TestVerifierProviderFailure(t *testing.T) {
|
||||
provider := VerifierProvider{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
config map[string]string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Nil context failure",
|
||||
ctx: nil,
|
||||
config: map[string]string{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
verifierInstance, close, err := provider.New(tt.ctx, tt.config)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("Expected error: %v, but got: %v", tt.wantErr, err)
|
||||
}
|
||||
if verifierInstance != nil {
|
||||
t.Fatal("Expected verifier instance to be nil")
|
||||
}
|
||||
if close != nil {
|
||||
if err := close(); err != nil {
|
||||
t.Fatalf("Test %q failed: cleanup function returned an error: %v", tt.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
120
pkg/plugin/implementation/signVerifier/signVerifier.go
Normal file
120
pkg/plugin/implementation/signVerifier/signVerifier.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package verifier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/blake2b"
|
||||
)
|
||||
|
||||
// Config struct for Verifier.
|
||||
type Config struct {
|
||||
}
|
||||
|
||||
// Verifier implements the Verifier interface.
|
||||
type Verifier struct {
|
||||
config *Config
|
||||
}
|
||||
|
||||
// New creates a new Verifier instance.
|
||||
func New(ctx context.Context, config *Config) (*Verifier, func() error, error) {
|
||||
v := &Verifier{config: config}
|
||||
|
||||
return v, v.Close, nil
|
||||
}
|
||||
|
||||
// Verify checks the signature for the given payload and public key.
|
||||
func (v *Verifier) Verify(ctx context.Context, body []byte, header []byte, publicKeyBase64 string) (bool, error) {
|
||||
createdTimestamp, expiredTimestamp, signature, err := parseAuthHeader(string(header))
|
||||
if err != nil {
|
||||
// TODO: Return appropriate error code when Error Code Handling Module is ready
|
||||
return false, fmt.Errorf("error parsing header: %w", err)
|
||||
}
|
||||
|
||||
signatureBytes, err := base64.StdEncoding.DecodeString(signature)
|
||||
if err != nil {
|
||||
// TODO: Return appropriate error code when Error Code Handling Module is ready
|
||||
return false, fmt.Errorf("error decoding signature: %w", err)
|
||||
}
|
||||
|
||||
currentTime := time.Now().Unix()
|
||||
if createdTimestamp > currentTime || currentTime > expiredTimestamp {
|
||||
// TODO: Return appropriate error code when Error Code Handling Module is ready
|
||||
return false, fmt.Errorf("signature is expired or not yet valid")
|
||||
}
|
||||
|
||||
createdTime := time.Unix(createdTimestamp, 0)
|
||||
expiredTime := time.Unix(expiredTimestamp, 0)
|
||||
|
||||
signingString := hash(body, createdTime.Unix(), expiredTime.Unix())
|
||||
|
||||
decodedPublicKey, err := base64.StdEncoding.DecodeString(publicKeyBase64)
|
||||
if err != nil {
|
||||
// TODO: Return appropriate error code when Error Code Handling Module is ready
|
||||
return false, fmt.Errorf("error decoding public key: %w", err)
|
||||
}
|
||||
|
||||
if !ed25519.Verify(ed25519.PublicKey(decodedPublicKey), []byte(signingString), signatureBytes) {
|
||||
// TODO: Return appropriate error code when Error Code Handling Module is ready
|
||||
return false, fmt.Errorf("signature verification failed")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// parseAuthHeader extracts signature values from the Authorization header.
|
||||
func parseAuthHeader(header string) (int64, int64, string, error) {
|
||||
header = strings.TrimPrefix(header, "Signature ")
|
||||
|
||||
parts := strings.Split(header, ",")
|
||||
signatureMap := make(map[string]string)
|
||||
|
||||
for _, part := range parts {
|
||||
keyValue := strings.SplitN(strings.TrimSpace(part), "=", 2)
|
||||
if len(keyValue) == 2 {
|
||||
key := strings.TrimSpace(keyValue[0])
|
||||
value := strings.Trim(keyValue[1], "\"")
|
||||
signatureMap[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
createdTimestamp, err := strconv.ParseInt(signatureMap["created"], 10, 64)
|
||||
if err != nil {
|
||||
// TODO: Return appropriate error code when Error Code Handling Module is ready
|
||||
return 0, 0, "", fmt.Errorf("invalid created timestamp: %w", err)
|
||||
}
|
||||
|
||||
expiredTimestamp, err := strconv.ParseInt(signatureMap["expires"], 10, 64)
|
||||
if err != nil {
|
||||
// TODO: Return appropriate error code when Error Code Handling Module is ready
|
||||
return 0, 0, "", fmt.Errorf("invalid expires timestamp: %w", err)
|
||||
}
|
||||
|
||||
signature := signatureMap["signature"]
|
||||
if signature == "" {
|
||||
// TODO: Return appropriate error code when Error Code Handling Module is ready
|
||||
return 0, 0, "", fmt.Errorf("signature missing in header")
|
||||
}
|
||||
|
||||
return createdTimestamp, expiredTimestamp, signature, nil
|
||||
}
|
||||
|
||||
// hash constructs a signing string for verification.
|
||||
func hash(payload []byte, createdTimestamp, expiredTimestamp int64) string {
|
||||
hasher, _ := blake2b.New512(nil)
|
||||
hasher.Write(payload)
|
||||
hashSum := hasher.Sum(nil)
|
||||
digestB64 := base64.StdEncoding.EncodeToString(hashSum)
|
||||
|
||||
return fmt.Sprintf("(created): %d\n(expires): %d\ndigest: BLAKE-512=%s", createdTimestamp, expiredTimestamp, digestB64)
|
||||
}
|
||||
|
||||
// Close releases resources (mock implementation returning nil).
|
||||
func (v *Verifier) Close() error {
|
||||
return nil
|
||||
}
|
||||
153
pkg/plugin/implementation/signVerifier/signVerifier_test.go
Normal file
153
pkg/plugin/implementation/signVerifier/signVerifier_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package verifier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// generateTestKeyPair generates a new ED25519 key pair for testing.
|
||||
func generateTestKeyPair() (string, string) {
|
||||
publicKey, privateKey, _ := ed25519.GenerateKey(nil)
|
||||
return base64.StdEncoding.EncodeToString(privateKey), base64.StdEncoding.EncodeToString(publicKey)
|
||||
}
|
||||
|
||||
// signTestData creates a valid signature for test cases.
|
||||
func signTestData(privateKeyBase64 string, body []byte, createdAt, expiresAt int64) string {
|
||||
privateKeyBytes, _ := base64.StdEncoding.DecodeString(privateKeyBase64)
|
||||
privateKey := ed25519.PrivateKey(privateKeyBytes)
|
||||
|
||||
signingString := hash(body, createdAt, expiresAt)
|
||||
signature := ed25519.Sign(privateKey, []byte(signingString))
|
||||
|
||||
return base64.StdEncoding.EncodeToString(signature)
|
||||
}
|
||||
|
||||
// TestVerifySuccessCases tests all valid signature verification cases.
|
||||
func TestVerifySuccess(t *testing.T) {
|
||||
privateKeyBase64, publicKeyBase64 := generateTestKeyPair()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
body []byte
|
||||
createdAt int64
|
||||
expiresAt int64
|
||||
}{
|
||||
{
|
||||
name: "Valid Signature",
|
||||
body: []byte("Test Payload"),
|
||||
createdAt: time.Now().Unix(),
|
||||
expiresAt: time.Now().Unix() + 3600,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
signature := signTestData(privateKeyBase64, tt.body, tt.createdAt, tt.expiresAt)
|
||||
header := `Signature created="` + strconv.FormatInt(tt.createdAt, 10) +
|
||||
`", expires="` + strconv.FormatInt(tt.expiresAt, 10) +
|
||||
`", signature="` + signature + `"`
|
||||
|
||||
verifier, close, _ := New(context.Background(), &Config{})
|
||||
valid, err := verifier.Verify(context.Background(), tt.body, []byte(header), publicKeyBase64)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, but got: %v", err)
|
||||
}
|
||||
if !valid {
|
||||
t.Fatal("Expected signature verification to succeed")
|
||||
}
|
||||
if close != nil {
|
||||
if err := close(); err != nil {
|
||||
t.Fatalf("Test %q failed: cleanup function returned an error: %v", tt.name, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifyFailureCases tests all invalid signature verification cases.
|
||||
func TestVerifyFailure(t *testing.T) {
|
||||
privateKeyBase64, publicKeyBase64 := generateTestKeyPair()
|
||||
_, wrongPublicKeyBase64 := generateTestKeyPair()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
body []byte
|
||||
header string
|
||||
pubKey string
|
||||
}{
|
||||
{
|
||||
name: "Missing Authorization Header",
|
||||
body: []byte("Test Payload"),
|
||||
header: "",
|
||||
pubKey: publicKeyBase64,
|
||||
},
|
||||
{
|
||||
name: "Malformed Header",
|
||||
body: []byte("Test Payload"),
|
||||
header: `InvalidSignature created="wrong"`,
|
||||
pubKey: publicKeyBase64,
|
||||
},
|
||||
{
|
||||
name: "Invalid Base64 Signature",
|
||||
body: []byte("Test Payload"),
|
||||
header: `Signature created="` + strconv.FormatInt(time.Now().Unix(), 10) +
|
||||
`", expires="` + strconv.FormatInt(time.Now().Unix()+3600, 10) +
|
||||
`", signature="!!INVALIDBASE64!!"`,
|
||||
pubKey: publicKeyBase64,
|
||||
},
|
||||
{
|
||||
name: "Expired Signature",
|
||||
body: []byte("Test Payload"),
|
||||
header: `Signature created="` + strconv.FormatInt(time.Now().Unix()-7200, 10) +
|
||||
`", expires="` + strconv.FormatInt(time.Now().Unix()-3600, 10) +
|
||||
`", signature="` + signTestData(privateKeyBase64, []byte("Test Payload"), time.Now().Unix()-7200, time.Now().Unix()-3600) + `"`,
|
||||
pubKey: publicKeyBase64,
|
||||
},
|
||||
{
|
||||
name: "Invalid Public Key",
|
||||
body: []byte("Test Payload"),
|
||||
header: `Signature created="` + strconv.FormatInt(time.Now().Unix(), 10) +
|
||||
`", expires="` + strconv.FormatInt(time.Now().Unix()+3600, 10) +
|
||||
`", signature="` + signTestData(privateKeyBase64, []byte("Test Payload"), time.Now().Unix(), time.Now().Unix()+3600) + `"`,
|
||||
pubKey: wrongPublicKeyBase64,
|
||||
},
|
||||
{
|
||||
name: "Invalid Expires Timestamp",
|
||||
body: []byte("Test Payload"),
|
||||
header: `Signature created="` + strconv.FormatInt(time.Now().Unix(), 10) +
|
||||
`", expires="invalid_timestamp"`,
|
||||
pubKey: publicKeyBase64,
|
||||
},
|
||||
{
|
||||
name: "Signature Missing in Headers",
|
||||
body: []byte("Test Payload"),
|
||||
header: `Signature created="` + strconv.FormatInt(time.Now().Unix(), 10) +
|
||||
`", expires="` + strconv.FormatInt(time.Now().Unix()+3600, 10) + `"`,
|
||||
pubKey: publicKeyBase64,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
verifier, close, _ := New(context.Background(), &Config{})
|
||||
valid, err := verifier.Verify(context.Background(), tt.body, []byte(tt.header), tt.pubKey)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Expected an error but got none")
|
||||
}
|
||||
if valid {
|
||||
t.Fatal("Expected verification to fail")
|
||||
}
|
||||
if close != nil {
|
||||
if err := close(); err != nil {
|
||||
t.Fatalf("Test %q failed: cleanup function returned an error: %v", tt.name, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
24
pkg/plugin/implementation/signer/cmd/plugin.go
Normal file
24
pkg/plugin/implementation/signer/cmd/plugin.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||
"github.com/beckn/beckn-onix/pkg/plugin/implementation/signer"
|
||||
)
|
||||
|
||||
// SignerProvider implements the definition.SignerProvider interface.
|
||||
type SignerProvider struct{}
|
||||
|
||||
// New creates a new Signer instance using the provided configuration.
|
||||
func (p SignerProvider) New(ctx context.Context, config map[string]string) (definition.Signer, func() error, error) {
|
||||
if ctx == nil {
|
||||
return nil, nil, errors.New("context cannot be nil")
|
||||
}
|
||||
|
||||
return signer.New(ctx, &signer.Config{})
|
||||
}
|
||||
|
||||
// Provider is the exported symbol that the plugin manager will look for.
|
||||
var Provider definition.SignerProvider = SignerProvider{}
|
||||
101
pkg/plugin/implementation/signer/cmd/plugin_test.go
Normal file
101
pkg/plugin/implementation/signer/cmd/plugin_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestSignerProviderSuccess verifies successful scenarios for SignerProvider.
|
||||
func TestSignerProviderSuccess(t *testing.T) {
|
||||
provider := SignerProvider{}
|
||||
|
||||
successTests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
config map[string]string
|
||||
}{
|
||||
{
|
||||
name: "Valid Config",
|
||||
ctx: context.Background(),
|
||||
config: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "Unexpected Config Key",
|
||||
ctx: context.Background(),
|
||||
config: map[string]string{"unexpected_key": "some_value"},
|
||||
},
|
||||
{
|
||||
name: "Empty Config",
|
||||
ctx: context.Background(),
|
||||
config: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "Config with empty TTL",
|
||||
ctx: context.Background(),
|
||||
config: map[string]string{"ttl": ""},
|
||||
},
|
||||
{
|
||||
name: "Config with negative TTL",
|
||||
ctx: context.Background(),
|
||||
config: map[string]string{"ttl": "-100"},
|
||||
},
|
||||
{
|
||||
name: "Config with non-numeric TTL",
|
||||
ctx: context.Background(),
|
||||
config: map[string]string{"ttl": "not_a_number"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range successTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
signer, close, err := provider.New(tt.ctx, tt.config)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Test %q failed: expected no error, but got: %v", tt.name, err)
|
||||
}
|
||||
if signer == nil {
|
||||
t.Fatalf("Test %q failed: signer instance should not be nil", tt.name)
|
||||
}
|
||||
if close != nil {
|
||||
if err := close(); err != nil {
|
||||
t.Fatalf("Cleanup function returned an error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignerProviderFailure verifies failure scenarios for SignerProvider.
|
||||
func TestSignerProviderFailure(t *testing.T) {
|
||||
provider := SignerProvider{}
|
||||
|
||||
failureTests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
config map[string]string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Nil Context",
|
||||
ctx: nil,
|
||||
config: map[string]string{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range failureTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
signerInstance, close, err := provider.New(tt.ctx, tt.config)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("Test %q failed: expected error: %v, got: %v", tt.name, tt.wantErr, err)
|
||||
}
|
||||
if signerInstance != nil {
|
||||
t.Fatalf("Test %q failed: expected signer instance to be nil", tt.name)
|
||||
}
|
||||
if close != nil {
|
||||
t.Fatalf("Test %q failed: expected cleanup function to be nil", tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
77
pkg/plugin/implementation/signer/signer.go
Normal file
77
pkg/plugin/implementation/signer/signer.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package signer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/blake2b"
|
||||
)
|
||||
|
||||
// Config holds the configuration for the signing process.
|
||||
type Config struct {
|
||||
}
|
||||
|
||||
// Signer implements the Signer interface and handles the signing process.
|
||||
type Signer struct {
|
||||
config *Config
|
||||
}
|
||||
|
||||
// New creates a new Signer instance with the given configuration.
|
||||
func New(ctx context.Context, config *Config) (*Signer, func() error, error) {
|
||||
s := &Signer{config: config}
|
||||
|
||||
return s, s.Close, nil
|
||||
}
|
||||
|
||||
// hash generates a signing string using BLAKE-512 hashing.
|
||||
func hash(payload []byte, createdAt, expiresAt int64) (string, error) {
|
||||
hasher, _ := blake2b.New512(nil)
|
||||
|
||||
_, err := hasher.Write(payload)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to hash payload: %w", err)
|
||||
}
|
||||
|
||||
hashSum := hasher.Sum(nil)
|
||||
digestB64 := base64.StdEncoding.EncodeToString(hashSum)
|
||||
|
||||
return fmt.Sprintf("(created): %d\n(expires): %d\ndigest: BLAKE-512=%s", createdAt, expiresAt, digestB64), nil
|
||||
}
|
||||
|
||||
// generateSignature signs the given signing string using the provided private key.
|
||||
func generateSignature(signingString []byte, privateKeyBase64 string) ([]byte, error) {
|
||||
privateKeyBytes, err := base64.StdEncoding.DecodeString(privateKeyBase64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding private key: %w", err)
|
||||
}
|
||||
|
||||
if len(privateKeyBytes) != ed25519.PrivateKeySize {
|
||||
return nil, errors.New("invalid private key length")
|
||||
}
|
||||
|
||||
privateKey := ed25519.PrivateKey(privateKeyBytes)
|
||||
return ed25519.Sign(privateKey, signingString), nil
|
||||
}
|
||||
|
||||
// Sign generates a digital signature for the provided payload.
|
||||
func (s *Signer) Sign(ctx context.Context, body []byte, privateKeyBase64 string, createdAt, expiresAt int64) (string, error) {
|
||||
signingString, err := hash(body, createdAt, expiresAt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signature, err := generateSignature([]byte(signingString), privateKeyBase64)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
// Close releases resources (mock implementation returning nil).
|
||||
func (s *Signer) Close() error {
|
||||
return nil
|
||||
}
|
||||
104
pkg/plugin/implementation/signer/signer_test.go
Normal file
104
pkg/plugin/implementation/signer/signer_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package signer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// generateTestKeys generates a test private and public key pair in base64 encoding.
|
||||
func generateTestKeys() (string, string) {
|
||||
publicKey, privateKey, _ := ed25519.GenerateKey(nil)
|
||||
return base64.StdEncoding.EncodeToString(privateKey), base64.StdEncoding.EncodeToString(publicKey)
|
||||
}
|
||||
|
||||
// TestSignSuccess tests the Sign method with valid inputs to ensure it produces a valid signature.
|
||||
func TestSignSuccess(t *testing.T) {
|
||||
privateKey, _ := generateTestKeys()
|
||||
config := Config{}
|
||||
signer, close, _ := New(context.Background(), &config)
|
||||
|
||||
successTests := []struct {
|
||||
name string
|
||||
payload []byte
|
||||
privateKey string
|
||||
createdAt int64
|
||||
expiresAt int64
|
||||
}{
|
||||
{
|
||||
name: "Valid Signing",
|
||||
payload: []byte("test payload"),
|
||||
privateKey: privateKey,
|
||||
createdAt: time.Now().Unix(),
|
||||
expiresAt: time.Now().Unix() + 3600,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range successTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
signature, err := signer.Sign(context.Background(), tt.payload, tt.privateKey, tt.createdAt, tt.expiresAt)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if len(signature) == 0 {
|
||||
t.Errorf("expected a non-empty signature, but got empty")
|
||||
}
|
||||
if close != nil {
|
||||
if err := close(); err != nil {
|
||||
t.Fatalf("Cleanup function returned an error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignFailure tests the Sign method with invalid inputs to ensure proper error handling.
|
||||
func TestSignFailure(t *testing.T) {
|
||||
config := Config{}
|
||||
signer, close, _ := New(context.Background(), &config)
|
||||
|
||||
failureTests := []struct {
|
||||
name string
|
||||
payload []byte
|
||||
privateKey string
|
||||
createdAt int64
|
||||
expiresAt int64
|
||||
expectErrString string
|
||||
}{
|
||||
{
|
||||
name: "Invalid Private Key",
|
||||
payload: []byte("test payload"),
|
||||
privateKey: "invalid_key",
|
||||
createdAt: time.Now().Unix(),
|
||||
expiresAt: time.Now().Unix() + 3600,
|
||||
expectErrString: "error decoding private key",
|
||||
},
|
||||
{
|
||||
name: "Short Private Key",
|
||||
payload: []byte("test payload"),
|
||||
privateKey: base64.StdEncoding.EncodeToString([]byte("short_key")),
|
||||
createdAt: time.Now().Unix(),
|
||||
expiresAt: time.Now().Unix() + 3600,
|
||||
expectErrString: "invalid private key length",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range failureTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := signer.Sign(context.Background(), tt.payload, tt.privateKey, tt.createdAt, tt.expiresAt)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but got none")
|
||||
} else if !strings.Contains(err.Error(), tt.expectErrString) {
|
||||
t.Errorf("expected error message to contain %q, got %v", tt.expectErrString, err)
|
||||
}
|
||||
if close != nil {
|
||||
if err := close(); err != nil {
|
||||
t.Fatalf("Cleanup function returned an error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user