Files
aitbc/apps/blockchain-node/src/aitbc_chain/database_encryption.py
aitbc 19d415a235
Some checks failed
Blockchain Synchronization Verification / sync-verification (push) Failing after 3s
CLI Tests / test-cli (push) Failing after 3s
Cross-Chain Functionality Tests / test-cross-chain-sync (push) Successful in 2s
Cross-Chain Functionality Tests / test-cross-chain-transactions (push) Successful in 3s
Cross-Chain Functionality Tests / test-cross-chain-bridge (push) Has been skipped
Cross-Chain Functionality Tests / test-multi-chain-consensus (push) Successful in 2s
Cross-Chain Functionality Tests / aggregate-results (push) Has been skipped
Deploy to Testnet / deploy-testnet (push) Successful in 1m12s
Documentation Validation / validate-docs (push) Failing after 8s
Documentation Validation / validate-policies-strict (push) Successful in 3s
Integration Tests / test-service-integration (push) Successful in 2m6s
Multi-Chain Island Architecture Tests / test-multi-chain-island (push) Successful in 2s
Multi-Node Blockchain Health Monitoring / health-check (push) Failing after 4s
P2P Network Verification / p2p-verification (push) Successful in 4s
Package Tests / Python package - aitbc-agent-sdk (push) Successful in 32s
Package Tests / Python package - aitbc-core (push) Successful in 14s
Package Tests / Python package - aitbc-crypto (push) Successful in 12s
Package Tests / Python package - aitbc-sdk (push) Successful in 9s
Package Tests / JavaScript package - aitbc-sdk-js (push) Successful in 8s
Package Tests / JavaScript package - aitbc-token (push) Successful in 17s
Python Tests / test-python (push) Successful in 15s
Security Scanning / security-scan (push) Successful in 27s
Node Failover Simulation / failover-test (push) Successful in 7s
Multi-Node Stress Testing / stress-test (push) Successful in 6s
Cross-Node Transaction Testing / transaction-test (push) Successful in 4s
feat: add SQLCipher database encryption support and consolidate agent documentation
- Add SQLCipher encryption for ait-mainnet database with configurable flag
- Add db_encryption_enabled and db_encryption_key_path config settings
- Implement encryption key loading and PRAGMA key setup via connection events
- Add shutdown_db function for proper database cleanup
- Export middleware classes in aitbc/__init__.py
- Fix import path in sync.py for settings
- Remove duplicate agent documentation from docs
2026-05-03 12:00:38 +02:00

283 lines
8.5 KiB
Python

"""Database encryption module for AITBC blockchain node.
This module provides AES-GCM encryption for SQLite database files at rest,
using the existing cryptography library. It supports key management,
encryption/decryption operations, and detection of encrypted databases.
"""
from __future__ import annotations
import os
import stat
from pathlib import Path
from typing import Optional
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
import secrets
# Magic header to identify encrypted databases
ENCRYPTION_MAGIC = b"AITBCENC"
ENCRYPTION_VERSION = 1
class KeyManager:
"""Manages encryption key generation, storage, and retrieval."""
def __init__(self, key_path: Path):
"""Initialize key manager.
Args:
key_path: Path to the key file.
"""
self.key_path = key_path
self._key: Optional[bytes] = None
def generate_key(self, password: Optional[str] = None) -> bytes:
"""Generate a new encryption key.
Args:
password: Optional password for key derivation. If None, generates random key.
Returns:
256-bit encryption key.
"""
if password:
# Derive key from password using PBKDF2
salt = secrets.token_bytes(16)
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100_000,
backend=default_backend()
)
key = kdf.derive(password.encode('utf-8'))
# Store salt with key for later derivation
return salt + key
else:
# Generate random key
return secrets.token_bytes(32)
def save_key(self, key: bytes) -> None:
"""Save encryption key to file with restricted permissions.
Args:
key: Encryption key to save.
"""
# Ensure parent directory exists
self.key_path.parent.mkdir(parents=True, exist_ok=True)
# Write key with restricted permissions
with open(self.key_path, 'wb') as f:
f.write(key)
# Set file permissions to 600 (owner read/write only)
os.chmod(self.key_path, stat.S_IRUSR | stat.S_IWUSR)
def load_key(self) -> Optional[bytes]:
"""Load encryption key from file.
Returns:
Encryption key or None if file doesn't exist.
"""
if not self.key_path.exists():
return None
with open(self.key_path, 'rb') as f:
return f.read()
def get_or_generate_key(self, password: Optional[str] = None) -> bytes:
"""Get existing key or generate a new one.
Args:
password: Optional password for key derivation.
Returns:
Encryption key.
"""
key = self.load_key()
if key is None:
key = self.generate_key(password)
self.save_key(key)
return key
def ensure_key_permissions(self) -> bool:
"""Ensure key file has restricted permissions.
Returns:
True if permissions are correct or file doesn't exist, False otherwise.
"""
if not self.key_path.exists():
return True
mode = self.key_path.stat().st_mode
return mode & 0o777 == 0o600
class DatabaseEncryptor:
"""Handles encryption and decryption of database files."""
def __init__(self, key: bytes):
"""Initialize encryptor with encryption key.
Args:
key: 256-bit encryption key.
"""
if len(key) < 32:
# If key has salt prefix (first 16 bytes), extract actual key
if len(key) >= 48:
salt = key[:16]
actual_key = key[16:48]
else:
raise ValueError("Encryption key must be at least 32 bytes")
else:
salt = key[:16] if len(key) > 32 else b''
actual_key = key[:32] if len(key) >= 32 else key
self.key = actual_key
self.salt = salt if len(key) > 32 else None
self.aesgcm = AESGCM(actual_key)
def encrypt_file(self, input_path: Path, output_path: Path) -> None:
"""Encrypt a database file.
Args:
input_path: Path to input database file.
output_path: Path to write encrypted database.
"""
# Read plaintext database
with open(input_path, 'rb') as f:
plaintext = f.read()
# Generate nonce
nonce = secrets.token_bytes(12)
# Encrypt data
ciphertext = self.aesgcm.encrypt(nonce, plaintext, None)
# Write encrypted file with magic header
with open(output_path, 'wb') as f:
f.write(ENCRYPTION_MAGIC)
f.write(bytes([ENCRYPTION_VERSION]))
f.write(nonce)
f.write(ciphertext)
def decrypt_file(self, input_path: Path, output_path: Path) -> None:
"""Decrypt an encrypted database file.
Args:
input_path: Path to encrypted database file.
output_path: Path to write decrypted database.
"""
# Read encrypted file
with open(input_path, 'rb') as f:
data = f.read()
# Verify magic header
if not data.startswith(ENCRYPTION_MAGIC):
raise ValueError("File is not an encrypted database")
# Extract version, nonce, and ciphertext
version = data[len(ENCRYPTION_MAGIC)]
if version != ENCRYPTION_VERSION:
raise ValueError(f"Unsupported encryption version: {version}")
nonce_start = len(ENCRYPTION_MAGIC) + 1
nonce_end = nonce_start + 12
nonce = data[nonce_start:nonce_end]
ciphertext = data[nonce_end:]
# Decrypt data
plaintext = self.aesgcm.decrypt(nonce, ciphertext, None)
# Write decrypted file
with open(output_path, 'wb') as f:
f.write(plaintext)
def is_encrypted(self, file_path: Path) -> bool:
"""Check if a database file is encrypted.
Args:
file_path: Path to database file.
Returns:
True if file is encrypted, False otherwise.
"""
if not file_path.exists():
return False
with open(file_path, 'rb') as f:
header = f.read(len(ENCRYPTION_MAGIC))
return header == ENCRYPTION_MAGIC
def get_encryption_key(key_path: Path) -> Optional[bytes]:
"""Get encryption key from file or generate new one.
Args:
key_path: Path to key file.
Returns:
Encryption key or None if encryption is disabled.
"""
key_manager = KeyManager(key_path)
return key_manager.get_or_generate_key()
def encrypt_database(db_path: Path, key: bytes) -> Path:
"""Encrypt a database file.
Args:
db_path: Path to database file.
key: Encryption key.
Returns:
Path to encrypted database file.
"""
encryptor = DatabaseEncryptor(key)
encrypted_path = db_path.with_suffix('.db.encrypted')
encryptor.encrypt_file(db_path, encrypted_path)
return encrypted_path
def decrypt_database(encrypted_path: Path, key: bytes, output_path: Optional[Path] = None) -> Path:
"""Decrypt an encrypted database file.
Args:
encrypted_path: Path to encrypted database file.
key: Encryption key.
output_path: Optional output path. If None, removes .encrypted suffix.
Returns:
Path to decrypted database file.
"""
encryptor = DatabaseEncryptor(key)
if output_path is None:
output_path = encrypted_path.with_suffix('').with_suffix('.db')
encryptor.decrypt_file(encrypted_path, output_path)
return output_path
def is_database_encrypted(db_path: Path) -> bool:
"""Check if a database file is encrypted.
Args:
db_path: Path to database file.
Returns:
True if database is encrypted, False otherwise.
"""
if not db_path.exists():
return False
# Check for magic header
with open(db_path, 'rb') as f:
header = f.read(len(ENCRYPTION_MAGIC))
return header == ENCRYPTION_MAGIC