mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
369 lines
11 KiB
Go
369 lines
11 KiB
Go
// Package keeper provides vault message handlers with consensus-based encryption
|
|
package keeper
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"cosmossdk.io/errors"
|
|
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
apiv1 "github.com/sonr-io/sonr/api/dwn/v1"
|
|
"github.com/sonr-io/sonr/x/dwn/types"
|
|
)
|
|
|
|
// RotateVaultKeys rotates encryption keys for existing vaults
|
|
func (ms msgServer) RotateVaultKeys(
|
|
ctx context.Context,
|
|
msg *types.MsgRotateVaultKeys,
|
|
) (*types.MsgRotateVaultKeysResponse, error) {
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
if err := msg.ValidateBasic(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Verify authority (only governance or validators can rotate keys)
|
|
if ms.k.authority != msg.Authority {
|
|
return nil, errors.Wrapf(
|
|
types.ErrInvalidAuthorityFormat,
|
|
"invalid authority; expected %s, got %s",
|
|
ms.k.authority,
|
|
msg.Authority,
|
|
)
|
|
}
|
|
|
|
// Check if key rotation is needed (unless forced)
|
|
if !msg.Force {
|
|
rotationDue := ms.k.encryptionSubkeeper.IsRotationDue(sdkCtx)
|
|
if !rotationDue {
|
|
return nil, errors.Wrap(
|
|
types.ErrInvalidRequest,
|
|
"key rotation not due (use force=true to override)",
|
|
)
|
|
}
|
|
}
|
|
|
|
var vaultsRotated uint32 = 0
|
|
|
|
if msg.VaultId != "" {
|
|
// Rotate keys for specific vault
|
|
vault, err := ms.k.OrmDB.VaultStateTable().Get(sdkCtx, msg.VaultId)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(
|
|
types.ErrVaultNotFound,
|
|
"vault %s not found",
|
|
msg.VaultId,
|
|
)
|
|
}
|
|
|
|
// Re-encrypt vault data with new consensus key
|
|
err = ms.rotateVaultKeys(sdkCtx, vault, msg.Reason)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to rotate keys for vault %s", msg.VaultId)
|
|
}
|
|
|
|
vaultsRotated = 1
|
|
} else {
|
|
// Rotate keys for all vaults
|
|
iter, err := ms.k.OrmDB.VaultStateTable().List(sdkCtx, nil)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to list vaults for rotation")
|
|
}
|
|
defer iter.Close()
|
|
|
|
for iter.Next() {
|
|
vault, err := iter.Value()
|
|
if err != nil {
|
|
ms.k.Logger().Error("Failed to get vault during rotation", "error", err)
|
|
continue
|
|
}
|
|
|
|
err = ms.rotateVaultKeys(sdkCtx, vault, msg.Reason)
|
|
if err != nil {
|
|
ms.k.Logger().Error("Failed to rotate vault keys",
|
|
"vault_id", vault.VaultId,
|
|
"error", err,
|
|
)
|
|
continue
|
|
}
|
|
|
|
vaultsRotated++
|
|
}
|
|
}
|
|
|
|
// Perform global key rotation
|
|
err := ms.k.encryptionSubkeeper.InitiateKeyRotation(sdkCtx, msg.Reason)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to initiate global key rotation")
|
|
}
|
|
|
|
// Get the new key version after rotation
|
|
newKeyVersion := ms.k.encryptionSubkeeper.GetCurrentKeyVersion(sdkCtx)
|
|
|
|
ms.k.Logger().Info("Vault key rotation completed",
|
|
"vaults_rotated", vaultsRotated,
|
|
"new_key_version", newKeyVersion,
|
|
"reason", msg.Reason,
|
|
"forced", msg.Force,
|
|
)
|
|
|
|
// Emit typed event for key rotation
|
|
event := &types.EventVaultKeysRotated{
|
|
VaultId: fmt.Sprintf("global-rotation-%d", newKeyVersion),
|
|
Owner: msg.Authority,
|
|
NewPublicKey: fmt.Sprintf("key-version-%d", newKeyVersion),
|
|
RotationHeight: uint64(sdkCtx.BlockHeight()),
|
|
BlockHeight: uint64(sdkCtx.BlockHeight()),
|
|
}
|
|
|
|
if err := sdkCtx.EventManager().EmitTypedEvent(event); err != nil {
|
|
ms.k.Logger().With("error", err).Error("Failed to emit EventVaultKeysRotated")
|
|
}
|
|
|
|
return &types.MsgRotateVaultKeysResponse{
|
|
VaultsRotated: vaultsRotated,
|
|
NewKeyVersion: newKeyVersion,
|
|
Success: true,
|
|
}, nil
|
|
}
|
|
|
|
// rotateVaultKeys re-encrypts a single vault's data with new consensus keys
|
|
func (ms msgServer) rotateVaultKeys(sdkCtx sdk.Context, vault any, reason string) error {
|
|
// Type assertion to ensure we have the correct vault type
|
|
vaultState, ok := vault.(*apiv1.VaultState)
|
|
if !ok {
|
|
return errors.Wrapf(
|
|
types.ErrInvalidRequest,
|
|
"invalid vault type: expected *apiv1.VaultState, got %T",
|
|
vault,
|
|
)
|
|
}
|
|
|
|
if vaultState == nil {
|
|
return errors.Wrap(types.ErrVaultNotFound, "vault state is nil")
|
|
}
|
|
|
|
// Validate vault has encrypted data to rotate
|
|
if vaultState.EnclaveData == nil {
|
|
return errors.Wrapf(
|
|
types.ErrInvalidRequest,
|
|
"vault %s has no enclave data to rotate",
|
|
vaultState.VaultId,
|
|
)
|
|
}
|
|
|
|
ms.k.Logger().Info("Starting vault key rotation",
|
|
"vault_id", vaultState.VaultId,
|
|
"owner", vaultState.Owner,
|
|
"reason", reason,
|
|
"block_height", sdkCtx.BlockHeight(),
|
|
)
|
|
|
|
// Check if encryption subkeeper is available
|
|
if ms.k.encryptionSubkeeper == nil {
|
|
return errors.Wrap(types.ErrInvalidRequest, "encryption subkeeper not available")
|
|
}
|
|
|
|
ctx := sdk.WrapSDKContext(sdkCtx)
|
|
|
|
// Get current encryption key version before rotation
|
|
oldKeyVersion := ms.k.encryptionSubkeeper.GetCurrentKeyVersion(ctx)
|
|
|
|
// Store original values for rollback if needed
|
|
originalPrivateData := make([]byte, len(vaultState.EnclaveData.PrivateData))
|
|
copy(originalPrivateData, vaultState.EnclaveData.PrivateData)
|
|
originalVersion := vaultState.EnclaveData.Version
|
|
|
|
// Step 1: Decrypt vault's encrypted private data using old consensus keys
|
|
// We need to reconstruct the encryption metadata for the old data
|
|
oldMetadata := &types.EncryptionMetadata{
|
|
KeyVersion: oldKeyVersion,
|
|
Algorithm: "AES-GCM",
|
|
EncryptionHeight: sdkCtx.BlockHeight(),
|
|
ValidatorSet: []string{}, // Will be populated by encryptionSubkeeper
|
|
}
|
|
|
|
decryptedData, err := ms.k.encryptionSubkeeper.DecryptWithConsensusKey(
|
|
ctx,
|
|
vaultState.EnclaveData.PrivateData,
|
|
oldMetadata,
|
|
)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to decrypt vault data for vault %s", vaultState.VaultId)
|
|
}
|
|
|
|
ms.k.Logger().Debug("Successfully decrypted vault data",
|
|
"vault_id", vaultState.VaultId,
|
|
"data_size", len(decryptedData),
|
|
"old_key_version", oldKeyVersion,
|
|
)
|
|
|
|
// Step 2: Re-encrypt vault data with new consensus keys
|
|
encryptedResult, err := ms.k.encryptionSubkeeper.EncryptWithConsensusKey(
|
|
ctx,
|
|
decryptedData,
|
|
"vault.enclave/v1",
|
|
)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to re-encrypt vault data for vault %s", vaultState.VaultId)
|
|
}
|
|
|
|
// Step 3: Update vault state with new encrypted data and metadata
|
|
vaultState.EnclaveData.PrivateData = encryptedResult.Ciphertext
|
|
vaultState.EnclaveData.Version = int64(encryptedResult.Metadata.KeyVersion)
|
|
|
|
// Update timestamps
|
|
vaultState.LastRefreshed = sdkCtx.BlockTime().Unix()
|
|
|
|
// Step 4: Validate data integrity using HMAC-SHA256
|
|
if err := ms.validateVaultIntegrity(sdkCtx, vaultState, decryptedData); err != nil {
|
|
// Rollback on validation failure
|
|
vaultState.EnclaveData.PrivateData = originalPrivateData
|
|
vaultState.EnclaveData.Version = originalVersion
|
|
return errors.Wrapf(err, "vault integrity validation failed for %s", vaultState.VaultId)
|
|
}
|
|
|
|
// Step 5: Update vault state in ORM database
|
|
if err := ms.k.OrmDB.VaultStateTable().Update(ctx, vaultState); err != nil {
|
|
// Rollback on database update failure
|
|
vaultState.EnclaveData.PrivateData = originalPrivateData
|
|
vaultState.EnclaveData.Version = originalVersion
|
|
return errors.Wrapf(err, "failed to update vault state for %s", vaultState.VaultId)
|
|
}
|
|
|
|
// Step 6: Update IPFS storage with re-encrypted vault export if applicable
|
|
if ms.k.ipfsClient != nil {
|
|
if err := ms.updateVaultInIPFS(ctx, vaultState, encryptedResult.Ciphertext); err != nil {
|
|
// Log warning but don't fail the rotation - IPFS is supplementary
|
|
ms.k.Logger().Warn("Failed to update vault in IPFS",
|
|
"vault_id", vaultState.VaultId,
|
|
"error", err,
|
|
)
|
|
}
|
|
}
|
|
|
|
// Get new key version after rotation
|
|
newKeyVersion := ms.k.encryptionSubkeeper.GetCurrentKeyVersion(ctx)
|
|
|
|
// Step 7: Log rotation event with audit trail for security compliance
|
|
ms.k.Logger().Info("Vault key rotation completed successfully",
|
|
"vault_id", vaultState.VaultId,
|
|
"owner", vaultState.Owner,
|
|
"old_key_version", oldKeyVersion,
|
|
"new_key_version", newKeyVersion,
|
|
"reason", reason,
|
|
"block_height", sdkCtx.BlockHeight(),
|
|
"data_size", len(encryptedResult.Ciphertext),
|
|
)
|
|
|
|
// Emit typed event for audit trail
|
|
rotationEvent := &types.EventVaultKeysRotated{
|
|
VaultId: vaultState.VaultId,
|
|
Owner: vaultState.Owner,
|
|
NewPublicKey: fmt.Sprintf("key-version-%d", newKeyVersion),
|
|
RotationHeight: uint64(sdkCtx.BlockHeight()),
|
|
BlockHeight: uint64(sdkCtx.BlockHeight()),
|
|
}
|
|
|
|
if err := sdkCtx.EventManager().EmitTypedEvent(rotationEvent); err != nil {
|
|
ms.k.Logger().With("error", err).Error("Failed to emit vault rotation event")
|
|
}
|
|
|
|
// Clean up sensitive data from memory
|
|
for i := range decryptedData {
|
|
decryptedData[i] = 0
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateVaultIntegrity validates the integrity of vault data after key rotation
|
|
func (ms msgServer) validateVaultIntegrity(
|
|
sdkCtx sdk.Context,
|
|
vaultState *apiv1.VaultState,
|
|
originalPlaintext []byte,
|
|
) error {
|
|
ctx := sdk.WrapSDKContext(sdkCtx)
|
|
|
|
// Re-decrypt the newly encrypted data to verify it matches the original
|
|
newMetadata := &types.EncryptionMetadata{
|
|
KeyVersion: ms.k.encryptionSubkeeper.GetCurrentKeyVersion(ctx),
|
|
Algorithm: "AES-GCM",
|
|
EncryptionHeight: sdkCtx.BlockHeight(),
|
|
ValidatorSet: []string{}, // Will be populated by encryptionSubkeeper
|
|
}
|
|
|
|
reDecrypted, err := ms.k.encryptionSubkeeper.DecryptWithConsensusKey(
|
|
ctx,
|
|
vaultState.EnclaveData.PrivateData,
|
|
newMetadata,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to re-decrypt for validation: %w", err)
|
|
}
|
|
|
|
// Compare byte-by-byte to ensure data integrity
|
|
if len(reDecrypted) != len(originalPlaintext) {
|
|
return fmt.Errorf("decrypted data length mismatch: expected %d, got %d",
|
|
len(originalPlaintext), len(reDecrypted))
|
|
}
|
|
|
|
for i := range originalPlaintext {
|
|
if reDecrypted[i] != originalPlaintext[i] {
|
|
return fmt.Errorf("data integrity check failed at byte %d", i)
|
|
}
|
|
}
|
|
|
|
// Clean up sensitive validation data
|
|
for i := range reDecrypted {
|
|
reDecrypted[i] = 0
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// updateVaultInIPFS updates the vault's IPFS storage with re-encrypted data
|
|
func (ms msgServer) updateVaultInIPFS(
|
|
ctx context.Context,
|
|
vaultState *apiv1.VaultState,
|
|
reencryptedData []byte,
|
|
) error {
|
|
if ms.k.ipfsClient == nil {
|
|
return fmt.Errorf("IPFS client not available")
|
|
}
|
|
|
|
// Create a vault export structure for IPFS storage
|
|
vaultExport := map[string]any{
|
|
"vault_id": vaultState.VaultId,
|
|
"owner": vaultState.Owner,
|
|
"encrypted_data": reencryptedData,
|
|
"version": vaultState.EnclaveData.Version,
|
|
"last_refreshed": vaultState.LastRefreshed,
|
|
"rotation_metadata": map[string]any{
|
|
"rotated_at": vaultState.LastRefreshed,
|
|
"key_version": vaultState.EnclaveData.Version,
|
|
},
|
|
}
|
|
|
|
// Serialize vault export to JSON
|
|
exportBytes, err := json.Marshal(vaultExport)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize vault export: %w", err)
|
|
}
|
|
|
|
// Store to IPFS and get new CID
|
|
newCID, err := ms.k.ipfsClient.Add(exportBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to store updated vault to IPFS: %w", err)
|
|
}
|
|
|
|
ms.k.Logger().Debug("Updated vault in IPFS",
|
|
"vault_id", vaultState.VaultId,
|
|
"new_cid", newCID,
|
|
"export_size", len(exportBytes),
|
|
)
|
|
|
|
return nil
|
|
}
|