mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-04 10:21:40 +00:00
@@ -0,0 +1,368 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user