diff --git a/internal/cryptor/cryptor.go b/internal/cryptor/cryptor.go new file mode 100644 index 0000000..5043c23 --- /dev/null +++ b/internal/cryptor/cryptor.go @@ -0,0 +1,123 @@ +// Package cryptor defines cryptographic interfaces for multi-tenant encrypt/decrypt operations. +// Sensitive data is handled via securemem.Data to prevent leakage into swap or core dumps. +package cryptor + +import ( + "context" + "errors" + "fmt" + + "github.com/openkcm/krypton/internal/securemem" + "github.com/openkcm/krypton/internal/spec" +) + +// EncryptRequest contains parameters for an encryption operation. +type EncryptRequest struct { + // TenantID identifies the tenant owning the key. + TenantID string + // KeyID is the unique identifier of the key. + KeyID string + // KeyVersion specifies which version of the key to use. + KeyVersion int + // Algorithm is the encryption algorithm to apply. + Algorithm spec.KeyAlgorithm + // Secret is the key material used for encryption. + // The Secret is nil if Cryptor manages its own secrets (e.g., HSM). + Secret *securemem.Data + // Plaintext is the data to encrypt. + // The Plaintext field should not be nil. + Plaintext *securemem.Data + // AAD is optional authenticated data bound to the ciphertext without being encrypted. + AAD []byte +} + +// EncryptResponse holds the result of an encryption operation. +type EncryptResponse struct { + // Ciphertext is the encrypted output. + Ciphertext *securemem.Data +} + +// DecryptRequest contains parameters for a decryption operation. +type DecryptRequest struct { + // TenantID identifies the tenant owning the key. + TenantID string + // KeyID is the unique identifier of the key. + KeyID string + // KeyVersion specifies which version of the key to use. + KeyVersion int + // Algorithm is the encryption algorithm that was used. + Algorithm spec.KeyAlgorithm + // Secret is the key material used for decryption. + // The Secret is nil if Cryptor manages its own secrets (e.g., HSM). + Secret *securemem.Data + // Ciphertext is the data to decrypt. + // The Ciphertext field should not be nil. + Ciphertext *securemem.Data + // AAD is the authenticated data that must match the value provided at encryption time. + AAD []byte +} + +// DecryptResponse holds the result of a decryption operation. +type DecryptResponse struct { + // Plaintext is the recovered data. + Plaintext *securemem.Data +} + +// Info describes a Cryptor implementation's capabilities. +type Info struct { + // Name is a human-readable identifier for the implementation. + Name string + // DecryptionSecretRequired is false if cryptor manages its own secret (e.g., HSM). + DecryptionSecretRequired bool +} + +// Encryptor performs encryption. +type Encryptor interface { + Encrypt(ctx context.Context, req EncryptRequest) (*EncryptResponse, error) +} + +// Decryptor performs decryption. +type Decryptor interface { + Decrypt(ctx context.Context, req DecryptRequest) (*DecryptResponse, error) +} + +// Cryptor combines Encryptor and Decryptor with metadata introspection. +type Cryptor interface { + Encryptor + Decryptor + Info() Info +} + +var ErrRequest = errors.New("invalid cryptographic request") + +func (req EncryptRequest) Validate() error { + if req.TenantID == "" { + return fmt.Errorf("invalid tenant ID: %w", ErrRequest) + } + if req.KeyID == "" { + return fmt.Errorf("invalid key ID: %w", ErrRequest) + } + if req.KeyVersion == 0 { + return fmt.Errorf("invalid key version: %w", ErrRequest) + } + if req.Plaintext == nil { + return fmt.Errorf("invalid plaintext: %w", ErrRequest) + } + return nil +} + +func (req DecryptRequest) Validate() error { + if req.TenantID == "" { + return fmt.Errorf("invalid tenant ID: %w", ErrRequest) + } + if req.KeyID == "" { + return fmt.Errorf("invalid key ID: %w", ErrRequest) + } + if req.KeyVersion == 0 { + return fmt.Errorf("invalid key version: %w", ErrRequest) + } + if req.Ciphertext == nil { + return fmt.Errorf("invalid ciphertext: %w", ErrRequest) + } + return nil +} diff --git a/internal/cryptor/cryptor_test.go b/internal/cryptor/cryptor_test.go new file mode 100644 index 0000000..871edaf --- /dev/null +++ b/internal/cryptor/cryptor_test.go @@ -0,0 +1,164 @@ +package cryptor_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/openkcm/krypton/internal/cryptor" + "github.com/openkcm/krypton/internal/securemem" +) + +func TestDecryptRequestValidate(t *testing.T) { + // given + tts := []struct { + name string + req cryptor.DecryptRequest + wantErr error + }{ + { + name: "valid request", + req: cryptor.DecryptRequest{ + TenantID: "tenant1", + KeyID: "key1", + KeyVersion: 1, + Ciphertext: &securemem.Data{}, + }, + wantErr: nil, + }, + { + name: "valid request with negative key version", + req: cryptor.DecryptRequest{ + TenantID: "tenant1", + KeyID: "key1", + KeyVersion: -1, + Ciphertext: &securemem.Data{}, + }, + wantErr: nil, + }, + { + name: "missing tenant ID", + req: cryptor.DecryptRequest{ + KeyID: "key1", + KeyVersion: 1, + Ciphertext: &securemem.Data{}, + }, + wantErr: cryptor.ErrRequest, + }, + { + name: "missing key ID", + req: cryptor.DecryptRequest{ + TenantID: "tenant1", + KeyVersion: 1, + Ciphertext: &securemem.Data{}, + }, + wantErr: cryptor.ErrRequest, + }, + { + name: "invalid key version", + req: cryptor.DecryptRequest{ + TenantID: "tenant1", + KeyID: "key1", + KeyVersion: 0, + Ciphertext: &securemem.Data{}, + }, + wantErr: cryptor.ErrRequest, + }, + { + name: "missing ciphertext", + req: cryptor.DecryptRequest{ + TenantID: "tenant1", + KeyID: "key1", + KeyVersion: 1, + }, + wantErr: cryptor.ErrRequest, + }, + } + + for _, tt := range tts { + t.Run(tt.name, func(t *testing.T) { + // when + err := tt.req.Validate() + + // then + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestEncryptRequestValidate(t *testing.T) { + // given + tts := []struct { + name string + req cryptor.EncryptRequest + wantErr error + }{ + { + name: "valid request", + req: cryptor.EncryptRequest{ + TenantID: "tenant1", + KeyID: "key1", + KeyVersion: 1, + Plaintext: &securemem.Data{}, + }, + wantErr: nil, + }, + { + name: "valid request with negative key version", + req: cryptor.EncryptRequest{ + TenantID: "tenant1", + KeyID: "key1", + KeyVersion: -1, + Plaintext: &securemem.Data{}, + }, + wantErr: nil, + }, + { + name: "missing tenant ID", + req: cryptor.EncryptRequest{ + KeyID: "key1", + KeyVersion: 1, + Plaintext: &securemem.Data{}, + }, + wantErr: cryptor.ErrRequest, + }, + { + name: "missing key ID", + req: cryptor.EncryptRequest{ + TenantID: "tenant1", + KeyVersion: 1, + Plaintext: &securemem.Data{}, + }, + wantErr: cryptor.ErrRequest, + }, + { + name: "invalid key version", + req: cryptor.EncryptRequest{ + TenantID: "tenant1", + KeyID: "key1", + KeyVersion: 0, + Plaintext: &securemem.Data{}, + }, + wantErr: cryptor.ErrRequest, + }, + { + name: "missing plaintext", + req: cryptor.EncryptRequest{ + TenantID: "tenant1", + KeyID: "key1", + KeyVersion: 1, + }, + wantErr: cryptor.ErrRequest, + }, + } + + for _, tt := range tts { + t.Run(tt.name, func(t *testing.T) { + // when + err := tt.req.Validate() + + // then + assert.ErrorIs(t, err, tt.wantErr) + }) + } +}