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{ diff --git a/internal/cryptor/secretgen.go b/internal/cryptor/secretgen.go new file mode 100644 index 0000000..2bc07ed --- /dev/null +++ b/internal/cryptor/secretgen.go @@ -0,0 +1,71 @@ +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) + } + + // 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 + }) + 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 +} diff --git a/internal/cryptor/secretgen_test.go b/internal/cryptor/secretgen_test.go new file mode 100644 index 0000000..8a155fb --- /dev/null +++ b/internal/cryptor/secretgen_test.go @@ -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{}{} + } + }) +}