mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
1174 lines
35 KiB
Go
1174 lines
35 KiB
Go
// Package keeper provides consensus-based encryption for DWN records using VRF
|
|
package keeper
|
|
|
|
import (
|
|
"context"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
|
|
"cosmossdk.io/log"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
"golang.org/x/crypto/hkdf"
|
|
|
|
apiv1 "github.com/sonr-io/sonr/api/dwn/v1"
|
|
"github.com/sonr-io/sonr/x/dwn/types"
|
|
)
|
|
|
|
// EncryptionSubkeeper handles consensus-based encryption operations
|
|
type EncryptionSubkeeper struct {
|
|
keeper *Keeper
|
|
logger log.Logger
|
|
vrfConsensus *VRFConsensus
|
|
}
|
|
|
|
// EncryptedData contains encrypted data with metadata
|
|
type EncryptedData struct {
|
|
Ciphertext []byte
|
|
Metadata *types.EncryptionMetadata
|
|
}
|
|
|
|
// NewEncryptionSubkeeper creates a new encryption subkeeper
|
|
func NewEncryptionSubkeeper(k *Keeper) *EncryptionSubkeeper {
|
|
es := &EncryptionSubkeeper{
|
|
keeper: k,
|
|
logger: k.logger.With("module", "encryption"),
|
|
}
|
|
es.vrfConsensus = NewVRFConsensus(k)
|
|
return es
|
|
}
|
|
|
|
// DeriveConsensusKey creates shared encryption key using VRF consensus
|
|
func (es *EncryptionSubkeeper) DeriveConsensusKey(
|
|
ctx context.Context,
|
|
consensusInput []byte,
|
|
) ([]byte, error) {
|
|
if len(consensusInput) == 0 {
|
|
return nil, fmt.Errorf("consensus input cannot be empty")
|
|
}
|
|
|
|
// Use existing VRF computation from keeper
|
|
vrfOutput, err := es.keeper.ComputeVRF(consensusInput)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("VRF computation failed: %w", err)
|
|
}
|
|
|
|
// Derive AES-256 key using HKDF with SHA-256
|
|
hkdfReader := hkdf.New(sha256.New, vrfOutput, nil, []byte("dwn-consensus-encryption"))
|
|
|
|
// Generate 32 bytes for AES-256
|
|
key := make([]byte, 32)
|
|
if _, err := io.ReadFull(hkdfReader, key); err != nil {
|
|
return nil, fmt.Errorf("key derivation failed: %w", err)
|
|
}
|
|
|
|
es.logger.Debug("Consensus key derived",
|
|
"input_len", len(consensusInput),
|
|
"vrf_output_len", len(vrfOutput),
|
|
"key_len", len(key),
|
|
)
|
|
|
|
return key, nil
|
|
}
|
|
|
|
// EncryptData encrypts data with consensus-derived key
|
|
func (es *EncryptionSubkeeper) EncryptData(
|
|
ctx context.Context,
|
|
plaintext []byte,
|
|
consensusInput []byte,
|
|
encryptionHeight int64,
|
|
) (*EncryptedData, error) {
|
|
if len(plaintext) == 0 {
|
|
return nil, fmt.Errorf("plaintext cannot be empty")
|
|
}
|
|
|
|
// Derive consensus-based encryption key
|
|
key, err := es.DeriveConsensusKey(ctx, consensusInput)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive consensus key: %w", err)
|
|
}
|
|
|
|
// Initialize AES cipher
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
|
|
}
|
|
|
|
// Create GCM mode for authenticated encryption
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create GCM mode: %w", err)
|
|
}
|
|
|
|
// Generate random nonce
|
|
nonce := make([]byte, gcm.NonceSize())
|
|
if _, randErr := io.ReadFull(rand.Reader, nonce); randErr != nil {
|
|
return nil, fmt.Errorf("failed to generate nonce: %w", randErr)
|
|
}
|
|
|
|
// Compute HMAC-SHA256 for data integrity verification before encryption
|
|
hmacKey := es.deriveHMACKey(key, nonce)
|
|
dataHMAC := es.computeHMAC(plaintext, hmacKey)
|
|
|
|
// Encrypt data with authenticated encryption
|
|
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
|
|
|
|
// Extract authentication tag (last 16 bytes of GCM ciphertext)
|
|
if len(ciphertext) < 16 {
|
|
return nil, fmt.Errorf("ciphertext too short")
|
|
}
|
|
authTag := ciphertext[len(ciphertext)-16:]
|
|
actualCiphertext := ciphertext[:len(ciphertext)-16]
|
|
|
|
// Create encryption metadata
|
|
metadata := &types.EncryptionMetadata{
|
|
Algorithm: "AES-256-GCM",
|
|
ConsensusInput: consensusInput,
|
|
Nonce: nonce,
|
|
AuthTag: authTag,
|
|
EncryptionHeight: encryptionHeight,
|
|
ValidatorSet: es.getValidatorSet(ctx),
|
|
DataHmac: dataHMAC,
|
|
}
|
|
|
|
es.logger.Info("Data encrypted successfully",
|
|
"plaintext_len", len(plaintext),
|
|
"ciphertext_len", len(actualCiphertext),
|
|
"nonce_len", len(nonce),
|
|
"auth_tag_len", len(authTag),
|
|
"encryption_height", encryptionHeight,
|
|
)
|
|
|
|
return &EncryptedData{
|
|
Ciphertext: actualCiphertext,
|
|
Metadata: metadata,
|
|
}, nil
|
|
}
|
|
|
|
// DecryptData decrypts data using consensus-derived key
|
|
func (es *EncryptionSubkeeper) DecryptData(
|
|
ctx context.Context,
|
|
encryptedData *EncryptedData,
|
|
) ([]byte, error) {
|
|
if encryptedData == nil || encryptedData.Metadata == nil {
|
|
return nil, fmt.Errorf("encrypted data or metadata cannot be nil")
|
|
}
|
|
|
|
metadata := encryptedData.Metadata
|
|
|
|
// Validate encryption metadata
|
|
if metadata.Algorithm != "AES-256-GCM" {
|
|
return nil, fmt.Errorf("unsupported encryption algorithm: %s", metadata.Algorithm)
|
|
}
|
|
|
|
// Derive the same consensus key used for encryption
|
|
key, err := es.DeriveConsensusKey(ctx, metadata.ConsensusInput)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to derive consensus key: %w", err)
|
|
}
|
|
|
|
// Initialize AES cipher
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
|
|
}
|
|
|
|
// Create GCM mode
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create GCM mode: %w", err)
|
|
}
|
|
|
|
// Validate nonce size
|
|
if len(metadata.Nonce) != gcm.NonceSize() {
|
|
return nil, fmt.Errorf(
|
|
"invalid nonce size: got %d, expected %d",
|
|
len(metadata.Nonce),
|
|
gcm.NonceSize(),
|
|
)
|
|
}
|
|
|
|
// Reconstruct full ciphertext (data + auth tag)
|
|
fullCiphertext := make([]byte, len(encryptedData.Ciphertext)+len(metadata.AuthTag))
|
|
copy(fullCiphertext, encryptedData.Ciphertext)
|
|
copy(fullCiphertext[len(encryptedData.Ciphertext):], metadata.AuthTag)
|
|
|
|
// Decrypt and verify
|
|
plaintext, err := gcm.Open(nil, metadata.Nonce, fullCiphertext, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decryption failed (authentication failure): %w", err)
|
|
}
|
|
|
|
// Verify HMAC for data integrity
|
|
if len(metadata.DataHmac) == 0 {
|
|
return nil, fmt.Errorf("missing HMAC in encryption metadata")
|
|
}
|
|
|
|
hmacKey := es.deriveHMACKey(key, metadata.Nonce)
|
|
hmacValid := es.verifyHMAC(plaintext, hmacKey, metadata.DataHmac)
|
|
if !hmacValid {
|
|
return nil, fmt.Errorf("HMAC verification failed: data integrity compromised")
|
|
}
|
|
|
|
// Log HMAC verification success
|
|
es.logger.Debug("HMAC verification successful",
|
|
"hmac_len", len(metadata.DataHmac),
|
|
"plaintext_len", len(plaintext),
|
|
)
|
|
|
|
es.logger.Info("Data decrypted successfully",
|
|
"ciphertext_len", len(encryptedData.Ciphertext),
|
|
"plaintext_len", len(plaintext),
|
|
"encryption_height", metadata.EncryptionHeight,
|
|
)
|
|
|
|
return plaintext, nil
|
|
}
|
|
|
|
// ValidateEncryptionMetadata validates encryption metadata for security
|
|
func (es *EncryptionSubkeeper) ValidateEncryptionMetadata(
|
|
metadata *types.EncryptionMetadata,
|
|
) error {
|
|
if metadata == nil {
|
|
return fmt.Errorf("metadata cannot be nil")
|
|
}
|
|
|
|
// Validate algorithm
|
|
if metadata.Algorithm != "AES-256-GCM" {
|
|
return fmt.Errorf("unsupported encryption algorithm: %s", metadata.Algorithm)
|
|
}
|
|
|
|
// Validate consensus input
|
|
if len(metadata.ConsensusInput) == 0 {
|
|
return fmt.Errorf("consensus input cannot be empty")
|
|
}
|
|
|
|
// Validate nonce
|
|
if len(metadata.Nonce) != 12 { // GCM standard nonce size
|
|
return fmt.Errorf("invalid nonce size: got %d, expected 12", len(metadata.Nonce))
|
|
}
|
|
|
|
// Validate auth tag
|
|
if len(metadata.AuthTag) != 16 { // GCM auth tag size
|
|
return fmt.Errorf("invalid auth tag size: got %d, expected 16", len(metadata.AuthTag))
|
|
}
|
|
|
|
// Validate encryption height
|
|
if metadata.EncryptionHeight < 0 {
|
|
return fmt.Errorf("encryption height cannot be negative")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getValidatorSet returns current validator set for consensus tracking
|
|
func (es *EncryptionSubkeeper) getValidatorSet(ctx context.Context) []string {
|
|
// Handle case when stakingKeeper is nil (for testing scenarios)
|
|
if es.keeper.stakingKeeper == nil {
|
|
es.logger.Debug("stakingKeeper is nil, returning empty validator set")
|
|
return []string{}
|
|
}
|
|
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
|
|
// Query bonded validators ordered by power (descending)
|
|
validators, err := es.keeper.stakingKeeper.GetBondedValidatorsByPower(sdkCtx)
|
|
if err != nil {
|
|
es.logger.Error("Failed to get bonded validators", "error", err)
|
|
return []string{}
|
|
}
|
|
|
|
if len(validators) == 0 {
|
|
es.logger.Debug("No bonded validators found")
|
|
return []string{}
|
|
}
|
|
|
|
// Get module parameters for minimum validator threshold
|
|
params, err := es.keeper.Params.Get(ctx)
|
|
if err != nil {
|
|
es.logger.Error("Failed to get module parameters", "error", err)
|
|
// Continue with default threshold if params unavailable
|
|
params.MinValidatorsForKeyGen = types.DefaultMinValidatorsForKeyGen
|
|
}
|
|
|
|
// Calculate minimum number of validators needed based on percentage
|
|
totalValidators := len(validators)
|
|
minValidators := (totalValidators * int(params.MinValidatorsForKeyGen)) / 100
|
|
if minValidators < 1 {
|
|
minValidators = 1 // Always include at least one validator
|
|
}
|
|
|
|
// Filter active validators (take up to minValidators count)
|
|
activeValidators := validators
|
|
if len(validators) > minValidators {
|
|
activeValidators = validators[:minValidators]
|
|
}
|
|
|
|
// Extract validator operator addresses
|
|
validatorAddresses := make([]string, 0, len(activeValidators))
|
|
for _, validator := range activeValidators {
|
|
// Get validator operator address (not consensus address)
|
|
valAddr := validator.GetOperator()
|
|
if valAddr != "" {
|
|
validatorAddresses = append(validatorAddresses, valAddr)
|
|
}
|
|
}
|
|
|
|
es.logger.Debug("Retrieved validator set for consensus tracking",
|
|
"total_bonded", totalValidators,
|
|
"min_threshold_pct", params.MinValidatorsForKeyGen,
|
|
"min_validators", minValidators,
|
|
"active_validators", len(validatorAddresses),
|
|
"block_height", sdkCtx.BlockHeight(),
|
|
)
|
|
|
|
return validatorAddresses
|
|
}
|
|
|
|
// IsConsensusInputUnique checks if consensus input has been used before
|
|
func (es *EncryptionSubkeeper) IsConsensusInputUnique(
|
|
ctx context.Context,
|
|
consensusInput []byte,
|
|
) (bool, error) {
|
|
if len(consensusInput) == 0 {
|
|
return false, fmt.Errorf("consensus input cannot be empty")
|
|
}
|
|
|
|
// Unwrap SDK context
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
|
|
// Hash consensus input using SHA256 for efficient storage
|
|
hasher := sha256.New()
|
|
hasher.Write(consensusInput)
|
|
hash := hasher.Sum(nil)
|
|
hashHex := hex.EncodeToString(hash)
|
|
|
|
// Query database to check if hash exists in used consensus inputs
|
|
// Using SaltStoreTable as it's the most appropriate table for storing hash data
|
|
saltTable := es.keeper.OrmDB.SaltStoreTable()
|
|
|
|
// Check if consensus input hash already exists
|
|
exists, err := saltTable.Has(ctx, hashHex)
|
|
if err != nil {
|
|
es.logger.Error("Failed to check consensus input uniqueness", "error", err, "hash", hashHex)
|
|
return false, fmt.Errorf("failed to query consensus input uniqueness: %w", err)
|
|
}
|
|
|
|
// If hash already exists, consensus input is not unique
|
|
if exists {
|
|
es.logger.Debug("Consensus input already used", "hash", hashHex[:16]+"...")
|
|
return false, nil
|
|
}
|
|
|
|
// Store new consensus input hash for future uniqueness checks
|
|
currentTime := sdkCtx.BlockTime().Unix()
|
|
|
|
// Get module parameters for key rotation cycles to calculate expiration
|
|
params, err := es.keeper.Params.Get(ctx)
|
|
if err != nil {
|
|
es.logger.Error("Failed to get module parameters", "error", err)
|
|
// Continue with default expiration if params unavailable
|
|
params.KeyRotationDays = types.DefaultKeyRotationDays
|
|
}
|
|
|
|
// Create salt store entry to track consensus input usage
|
|
// Using original consensus input as salt for verification if needed
|
|
saltEntry := &apiv1.SaltStore{
|
|
RecordId: hashHex,
|
|
SaltValue: consensusInput,
|
|
CreatedAt: currentTime,
|
|
}
|
|
|
|
// Insert the consensus input hash into storage
|
|
if err := saltTable.Insert(ctx, saltEntry); err != nil {
|
|
es.logger.Error(
|
|
"Failed to store consensus input hash",
|
|
"error",
|
|
err,
|
|
"hash",
|
|
hashHex[:16]+"...",
|
|
)
|
|
return false, fmt.Errorf("failed to store consensus input hash: %w", err)
|
|
}
|
|
|
|
// Perform cleanup of old entries to prevent storage bloat
|
|
// Calculate expiration based on key rotation cycles
|
|
// Convert days to seconds (assuming key rotation cycles align with time-based expiration)
|
|
expirationSeconds := int64(params.KeyRotationDays) * 24 * 60 * 60
|
|
expirationTime := currentTime - expirationSeconds
|
|
|
|
// Use async cleanup to avoid blocking the main operation
|
|
go func() {
|
|
// Create background context for cleanup operation
|
|
cleanupCtx := context.Background()
|
|
|
|
// Query and delete old entries using created_at index
|
|
createdAtIndex := apiv1.SaltStoreCreatedAtIndexKey{}.WithCreatedAt(expirationTime)
|
|
|
|
// Use DeleteRange to clean up expired entries efficiently
|
|
// Delete all entries from beginning of time to expiration time
|
|
err := saltTable.DeleteRange(cleanupCtx,
|
|
apiv1.SaltStoreCreatedAtIndexKey{}.WithCreatedAt(0), // From beginning
|
|
createdAtIndex, // To expiration time
|
|
)
|
|
if err != nil {
|
|
es.logger.Error("Failed to cleanup expired consensus inputs", "error", err)
|
|
return
|
|
}
|
|
|
|
es.logger.Debug("Cleaned up expired consensus inputs", "expiration_time", expirationTime)
|
|
}()
|
|
|
|
es.logger.Debug("Consensus input is unique and stored", "hash", hashHex[:16]+"...")
|
|
return true, nil
|
|
}
|
|
|
|
// GetEncryptionStats returns encryption statistics for monitoring using ORM data
|
|
func (es *EncryptionSubkeeper) GetEncryptionStats(
|
|
ctx context.Context,
|
|
) (*types.EncryptionStats, error) {
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
|
|
// Count total encrypted records by iterating through all records
|
|
totalEncryptedRecords := uint64(0)
|
|
lastEncryptionHeight := int64(0)
|
|
|
|
// Query all records and count those with encryption metadata
|
|
recordIter, err := es.keeper.OrmDB.DWNRecordTable().List(ctx, apiv1.DWNRecordPrimaryKey{})
|
|
if err != nil {
|
|
es.logger.Error("Failed to list records for encryption stats", "error", err)
|
|
} else {
|
|
defer recordIter.Close()
|
|
for recordIter.Next() {
|
|
record, iterErr := recordIter.Value()
|
|
if iterErr != nil {
|
|
continue
|
|
}
|
|
|
|
// Check if record has encryption metadata
|
|
if record.EncryptionMetadata != nil {
|
|
totalEncryptedRecords++
|
|
if record.EncryptionMetadata.EncryptionHeight > lastEncryptionHeight {
|
|
lastEncryptionHeight = record.EncryptionMetadata.EncryptionHeight
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get key state statistics
|
|
keyState, keyStateErr := es.getStoredKeyState(ctx)
|
|
totalKeyRotations := uint64(0)
|
|
activeValidators := uint64(0)
|
|
singleNodeMode := true
|
|
|
|
if keyStateErr == nil {
|
|
if keyState.KeyVersion > 0 {
|
|
totalKeyRotations = keyState.KeyVersion
|
|
}
|
|
activeValidators = uint64(len(keyState.ValidatorSet))
|
|
singleNodeMode = keyState.SingleNodeMode
|
|
}
|
|
|
|
// Count VRF contributions for additional metrics
|
|
totalVrfContributions := uint64(0)
|
|
vrfIter, vrfErr := es.keeper.OrmDB.VRFContributionTable().
|
|
List(ctx, apiv1.VRFContributionPrimaryKey{})
|
|
if vrfErr == nil {
|
|
defer vrfIter.Close()
|
|
for vrfIter.Next() {
|
|
_, iterErr := vrfIter.Value()
|
|
if iterErr == nil {
|
|
totalVrfContributions++
|
|
}
|
|
}
|
|
}
|
|
|
|
// Safely convert uint64 to int64 to avoid overflow
|
|
var totalEncryptedRecordsInt64 int64
|
|
if totalEncryptedRecords > 9223372036854775807 { // Max int64
|
|
totalEncryptedRecordsInt64 = 9223372036854775807
|
|
} else {
|
|
totalEncryptedRecordsInt64 = int64(totalEncryptedRecords)
|
|
}
|
|
|
|
stats := &types.EncryptionStats{
|
|
TotalEncryptedRecords: totalEncryptedRecordsInt64,
|
|
TotalDecryptionErrors: 0, // This would need to be tracked separately in production
|
|
LastEncryptionHeight: lastEncryptionHeight,
|
|
}
|
|
|
|
es.logger.Debug("Collected encryption statistics",
|
|
"encrypted_records", totalEncryptedRecords,
|
|
"last_encryption_height", lastEncryptionHeight,
|
|
"key_rotations", totalKeyRotations,
|
|
"active_validators", activeValidators,
|
|
"vrf_contributions", totalVrfContributions,
|
|
"single_node_mode", singleNodeMode,
|
|
"current_block_height", sdkCtx.BlockHeight(),
|
|
)
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
// EncryptWithConsensusKey encrypts data using the current consensus-derived encryption key
|
|
func (es *EncryptionSubkeeper) EncryptWithConsensusKey(
|
|
ctx context.Context,
|
|
plaintext []byte,
|
|
protocol string,
|
|
) (*EncryptedData, error) {
|
|
if len(plaintext) == 0 {
|
|
return nil, fmt.Errorf("plaintext cannot be empty")
|
|
}
|
|
|
|
// Create consensus input based on protocol
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
consensusInput := fmt.Appendf(nil, "protocol:%s:height:%d", protocol, sdkCtx.BlockHeight())
|
|
|
|
// Use the unified encryption key derivation
|
|
encryptionKey, err := es.getEncryptionKey(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get encryption key: %w", err)
|
|
}
|
|
|
|
// Perform AES-256-GCM encryption
|
|
return es.encryptWithKey(ctx, plaintext, encryptionKey, consensusInput, sdkCtx.BlockHeight())
|
|
}
|
|
|
|
// encryptWithKey performs the actual AES-256-GCM encryption
|
|
func (es *EncryptionSubkeeper) encryptWithKey(
|
|
ctx context.Context,
|
|
plaintext []byte,
|
|
key []byte,
|
|
consensusInput []byte,
|
|
blockHeight int64,
|
|
) (*EncryptedData, error) {
|
|
// Initialize AES cipher
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
|
|
}
|
|
|
|
// Create GCM mode for authenticated encryption
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create GCM mode: %w", err)
|
|
}
|
|
|
|
// Generate random nonce
|
|
nonce := make([]byte, gcm.NonceSize())
|
|
if _, randErr := io.ReadFull(rand.Reader, nonce); randErr != nil {
|
|
return nil, fmt.Errorf("failed to generate nonce: %w", randErr)
|
|
}
|
|
|
|
// Encrypt data with authenticated encryption
|
|
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
|
|
|
|
// Extract authentication tag (last 16 bytes of GCM ciphertext)
|
|
if len(ciphertext) < 16 {
|
|
return nil, fmt.Errorf("ciphertext too short")
|
|
}
|
|
authTag := ciphertext[len(ciphertext)-16:]
|
|
actualCiphertext := ciphertext[:len(ciphertext)-16]
|
|
|
|
// Compute HMAC-SHA256 for data integrity verification
|
|
hmacKey := es.deriveHMACKey(key, nonce)
|
|
dataHMAC := es.computeHMAC(plaintext, hmacKey)
|
|
|
|
// Create encryption metadata
|
|
metadata := &types.EncryptionMetadata{
|
|
Algorithm: "AES-256-GCM",
|
|
ConsensusInput: consensusInput,
|
|
Nonce: nonce,
|
|
AuthTag: authTag,
|
|
EncryptionHeight: blockHeight,
|
|
ValidatorSet: es.getValidatorSet(ctx),
|
|
KeyVersion: es.calculateKeyEpoch(blockHeight), // Approximate 24h epochs
|
|
SingleNodeMode: es.isSingleNodeMode(ctx),
|
|
DataHmac: dataHMAC,
|
|
}
|
|
|
|
es.logger.Info("Data encrypted with consensus key",
|
|
"plaintext_len", len(plaintext),
|
|
"ciphertext_len", len(actualCiphertext),
|
|
"single_node_mode", es.isSingleNodeMode(ctx),
|
|
)
|
|
|
|
return &EncryptedData{
|
|
Ciphertext: actualCiphertext,
|
|
Metadata: metadata,
|
|
}, nil
|
|
}
|
|
|
|
// InitiateKeyRotation starts the key rotation process
|
|
func (es *EncryptionSubkeeper) InitiateKeyRotation(ctx context.Context, reason string) error {
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
currentEpoch := es.calculateKeyEpoch(sdkCtx.BlockHeight()) // Approximate 24h epochs
|
|
|
|
// Check if we've already rotated for this epoch
|
|
keyState, err := es.getStoredKeyState(ctx)
|
|
if err == nil && keyState.KeyVersion == currentEpoch {
|
|
// Already rotated for this epoch, skip
|
|
return nil
|
|
}
|
|
|
|
es.logger.Info("Initiating key rotation",
|
|
"reason", reason,
|
|
"block_height", sdkCtx.BlockHeight(),
|
|
"key_epoch", currentEpoch,
|
|
)
|
|
|
|
// 1. Generate new encryption key using unified approach
|
|
newKey, err := es.getEncryptionKey(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate new encryption key: %w", err)
|
|
}
|
|
|
|
// 2. Create new key state
|
|
keyState = &types.EncryptionKeyState{
|
|
CurrentKey: newKey,
|
|
KeyVersion: currentEpoch,
|
|
ValidatorSet: es.getValidatorSet(ctx),
|
|
Contributions: []*types.VRFContribution{}, // Initialize empty slice
|
|
LastRotation: sdkCtx.BlockTime().Unix(),
|
|
NextRotation: calculateNextRotation(sdkCtx.BlockTime().Unix()),
|
|
SingleNodeMode: es.isSingleNodeMode(ctx),
|
|
}
|
|
|
|
// 3. Store the new key state (placeholder implementation for now)
|
|
err = es.storeKeyState(ctx, keyState)
|
|
if err != nil {
|
|
// For now, log the error but continue since storage is not implemented
|
|
es.logger.Warn("Key state storage failed (not yet implemented)",
|
|
"error", err,
|
|
"key_version", currentEpoch,
|
|
)
|
|
}
|
|
|
|
// 4. Emit key rotation event
|
|
var oldKeyVersion uint64
|
|
if prevKeyState, err := es.getStoredKeyState(ctx); err == nil {
|
|
oldKeyVersion = prevKeyState.KeyVersion
|
|
} else {
|
|
oldKeyVersion = 0 // First rotation
|
|
}
|
|
|
|
event := &types.EventKeyRotation{
|
|
OldKeyVersion: oldKeyVersion,
|
|
NewKeyVersion: currentEpoch,
|
|
Reason: reason,
|
|
BlockHeight: uint64(sdkCtx.BlockHeight()),
|
|
SingleNodeMode: es.isSingleNodeMode(ctx),
|
|
ValidatorCount: uint32(len(es.getValidatorSet(ctx))),
|
|
}
|
|
|
|
if err := sdkCtx.EventManager().EmitTypedEvent(event); err != nil {
|
|
es.logger.Error("Failed to emit key rotation event",
|
|
"error", err,
|
|
"key_version", currentEpoch,
|
|
)
|
|
}
|
|
|
|
es.logger.Info("Key rotation completed successfully",
|
|
"old_key_version", oldKeyVersion,
|
|
"new_key_version", currentEpoch,
|
|
"single_node_mode", es.isSingleNodeMode(ctx),
|
|
"reason", reason,
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsRotationDue checks if a scheduled key rotation is due
|
|
func (es *EncryptionSubkeeper) IsRotationDue(ctx context.Context) bool {
|
|
keyState, err := es.getStoredKeyState(ctx)
|
|
if err != nil {
|
|
// No stored key state means initial rotation is due
|
|
es.logger.Info("No stored key state found, rotation due for initialization")
|
|
return true
|
|
}
|
|
|
|
currentTime := sdk.UnwrapSDKContext(ctx).BlockTime().Unix()
|
|
|
|
// Check if scheduled rotation time has passed
|
|
if currentTime >= keyState.NextRotation {
|
|
es.logger.Info("Scheduled rotation time reached",
|
|
"current_time", currentTime,
|
|
"next_rotation", keyState.NextRotation,
|
|
)
|
|
return true
|
|
}
|
|
|
|
// Check if validator set has changed significantly (>10%)
|
|
validatorSetChanged := es.hasValidatorSetChanged(ctx, 0.1)
|
|
if validatorSetChanged {
|
|
es.logger.Info("Validator set changed significantly, rotation due")
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// calculateNextRotation calculates the next scheduled rotation time (30 days from current)
|
|
func calculateNextRotation(currentTime int64) int64 {
|
|
// 30 days in seconds
|
|
thirtyDays := int64(30 * 24 * 60 * 60)
|
|
return currentTime + thirtyDays
|
|
}
|
|
|
|
// storeKeyState stores the encryption key state to persistent storage using ORM
|
|
func (es *EncryptionSubkeeper) storeKeyState(
|
|
ctx context.Context,
|
|
keyState *types.EncryptionKeyState,
|
|
) error {
|
|
// Convert to API type for ORM storage
|
|
apiKeyState := &apiv1.EncryptionKeyState{
|
|
KeyVersion: keyState.KeyVersion,
|
|
CurrentKey: keyState.CurrentKey,
|
|
ValidatorSet: keyState.ValidatorSet,
|
|
LastRotation: keyState.LastRotation,
|
|
NextRotation: keyState.NextRotation,
|
|
SingleNodeMode: keyState.SingleNodeMode,
|
|
Contributions: make([]*apiv1.VRFContribution, len(keyState.Contributions)),
|
|
}
|
|
|
|
// Convert VRF contributions
|
|
for i, contrib := range keyState.Contributions {
|
|
apiKeyState.Contributions[i] = &apiv1.VRFContribution{
|
|
ValidatorAddress: contrib.ValidatorAddress,
|
|
Randomness: contrib.Randomness,
|
|
Proof: contrib.Proof,
|
|
BlockHeight: contrib.BlockHeight,
|
|
Timestamp: contrib.Timestamp,
|
|
}
|
|
}
|
|
|
|
// Use Save method to insert or update
|
|
if err := es.keeper.OrmDB.EncryptionKeyStateTable().Save(ctx, apiKeyState); err != nil {
|
|
return fmt.Errorf("failed to save encryption key state: %w", err)
|
|
}
|
|
|
|
es.logger.Info("Key state stored successfully",
|
|
"key_version", keyState.KeyVersion,
|
|
"validator_count", len(keyState.ValidatorSet),
|
|
"last_rotation", keyState.LastRotation,
|
|
"next_rotation", keyState.NextRotation,
|
|
"single_node_mode", keyState.SingleNodeMode,
|
|
"block_height", sdk.UnwrapSDKContext(ctx).BlockHeight(),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// getStoredKeyState retrieves the current encryption key state using ORM
|
|
func (es *EncryptionSubkeeper) getStoredKeyState(
|
|
ctx context.Context,
|
|
) (*types.EncryptionKeyState, error) {
|
|
// Get the current key version
|
|
currentKeyVersion := es.GetCurrentKeyVersion(ctx)
|
|
|
|
// Try to retrieve the stored key state for current version
|
|
apiKeyState, err := es.keeper.OrmDB.EncryptionKeyStateTable().Get(ctx, currentKeyVersion)
|
|
if err != nil {
|
|
// If not found, try the previous version as fallback
|
|
if currentKeyVersion > 0 {
|
|
apiKeyState, err = es.keeper.OrmDB.EncryptionKeyStateTable().
|
|
Get(ctx, currentKeyVersion-1)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("no stored key state found for versions %d or %d: %w",
|
|
currentKeyVersion, currentKeyVersion-1, err)
|
|
}
|
|
} else {
|
|
return nil, fmt.Errorf("no stored key state found for version %d: %w", currentKeyVersion, err)
|
|
}
|
|
}
|
|
|
|
// Convert from API type back to types
|
|
keyState := &types.EncryptionKeyState{
|
|
KeyVersion: apiKeyState.KeyVersion,
|
|
CurrentKey: apiKeyState.CurrentKey,
|
|
ValidatorSet: apiKeyState.ValidatorSet,
|
|
LastRotation: apiKeyState.LastRotation,
|
|
NextRotation: apiKeyState.NextRotation,
|
|
SingleNodeMode: apiKeyState.SingleNodeMode,
|
|
Contributions: make([]*types.VRFContribution, len(apiKeyState.Contributions)),
|
|
}
|
|
|
|
// Convert VRF contributions
|
|
for i, contrib := range apiKeyState.Contributions {
|
|
keyState.Contributions[i] = &types.VRFContribution{
|
|
ValidatorAddress: contrib.ValidatorAddress,
|
|
Randomness: contrib.Randomness,
|
|
Proof: contrib.Proof,
|
|
BlockHeight: contrib.BlockHeight,
|
|
Timestamp: contrib.Timestamp,
|
|
}
|
|
}
|
|
|
|
es.logger.Debug("Retrieved stored key state",
|
|
"key_version", keyState.KeyVersion,
|
|
"validator_count", len(keyState.ValidatorSet),
|
|
"single_node_mode", keyState.SingleNodeMode,
|
|
"contributions_count", len(keyState.Contributions),
|
|
)
|
|
|
|
return keyState, nil
|
|
}
|
|
|
|
// CheckAndPerformRotation checks if rotation is due and performs it if needed
|
|
func (es *EncryptionSubkeeper) CheckAndPerformRotation(ctx context.Context) error {
|
|
// Check if encryption is enabled before attempting rotation
|
|
params, err := es.keeper.Params.Get(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get DWN params: %w", err)
|
|
}
|
|
|
|
if !params.EncryptionEnabled {
|
|
// Encryption disabled - skip rotation silently
|
|
return nil
|
|
}
|
|
|
|
if !es.IsRotationDue(ctx) {
|
|
return nil
|
|
}
|
|
|
|
// Determine rotation reason
|
|
reason := "scheduled_rotation"
|
|
|
|
// Check if due to validator set change
|
|
validatorSetChanged := es.hasValidatorSetChanged(ctx, 0.1)
|
|
if validatorSetChanged {
|
|
reason = "validator_set_change"
|
|
}
|
|
|
|
// Perform the rotation
|
|
return es.InitiateKeyRotation(ctx, reason)
|
|
}
|
|
|
|
// getEncryptionKey provides unified encryption key derivation with single-node fallback
|
|
func (es *EncryptionSubkeeper) getEncryptionKey(ctx context.Context) ([]byte, error) {
|
|
validators, err := es.getActiveValidators(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get active validators: %w", err)
|
|
}
|
|
|
|
if len(validators) == 1 {
|
|
// Single-node development mode - deterministic key generation
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
chainID := sdkCtx.ChainID()
|
|
blockHeight := sdkCtx.BlockHeight()
|
|
|
|
// 30-day epochs (assuming 6s blocks): 30 days * 24 hours * 60 minutes * 10 blocks/minute
|
|
keyEpoch := blockHeight / (30 * 24 * 60 * 10)
|
|
|
|
input := fmt.Sprintf("%s:%d", chainID, keyEpoch)
|
|
|
|
es.logger.Info("Using single-node fallback for encryption key",
|
|
"chain_id", chainID,
|
|
"key_epoch", keyEpoch,
|
|
"block_height", blockHeight,
|
|
)
|
|
|
|
return es.keeper.ComputeVRF([]byte(input))
|
|
}
|
|
|
|
// Multi-validator consensus mode
|
|
es.logger.Info("Using multi-validator consensus for encryption key",
|
|
"validator_count", len(validators),
|
|
)
|
|
|
|
return es.deriveConsensusKey(ctx, validators)
|
|
}
|
|
|
|
// deriveConsensusKey implements multi-validator consensus key derivation
|
|
func (es *EncryptionSubkeeper) deriveConsensusKey(
|
|
ctx context.Context,
|
|
validators []any,
|
|
) ([]byte, error) {
|
|
// Create consensus input based on validators
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
keyEpoch := es.calculateKeyEpoch(sdkCtx.BlockHeight()) // 24h epochs
|
|
|
|
consensusInput := fmt.Appendf(nil, "consensus:%s:%d:%d",
|
|
sdkCtx.ChainID(), keyEpoch, len(validators))
|
|
|
|
// Use VRF to derive consensus key
|
|
return es.keeper.ComputeVRF(consensusInput)
|
|
}
|
|
|
|
// getActiveValidators returns the current active validators
|
|
func (es *EncryptionSubkeeper) getActiveValidators(ctx context.Context) ([]any, error) {
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
validators, err := es.keeper.stakingKeeper.GetBondedValidatorsByPower(sdkCtx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get bonded validators: %w", err)
|
|
}
|
|
|
|
// Convert to interface slice
|
|
result := make([]any, len(validators))
|
|
for i, validator := range validators {
|
|
result[i] = validator
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// isSingleNodeMode checks if running in single-node mode
|
|
func (es *EncryptionSubkeeper) isSingleNodeMode(ctx context.Context) bool {
|
|
validators, err := es.getActiveValidators(ctx)
|
|
if err != nil {
|
|
return true // Default to single-node if can't get validators
|
|
}
|
|
return len(validators) == 1
|
|
}
|
|
|
|
// hasValidatorSetChanged checks if validator set changed significantly (>threshold% change)
|
|
func (es *EncryptionSubkeeper) hasValidatorSetChanged(ctx context.Context, threshold float64) bool {
|
|
// Get current validator set
|
|
currentValidators, err := es.getActiveValidators(ctx)
|
|
if err != nil {
|
|
es.logger.Error("Failed to get current validators", "error", err)
|
|
return false
|
|
}
|
|
|
|
// Get stored key state to compare with previous validator set
|
|
keyState, err := es.getStoredKeyState(ctx)
|
|
if err != nil {
|
|
// No previous key state means this is the first key generation
|
|
// Only trigger rotation if we have validators
|
|
if len(currentValidators) > 0 {
|
|
es.logger.Info("No previous validator set found, initial rotation needed")
|
|
return true
|
|
}
|
|
// No validators and no previous state - no rotation needed
|
|
return false
|
|
}
|
|
|
|
previousValidators := keyState.ValidatorSet
|
|
|
|
// Build sets for comparison
|
|
currentSet := make(map[string]bool)
|
|
for _, validator := range currentValidators {
|
|
currentSet[fmt.Sprintf("%v", validator)] = true
|
|
}
|
|
|
|
previousSet := make(map[string]bool)
|
|
for _, validator := range previousValidators {
|
|
previousSet[validator] = true
|
|
}
|
|
|
|
// Count added and removed validators
|
|
added := 0
|
|
for validator := range currentSet {
|
|
if !previousSet[validator] {
|
|
added++
|
|
}
|
|
}
|
|
|
|
removed := 0
|
|
for validator := range previousSet {
|
|
if !currentSet[validator] {
|
|
removed++
|
|
}
|
|
}
|
|
|
|
totalChange := added + removed
|
|
totalValidators := len(currentValidators)
|
|
if len(previousValidators) > totalValidators {
|
|
totalValidators = len(previousValidators)
|
|
}
|
|
|
|
// Avoid division by zero
|
|
if totalValidators == 0 {
|
|
return false
|
|
}
|
|
|
|
// If previous validators were empty and we now have validators, consider it a change
|
|
if len(previousValidators) == 0 && len(currentValidators) > 0 {
|
|
es.logger.Info("Validator set initialized",
|
|
"current_validators", len(currentValidators),
|
|
"previous_validators", 0,
|
|
)
|
|
return true
|
|
}
|
|
|
|
// If no actual changes in the validator addresses, no rotation needed
|
|
if added == 0 && removed == 0 {
|
|
return false
|
|
}
|
|
|
|
changePercentage := float64(totalChange) / float64(totalValidators)
|
|
changed := changePercentage > threshold
|
|
|
|
es.logger.Info("Validator set change analysis",
|
|
"current_validators", len(currentValidators),
|
|
"previous_validators", len(previousValidators),
|
|
"added", added,
|
|
"removed", removed,
|
|
"change_percentage", changePercentage,
|
|
"threshold", threshold,
|
|
"changed", changed,
|
|
)
|
|
|
|
return changed
|
|
}
|
|
|
|
// calculateKeyEpoch safely converts block height to key epoch with overflow protection
|
|
func (es *EncryptionSubkeeper) calculateKeyEpoch(blockHeight int64) uint64 {
|
|
if blockHeight < 0 {
|
|
return 0
|
|
}
|
|
// 24h epochs assuming 6s blocks: 14400 blocks per day
|
|
epoch := blockHeight / 14400
|
|
if epoch < 0 {
|
|
return 0
|
|
}
|
|
return uint64(epoch)
|
|
}
|
|
|
|
// GetCurrentKeyVersion returns the current key version/epoch
|
|
func (es *EncryptionSubkeeper) GetCurrentKeyVersion(ctx context.Context) uint64 {
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
return es.calculateKeyEpoch(sdkCtx.BlockHeight())
|
|
}
|
|
|
|
// DecryptWithConsensusKey decrypts data using consensus-derived encryption keys
|
|
func (es *EncryptionSubkeeper) DecryptWithConsensusKey(
|
|
ctx context.Context,
|
|
ciphertext []byte,
|
|
metadata *types.EncryptionMetadata,
|
|
) ([]byte, error) {
|
|
if len(ciphertext) == 0 {
|
|
return nil, fmt.Errorf("ciphertext cannot be empty")
|
|
}
|
|
|
|
if metadata == nil {
|
|
return nil, fmt.Errorf("encryption metadata cannot be nil")
|
|
}
|
|
|
|
// Verify algorithm support
|
|
if metadata.Algorithm != "AES-256-GCM" {
|
|
return nil, fmt.Errorf("unsupported encryption algorithm: %s", metadata.Algorithm)
|
|
}
|
|
|
|
// Get the encryption key for decryption
|
|
// For now, use current key - in production, would use key versioning
|
|
encryptionKey, err := es.getEncryptionKey(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get encryption key: %w", err)
|
|
}
|
|
|
|
// Perform AES-256-GCM decryption
|
|
plaintext, err := es.decryptWithKey(ciphertext, encryptionKey, metadata)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decryption failed: %w", err)
|
|
}
|
|
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
es.logger.Debug("Data decrypted successfully",
|
|
"ciphertext_len", len(ciphertext),
|
|
"plaintext_len", len(plaintext),
|
|
"algorithm", metadata.Algorithm,
|
|
"key_version", metadata.KeyVersion,
|
|
"block_height", sdkCtx.BlockHeight(),
|
|
)
|
|
|
|
return plaintext, nil
|
|
}
|
|
|
|
// decryptWithKey performs the actual AES-256-GCM decryption
|
|
func (es *EncryptionSubkeeper) decryptWithKey(
|
|
ciphertext []byte,
|
|
key []byte,
|
|
metadata *types.EncryptionMetadata,
|
|
) ([]byte, error) {
|
|
// Create AES cipher
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
|
|
}
|
|
|
|
// Create GCM mode
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create GCM: %w", err)
|
|
}
|
|
|
|
// Validate nonce size
|
|
if len(metadata.Nonce) != gcm.NonceSize() {
|
|
return nil, fmt.Errorf(
|
|
"invalid nonce size: expected %d, got %d",
|
|
gcm.NonceSize(),
|
|
len(metadata.Nonce),
|
|
)
|
|
}
|
|
|
|
// Combine ciphertext and auth tag for GCM decryption
|
|
combinedData := make([]byte, len(ciphertext)+len(metadata.AuthTag))
|
|
copy(combinedData, ciphertext)
|
|
copy(combinedData[len(ciphertext):], metadata.AuthTag)
|
|
|
|
// Decrypt using GCM
|
|
plaintext, err := gcm.Open(nil, metadata.Nonce, combinedData, metadata.ConsensusInput)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GCM decryption failed: %w", err)
|
|
}
|
|
|
|
// Verify HMAC for additional data integrity checking
|
|
if len(metadata.DataHmac) > 0 {
|
|
hmacKey := es.deriveHMACKey(key, metadata.Nonce)
|
|
if !es.verifyHMAC(plaintext, hmacKey, metadata.DataHmac) {
|
|
return nil, fmt.Errorf("HMAC verification failed: data integrity compromised")
|
|
}
|
|
}
|
|
|
|
return plaintext, nil
|
|
}
|
|
|
|
// deriveHMACKey derives an HMAC key from the encryption key and nonce
|
|
func (es *EncryptionSubkeeper) deriveHMACKey(encryptionKey, nonce []byte) []byte {
|
|
// Use HKDF to derive HMAC key from encryption key and nonce
|
|
hkdfReader := hkdf.New(sha256.New, encryptionKey, nonce, []byte("dwn-hmac-key-derivation"))
|
|
|
|
// Generate 32 bytes for HMAC-SHA256 key
|
|
hmacKey := make([]byte, 32)
|
|
if _, err := io.ReadFull(hkdfReader, hmacKey); err != nil {
|
|
es.logger.Error("Failed to derive HMAC key", "error", err)
|
|
return nil
|
|
}
|
|
|
|
return hmacKey
|
|
}
|
|
|
|
// computeHMAC computes HMAC-SHA256 of data using the provided key
|
|
func (es *EncryptionSubkeeper) computeHMAC(data, key []byte) []byte {
|
|
if len(key) == 0 {
|
|
es.logger.Error("HMAC key is empty")
|
|
return nil
|
|
}
|
|
|
|
h := hmac.New(sha256.New, key)
|
|
h.Write(data)
|
|
return h.Sum(nil)
|
|
}
|
|
|
|
// verifyHMAC verifies HMAC-SHA256 of data against expected HMAC
|
|
func (es *EncryptionSubkeeper) verifyHMAC(data, key, expectedHMAC []byte) bool {
|
|
if len(key) == 0 || len(expectedHMAC) == 0 {
|
|
es.logger.Error("HMAC key or expected HMAC is empty")
|
|
return false
|
|
}
|
|
|
|
computedHMAC := es.computeHMAC(data, key)
|
|
if len(computedHMAC) == 0 {
|
|
return false
|
|
}
|
|
|
|
// Use constant-time comparison to prevent timing attacks
|
|
return hmac.Equal(expectedHMAC, computedHMAC)
|
|
}
|