Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion internal/cryptor/aes256gcm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
72 changes: 72 additions & 0 deletions internal/cryptor/secretgen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package cryptor

import (
"context"
"crypto/rand"
"errors"
"fmt"

"github.com/openkcm/krypton/internal/securemem"
)

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.
type GenerateSecretResponse struct {
Secret *securemem.Data
}

// SecretGenerator generates cryptographic secret keys into secure memory.
type SecretGenerator interface {
GenerateSecret(ctx context.Context) (*GenerateSecretResponse, error)
}

// 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.
// The caller must call resp.Secret.Destroy() when the key is no longer needed.
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(secretKey, 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)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically rand.Read never returns an error. The signature is chosen like this only to satisfy io.Reader

https://pkg.go.dev/crypto/rand#Read

So we could make it shorter and add a comment about the no-error behaviour


return nil
})
if err != nil {
return nil, err
}

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()
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
}
55 changes: 55 additions & 0 deletions internal/cryptor/secretgen_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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 generate secret successfully", func(t *testing.T) {
// when
res, err := subj.GenerateSecret(ctx)
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, "secretKey", 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)
t.Cleanup(func() {
if res != nil {
_ = res.Secret.Destroy()
}
})

// then
assert.NoError(t, err)
require.NotNil(t, res)
assert.Equal(t, "secretKey", res.Secret.Name())

key := string(res.Secret.SecureBytes())
assert.NotContains(t, foundKeys, key)
foundKeys[key] = struct{}{}
}
})
}
Loading