Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions cmd/app/commands/master_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"log/slog"
"time"

cryptoService "github.com/allisson/secrets/internal/crypto/service"
cryptoDomain "github.com/allisson/secrets/internal/crypto/domain"
)

// RunCreateMasterKey generates a cryptographically secure 32-byte master key for envelope encryption.
Expand All @@ -27,7 +27,7 @@ import (
// Security: Never use localsecrets provider in production. Use cloud KMS providers (gcpkms, awskms, azurekeyvault).
func RunCreateMasterKey(
ctx context.Context,
kmsService cryptoService.KMSService,
kmsService cryptoDomain.KMSService,
logger *slog.Logger,
writer io.Writer,
keyID string,
Expand Down
4 changes: 2 additions & 2 deletions cmd/app/commands/rotate_master_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import (
"strings"
"time"

cryptoService "github.com/allisson/secrets/internal/crypto/service"
cryptoDomain "github.com/allisson/secrets/internal/crypto/domain"
)

func RunRotateMasterKey(
ctx context.Context,
kmsService cryptoService.KMSService,
kmsService cryptoDomain.KMSService,
logger *slog.Logger,
writer io.Writer,
keyID, kmsProvider, kmsKeyURI, existingMasterKeys, existingActiveKeyID string,
Expand Down
2 changes: 1 addition & 1 deletion internal/app/di.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ type Container struct {
// Services
aeadManager cryptoService.AEADManager
keyManager cryptoService.KeyManager
kmsService cryptoService.KMSService
kmsService cryptoDomain.KMSService
secretService authService.SecretService
tokenService authService.TokenService

Expand Down
4 changes: 2 additions & 2 deletions internal/app/di_crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func (c *Container) KeyManager() cryptoService.KeyManager {
}

// KMSService returns the KMS service.
func (c *Container) KMSService() cryptoService.KMSService {
func (c *Container) KMSService() cryptoDomain.KMSService {
c.kmsServiceInit.Do(func() {
c.kmsService = c.initKMSService()
})
Expand Down Expand Up @@ -156,7 +156,7 @@ func (c *Container) initKeyManager() cryptoService.KeyManager {
}

// initKMSService creates the KMS service for encrypting/decrypting master keys.
func (c *Container) initKMSService() cryptoService.KMSService {
func (c *Container) initKMSService() cryptoDomain.KMSService {
return cryptoService.NewKMSService()
}

Expand Down
8 changes: 6 additions & 2 deletions internal/crypto/domain/master_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,12 @@ func loadMasterKeyChainFromKMS(
)

// Make a copy of the key data before storing to prevent issues if the underlying
// slice is reused. The original 'key' slice ownership is transferred to the keychain.
mkc.keys.Store(id, &MasterKey{ID: id, Key: key})
// slice is reused. The original 'key' slice from KMS is zeroed after copying.
keyCopy := make([]byte, len(key))
copy(keyCopy, key)
Zero(key)

mkc.keys.Store(id, &MasterKey{ID: id, Key: keyCopy})
}

if _, ok := mkc.Get(active); !ok {
Expand Down
9 changes: 8 additions & 1 deletion internal/crypto/service/key_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func (km *KeyManagerService) CreateKek(
if _, err := rand.Read(kekKey); err != nil {
return cryptoDomain.Kek{}, fmt.Errorf("failed to generate KEK: %w", err)
}
defer cryptoDomain.Zero(kekKey)

// Create cipher using AEADManager
aead, err := km.aeadManager.CreateCipher(masterKey.Key, alg)
Expand All @@ -46,12 +47,17 @@ func (km *KeyManagerService) CreateKek(
return cryptoDomain.Kek{}, fmt.Errorf("failed to encrypt KEK: %w", err)
}

// The plaintext KEK is included in the returned struct for in-memory use (e.g. initial setup)
// but the local variable kekKey is zeroed upon return for security.
keyCopy := make([]byte, len(kekKey))
copy(keyCopy, kekKey)

kek := cryptoDomain.Kek{
ID: uuid.Must(uuid.NewV7()),
MasterKeyID: masterKey.ID,
Algorithm: alg,
EncryptedKey: encryptedKey,
Key: kekKey,
Key: keyCopy,
Nonce: nonce,
Version: 1,
CreatedAt: time.Now().UTC(),
Expand Down Expand Up @@ -92,6 +98,7 @@ func (km *KeyManagerService) CreateDek(
if _, err := rand.Read(dekKey); err != nil {
return cryptoDomain.Dek{}, fmt.Errorf("failed to generate DEK: %w", err)
}
defer cryptoDomain.Zero(dekKey)

// Create cipher using AEADManager with KEK's algorithm
aead, err := km.aeadManager.CreateCipher(kek.Key, kek.Algorithm)
Expand Down
15 changes: 4 additions & 11 deletions internal/crypto/service/kms_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,14 @@ import (
_ "gocloud.dev/secrets/localsecrets"
)

// KMSService implements domain.KMSService for KMS operations using gocloud.dev/secrets.
type KMSService interface {
// OpenKeeper opens a secrets.Keeper for the configured KMS provider.
// Returns an error if the KMS provider URI is invalid or connection fails.
OpenKeeper(ctx context.Context, keyURI string) (cryptoDomain.KMSKeeper, error)
}

// kmsService implements KMSService using gocloud.dev/secrets.
type kmsService struct{}

// NewKMSService creates a new KMS service instance.
func NewKMSService() KMSService {
func NewKMSService() cryptoDomain.KMSService {
return &kmsService{}
}

// kmsService implements domain.KMSService using gocloud.dev/secrets.
type kmsService struct{}

// OpenKeeper opens a secrets.Keeper for the configured KMS provider using the keyURI.
// Supports: gcpkms://, awskms://, azurekeyvault://, hashivault://, base64key://
// Returns a KMSKeeper which *secrets.Keeper implements.
Expand Down
90 changes: 48 additions & 42 deletions internal/crypto/usecase/dek_usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,66 +20,72 @@ type dekUseCase struct {

// Rewrap finds DEKs that are not encrypted with the specified KEK ID,
// decrypts them using their old KEKs, and re-encrypts them with the new KEK.
// Returns the number of DEKs rewrapped in this batch.
// Returns the number of DEKs rewrapped in this batch. Executes in a transaction.
func (d *dekUseCase) Rewrap(
ctx context.Context,
kekChain *cryptoDomain.KekChain,
newKekID uuid.UUID,
batchSize int,
) (int, error) {
// 1. Fetch batch of DEKs not using the new KEK ID
deks, err := d.dekRepo.GetBatchNotKekID(ctx, newKekID, batchSize)
if err != nil {
return 0, err
}
var rewrappedCount int

if len(deks) == 0 {
return 0, nil
}
err := d.txManager.WithTx(ctx, func(ctx context.Context) error {
// 1. Fetch batch of DEKs not using the new KEK ID
deks, err := d.dekRepo.GetBatchNotKekID(ctx, newKekID, batchSize)
if err != nil {
return err
}

// 2. Get the new KEK from the chain
newKek, ok := kekChain.Get(newKekID)
if !ok {
return 0, cryptoDomain.ErrKekNotFound
}
if newKek.Key == nil {
return 0, cryptoDomain.ErrDecryptionFailed // or another appropriate error indicating unwrapped KEK is needed
}
if len(deks) == 0 {
return nil
}

// 3. Process each DEK in the batch
for _, dek := range deks {
// Get the old KEK
oldKek, ok := kekChain.Get(dek.KekID)
// 2. Get the new KEK from the chain
newKek, ok := kekChain.Get(newKekID)
if !ok {
return 0, cryptoDomain.ErrKekNotFound
return cryptoDomain.ErrKekNotFound
}

// Decrypt the DEK plaintext key using the old KEK
dekKey, err := d.keyManager.DecryptDek(dek, oldKek)
if err != nil {
return 0, err
if newKek.Key == nil {
return cryptoDomain.ErrDecryptionFailed
}

// Encrypt the DEK plaintext key using the new KEK
encryptedKey, nonce, err := d.keyManager.EncryptDek(dekKey, newKek)
if err != nil {
// 3. Process each DEK in the batch
for _, dek := range deks {
// Get the old KEK
oldKek, ok := kekChain.Get(dek.KekID)
if !ok {
return cryptoDomain.ErrKekNotFound
}

// Decrypt the DEK plaintext key using the old KEK
dekKey, err := d.keyManager.DecryptDek(dek, oldKek)
if err != nil {
return err
}

// Encrypt the DEK plaintext key using the new KEK
encryptedKey, nonce, err := d.keyManager.EncryptDek(dekKey, newKek)
cryptoDomain.Zero(dekKey)
return 0, err
}
cryptoDomain.Zero(dekKey)
if err != nil {
return err
}

// Update DEK entity
dek.KekID = newKekID
dek.EncryptedKey = encryptedKey
dek.Nonce = nonce
// Update DEK entity
dek.KekID = newKekID
dek.EncryptedKey = encryptedKey
dek.Nonce = nonce

// Save updated DEK
if err := d.dekRepo.Update(ctx, dek); err != nil {
return 0, err
// Save updated DEK
if err := d.dekRepo.Update(ctx, dek); err != nil {
return err
}
}
}

return len(deks), nil
rewrappedCount = len(deks)
return nil
})

return rewrappedCount, err
}

// NewDekUseCase creates a new DekUseCase instance.
Expand Down
Loading
Loading