fix: Prevent orphaned encrypted data when encryption key is deleted
- crypto.go: Add runtime validation to Encrypt() that verifies the key file still exists on disk before encrypting. If the key was deleted while Pulse is running, encryption now fails with a clear error instead of creating orphaned data that can never be decrypted. - hot-dev.sh: Auto-generate encryption key for production data directory (/etc/pulse) when HOT_DEV_USE_PROD_DATA=true and key is missing. This prevents startup failures and ensures encrypted data can be created. - Added test TestEncryptRefusesAfterKeyDeleted to verify the protection works.
This commit is contained in:
parent
8f746ede2a
commit
62b5580193
3 changed files with 71 additions and 2 deletions
|
|
@ -16,18 +16,25 @@ import (
|
|||
|
||||
// CryptoManager handles encryption/decryption of sensitive data
|
||||
type CryptoManager struct {
|
||||
key []byte
|
||||
key []byte
|
||||
keyPath string // Path to the encryption key file for runtime validation
|
||||
}
|
||||
|
||||
// NewCryptoManagerAt creates a new crypto manager with an explicit data directory override.
|
||||
func NewCryptoManagerAt(dataDir string) (*CryptoManager, error) {
|
||||
if dataDir == "" {
|
||||
dataDir = utils.GetDataDir()
|
||||
}
|
||||
keyPath := filepath.Join(dataDir, ".encryption.key")
|
||||
|
||||
key, err := getOrCreateKeyAt(dataDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get encryption key: %w", err)
|
||||
}
|
||||
|
||||
return &CryptoManager{
|
||||
key: key,
|
||||
key: key,
|
||||
keyPath: keyPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +158,20 @@ func getOrCreateKeyAt(dataDir string) ([]byte, error) {
|
|||
}
|
||||
|
||||
// Encrypt encrypts data using AES-GCM
|
||||
// SAFETY: Verifies the encryption key file still exists on disk before encrypting.
|
||||
// This prevents orphaned encrypted data if the key was deleted while Pulse was running.
|
||||
func (c *CryptoManager) Encrypt(plaintext []byte) ([]byte, error) {
|
||||
// CRITICAL: Verify the key file still exists on disk before encrypting
|
||||
// This prevents creating orphaned encrypted data that can never be decrypted
|
||||
if c.keyPath != "" {
|
||||
if _, err := os.Stat(c.keyPath); os.IsNotExist(err) {
|
||||
log.Error().
|
||||
Str("keyPath", c.keyPath).
|
||||
Msg("CRITICAL: Encryption key file has been deleted - refusing to encrypt to prevent orphaned data")
|
||||
return nil, fmt.Errorf("encryption key file deleted - cannot encrypt (would create orphaned data)")
|
||||
}
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(c.key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -260,3 +260,45 @@ func TestLargeDataEncryption(t *testing.T) {
|
|||
t.Error("Large data round-trip failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptRefusesAfterKeyDeleted(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cm, err := NewCryptoManagerAt(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("NewCryptoManagerAt() error: %v", err)
|
||||
}
|
||||
|
||||
// Encrypt should work initially
|
||||
plaintext := []byte("test data")
|
||||
encrypted, err := cm.Encrypt(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Initial Encrypt() failed: %v", err)
|
||||
}
|
||||
|
||||
// Decrypt should also work
|
||||
_, err = cm.Decrypt(encrypted)
|
||||
if err != nil {
|
||||
t.Fatalf("Initial Decrypt() failed: %v", err)
|
||||
}
|
||||
|
||||
// Now delete the key file (simulating what happened in the bug)
|
||||
keyPath := filepath.Join(tmpDir, ".encryption.key")
|
||||
if err := os.Remove(keyPath); err != nil {
|
||||
t.Fatalf("Failed to remove key file: %v", err)
|
||||
}
|
||||
|
||||
// Encrypt should now FAIL to prevent orphaned data
|
||||
_, err = cm.Encrypt([]byte("new data"))
|
||||
if err == nil {
|
||||
t.Error("Encrypt() should fail after key file is deleted")
|
||||
}
|
||||
|
||||
// Decrypt should still work (key is in memory)
|
||||
decrypted, err := cm.Decrypt(encrypted)
|
||||
if err != nil {
|
||||
t.Fatalf("Decrypt() should still work with in-memory key: %v", err)
|
||||
}
|
||||
if !bytes.Equal(decrypted, plaintext) {
|
||||
t.Error("Decrypt() returned wrong data")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -273,6 +273,13 @@ else
|
|||
log_info "Generated dev encryption key at ${DEV_KEY_FILE}"
|
||||
fi
|
||||
export PULSE_ENCRYPTION_KEY="$(<"${DEV_KEY_FILE}")"
|
||||
elif [[ ${HOT_DEV_USE_PROD_DATA:-false} == "true" ]]; then
|
||||
# Production data mode but no key - generate one to prevent orphaned encrypted data
|
||||
log_warn "No encryption key found for ${PULSE_DATA_DIR}. Generating new key..."
|
||||
openssl rand -base64 32 > "${PULSE_DATA_DIR}/.encryption.key"
|
||||
chmod 600 "${PULSE_DATA_DIR}/.encryption.key"
|
||||
export PULSE_ENCRYPTION_KEY="$(<"${PULSE_DATA_DIR}/.encryption.key")"
|
||||
log_info "Generated new encryption key at ${PULSE_DATA_DIR}/.encryption.key"
|
||||
else
|
||||
log_warn "No encryption key found for ${PULSE_DATA_DIR}. Encrypted config may fail to load."
|
||||
fi
|
||||
|
|
|
|||
Loading…
Reference in a new issue