From 8a2ef60d340b46588c6a164d7a17b8882e78caae Mon Sep 17 00:00:00 2001 From: jK <33685667+jithinkunjachan@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:23:08 +0200 Subject: [PATCH 1/4] feat: add AES-256 secret key generation with secure memory Signed-off-by: jK <33685667+jithinkunjachan@users.noreply.github.com> --- internal/cryptor/secretgen.go | 89 +++++++++++++++++++++++++++ internal/cryptor/secretgen_test.go | 96 ++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 internal/cryptor/secretgen.go create mode 100644 internal/cryptor/secretgen_test.go diff --git a/internal/cryptor/secretgen.go b/internal/cryptor/secretgen.go new file mode 100644 index 0000000..fc3948b --- /dev/null +++ b/internal/cryptor/secretgen.go @@ -0,0 +1,89 @@ +package cryptor + +import ( + "context" + "crypto/rand" + "errors" + "fmt" + + "github.com/openkcm/krypton/internal/securemem" +) + +// GenerateSecretRequest specifies the algorithm and logical name for a new secret key. +type GenerateSecretRequest struct { + Algorithm KeyAlgorithm + Name string +} + +// GenerateSecretResponse holds the generated secret stored in secure (mlock'd) memory. +// The caller is responsible for calling Secret.Destroy() when the key is no longer needed. +type GenerateSecretResponse struct { + Secret *securemem.Data +} + +// SecretGenerator generates cryptographic secret keys into secure memory. +type SecretGenerator interface { + GenerateSecret(ctx context.Context, req GenerateSecretRequest) (*GenerateSecretResponse, error) +} + +// ErrSecretGenRequest indicates that the secret generation request is invalid, +// such as unsupported algorithm or missing name. +var ErrSecretGenRequest = errors.New("invalid secret generation request") + +// ErrAllocatedSecretNotFound indicates that the secret key generated and allocated in the vault cannot be found. +var ErrAllocatedSecretNotFound = errors.New("allocated secret not found in vault") + +// AES256SecretGenerator generates 256-bit AES keys using crypto/rand. +// The generated key material is stored in mlock'd memory and never touches the Go heap. +type AES256SecretGenerator struct{} + +var _ SecretGenerator = &AES256SecretGenerator{} + +// NewAES256SecretGenerator returns a ready-to-use secret generator. +func NewAES256SecretGenerator() *AES256SecretGenerator { + return &AES256SecretGenerator{} +} + +// GenerateSecret generates a new AES-256 secret key and stores it in mlock'd memory. +// Returns ErrSecretGenRequest if Algorithm is not KeyAlgorithmAES256 or Name is empty. +// The caller must call resp.Secret.Destroy() when the key is no longer needed. +func (a *AES256SecretGenerator) GenerateSecret(ctx context.Context, req GenerateSecretRequest) (*GenerateSecretResponse, error) { + if req.Algorithm != KeyAlgorithmAES256 { + return nil, fmt.Errorf("unsupported key algorithm: %w", ErrSecretGenRequest) + } + + if req.Name == "" { + return nil, fmt.Errorf("name is empty: %w", ErrSecretGenRequest) + } + + resp, err := securemem.Run(ctx, func(ctx context.Context, hReq *securemem.HandlerRequest) error { + b, err := hReq.PersistentVault().Reserve(req.Name, 32) + if err != nil { + return fmt.Errorf("failed to allocate new securemem bytes: %w", err) + } + + _, err = rand.Read(b) + if err != nil { + return fmt.Errorf("failed to generate random bytes: %w", err) + } + + return nil + }) + if err != nil { + return nil, err + } + + secret, ok := resp.MemVault().Get(req.Name) + if !ok { + // This should never happen since we just reserved this memory, but if it does, destroy all vault data to be safe. + err = resp.MemVault().DestroyAll() + if err != nil { + return nil, err + } + return nil, fmt.Errorf("allocated secret not found in vault after generation: %w", ErrAllocatedSecretNotFound) + } + + return &GenerateSecretResponse{ + Secret: secret, + }, nil +} diff --git a/internal/cryptor/secretgen_test.go b/internal/cryptor/secretgen_test.go new file mode 100644 index 0000000..e378b24 --- /dev/null +++ b/internal/cryptor/secretgen_test.go @@ -0,0 +1,96 @@ +package cryptor_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/openkcm/krypton/internal/cryptor" +) + +func TestAES256SecretGen(t *testing.T) { + // given + ctx := t.Context() + subj := cryptor.NewAES256SecretGenerator() + + t.Run("should return error", func(t *testing.T) { + // given + tts := []struct { + name string + request cryptor.GenerateSecretRequest + }{ + { + name: "if algorithm is not supported", + request: cryptor.GenerateSecretRequest{ + Algorithm: "unknown", + Name: "name", + }, + }, + { + name: "if name is empty", + request: cryptor.GenerateSecretRequest{ + Algorithm: cryptor.KeyAlgorithmAES256, + Name: "", + }, + }, + } + + for _, tt := range tts { + t.Run(tt.name, func(t *testing.T) { + // when + res, err := subj.GenerateSecret(ctx, tt.request) + + // then + assert.Error(t, err) + assert.ErrorIs(t, err, cryptor.ErrSecretGenRequest) + assert.Nil(t, res) + }) + } + }) + + t.Run("should generate secret successfully", func(t *testing.T) { + // when + res, err := subj.GenerateSecret(ctx, cryptor.GenerateSecretRequest{ + Name: "secret-key", + Algorithm: cryptor.KeyAlgorithmAES256, + }) + t.Cleanup(func() { + if res != nil { + _ = res.Secret.Destroy() + } + }) + + // then + assert.NoError(t, err) + require.NotNil(t, res) + assert.Len(t, res.Secret.SecureBytes(), 32) + assert.Equal(t, "secret-key", res.Secret.Name()) + }) + + t.Run("should generate secret randomly", func(t *testing.T) { + foundKeys := make(map[string]struct{}) + + for range 1000 { + // when + res, err := subj.GenerateSecret(ctx, cryptor.GenerateSecretRequest{ + Name: "secret", + Algorithm: cryptor.KeyAlgorithmAES256, + }) + t.Cleanup(func() { + if res != nil { + _ = res.Secret.Destroy() + } + }) + + // then + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, "secret", res.Secret.Name()) + + key := string(res.Secret.SecureBytes()) + assert.NotContains(t, foundKeys, key) + foundKeys[key] = struct{}{} + } + }) +} From d8e403e631b69ff155703e8855c6a8cdc3c656ab Mon Sep 17 00:00:00 2001 From: jK <33685667+jithinkunjachan@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:29:55 +0200 Subject: [PATCH 2/4] docs: add godoc comments to AES-256-GCM cryptor exports Signed-off-by: jK <33685667+jithinkunjachan@users.noreply.github.com> --- internal/cryptor/aes256gcm.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/cryptor/aes256gcm.go b/internal/cryptor/aes256gcm.go index 22b8a40..7560d5e 100644 --- a/internal/cryptor/aes256gcm.go +++ b/internal/cryptor/aes256gcm.go @@ -12,18 +12,26 @@ import ( ) const ( - plainTextKey = "plainText" + // plainTextKey is the vault key used to store decrypted plaintext in secure memory. + plainTextKey = "plainText" + // cipherTextKey is the vault key used to store encrypted ciphertext in secure memory. cipherTextKey = "cipherText" ) +// AES256GCM implements the Cryptor interface using AES-256 in Galois/Counter Mode. +// All key material and intermediate plaintext/ciphertext are handled in mlock'd +// memory via securemem to prevent leakage into swap or core dumps. type AES256GCM struct { info Info } var _ Cryptor = &AES256GCM{} +// ErrAllocatedDataNotFound indicates that data reserved in the secure memory vault +// could not be retrieved after the cryptographic operation completed. var ErrAllocatedDataNotFound = errors.New("allocated data not found in vault") +// NewAES256GCM returns a ready-to-use AES-256-GCM cryptor. func NewAES256GCM() *AES256GCM { return &AES256GCM{ info: Info{ From 4395963d301f6d8c04dd97cfccc5ca3b3d77d761 Mon Sep 17 00:00:00 2001 From: jK <33685667+jithinkunjachan@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:50:44 +0200 Subject: [PATCH 3/4] refactor: remove request Signed-off-by: jK <33685667+jithinkunjachan@users.noreply.github.com> --- internal/cryptor/secretgen.go | 27 +++------------- internal/cryptor/secretgen_test.go | 49 +++--------------------------- 2 files changed, 9 insertions(+), 67 deletions(-) diff --git a/internal/cryptor/secretgen.go b/internal/cryptor/secretgen.go index fc3948b..3556583 100644 --- a/internal/cryptor/secretgen.go +++ b/internal/cryptor/secretgen.go @@ -9,11 +9,7 @@ import ( "github.com/openkcm/krypton/internal/securemem" ) -// GenerateSecretRequest specifies the algorithm and logical name for a new secret key. -type GenerateSecretRequest struct { - Algorithm KeyAlgorithm - Name string -} +const secretKey = "secretKey" // GenerateSecretResponse holds the generated secret stored in secure (mlock'd) memory. // The caller is responsible for calling Secret.Destroy() when the key is no longer needed. @@ -23,13 +19,9 @@ type GenerateSecretResponse struct { // SecretGenerator generates cryptographic secret keys into secure memory. type SecretGenerator interface { - GenerateSecret(ctx context.Context, req GenerateSecretRequest) (*GenerateSecretResponse, error) + GenerateSecret(ctx context.Context) (*GenerateSecretResponse, error) } -// ErrSecretGenRequest indicates that the secret generation request is invalid, -// such as unsupported algorithm or missing name. -var ErrSecretGenRequest = errors.New("invalid secret generation request") - // ErrAllocatedSecretNotFound indicates that the secret key generated and allocated in the vault cannot be found. var ErrAllocatedSecretNotFound = errors.New("allocated secret not found in vault") @@ -45,19 +37,10 @@ func NewAES256SecretGenerator() *AES256SecretGenerator { } // GenerateSecret generates a new AES-256 secret key and stores it in mlock'd memory. -// Returns ErrSecretGenRequest if Algorithm is not KeyAlgorithmAES256 or Name is empty. // The caller must call resp.Secret.Destroy() when the key is no longer needed. -func (a *AES256SecretGenerator) GenerateSecret(ctx context.Context, req GenerateSecretRequest) (*GenerateSecretResponse, error) { - if req.Algorithm != KeyAlgorithmAES256 { - return nil, fmt.Errorf("unsupported key algorithm: %w", ErrSecretGenRequest) - } - - if req.Name == "" { - return nil, fmt.Errorf("name is empty: %w", ErrSecretGenRequest) - } - +func (a *AES256SecretGenerator) GenerateSecret(ctx context.Context) (*GenerateSecretResponse, error) { resp, err := securemem.Run(ctx, func(ctx context.Context, hReq *securemem.HandlerRequest) error { - b, err := hReq.PersistentVault().Reserve(req.Name, 32) + b, err := hReq.PersistentVault().Reserve(secretKey, 32) if err != nil { return fmt.Errorf("failed to allocate new securemem bytes: %w", err) } @@ -73,7 +56,7 @@ func (a *AES256SecretGenerator) GenerateSecret(ctx context.Context, req Generate return nil, err } - secret, ok := resp.MemVault().Get(req.Name) + secret, ok := resp.MemVault().Get(secretKey) if !ok { // This should never happen since we just reserved this memory, but if it does, destroy all vault data to be safe. err = resp.MemVault().DestroyAll() diff --git a/internal/cryptor/secretgen_test.go b/internal/cryptor/secretgen_test.go index e378b24..8a155fb 100644 --- a/internal/cryptor/secretgen_test.go +++ b/internal/cryptor/secretgen_test.go @@ -14,47 +14,9 @@ func TestAES256SecretGen(t *testing.T) { ctx := t.Context() subj := cryptor.NewAES256SecretGenerator() - t.Run("should return error", func(t *testing.T) { - // given - tts := []struct { - name string - request cryptor.GenerateSecretRequest - }{ - { - name: "if algorithm is not supported", - request: cryptor.GenerateSecretRequest{ - Algorithm: "unknown", - Name: "name", - }, - }, - { - name: "if name is empty", - request: cryptor.GenerateSecretRequest{ - Algorithm: cryptor.KeyAlgorithmAES256, - Name: "", - }, - }, - } - - for _, tt := range tts { - t.Run(tt.name, func(t *testing.T) { - // when - res, err := subj.GenerateSecret(ctx, tt.request) - - // then - assert.Error(t, err) - assert.ErrorIs(t, err, cryptor.ErrSecretGenRequest) - assert.Nil(t, res) - }) - } - }) - t.Run("should generate secret successfully", func(t *testing.T) { // when - res, err := subj.GenerateSecret(ctx, cryptor.GenerateSecretRequest{ - Name: "secret-key", - Algorithm: cryptor.KeyAlgorithmAES256, - }) + res, err := subj.GenerateSecret(ctx) t.Cleanup(func() { if res != nil { _ = res.Secret.Destroy() @@ -65,7 +27,7 @@ func TestAES256SecretGen(t *testing.T) { assert.NoError(t, err) require.NotNil(t, res) assert.Len(t, res.Secret.SecureBytes(), 32) - assert.Equal(t, "secret-key", res.Secret.Name()) + assert.Equal(t, "secretKey", res.Secret.Name()) }) t.Run("should generate secret randomly", func(t *testing.T) { @@ -73,10 +35,7 @@ func TestAES256SecretGen(t *testing.T) { for range 1000 { // when - res, err := subj.GenerateSecret(ctx, cryptor.GenerateSecretRequest{ - Name: "secret", - Algorithm: cryptor.KeyAlgorithmAES256, - }) + res, err := subj.GenerateSecret(ctx) t.Cleanup(func() { if res != nil { _ = res.Secret.Destroy() @@ -86,7 +45,7 @@ func TestAES256SecretGen(t *testing.T) { // then assert.NoError(t, err) require.NotNil(t, res) - assert.Equal(t, "secret", res.Secret.Name()) + assert.Equal(t, "secretKey", res.Secret.Name()) key := string(res.Secret.SecureBytes()) assert.NotContains(t, foundKeys, key) From 8415db555b38b7d80bded4b0b8a5dac83caae51e Mon Sep 17 00:00:00 2001 From: jK <33685667+jithinkunjachan@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:33:38 +0200 Subject: [PATCH 4/4] remove error check Signed-off-by: jK <33685667+jithinkunjachan@users.noreply.github.com> --- internal/cryptor/secretgen.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/cryptor/secretgen.go b/internal/cryptor/secretgen.go index 3556583..2bc07ed 100644 --- a/internal/cryptor/secretgen.go +++ b/internal/cryptor/secretgen.go @@ -45,10 +45,9 @@ func (a *AES256SecretGenerator) GenerateSecret(ctx context.Context) (*GenerateSe return fmt.Errorf("failed to allocate new securemem bytes: %w", err) } - _, err = rand.Read(b) - if err != nil { - return fmt.Errorf("failed to generate random bytes: %w", err) - } + // ignoring the error because crypto/rand.Read always returns len(b), nil; + // it never returns an error (it panics on catastrophic system entropy failure). + rand.Read(b) return nil })